Skip to main content
Glama
android-phone-control-mcp

android-phone-control-mcp

by alikhanpkfhg

android-phone-control-mcp

An MCP (Model Context Protocol) server that lets an AI assistant drive a real Android phone — yours — from FreeMilk or any other MCP client, using nothing but Termux on the phone itself. No laptop, no VPS, no paid sandbox, no paid hosting.

It speaks MCP Streamable HTTP on 127.0.0.1:8787, requires a locally generated bearer token on every request, reuses the phone bridge that is already running on 127.0.0.1:8766, and publishes only port 8787 through a Cloudflare Quick Tunnel.

┌─────────────┐   HTTPS + Bearer token   ┌──────────────────────────┐
│  FreeMilk   │  ──────────────────────> | cloudflared quick tunnel  |
│  (MCP       │                          +------------+-------------+
│   client)   │                                       | http://127.0.0.1:8787
└─────────────┘                                       v
                                        +-----------------------------+
                                        |  MCP server (Node.js)       |
                                        |  Streamable HTTP, token auth|
                                        +------+---------------+------+
                                               |               |
                            http://127.0.0.1:8766          adb shell
                                               v               v
                                        +-----------------------------+
                                        |   YOUR ANDROID PHONE        |
                                        +-----------------------------+

The bridge on port 8766 is used from localhost only and is never part of the tunnel. Only the MCP port (8787) is published.

Tools

Tool

What it does

phone_status

Bridge/adb reachability, attached device, screen size. Read-only.

phone_screenshot

Current screen as real MCP image content (PNG). Bridge first, adb exec-out screencap -p as fallback. Read-only.

phone_tap

Tap a pixel coordinate.

phone_swipe

Swipe between two coordinates with a duration.

phone_press_key

HOME, BACK, APP_SWITCH, ENTER, TAB, DEL, DPAD_*, VOLUME_*, POWER, WAKEUP, or an explicit KEYCODE_*.

phone_launch_app

Bring an installed app to the foreground by package name.

phone_type_text

Type printable ASCII into the focused field.

There is no shell tool. Nothing in this server can run arbitrary commands; every tool maps to one fixed adb shell input … (or monkey / am start) invocation built from validated arguments.

Related MCP server: mcp-android

Requirements

  • Android phone (developed against a TECNO SPARK 20, Android 13).

  • Termux from F-Droid.

  • python, git, android-tools (adb) — the installer adds what is missing.

  • Wireless debugging already paired (adb devices shows the phone as device).

  • Your existing phone bridge answering on http://127.0.0.1:8766/health and /screenshot (optional: without it, screenshots fall back to adb).

  • Node.js 18+ (pkg install -y nodejs-lts) — the installer does this for you.

One-command install

Paste this single line into Termux:

pkg install -y curl git nodejs-lts && curl -fsSL https://raw.githubusercontent.com/alikhanpkfhg/android-phone-control-mcp/main/install.sh | sh

The installer:

  • clones the repository to ~/android-phone-control-mcp (or updates an existing clone)

  • installs nodejs, git, android-tools, curl and cloudflared (Termux repo, or the official static binary if the package is missing)

  • generates a random 32-byte bearer token in ~/.android-phone-control-mcp/token (mode 600) and keeps it across restarts

  • writes ~/.android-phone-control-mcp/config.env (never contains the token)

  • checks the existing bridge on 8766 and adb devices

  • prints the token and the next commands

It has no npm dependencies, so there is nothing to npm install.

Start it

sh ~/android-phone-control-mcp/start.sh

It starts the MCP server on 127.0.0.1:8787, starts a Cloudflare Quick Tunnel, waits for a registered connector, then verifies the public URL really reaches this server before recording it, and prints:

  local MCP endpoint  : http://127.0.0.1:8787/mcp
  public MCP endpoint : https://random-words.trycloudflare.com/mcp
  bearer token        : <your token>

Other commands:

sh ~/android-phone-control-mcp/status.sh     # state, URLs, token, logs
sh ~/android-phone-control-mcp/stop.sh       # stop server + tunnel
sh ~/android-phone-control-mcp/report.sh     # paste-able report, no token in it
node ~/android-phone-control-mcp/scripts/selftest.js   # live smoke test
PHONE_MCP_TUNNEL=0 sh ~/android-phone-control-mcp/start.sh   # local only

report.sh prints the version, both endpoints, a live check of the public tunnel, every local protocol check and the phone's real state. It never prints the bearer token, so its output is safe to paste into a chat or an issue.

Logs: ~/.android-phone-control-mcp/logs/mcp.log and logs/cloudflared.log.

Cloudflare Quick Tunnel hostnames are not stable: every restart gets a new https://*.trycloudflare.com name, so the endpoint has to be re-added each time. For a hostname that survives restarts, use the ngrok provider below.

A tunnel that stays the same (ngrok, free)

