Skip to main content
Glama

termux-mcp

An MCP server that turns an Android phone running Termux into a remote machine you can drive with an AI agent.

Connect a client on your laptop, ask it to run a command, read a file, check the battery, take a screenshot or send yourself a notification — and it happens on the phone.

  • 45 tools: shell, background jobs, tmux sessions, file management, the whole termux-api surface, package management and device state.

  • Two transports from one codebase: stdio for a client on the same device, Streamable HTTP with bearer tokens for a client anywhere on your network.

  • Safe by construction, not safe by luck: a confirmation gate on destructive commands, a jailed file API, token scopes, and an audit log of every call.

WARNING

This server grants shell access to your phone. Anyone holding a full token can run anything Termux can run, read your files and use the Android APIs granted to Termux. Treat the token like a password, keep the HTTP bind address off the public internet, and prefer the read-only token for anything that only needs to look. Read SECURITY.md before you expose it to a network.


Install

One command. It installs the project, writes a config, starts the server and tells you what to type on your PC:

curl -fsSL https://raw.githubusercontent.com/YSCodex/termux-mcp/main/install.sh | bash

Add flags after bash -s --:

curl -fsSL https://raw.githubusercontent.com/YSCodex/termux-mcp/main/install.sh \
  | bash -s -- --with-tmux --with-api --with-opencode

The installer checks for Termux, git, node and curl — and installs them with pkg if they are missing — then clones to ~/termux-mcp and runs scripts/setup.sh. It is safe to run twice: it updates an existing checkout, leaves a dirty one alone, and never rotates your tokens unless you pass --force. --dir, --ref and --source change the location, version or origin.

To do it by hand instead:

git clone https://github.com/YSCodex/termux-mcp.git
cd termux-mcp
bash scripts/setup.sh          # or: npm install && node bin/termux-mcp.mjs --init

If you put the checkout on shared storage (/storage/emulated/0, /sdcard — which termux-setup-storage exposes) you must add --no-bin-links, because Android's FUSE filesystem cannot create the symlinks npm wants in node_modules/.bin. The installer and the setup script both detect this and do it for you.

Requirements

Device

Android with Termux installed

Node

20 or newer (pkg install nodejs) — installed by the installer if absent

For termux-api tools

pkg install termux-api and the Termux:API app

For tmux tools

pkg install tmux

The server itself is pure JavaScript. There is nothing to compile.

Two Android quirks worth knowing

  1. /storage is mounted noexec. A file there cannot be executed, so there is no #!/usr/bin/env node shortcut on shared storage. Always launch with an explicit node path:

    node ~/../storage/emulated/0/termux-mcp/bin/termux-mcp.mjs --stdio

    From inside Termux the node binary is $PREFIX/bin/node, usually /data/data/com.termux/files/usr/bin/node.

  2. Termux needs a real terminal. Running inside a proot distro works, but commands that expect a TTY — and everything that touches the Android window system — behave better in a genuine Termux session.

Related MCP server: android-shizuku-mcp

Quick start

The one-liner. It installs dependencies, writes a config with two tokens, starts the server and prints what to type on your PC:

bash scripts/setup.sh

Useful flags: --bind 0.0.0.0 (the default, so a PC on the hotspot can reach the phone), --port 8737, --with-tmux, --with-api, --with-opencode, --no-http, --force to rotate tokens. It is idempotent, and it will not touch an existing config unless you pass --force.

Doing it by hand instead:

# 1. Create a config with two freshly generated tokens
node bin/termux-mcp.mjs --init --bind 0.0.0.0 --port 8737

# 2a. Serve on stdio for a client on this device
node bin/termux-mcp.mjs --stdio

# 2b. Or serve on HTTP for a client somewhere else
node bin/termux-mcp.mjs --http --bind 0.0.0.0 --port 8737

--init prints your tokens once. They are written to ~/.termux-mcp/config.json with mode 600. Keep that file out of version control — it is the only thing between a client and your phone.

Reaching the phone from a PC over the hotspot

0.0.0.0 makes the server listen on every interface, so a laptop joined to the phone's hotspot can connect. Two things make that safe enough to be convenient:

  • Bearer tokens. Every request needs one. The read token is enough for browsing.

  • A private-network Host allowlist. http.allowedHosts defaults to loopback plus 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12 and 100.64.0.0/10 — a Wi-Fi network, the phone's own hotspot, or a VPN. It is deliberately not 0.0.0.0/0, so a public client cannot forge a Host header and get in, and the DNS-rebinding guard stays armed.

Turn the hotspot on, then on the PC use the address the server printed at startup:

curl http://192.168.43.1:8737/health

The Android hotspot gateway is normally 192.168.43.1, but read the banner rather than assume it. The listener is IPv4 only, so a globally routable IPv6 address on the phone is not exposed — the server says so at startup if it finds one.

