termux-mcp
Turns an Android phone running Termux into a remotely drivable device. Exposes Android and Termux:API capabilities such as battery status, clipboard access, toasts, vibration, Wi-Fi info/scanning, location, screenshots, camera, microphone recording, notifications, dialogs, opening URLs, sharing, sensor listing/reading, telephony info, call logs, contacts, SMS, text-to-speech, and media listing.
Provides persistent interactive terminal session management through tmux, with tools to open sessions, send commands, capture output, list sessions, and kill sessions.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@termux-mcpcheck my phone's battery level and send me a notification"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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-apisurface, package management and device state.Two transports from one codebase:
stdiofor 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.
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 | bashAdd flags after bash -s --:
curl -fsSL https://raw.githubusercontent.com/YSCodex/termux-mcp/main/install.sh \
| bash -s -- --with-tmux --with-api --with-opencodeThe 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 --initIf 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 ( |
For termux-api tools |
|
For tmux tools |
|
The server itself is pure JavaScript. There is nothing to compile.
Two Android quirks worth knowing
/storageis mountednoexec. A file there cannot be executed, so there is no#!/usr/bin/env nodeshortcut on shared storage. Always launch with an explicit node path:node ~/../storage/emulated/0/termux-mcp/bin/termux-mcp.mjs --stdioFrom inside Termux the node binary is
$PREFIX/bin/node, usually/data/data/com.termux/files/usr/bin/node.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.shUseful 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.allowedHostsdefaults to loopback plus192.168.0.0/16,10.0.0.0/8,172.16.0.0/12and100.64.0.0/10— a Wi-Fi network, the phone's own hotspot, or a VPN. It is deliberately not0.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/healthThe 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 --stdioClaude 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 |
| Run a command, return stdout, stderr and the exit code. |
| Start a detached command, get a job id and a log file. |
| Every job, with pid, log path and whether it is still alive. |
| Tail of a job's log. |
| Signal a job's whole process group. |
| Persistent interactive sessions. |
| Which optional pieces work right now (tmux, which |
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 |
|
| Set |
|
|
|
|
| |
|
| |
| loopback + | Exact hosts, CIDR ranges, or |
| none |
|
| Termux home, | File API roots. |
| system, vendor, proc, dev, etc, other apps' data | Supports |
| 256 KiB | Per command, stdout and stderr combined. |
| 30 s | Raised by |
| 8 | Foreground queue and background jobs. |
|
| Binaries |
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-mcptermux-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 transportsThe 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
This server cannot be deployed
Maintenance
Related MCP Connectors
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Melaya is a remote MCP server. It gives an assistant hands on your own Android phone and browser: it reads the screen through the accessibility tree, then taps, types and navigates inside the apps and sites you allow-list, with no per-app API. It also builds, schedules and runs agent pipelines across 6k+ connected tools. OAuth 2.1, nothing to install.
Control real Android and iOS devices with LLM agents — tap, swipe, type, automate flows.
Remote shell and detached long-running jobs on your own machines — no SSH, open ports or VPN.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables 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.1MIT
- AlicenseNot gradedqualityDmaintenanceEnables LLM agents to control Android devices securely via Shizuku and Termux:API, providing a universal shell tool for command execution and persistent sessions.6Apache 2.0
- AlicenseNot gradedqualityFmaintenanceZero-dependency Model Context Protocol server for Termux (Android) that gives shell access to your device.15 npm2MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to remotely control an Android device from a PC, using Termux API for camera, location, SMS, notifications, and shell commands over SSH or SSE.MIT