The Quick Tunnel is fine for a first test but it is the wrong tool for daily use: the hostname changes on every restart, and Cloudflare's own log says these account-less tunnels carry no uptime guarantee.

ngrok's free plan gives you one dev domain per account (something like your-name.ngrok-free.app) that does not change when the agent restarts, and free endpoints have no timeout — they stay online as long as the agent runs. Limits that matter here: 1 GB out and 20,000 HTTP requests per month, 3 online endpoints. The interstitial page ngrok shows on the free tier only affects HTML browser traffic and does not apply to API clients.

One-time setup:

# 1. create a free account at https://dashboard.ngrok.com/signup
# 2. install the agent (Termux package, or the download command shown at
#    https://dashboard.ngrok.com/get-started/setup)
pkg install -y ngrok

# 3. give the agent YOUR ngrok authtoken (from the dashboard)
ngrok config add-authtoken <your-authtoken>

# 4. switch the provider
echo 'PHONE_MCP_TUNNEL_PROVIDER=ngrok' >> ~/.android-phone-control-mcp/config.env

# 5. start again
sh ~/android-phone-control-mcp/start.sh

There are now two different secrets, and they are not interchangeable:

  • the ngrok authtoken proves your agent belongs to your ngrok account. It goes into ngrok config, never into an MCP client.

  • the MCP bearer token at ~/.android-phone-control-mcp/token is what the client sends on every request. Start it with the PHONE_MCP_TUNNEL_PROVIDER=ngrok line above and start.sh prints it.

Keeping the agent alive is the other half of staying reachable: hold termux-wake-lock, disable battery optimisation for Termux, and run it inside tmux so closing the session does not kill it.

Why not Tailscale Funnel

It is a reasonable option — stable *.ts.net hostnames and no domain to buy — but it is still in beta, needs the Tailscale CLI on the device and HTTPS certificates plus a policy attribute in the tailnet, and it can only listen on ports 443, 8443 and 10000, so the MCP port would have to move. ngrok is a single binary and one config line.

Add it to FreeMilk

  1. Run start.sh and copy the two values it prints.

  2. In FreeMilk open Settings → Integrations → Add MCP server.

  3. Transport: Streamable HTTP.

  4. URL: https://<the-printed-host>.trycloudflare.com/mcp (or http://127.0.0.1:8787/mcp if you are on the same phone).

  5. Authentication: Bearer token with the printed token.

  6. Save, then ask the assistant to call phone_status and phone_screenshot.

Security model

  • Token: 32 random bytes (base64url) generated on first install, stored at ~/.android-phone-control-mcp/token with mode 600, never committed, never printed in full by the logs (only abcd...mnop). Every /mcp request is checked with a constant-time comparison; missing or wrong tokens get 401.

  • Loopback only: the MCP server binds 127.0.0.1 by default and refuses a non-loopback MCP_HOST unless PHONE_MCP_ALLOW_REMOTE_BIND=1 is set explicitly.

  • One published port: the tunnel targets http://127.0.0.1:8787 and nothing else. The code refuses to build a tunnel for the bridge port, and the config refuses to let the MCP port equal the bridge port.

  • The bridge stays private: http://127.0.0.1:8766 is only ever contacted from localhost by the MCP server.

  • No shell: no tool accepts a command, a path or a URL. Arguments are validated against fixed schemas; unknown keys, out-of-range coordinates, non-ASCII text and strange package names are rejected before anything reaches adb.

  • Body limits: request bodies are capped (2 MB) and JSON parse errors return -32700.

Confirmation of irreversible actions

PHONE_MCP_CONFIRM controls the server side gate:

Mode

Behaviour

auto (default)

phone_type_text and phone_press_key with ENTER are refused unless the call carries "confirm": true.

strict

Every input tool (tap, swipe, press_key, launch_app, type_text) requires "confirm": true.

none

No server side gate (not recommended).

Read-only tools (phone_status, phone_screenshot) never require confirmation.

On top of the gate, the server's MCP instructions tell the assistant to ask the user before payments, purchases, account deletion, sending messages, granting permissions, accepting terms, or any other irreversible action. Typing and pressing ENTER are the exact primitives those flows are built from, which is why they are gated by default.

This is a remote control for your own phone. Keep the token secret, keep the tunnel short-lived, and stop the tunnel (stop.sh) when you are not using it.

Configuration

~/.android-phone-control-mcp/config.env (created by the installer):

MCP_HOST=127.0.0.1
MCP_PORT=8787
BRIDGE_URL=http://127.0.0.1:8766
PHONE_MCP_CONFIRM=auto
CLOUDFLARED_PROTOCOL=http2

Any of the server's environment variables can also be exported before start.sh: MCP_HOST, MCP_PORT, BRIDGE_URL, BRIDGE_INPUT (auto/adb/bridge), ADB_BIN, ADB_TIMEOUT_MS, BRIDGE_TIMEOUT_MS, MAX_TEXT_LENGTH, PHONE_MCP_CONFIRM, PHONE_MCP_HOME, TOKEN_FILE, LOG_FILE, MCP_MAX_BODY_BYTES, CLOUDFLARED_PROTOCOL.