If you would rather not expose a port at all, use the SSH recipe below. It is strictly better; the hotspot route is the convenient one.

Client setup

opencode

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "termux": {
      "type": "local",
      "command": [
        "/data/data/com.termux/files/usr/bin/node",
        "/storage/emulated/0/fun/termux-mcp/bin/termux-mcp.mjs",
        "--stdio"
      ],
      "enabled": true,
      "timeout": 20000
    }
  }
}

For the phone as a remote target, point opencode at the HTTP endpoint instead:

{
  "mcp": {
    "termux": {
      "type": "remote",
      "url": "http://100.64.0.1:8737/mcp",
      "enabled": true,
      "oauth": false,
      "headers": { "Authorization": "Bearer {env:TERMUX_MCP_TOKEN}" },
      "timeout": 120000
    }
  }
}

Claude Code

Over HTTP:

claude mcp add --transport http termux http://100.64.0.1:8737/mcp \
  --header "Authorization: Bearer $TERMUX_MCP_TOKEN"

Over SSH, which avoids opening a port at all:

claude mcp add termux -- ssh -T phone \
  /data/data/com.termux/files/usr/bin/node \
  /storage/emulated/0/fun/termux-mcp/bin/termux-mcp.mjs --stdio

Claude Desktop

Claude Desktop speaks stdio only, so tunnel the phone over SSH and point it at the tunnel:

claude_desktop_config.json:

{
  "mcpServers": {
    "termux": {
      "command": "ssh",
      "args": [
        "-T", "-N", "-L", "8737:127.0.0.1:8737", "phone",
        "/data/data/com.termux/files/usr/bin/node",
        "/storage/emulated/0/fun/termux-mcp/bin/termux-mcp.mjs", "--stdio"
      ]
    }
  }
}

VS Code, Cursor

{
  "servers": {
    "termux": {
      "type": "stdio",
      "command": "/data/data/com.termux/files/usr/bin/node",
      "args": ["/storage/emulated/0/fun/termux-mcp/bin/termux-mcp.mjs", "--stdio"]
    }
  }
}

Cursor uses the same shape under mcpServers in .cursor/mcp.json.

Tools

Shell

Tool

What it does

shell

Run a command, return stdout, stderr and the exit code. cwd, env, timeout_ms, max_output_bytes.

shell_bg

Start a detached command, get a job id and a log file.

shell_jobs

Every job, with pid, log path and whether it is still alive.

shell_logs

Tail of a job's log.

shell_kill

Signal a job's whole process group.

tmux_open / tmux_send / tmux_capture / tmux_list / tmux_kill

Persistent interactive sessions.

tools_status

Which optional pieces work right now (tmux, which termux-* binaries exist).

Files

fs_read, fs_write, fs_list, fs_stat, fs_mkdir, fs_move, fs_delete, fs_search, fs_tar, fs_download.

fs_read takes offset and limit for line ranges, or encoding: "base64" for binaries. fs_tar is the fast path for bulk transfer in either direction. fs_download fetches a URL straight to disk.

Termux and Android

api_run for any installed termux-* helper, plus first-class tools: battery_status, clipboard_get, clipboard_set, toast, vibrate, wifi_info, wifi_scan, location, screenshot, camera_photo, mic_record, notification, dialog, open_url, share, sensor_list, sensor_read, telephony_info, call_log, contact_list, sms_list, sms_send, tts_speak, media_list.

Media tools write into ~/.termux-mcp/media/ and return the path; pass include_image: true to get the picture inline as an image block.

Device state and maintenance

system_info, network_info, storage_info, proc_list, proc_kill, pkg_list, pkg_install, pkg_upgrade, audit_tail, termux_info.

Safety model

Destructive commands need confirm: true. A command that matches the destructive set — recursive deletes, mkfs, dd to a device, writes to a block device, fork bombs, package removal, reboots, su, SELinux tampering, writes into /system — is refused until the caller passes confirm: true. The same applies to fs_delete, fs_move over an existing file, fs_tar extract, pkg_upgrade, proc_kill, sms_send and tmux_kill.

The file API is jailed. fs_* tools only touch paths under paths.allow, minus anything under paths.deny. Deny entries can carve out exceptions, which is how $PREFIX stays reachable while the rest of /data/data does not. The audit log and the config file are protected: no tool can rewrite them.

The shell is not jailed. That is the point of the tool, and it is why the destructive gate exists. If you need a hard boundary, give the client a read token and it will not reach the shell at all.

Tokens carry scopes. read reaches the read-only tools; full reaches everything. A tool is scoped by its readOnlyHint annotation, so the two lists cannot drift apart.

Every call is audited. Tool name, caller, arguments (truncated), duration and outcome go to ~/.termux-mcp/audit.log as JSON lines, rotating at 5 MiB. Read it back with audit_tail.

