Skip to main content

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

ToolSignatureWhat it does
list_devices()All known devices (phones + modems), online and offline.
device_info(serial)Full details for one device.
screenshot(serial) → PNG imageCurrent 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. Default http://127.0.0.1:5002. The /api/android prefix is added automatically.

Backend auth (pick ONE):

  • OMNIWEB_JWT — a pre-minted backend access token (Bearer), OR
  • OMNIWEB_JWT_SECRET — the backend's JWT_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_TRANSPORThttp (default, streamable HTTP for remote clients) or stdio (local claude mcp add).
  • MCP_HOST / MCP_PORT — bind address for http transport. Default 127.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 /mcp the token is checked by middleware; / and /health are 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 reads OMNIWEB_JWT_SECRET. Set OMNIWEB_JWT_SECRET explicitly in android-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 &lt;MCP_AUTH_TOKEN> header.

Security notes

  • Device control requires the configured MCP_AUTH_TOKEN on 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 /mcp with the bearer token has full device control. Treat MCP_AUTH_TOKEN like a password and front it with TLS (e.g. via nginx) for off-host access.