How the phone is driven

  • Screenshots — GET http://127.0.0.1:8766/screenshot (raw image or JSON with base64), then adb exec-out screencap -p.

  • Input — the server first tries the bridge (POST /tap, /swipe, /key, /text, /launch). If the bridge does not implement those routes (the stock bridge only serves /health and /screenshot), it falls back to adb automatically: input tap, input swipe, input keyevent, input text, monkey -p <pkg> with am start fallback. Force either path with BRIDGE_INPUT=adb or BRIDGE_INPUT=bridge.

  • Screen size — adb shell wm size, cached for 60 s and used to reject coordinates outside the display.

Known limitation

adb shell input text can only type printable ASCII. Emoji, accents, CJK and other characters cannot be injected that way. For those, tap the on-screen keyboard keys with phone_tap (or use a helper IME). The tool says so instead of silently failing.

Testing

Automated checks run in GitHub Actions (.github/workflows/ci.yml, Node 20 and 22):

  • sh -n syntax check of every shell script

  • a "no secret files tracked" check (token, .env, private keys, tunnel-url.txt) and a scan for credential-looking strings

  • node --test test/*.test.js:

    • unit tests for config, tunnel-target safety, token generation/permissions/reuse, constant-time comparison, bearer parsing, coordinate/key/text/package validation, screen bounds, adb device parsing, input text escaping

    • integration tests that boot the real HTTP server against a fake bridge and a fake adb and exercise: /health without a token, 401 for missing/wrong/Basic auth, initialize (current and older protocol versions), tools/list (all seven tools with schemas), phone_status, phone_screenshot (bridge image, bridge base64 JSON, adb fallback with the bridge down), coordinate validation, confirm gating in auto/strict/none modes, unknown tools/methods, malformed JSON, batch requests, notifications (202), and clients that only accept text/event-stream

    • a test asserting the tunnel target is 127.0.0.1:8787 and that 8766 never appears in the cloudflared arguments

  • scripts/selftest.js repeats the important checks against a running installation, including that the public tunnel refuses anonymous requests

What has been verified here, and what has not

Verified in CI (no phone involved): protocol handling, authentication, argument validation, confirmation gating, image content shape, adb/bridge fallback logic, tunnel target safety, tunnel liveness verification, and shell script syntax.

Not verified from here: the behaviour of the real TECNO SPARK 20 through the real bridge and real adb. This repository was written without a device attached. Run node ~/android-phone-control-mcp/scripts/selftest.js on the phone after start.sh; it exercises phone_status and phone_screenshot against the real device and reports PASS/FAIL per check. Only claim the phone works after that output shows phone_screenshot returning a real screen image.

Troubleshooting

the MCP server did not become healthy — read ~/.android-phone-control-mcp/logs/mcp.log. A port clash shows as EADDRINUSE; something else is already using 8787.

phone_screenshot fails — check adb devices shows your phone as device, then check curl http://127.0.0.1:8766/health. Either path alone is enough.

adb command failed: ... unauthorized — re-accept the wireless debugging prompt on the phone.

Tunnel URL 530 / 502 / 1033 — the Cloudflare edge has no live connector for that hostname. A trycloudflare hostname outlives the cloudflared process that created it, so a URL read from the log (or from tunnel-url.txt) can look perfectly valid while being completely dead. Run sh start.sh: it notices the recorded tunnel does not answer and replaces it. Never reuse a hostname from an old run.

FreeMilk cannot connect — make sure the URL ends in /mcp, the transport is Streamable HTTP, and the token was pasted without a trailing space.

Cloudflared will not start in Termux — CLOUDFLARED_PROTOCOL=http2 (the default) avoids the QUIC problems common on mobile networks. If the downloaded static binary refuses to run on your Android version, install the Termux package instead (pkg install -y cloudflared) and re-run start.sh; the script picks up whichever binary it finds.

Uninstall

sh ~/android-phone-control-mcp/stop.sh
rm -rf ~/android-phone-control-mcp ~/.android-phone-control-mcp

License

MIT. You are responsible for what you do with it: use it on devices you own.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI agents to directly control Android devices via Termux, providing 120+ tools for screen manipulation, file management, app control, and system operations with layered loading and security gating.
    4
    MIT
  • 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
    A
    maintenance
    Enables AI agents to remotely control a real Android phone over MCP with no ADB, root, or emulator, by connecting a broker server to a sideloaded APK. Exposes tools to list devices, capture screenshots, read the UI tree, open links and deep links, and perform taps, swipes, typing, and global navigation actions.
    1
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to see and control Android devices through ADB and scrcpy, including screenshots, taps, swipes, typing, app management, UI inspection, file transfer, shell commands, audio/video streaming, and clipboard access.
    365 npm
    MIT