DroidLab MCP
Provides tools to control an Android emulator: boot and stop emulators, drive the UI with taps, swipes, typing and hardware keys, capture screenshots and UI dumps, install APKs, transfer files, read logs, manage clipboard, and open a browser view for human observation.
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., "@DroidLab MCPBoot the emulator, install app.apk, and launch the app."
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.
DroidLab MCP
Android emulator under full control of an AI agent. DroidLab is an MCP (Model Context Protocol) server that lets any MCP client — Claude Desktop, OpenCode, Cursor, a custom agent — boot an Android emulator, drive its UI (tap / swipe / type / keys), read the screen (screenshot + UI element tree), install APKs, read logs, and use the clipboard. While the agent works, a human can watch the live screen in a regular browser over the local network.
Agent first. The MCP server is the product; the browser is the observation deck. An agent can set up and use the entire toolset without any human in the loop.
[MCP client / agent] ──stdio──> [MCP server] ──adb──> [Android emulator]
│
[Browser / human] <──WebSocket──> [Node.js bridge] <──┘ (video + input relay)
H.264 via WebCodecsTable of contents
Related MCP server: phone-mcp-server
Requirements
Dependency | Notes |
Node.js ≥ 18 | bridge + MCP server |
Android SDK |
|
scrcpy 4.x | H.264 stream + control channel; |
openssl | self-signed TLS cert for the bridge (HTTPS) |
Linux, macOS and Windows are supported. The bridge serves HTTPS (self-signed cert, generated on first start) because WebCodecs VideoDecoder requires a secure context. Open the access URL once and accept the certificate warning ("Continue to droidlab.local / unsafe").
Self-bootstrap: missing pieces are downloaded on demand. env_start creates a missing AVD by itself — it derives the API level from the AVD name (API33 → system-images;android-33;google_apis;<host ABI>, arm64 hosts get arm64-v8a), downloads the image via sdkmanager (pending SDK licenses are auto-accepted, 30-min cap) and runs avdmanager create avd -d pixel_7. If sdkmanager/avdmanager are absent, the official cmdline-tools package is fetched into <sdk>/cmdline-tools/latest; when no system java exists, Android Studio's bundled JBR is wired into JAVA_HOME/PATH. Missing scrcpy is downloaded to ~/bin/scrcpy/ (release v4.1 asset for the platform + scrcpy-server jar) before the bridge starts; SCRCPY/SCRCPY_SERVER env vars override the lookup. Downloads need network access.
Agent quick start
git clone https://github.com/cirkasssian/Droid-Lab-MCP.git droidlab && cd droidlab
bash scripts/install-mcp.shThe installer is safe by design: it locates node at known absolute paths and never invokes brew upgrade/brew reinstall (a bare brew operation can collateral-upgrade unrelated apps — this is exactly how an improvised install broke opencode on 2026-09-14: brew reinstall node → brew replaced the opencode binary → every prompt failed with "Failed to send prompt"). If node is missing, it installs it with collateral-upgrade guards, smoke-tests the MCP handshake, and registers the server in ~/.config/opencode/opencode.json[c] (idempotent, with a backup).
Manual registration (any MCP client) — always use the absolute node path; GUI clients do not inherit the interactive shell PATH, so a bare "node" silently fails there:
{
"mcpServers": {
"droidlab": {
"command": "/opt/homebrew/bin/node",
"args": ["/absolute/path/to/droidlab/mcp/server.mcp.mjs"]
}
}
}opencode (opencode.jsonc) uses the "mcp" block format: "command": ["/opt/homebrew/bin/node", "/path/to/mcp/server.mcp.mjs"] plus "env": { "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" } as a fallback.
A typical first session:
env_start— boots the emulator (cold boot) and the bridge; blocks until Android is up.screenshot/ui_dump/wait_for— see the screen, locate elements by text orresource-id.tap/swipe/text/key— drive the UI in native pixels (default screen 1080×2400).install_apk/push_file/pull_file/logcat/clipboard_get— install and inspect.access_start— when a human needs to watch: returns a tokenized LAN URL.env_stop— shut everything down when done.
All long operations (boot, image download, APK install, file transfer) support MCP cancellation (notifications/cancelled) and report progress (notifications/progress).
shell(rawadb shell) is included deliberately for full control and diagnostics; it is annotateddestructiveHint: true. Prefer dedicated tools when they cover the task — annotations and structured output make them safer and easier to parse.
Tools
44 tools. Status tools (env_status, device_state, env_list, system_images_list, list_devices) also return structuredContent (MCP 2025-06-18). All tools declare MCP annotations (readOnlyHint / destructiveHint / idempotentHint).
Many tools accept a required confirm: true argument. It has no functional effect — it exists to prevent a known LLM failure mode: when a tool's arguments are all optional, some models emit a bare { (truncated JSON) instead of {} for empty calls, which the MCP client rejects with JSON parsing failed: Text: {. Requiring confirm forces the model to generate a complete {"confirm":true} object, eliminating the truncation.
Lifecycle
Tool | Description |
| Emulator (cold boot, device state is lost on stop) + bridge on loopback. Idempotent (accepts an already-running external emulator), mutex-protected. |
| Graceful shutdown → verified kill → stale-lock removal. |
| Processes (bridge/emulator), boot state, device info, input mode. |
| Entries of the emulator registry + all AVDs discovered in the SDK. |
|
|
| Restart the local adb server (kill-server + start-server) — for a wedged adb: device gone from |
SDK / AVD management
Tool | Description |
| Installed system images + ones available for download. |
|
|
|
|
| Available device profiles ( |
Device interaction
Tool | Description |
| Inline JPEG (720×1600) + full PNG saved to |
| Tree of visible elements (class / text / resource-id / clickable / bounds + center in native pixels). XML saved to |
| Server-side polling of the UI tree until an element appears; criteria combine with AND; returns ready-to-tap centers. |
| Tap at native pixels. |
| Swipe; |
| Two-finger pinch-zoom at a point; |
| Lock portrait/landscape or restore auto-rotation ( |
| Scroll at a point; |
| Named key ( |
| Type Unicode text (via ADBKeyBoard) into the focused field. |
| Read / write the device clipboard. |
|
|
| File transfer with the device (cancellable). |
| Launch via monkey / force-stop. |
| VIEW intent: https links, app links, custom schemes. |
|
|
|
|
|
|
| List packages, optional substring filter, include system apps. |
| Snapshot of the device log with filters. |
| Raw |
| Emulator console ( |
| Full Android bug report → zip in |
| Tail of host-side logs: |
| Processes, boot, Android/API version, screen, foreground app, input mode. |
| Change the stream resolution (see Latency model). |
Network access (for humans)
Tool | Description |
| Bridge → |
| Back to loopback; LAN access cut off. |
| Grant / revoke browser input. The setting is persisted to |
| Restart the relay process without touching the emulator: applies bridge code changes, recovers a hung/dead bridge. Preserves host binding and input mode; access token regenerates (new URL in the reply). |
Configuration
Tool | Description |
| Read or update the persisted configuration ( |
On the first env_start (when config.json does not exist yet), the reply includes a note offering to customize the defaults via mcp_config.
Resources
URI | Content |
| Current environment state (JSON) |
| Latest full-resolution screenshot (PNG) |
| Any saved artifact: screenshots, UI dumps |
Human quick start
Start the stack manually (or just ask the agent: "start the emulator and let me watch"):
# 1. Emulator
~/Android/Sdk/emulator/emulator -avd API33 -no-window -no-snapshot &
# 2. Bridge (starts scrcpy on its own)
node web/server.jsOpen https://<host>:8090 in a browser (accept the self-signed cert warning once). The agent controls the stack over MCP and is the only party that opens network access (access_start) or unlocks browser input (set_dev_input).
Headless machine? Tunnel instead of exposing the port:
ssh -L 8090:localhost:8090 user@headless -N
# then: https://localhost:8090/?token=<accessToken> (localhost is a secure context — no cert warning)Browser controls: click = tap, drag = swipe, wheel = scroll, keyboard = device input (printable text, Backspace, Enter, arrows, Esc), plus Back / Home / Recents / fullscreen buttons and a sound toggle (device audio is streamed as opus; the browser starts muted — autoplay policy). APKs can be dragged into the window (adb install -r -t); other dropped files land in /sdcard/Download/. Ctrl+C / Ctrl+V bridge the host clipboard with the device. Touch coordinates are mapped to the device's native screen size (queried via wm size), so tablets and phones both work correctly. The header shows FPS and the actual downlink bitrate (KB/s or MB/s, max across active h264 clients, updated 1×/sec). The canvas is hidden until the device's real aspect ratio is known (via /state), so the placeholder silhouette matches the actual device (tablet/phone).
Architecture
Component | Path | Purpose |
MCP server |
| stdio server: emulator lifecycle, input, screenshots, access control |
Emulator registry |
| AVD entries: name, avd, note, device. Migrated from |
Configuration |
| Persisted user config: port, requireToken, defaultAvd, inputEnabled, etc. Created on first |
Web bridge |
| HTTP + WebSocket, scrcpy host (video + audio + control), broadcast |
Frontend |
| Canvas rendering (WebCodecs), input, FPS, audio |
Launcher |
| Runs |
Primary video path — raw scrcpy-server, two instances per session:
a video instance (
control=false,audio=opus) for the stream, and a ctrl instance (video=false) for input/clipboard — withvideo=truethe server maps touch coordinates through the video frame and display pixels never arrive;the bridge pushes
scrcpy-server.jar, connects the sockets viaadb reverse localabstract:scrcpy_<scid>and splits the stream into access units[u64 pts_flags][u32 size](bit 62 = config, bit 61 = keyframe);the browser decodes H.264 through WebCodecs
VideoDecoder; the stream is content-driven (no frames on a static screen) and has no recording time limit.
Fallback: VIDEO_SRC=screenrecord (180 s limit, auto-recycled at 170 s) when raw scrcpy is unavailable.
If the emulator dies, the bridge restores the stream automatically once the device is back.
Security model
TLS. The bridge serves HTTPS with a self-signed cert (generated on first start, cached in
~/.local/state/droidlab/). WebCodecs requires a secure context — plain HTTP hidesVideoDecoder. Accept the cert warning once per browser.Tokens. The bridge requires an access token (
WEB_ACCESS_TOKEN, generated per bridge start): HTTP and WS without?token=get401. Browser input is gated by a separateWEB_CONTROL_TOKEN, so an observer cannot unlock input by itself. A manual start withoutWEB_ACCESS_TOKENruns unauthenticated (trusted LAN only).Privileged actions. APK install / file push is not available to observers: when
WEB_CONTROL_TOKENis set,/pushadditionally requiresctoken=<WEB_CONTROL_TOKEN>— use the MCPinstall_apk/push_filetools instead. WithoutWEB_CONTROL_TOKEN(manual mode) the browser drag&drop works as before.Hardened input path. WS input messages are rate-limited (200 msg/s per connection) and every coordinate/keycode is validated as a finite number before it can reach adb; token comparisons are constant-time (
crypto.timingSafeEqual).Role separation. The agent works through MCP; the developer watches (and taps only after
set_dev_input(true)). While the agent works, browser input is blocked server-side.No shell injection surface. No raw
adb shelltool; the network surface is one port with token auth.
Configuration
Environment variables (all optional):
Variable | Default | Description |
|
| Bridge port |
|
| Bridge interface |
| — | Bridge HTTP/WS token; without it access is open |
| — | Input-control token; browser input disabled until |
|
| Manual start: |
| per-OS SDK path | adb binary |
|
| Server jar for the raw host |
| SDK | Emulator binary |
| first entry of the emulator registry | Default AVD |
| — | Extra emulator arguments |
|
| Boot wait limit |
|
| Bridge port controlled by the MCP |
|
|
|
|
| RTT-probe downgrade threshold |
|
| Congestion-free seconds before an upgrade |
|
| WS backlog safety net for a downgrade |
|
| Minimum interval between switches |
|
| Backlog check period |
End-to-end test: npm run e2e:mcp (boots the stack, exercises the toolset over real MCP stdio). CI runs a lighter smoke test on every push (node scripts/check-tools.mjs — tools/list + annotations, no emulator needed; see .github/workflows/ci.yml).
WebSocket protocol
Client → server, the first message picks the codec:
{"type":"init","codec":"h264"}h264 is selected automatically when window.VideoDecoder exists; force it with ?codec=h264.
Input commands:
{"type":"tap","x":540,"y":1200}
{"type":"swipe","x1":540,"y1":10,"x2":540,"y2":1440,"ms":300}
{"type":"key","code":4}
{"type":"text","text":"hello"}Server → client: binary frames [1 byte flag][payload] (bit 0 = keyframe for H.264 AUs, Annex-B; flag 0x02 = opus audio packet, 0x03 = OpusHead config — both raw from the device, 48 kHz stereo), {"type":"res","name":"486x1080"} on resolution changes, {"type":"bitrate","kbps":N} every second (actual downlink bitrate, max across active h264 clients), and a 1 Hz ping (RTT probe for ABR).
Latency model
Resolution is managed automatically (ABR) from network throughput; the ladder is 324x720 → 486x1080, 1004x2231 is available via API only.
Tier | H.264 encode | Bandwidth |
324x720 | 324x720 @3M | ~0.5 Mbit/s |
486x1080 | 486x1080 @4M | ~2 Mbit/s |
1004x2231 | encoded at 486x1080 | ~2 Mbit/s |
The emulator's software encoder holds real-time only up to ~486×1080 when the emulator runs with software rendering (no hardware GPU passthrough), so the full tier is deliberately downscaled. Measured tap → visible change: ~120–400 ms (H.264). Native 1080×2400 is not real-time with a software encoder (1.2–5 s) and is not used.
A resolution switch does not flicker: the scrcpy video host restarts with the new max_size (the input channel stays up), the client resizes the canvas immediately, filters frames of the old size and covers the canvas until the first frame of the target size arrives.
Troubleshooting
A system image does not boot — some images are finicky about the host virtualization stack; if one fails to reach
sys.boot_completed, try another API level (API 33 is a stable default; the emulator registry carries per-AVD notes).Old API levels (14–19, 27) fail self-bootstrap on x86_64 hosts —
ensureAvdderives the ABI from host arch (x86_64) but those API levels only havegoogle_apis/x86(32-bit) images. Create the AVD manually viaavd_createwith anx86package, or set adeviceprofile that matches.Native H.264 (1080×2400) is not real-time with a software-rendered emulator — by design, see Latency model.
env_stopwaits up to 25 s for a graceful emulator exit before a verified kill; SIGKILL mid-shutdown can wedge qemu in kernel D-state and leave stale AVD locks, whichenv_start/env_stopclean up themselves.Emulator killed externally (e.g. by the OOM killer) — the MCP watchdog auto-restarts it with the original arguments (guarded: max 5 restarts per 5 min, then it gives up and logs to
crash.log), and the browser shows a crash banner until the stream recovers.
License
MIT © Shamil (cirkasssian)
This server cannot be deployed
Maintenance
Related MCP Connectors
Disposable cloud Android emulators for coding agents: run an APK or PR build, tap, type, screenshot.
Control real Android and iOS devices with LLM agents — tap, swipe, type, automate flows.
Live browser debugging for AI assistants — DOM, console, network via MCP.
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.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI clients to control an Android emulator via MCP, allowing tasks like tapping, typing, swiping, taking screenshots, and installing apps through natural language commands.MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to control Android phones via MCP and HTTP. Supports screen capture, taps, swipes, text input, and app management.6AGPL 3.0
- FlicenseNot gradedqualityBmaintenanceEnables streaming and control of Android devices via browser, with UI tree inspection, Logcat, and MCP tool integration for AI agents.3-
- FlicenseAqualityCmaintenanceEnables installing, launching, and inspecting Android APKs on a dedicated emulator, with screen capture, UI tree inspection, and touch/navigation control via MCP tools.10-