Expose an Android phone's Shizuku shell (uid 2000, like adb shell) to AIs as
an MCP tool — without VPN, adb, or sshd. The phone holds a single
outbound WebSocket to a relay on example.com; AIs call the relay's MCP endpoint.
┌─────────┐ MCP run_shell ┌──────────────────────┐ WS (outbound) ┌──────────────┐
│ AI │ ──HTTPS+Bearer──▶ │ example.com relay+MCP │ ◀── phone dials ──│ phone APK │
│(Claude) │ ◀── stdout/code── │ (Node, Dokploy) │ ── exec cmd ─────▶│ Shizuku→shell │
└─────────┘ └──────────────────────┘ └──────────────┘
Phone has zero inbound exposure (works behind SKT CGNAT). No VPN, no adb, no sshd.
server/— Node/TS. Streamable-HTTP MCP server (run_shell,list_devices)- WS relay the phone connects to. Bearer auth for AIs, shared token for the phone.
app/— Android (Kotlin). One installable APK: binds a ShizukuUserServiceto run commands as shell uid, a foreground service holds the outbound WS, auto-starts on boot.
# server (typecheck + e2e smoke test with a fake agent)
cd server && npm install && npx tsc && node test/smoke.mjs
# APK (Android SDK + Gradle run inside Docker; host stays clean)
cd app && ./build-apk.sh # -> app/rish-mcp-agent.apkTokens live in server/.env (gitignored). Deploy docker-compose.yml as a Dokploy
Compose app with AI_TOKEN/DEVICE_TOKEN env, routed by Traefik to mcp.example.com.
Add a Cloudflare A record mcp.example.com → <server-ip> (orange/proxied is fine for
HTTPS+WSS over :443).
Fully headless when you already have a Shizuku shell (rish / adb). -g
auto-grants Shizuku's API_V23 permission, and am extras provision the agent —
no taps, no typing on the device:
SIZE # not needed; stream the apk over stdin as the shell user
TOKEN=<DEVICE_TOKEN>
# 1. push + install with runtime perms granted (grants Shizuku too)
rish -c 'cat > /data/local/tmp/r.apk' < rish-mcp-agent.apk
rish -c 'pm install -r -g /data/local/tmp/r.apk; rm -f /data/local/tmp/r.apk'
# 2. provision relay URL + token and start the agent
rish -c "am start -n kr.scin.rishmcp/.MainActivity \
--es relay wss://mcp.example.com/agent --es token $TOKEN --ez autostart true"The foreground service connects out to the relay and survives reboot (BootReceiver).
To re-point an already-running agent at a new relay, am force-stop kr.scin.rishmcp
first, then re-run step 2 (the service reads config on fresh start).
Manual alternative: open the app, Grant Shizuku permission, paste relay URL + token, Save & Start.
{
"mcpServers": {
"phone": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer <AI_TOKEN>" }
}
}
}Then the AI has tools run_shell({cmd, deviceId?, timeoutMs?}) and list_devices().
- This grants shell-level remote execution on the phone to anyone holding
AI_TOKEN. Treat it like an SSH private key. Rotate by changing env + restarting the stack. - The phone only trusts the relay it dials; it never accepts inbound connections.
- Scope is the owner's own device for personal automation.