OmniWeb Android-control MCP server
An MCP server that wraps the OmniWeb
"Android middleware" (the omniweb-android Flask service, /api/android/*) so
an MCP client — Claude Code, Claude Desktop, or a custom client — can drive the
lab's Android phones and USB modems.
It is a thin HTTP client over the existing REST API (it imports no Flask
internals), so it works against any reachable omniweb-android instance.
Tools exposed
| Tool | Signature | What it does |
|---|---|---|
list_devices | () | All known devices (phones + modems), online and offline. |
device_info | (serial) | Full details for one device. |
screenshot | (serial) → PNG image | Current screen (universal: scrcpy phones AND old screencap devices). |
tap | (serial, x, y) | Tap at pixel coordinates. |
swipe | (serial, x1, y1, x2, y2, ms=200) | Swipe/drag/fling. |
input_text | (serial, text) | Type into the focused field. |
key | (serial, name) | Press back/home/recent/enter/del/power/menu/volume_up/volume_down. |
adb_shell | (serial, command, timeout=30) → {stdout, stderr, return_code} | Arbitrary adb shell (power tool). |
list_scripts | () | The device test-script library. |
run_script | (serials, file, params={}) | Fan a script across devices; returns run handles. |
get_run | (run_id) | Result of a script run (status/stdout/return_code). |
at_command | (serial, command, read_ms=0, timeout_ms=0) | Raw AT command to a USB modem. |
send_sms | (serial, number, text) | Send an SMS from a modem. |
Configuration (environment variables)
Backend target:
OMNIWEB_API_BASE— base URL of the android service. Defaulthttp://127.0.0.1:5002. The/api/androidprefix is added automatically.
Backend auth (pick ONE):
OMNIWEB_JWT— a pre-minted backend access token (Bearer), OROMNIWEB_JWT_SECRET— the backend'sJWT_SECRET_KEY. The server then mints a short-lived admin JWT itself and auto-refreshes it.OMNIWEB_JWT_TTL(seconds, default 3600) controls its lifetime.
This MCP server's own front door:
MCP_TRANSPORT—http(default, streamable HTTP for remote clients) orstdio(localclaude mcp add).MCP_HOST/MCP_PORT— bind address for http transport. Default127.0.0.1:5005.MCP_AUTH_TOKEN— bearer token a remote MCP client must present. Required to bind to anything other than localhost — the server refuses a non-loopback bind without it (fail closed). On/mcpthe token is checked by middleware;/and/healthare open for health checks.
Run it
venv (self-contained)
cd /opt/omniweb-mcp # or wherever you put server.py + requirements.txt
python3 -m venv venv
venv/bin/pip install -r requirements.txt
export OMNIWEB_API_BASE=http://127.0.0.1:5002
export OMNIWEB_JWT_SECRET=<the backend JWT_SECRET_KEY>
export MCP_TRANSPORT=http
export MCP_HOST=0.0.0.0
export MCP_PORT=5005
export MCP_AUTH_TOKEN=<a long random token>
venv/bin/python server.py
systemd (production)
A unit is provided at packaging/omniweb-android-mcp.service. It reads
/etc/omniweb/omniweb.env (for JWT_SECRET_KEY → it is re-exported as
OMNIWEB_JWT_SECRET, see below) plus /etc/omniweb/android-mcp.env for the
MCP-specific secrets. Create that drop-in (chmod 600):
# /etc/omniweb/android-mcp.env
OMNIWEB_JWT_SECRET=<same value as JWT_SECRET_KEY in omniweb.env>
MCP_AUTH_TOKEN=<a long random token>
Then:
sudo cp packaging/omniweb-android-mcp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now omniweb-android-mcp
The backend env file names the JWT secret
JWT_SECRET_KEY; this server readsOMNIWEB_JWT_SECRET. SetOMNIWEB_JWT_SECRETexplicitly inandroid-mcp.env(copy the value) so the names line up.
Connect from Claude Code
Remote (streamable HTTP), with auth:
claude mcp add --transport http android-control http://<host>:5005/mcp \
--header "Authorization: Bearer <MCP_AUTH_TOKEN>"
Local (stdio), pointing the server at a backend and minting its own JWT:
claude mcp add android-control \
-e OMNIWEB_API_BASE=http://127.0.0.1:5002 \
-e OMNIWEB_JWT_SECRET=<backend JWT_SECRET_KEY> \
-e MCP_TRANSPORT=stdio \
-- /opt/omniweb-mcp/venv/bin/python /opt/omniweb-mcp/server.py
Then in Claude Code: /mcp to confirm it connected, and try
list_devices.
Connect from Claude Desktop
claude_desktop_config.json — stdio launch:
{
"mcpServers": {
"android-control": {
"command": "/opt/omniweb-mcp/venv/bin/python",
"args": ["/opt/omniweb-mcp/server.py"],
"env": {
"OMNIWEB_API_BASE": "http://127.0.0.1:5002",
"OMNIWEB_JWT_SECRET": "<backend JWT_SECRET_KEY>",
"MCP_TRANSPORT": "stdio"
}
}
}
}
For a remote streamable-HTTP server from Claude Desktop, use an mcp-remote
bridge or the HTTP server config supported by your Desktop version, pointing at
http://<host>:5005/mcp with the Authorization: Bearer <MCP_AUTH_TOKEN>
header.
Security notes
- Device control requires the configured
MCP_AUTH_TOKENon the HTTP transport. With no token the server only allows a loopback bind and prints a warning; a non-loopback bind without a token is refused. - The server authenticates to the backend as an admin (minted JWT or supplied
token), so anyone who can reach
/mcpwith the bearer token has full device control. TreatMCP_AUTH_TOKENlike a password and front it with TLS (e.g. via nginx) for off-host access.