HTTP is locked down. Bearer token compared in constant time, Host and Origin header validation against http.allowedHosts to blunt DNS rebinding, /health with no sensitive data, and a 403 for any request whose Host is not allowed.

Configuration

~/.termux-mcp/config.json, written by --init. See config.example.json for a fully commented starting point. Point TERMUX_MCP_CONFIG elsewhere to keep several profiles.

Key

Default

Notes

http.enabled

true

Set false to keep the file around but serve nothing.

http.bind

127.0.0.1

0.0.0.0 listens everywhere, which is what a PC on the hotspot needs.

http.port

8737

http.path

/mcp

http.allowedHosts

loopback + 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10

Exact hosts, CIDR ranges, or * to accept anything. CIDR entries switch the guard from exact matching to range matching.

tokens[]

none

{ name, token, scopes } where scopes is read or full.

paths.allow

Termux home, $PREFIX, /storage/emulated/0, /tmp

File API roots.

paths.deny

system, vendor, proc, dev, etc, other apps' data

Supports { path, except: [] }.

limits.maxOutputBytes

256 KiB

Per command, stdout and stderr combined.

limits.defaultTimeoutMs

30 s

Raised by timeout_ms, capped by maxTimeoutMs.

limits.maxConcurrentJobs

8

Foreground queue and background jobs.

api.deny

termux-reboot, termux-chroot, …

Binaries api_run will not launch.

Environment overrides: TERMUX_MCP_CONFIG, TERMUX_MCP_TOKEN, TERMUX_MCP_BIND, TERMUX_MCP_PORT, TERMUX_MCP_PATH, TERMUX_MCP_HTTP=0, TERMUX_MCP_STATE_DIR.

Run it at boot

Install Termux:Boot, then:

mkdir -p ~/.termux/boot
cat > ~/.termux/boot/termux-mcp <<'EOF'
#!/data/data/com.termux/files/usr/bin/bash
termux-wake-lock
/data/data/com.termux/files/usr/bin/node \
  /storage/emulated/0/fun/termux-mcp/bin/termux-mcp.mjs --http \
  >> ~/.termux-mcp/boot.log 2>&1
EOF
chmod +x ~/.termux/boot/termux-mcp

termux-wake-lock keeps the CPU awake so long commands are not killed when the screen sleeps.

Troubleshooting

Every termux-api tool times out or returns nothing. The binaries and the app are separate installs. Run pkg install termux-api, install the Termux:API app, then termux-api-start. Call tools_status to see what is actually present.

A tool says a path is outside the allowed roots. That is the jail. Add the path to paths.allow, or use shell with a full token if you genuinely need it.

A destructive command was refused. That is the gate. If you meant it, pass confirm: true.

EACCES when launching the server. You are on noexec shared storage. Launch with node /path/to/bin/termux-mcp.mjs, do not try to execute the file.

npm fails with EPERM or ENOENT on symlinks. You are on shared storage. Use npm install --no-bin-links.

The HTTP client cannot connect. Check the bind address is reachable and that the address is in http.allowedHosts; the Host check answers 403 to everything else. curl /health from a browser-free shell to confirm the server is alive.

A PC on the hotspot gets 403. The Host header carries the address the client dialled, so it must fall inside http.allowedHosts. The default private ranges cover a normal hotspot; if you are bridging into some other subnet, add it, or add the exact address. Note that allowedHosts is the client's view of the address, not the server's.

"x.x.x.x" is not an address on this device. Your VPN address changed — Android hands out a new one when the phone reconnects on a different cellular interface. The server lists its current addresses before it exits. Either update http.bind and http.allowedHosts, or pass the new address for one run with --bind <addr> --port <port>, or bind to 0.0.0.0 with the private-range allowlist and stop caring. The stdio transport never has this problem.

Remote tools over HTTP are slow or flaky. Put the phone and the client on the same VPN (Tailscale or WireGuard) and bind to that address instead of relying on a carrier network.

Development

npm install --no-bin-links   # on shared storage
node scripts/check.mjs       # syntax check and credential scan
node test/smoke.mjs          # 36 tests: config, tools, policy, both transports

The smoke test runs entirely against a temporary directory, boots the server over stdio and over HTTP, and asserts that a bad token gets a 401, a bad Host gets a 403, a private-network Host is accepted while a public one is not, a read token cannot write, and a destructive command without confirm is refused. It needs no Android device, which is why CI can run it on Linux.

Add a tool in src/tools/, give it a schema, set annotations.readOnlyHint correctly — the scope gate reads that flag — and register it in the matching register*Tools function.

Status

Beta. The tool surface and the safety gates are tested; the termux-api wrappers track the flags shipped by the installed termux-api package and may need adjusting when Termux changes them. tmux support is untested here because tmux was not installed on the test device.

License

MIT

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.
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables LLM agents to control Android devices securely via Shizuku and Termux:API, providing a universal shell tool for command execution and persistent sessions.
    6
    Apache 2.0