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: mobila
Requirements
Dependency | Notes |
Node.js ≥ 18 | bridge + MCP server |
Android SDK |
|
ffmpeg | WebP fallback path only |
Xvfb + xdotool | WebP fallback path only (Linux) |
scrcpy 4.x | H.264 stream + control channel; |
Linux, macOS and Windows are supported. The primary H.264 path works everywhere; the WebP fallback (browsers without WebCodecs) is Linux-only.
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 Xvfb (Linux), 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).
There is no raw
adb shelltool by design: an arbitrary command is an injection vector and a bypass of tool annotations. The 33 tools below cover the needed surface.
Tools
33 tools. Status tools (env_status, device_state, env_list, system_images_list) also return structuredContent (MCP 2025-06-18). All tools declare MCP annotations (readOnlyHint / destructiveHint / idempotentHint).
Lifecycle
Tool | Description |
| Xvfb + 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/xvfb), boot state, device info, input mode. |
| Entries of |
|
|
| 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. |
|
|
|
|
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; |
| 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. |
| 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. |
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. Xvfb (Linux, WebP fallback only)
Xvfb :99 -screen 0 1080x2400x24 &
# 2. Emulator
~/Android/Sdk/emulator/emulator -avd API33 -no-window -no-audio -no-snapshot &
# 3. Bridge (starts scrcpy/ffmpeg on its own)
node web/server.jsOpen http://<host>:8090 in a browser. 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: http://localhost:8090/?token=<accessToken>Browser controls: click = tap, drag = swipe, wheel = scroll, keyboard = device input (printable text, Backspace, Enter, arrows, Esc), plus Back / Home / Recents / fullscreen buttons. 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.
Architecture
Component | Path | Purpose |
MCP server |
| stdio server: emulator lifecycle, input, screenshots, access control |
Emulator config |
| Human-readable AVD names ( |
Web bridge |
| HTTP + WebSocket, scrcpy host (video + control), fallbacks, broadcast |
Frontend |
| Canvas rendering (WebCodecs / createImageBitmap), input, FPS |
Launcher |
| Runs |
Primary video path — raw scrcpy-server, two instances per session:
a video instance (
control=false) 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.
Fallbacks: VIDEO_SRC=screenrecord (180 s limit, auto-recycled at 170 s) and a WebP path (scrcpy → Xvfb → ffmpeg x11grab) for browsers without WebCodecs (?codec=webp).
If the emulator dies, the bridge restores the stream automatically once the device is back.
Security model
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).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 |
|
| ffmpeg binary |
|
| scrcpy binary (WebP fallback) |
|
| Server jar for the raw host |
| SDK | Emulator binary |
| first entry of | 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 |
|
| X display for x11grab / scrcpy (WebP fallback) |
End-to-end test: npm run e2e:mcp (boots the stack, exercises the toolset over real MCP stdio).
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 or ?codec=webp.
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; full WebP frames in fallback mode), {"type":"res","name":"486x1080"} on resolution changes, 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 on both paths. Measured tap → visible change: ~120–400 ms (H.264), ~200 ms (WebP). 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;emulators.jsoncarries per-AVD notes).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 bridge restores the stream after the emulator is back; restart it manually if needed.
License
MIT © Shamil (cirkasssian)
This server cannot be deployed
Maintenance
Related MCP Connectors
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.
Drive real devices from your AI Coding tool. Embed a client SDK (Unity, Godot, Flutter, iOS/macOS, Android, React Native, Web) in your app, then capture screenshots, traverse the UI tree, inject taps and key events, and run automated test tasks on the physical device over a secure relay.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables MCP-compatible agents to control an Android device over the network via ADB, providing tools for shell commands, screen capture, UI inspection, file operations, and input simulation.7 npmMIT
- 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 gradedqualityBmaintenanceEnables AI agents to control Android phones via MCP and HTTP. Supports screen capture, taps, swipes, text input, and app management.AGPL 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-