Skip to main content
Glama
WARNING

Latest MCP required. PolyScreen speaks the current Model Context Protocol revision (2026-07-28). Use a recent Cursor / Claude / VS Code / Codex (or other) MCP host that negotiates that era. Older 2025-only clients may connect on a legacy path but will miss modern features such as cacheable tools/list hints and subscriptions/listen.


Why

Existing mobile MCP servers commonly:

  • confuse Android logical display IDs with SurfaceFlinger physical IDs;

  • capture one display while injecting input into another;

  • expose only a small hard-coded set of physical buttons;

  • treat uiautomator dump as multi-display aware when it is not;

  • return prose that agents must parse;

  • expose unrestricted shell commands as ordinary tools.

PolyScreen keeps those boundaries explicit and returns structured evidence for every operation. The portable core uses the official adb executable and probes each connected device at runtime instead of assuming capabilities from the Android version.

On npm: polyscreen-mcp · Repo: Zyzto/polyscreen-mcp


Related MCP server: android-mcp-server

Features at a glance

Area

What you get

Displays

Logical ↔ physical ID correlation; capture/input stay on the same display

Input

Device-probed input help — keys, gamepad, touch, display targeting

UI

Snapshot / find / wait without treating uiautomator as multi-display

Evidence

Structured JSON tool results (not prose agents must scrape)

Sessions

Async record / focus / logcat with marks and wall-clock join keys

Analysis

Black/dim flash detection via ffmpeg; theme-flash reports

Profiles

Compact core plus opt-in apps, diagnostics, files, performance, companion

Transport

Stdio default; loopback Streamable HTTP with Host/Origin + optional bearer

Platforms: Android 11+ (multi-display baseline). Host: Node.js 22.12+, adb on PATH.


Requirements

  • Node.js 22.12 or newer

  • pnpm 11

  • Android platform tools with adb on PATH, or ADB_PATH=/absolute/path/to/adb

  • Android 11 or newer for the supported multi-display baseline

  • ffmpeg and ffprobe on PATH for mobile_analyze_recording

  • JDK 17 and Android SDK only when building the optional companion

Install and build

pnpm install
pnpm check
pnpm build

Stdio remains the default. The server speaks MCP 2026-07-28 (stateless Streamable HTTP via createMcpHandler) and still serves legacy 2025-era clients from the same factory. Prefer a client on that revision (or newer) for cacheable tools/list hints and subscriptions/listen. Wire-up snippets for Cursor, Claude, VS Code, Windsurf, and others: Client configs.

Tool profiles

The default core profile is deliberately compact. Additional profiles advertise tools only when requested.

Profile

Capabilities

core

Server info, devices, displays, screenshots, async record/analyze, async focus traces, artifacts list/prune, UI, input, apps

apps

Packages, default-app roles, notifications (list/get/post), and scoped broadcasts

diagnostics

Dumpsys slices, activity tops, layer hints, crash list/get, logcat snapshot/start/stop, wake, night mode, debuggable shared_prefs read

files

Constrained push/pull under approved roots

performance

CPU, power, battery, memory, and frame snapshots

device-admin

Explicit runtime permission grant/revoke

companion

All-display accessibility windows and explicit key press/down/up

all

Every implemented profile

unsafe and emulator profiles are reserved but do not expose raw shell in this release.

Client configs

Stdio launch (pin the published version):

npx -y polyscreen-mcp@0.9.0 --profile core diagnostics

diagnostics is required for logcat start/stop, activity tops, wake, and night-mode tools. After editing config or reconnecting, call mobile_server_info once and confirm version, toolCount, and detective tools match a fresh tools/list. Prefer an MCP host that speaks 2026-07-28 so the client negotiates the modern era instead of falling back to 2025 initialize.

Client

Config file

Root key

Cursor

.cursor/mcp.json (project) or ~/.cursor/mcp.json

mcpServers

Claude Desktop

macOS ~/Library/Application Support/Claude/claude_desktop_config.json · Linux ~/.config/Claude/claude_desktop_config.json · Windows %APPDATA%\Claude\claude_desktop_config.json

mcpServers

Claude Code

.mcp.json (project) or ~/.claude/settings.json

mcpServers

VS Code / Copilot

.vscode/mcp.json or MCP: Open User Configuration

servers (+ "type": "stdio")

Windsurf

~/.codeium/windsurf/mcp_config.json

mcpServers

Continue

.continue/mcpServers/*.yaml (preferred) or ~/.continue/config.yaml

mcpServers

Zed

~/.config/zed/settings.json

context_servers

Gemini CLI

~/.gemini/settings.json

mcpServers

Cline / Roo

MCP Servers panel → Edit Configuration

mcpServers

Cursor / Claude Desktop / Windsurf / Claude Code / Gemini CLI / Cline

Same mcpServers shape (merge into the existing object):

{
  "mcpServers": {
    "polyscreen": {
      "command": "npx",
      "args": ["-y", "polyscreen-mcp@0.9.0", "--profile", "core", "diagnostics"]
    }
  }
}

If the tool list looks stale in Cursor, toggle the server off/on so tool-list change notifications (list_changed / subscriptions/listen) are applied.

VS Code (GitHub Copilot)

Workspace .vscode/mcp.json — note the servers root key (not mcpServers):

{
  "servers": {
    "polyscreen": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "polyscreen-mcp@0.9.0", "--profile", "core", "diagnostics"]
    }
  }
}

Continue

Preferred: workspace .continue/mcpServers/polyscreen.yaml (Continue also accepts the same mcpServers list in ~/.continue/config.yaml, and can load Claude/Cursor-style JSON dropped into .continue/mcpServers/):

name: PolyScreen MCP
version: 0.9.0
schema: v1
mcpServers:
  - name: polyscreen
    type: stdio
    command: npx
    args:
      - -y
      - polyscreen-mcp@0.9.0
      - --profile
      - core
      - diagnostics

Zed

~/.config/zed/settings.json (flat command + args — not a nested command.path object):

{
  "context_servers": {
    "polyscreen": {
      "command": "npx",
      "args": ["-y", "polyscreen-mcp@0.9.0", "--profile", "core", "diagnostics"]
    }
  }
}

Local Streamable HTTP (any client with URL transport)

polyscreen-mcp --listen 3300 --token "replace-with-a-secret"

Endpoint: http://127.0.0.1:3300/mcp. HTTP always binds to loopback, validates Host and Origin, and optionally requires the configured bearer token.

Core tools

  • mobile_server_info

  • mobile_devices_list

  • mobile_device_inspect

  • mobile_displays_list

  • mobile_screen_capture

  • mobile_screen_capture_pair

  • mobile_screen_record

  • mobile_sessions_status

  • mobile_record_start / mobile_record_mark / mobile_record_stop

  • mobile_analyze_recording (mode: "flash" for regression hunting)

  • mobile_theme_flash_report

  • mobile_focus_trace (blocking) / mobile_focus_trace_start / mobile_focus_trace_stop

  • mobile_artifacts_list / mobile_artifacts_prune

  • mobile_ui_snapshot / mobile_ui_find / mobile_ui_wait — each node includes ref (@nN)

  • mobile_input_tap / mobile_input_double_tap / mobile_input_long_press / mobile_input_swipe / mobile_input_drag

  • mobile_input_key / mobile_input_key_sequence / mobile_input_key_combination / mobile_input_text. mobile_input_key_sequence sends a timed sequence: digits between actions are millisecond pauses, not a simultaneous chord, and x,y tokens are taps (x,y+x,y is a touch combo with no pause between points). stepped.checks runs that walk one action at a time and polls for a UI node before the next action.

  • mobile_batch — run up to 16 registered tools in one call (screenshots and blocking records stay outside the batch)

  • mobile_app_inspect / mobile_app_launch / mobile_app_stop / mobile_app_relaunch_on_displays / mobile_app_foreground

  • mobile_app_install / mobile_app_uninstall

  • mobile_open_url (http and https only)

  • mobile_orientation / mobile_device_fold / mobile_location / mobile_clipboard

Every display-sensitive tool takes a framework logical displayId. Screenshot and recording implementations resolve that to a SurfaceFlinger physical ID internally. mobile_screen_capture also returns coordinateSpace: multiply a screenshot pixel by scaleX and scaleY to get the logical point mobile_input_tap expects. Returned screenshots are downscaled when the long edge exceeds 1280px, and coordinateSpace describes the bytes that were returned. UI refs (@nN) stay valid across Streamable HTTP requests until the next UI read on that serial and display. mobile_input_swipe, mobile_input_drag, and mobile_input_text accept those refs. A non-default display UI dump uses the companion hierarchy when a companion session is active.

Screenshots can be retained with saveArtifact. Artifact listings are metadata stubs only (uri, name, mimeType, sizeBytes). Binary bytes are read on demand via resources/read — prefer mobile_analyze_recording JSON over many mobile_screen_capture images when hunting flashes.

Server info and tool-list integrity

mobile_server_info returns { version, profiles, toolCount, toolNames, artifactRoot }. On startup the process logs the registered tool count and names to stderr. HTTP publishes tool-list changes via subscriptions/listen (handler.notify.toolsChanged()); stdio clients should refresh from tools/list / mobile_server_info after reconnect.

Devices and serials

mobile_devices_list returns exact ADB serials plus:

  • reachable — short get-state probe (stale wireless/mDNS entries often fail here);

  • hardwareSerial — ro.serialno when the probe succeeds;

  • preferred / preferredSerial — when the same hardware appears under multiple serials (for example TCP IP:port and an adb-tls mDNS name), the list prefers a reachable TCP serial;

  • aliases — other serials in the same hardware group (only when hardwareSerial matches; empty product/model metadata never collapses unrelated devices).

Never invent a serial. Pass the exact preferred value from this list into every other tool.

Async recording and visual analysis

mobile_screen_record blocks for its full durationSeconds. Prefer the async session when input must interleave:

Tool

Purpose

mobile_sessions_status

List active record / focus / logcat sessions (recover IDs after reconnect).

mobile_record_start

Start on-device screenrecord for one logical display. Returns { recordId, pathHint, startedAtIso }.

mobile_record_mark

Label a point with offsetMs + wallClockIso.

mobile_record_stop

Finalize MP4 + marks sidecar.

mobile_analyze_recording

Quantify black/dim frames from path or artifactUri (hasBlackFlash / hasDimFlash).

Async sessions do not hold the mutation queue.

mobile_analyze_recording options:

Option

Default

Meaning

mode

default

flash sets sample export on and denser summary

fps

30

Sample rate

blackThreshold

16

Mean gray below this = black (true_black band)

dimThreshold

80

Mean gray below this = dim

exportSampleFrames

false (true in flash)

PNGs at first/last black and each mark

includeFullTimeline

false

Include full per-frame meanGrayTimeline (large)

timelineDownsampleMs

200 (100 in flash)

Step for compact timelineSummary

Compact fields always returned: counts, blackRuns/dimRuns, marks, timelineSummary, bucketCounts, optional samples.

mobile_theme_flash_report reads night mode, runs mode: "flash" analysis, and lists marks whose offsets fall inside any reported black/dim run (± markWindowMs). Run lists are capped (blackRunsTruncated / dimRunsTruncated).

Buckets (OEM-agnostic heuristics; mean-gray ranges are examples, not universal truth):

Bucket

Example mean gray

Meaning

true_black

~0–15

Empty / compositor black

near_black_content

~16–29

Near-black splash/content (not empty)

system_launcher_idle

~30–55

Mid-gray idle chrome / wallpaper-like band

dark_app_ui

~56–119

Dark theme / dark splash

light_app_ui

~190–230

Light app UI

other

remainder

Unclassified

Calibrate with exportSampleFrames on the device under test — thresholds alone are insufficient for regressions.

Time bases (joining record / focus / logcat)

Record/focus sessions share wall-clock ISO timestamps from host Date.now(). Logcat session bounds use host ISO; individual lines keep Android threadtime:

Source

Fields

Record marks

offsetMs from record start, wallClockIso

Focus samples

tMs from focus session start, wallClockIso, optional recordOffsetMs when boundRecordId is set

Logcat session

Host startedAtIso / stoppedAtIso (+ optional boundRecordStartedAtIso); each line keeps Android threadtime

Join record marks and focus on wallClockIso or recordOffsetMs when boundRecordId is set. Logcat lines are device threadtime — correlate the session window to the bound recording via boundRecordStartedAtIso / host session bounds, not per-line recordOffsetMs.

Focus timeline

Tool

Purpose

mobile_focus_trace

Blocking sample for a fixed durationMs (default 250ms interval, ring buffer)

mobile_focus_trace_start / stop

Non-blocking; bind with boundRecordId for recordOffsetMs

Stop results include changes / changeCount (focus transitions per display) — prefer that over raw samples when hunting launcher/app swaps. Default maxSamples is 800 (covers 180s @ 250ms). If the ring evicts, truncated/droppedSamples report it. Large responses also write a JSON artifact (samplesArtifactUri) and inline only a prefix unless includeAllSamples is true.

Artifacts

Tool

Purpose

mobile_artifacts_list

Metadata stubs only

mobile_artifacts_prune

maxAgeMs / maxCount, with dryRun

Diagnostics extras (diagnostics profile)

  • mobile_logcat / mobile_logcat_start / mobile_logcat_stop

  • mobile_crashes_list / mobile_crash_get (dropbox crash, ANR, and tombstone tags)

  • mobile_diagnostics_collect / mobile_diagnostics_activity_tops / mobile_diagnostics_layer_hints

  • mobile_power_wake / mobile_uimode_get / mobile_uimode_set

  • mobile_app_prefs_read (debuggable run-as shared_prefs)

mobile_app_launch / mobile_app_stop / mobile_app_relaunch_on_displays cover per-package force-stop and display-targeted launch (including stop→launch recipes across multiple logical displays).

Default apps (apps profile)

Tool

Purpose

mobile_default_apps_list

RoleManager holders for common defaults (home, browser, dialer, sms, …) via dumpsys role

mobile_default_app_get

Holders for one role (short name or android.app.role.*)

mobile_default_app_set

cmd role add-role-holder (exclusive clear+add by default); optional homeComponent for HOME

mobile_default_app_clear

clear-role-holders or remove-role-holder when packageName is set

Requires shell access to RoleManager (typical on userdebug/eng or with suitable privileges). bypassQualification: true can help assign roles to test APKs. mobile_broadcast_send covers app debug broadcasts.

Notifications (apps profile)

Tool

Purpose

mobile_notifications_list

Active keys via cmd notification list; optional packageName / includeDetails (title/text)

mobile_notification_get

Details for one key (userId|package|id|tag|uid)

mobile_notification_post

Post as com.android.shell with tag + text (optional title)

Posted notifications are owned by the shell package. Keys contain | and are quoted automatically for the device shell.

Visual regression workflow (device-agnostic)

Always discover IDs at runtime. Never hardcode serials, logical display IDs, launcher packages, or OEM model names.

  1. mobile_devices_list → pick a reachable / preferred serial

  2. mobile_displays_list → note the logical displayId(s) under test

  3. mobile_server_info — confirm version and detective tools after reconnect

  4. mobile_record_start on the logical display under test

  5. Optional (diagnostics): mobile_logcat_start with boundRecordId

  6. mobile_focus_trace_start for the relevant displayIds (optionally same boundRecordId)

  7. mobile_record_mark → input (tap / key / gamepad / HOME) → mobile_record_mark

  8. Stop focus/logcat → mobile_record_stop

  9. mobile_analyze_recording with mode: "flash" and/or mobile_theme_flash_report

  10. Assert: no unexpected hasBlackFlash / true_black runs; use focus changes to see if another package briefly owned the display. Treat system_launcher_idle as mid-gray idle chrome — not a black flash.

Optional cookbook: dual-display handhelds

Some handhelds expose two logical displays (for example internal + presentation). After mobile_displays_list, pass both IDs to mobile_focus_trace_start / mobile_screen_capture_pair / mobile_app_relaunch_on_displays. Use mobile_sessions_status if a reconnect loses session IDs. These devices are stress cases for the same APIs — not a separate tool surface.

Multi-display model

Android has multiple identifier spaces:

  • logical display IDs are small framework integers used by WindowManager, ActivityManager, input -d, and accessibility;

  • physical display IDs are unsigned 64-bit SurfaceFlinger identifiers used by screencap -d and screenrecord --display-id;

  • virtual displays may have a logical ID but no capturable physical ID.

Physical IDs are represented as decimal strings so JavaScript never loses precision. Correlation uses DisplayInfo.uniqueId, display addresses, and bounded evidence from:

  • dumpsys display

  • dumpsys SurfaceFlinger --display-id

  • dumpsys input

  • dumpsys window displays

  • dumpsys activity activities

Requested activity placement is always treated as a request. Callers should inspect tasks and window focus to verify the observed result.

Input behavior

The ADB backend probes the device's own input help and exposes only supported options:

  • keyboard, dpad, gamepad, and touchscreen sources;

  • display targeting with -d;

  • key press, long press, double tap, and explicit duration where supported;

  • any symbolic or numeric Android keycode, including face buttons, shoulders, triggers, thumb buttons, START, SELECT, and MODE;

  • taps, swipes, text, and modern command capabilities reported by inspection.

ADB shell gamepad events still use a synthetic virtual device identity. They are not equivalent to a physical controller descriptor.

Points outside the target display are rejected. input tap exits 0 for off-screen coordinates and drops the event, which is indistinguishable from a tap that did nothing.

Where to tap

Android delivers a touch to the last-painted clickable view under the point, so the geometric centre of a label is frequently the wrong coordinate: the label itself is usually not clickable, and floating search bars, install buttons, and bottom sheets are painted over the rows behind them.

Every node from mobile_ui_snapshot, mobile_ui_find, and mobile_ui_wait therefore carries a resolved tap plan alongside its raw geometry:

Field

Meaning

center

Raw geometric centre of bounds. Reported as-is; it may belong to another view.

ref

@n plus nodeIndex. Pass it to mobile_input_tap, mobile_input_double_tap, or mobile_input_long_press. Valid until the next UI read on that serial and display.

tap

{ x, y, nodeIndex, via, adjusted } — the point that reaches this node. Pass this, or ref, to mobile_input_tap.

tappable

false when the node is off-screen or completely covered.

occludedBy

The clickable node that would swallow a tap on center.

warnings

Why the plan differs from center, or why there is no plan.

Inside tap, nodeIndex and via name the view that receives the click — self, an ancestor container, or unhandled when nothing in the branch is exposed as clickable and the tap depends on an undeclared touch handler. adjusted is true when the point had to move off that view's own centre to dodge something painted over it. When a node is not tappable, scroll it clear or dismiss the overlay named in occludedBy instead of tapping blind.

A disabled control counts as an occluder. View.onTouchEvent returns clickable before it checks whether the view is enabled, so a greyed-out button still swallows every touch that lands on it.

Boolean node attributes are reported as a flags array listing only what is true, for example "flags": ["clickable", "enabled", "focusable"]. Empty strings and false flags are omitted rather than serialised, which keeps a 500-node snapshot readable.

Optional companion

Build:

gradle -p companion :app:assembleDebug :app:assembleDebugAndroidTest :fixture:assembleDebug

Outputs:

  • companion/app/build/outputs/apk/debug/app-debug.apk

  • companion/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk

Enable the companion profile, then call:

  1. mobile_companion_install

  2. mobile_companion_start

  3. mobile_companion_key or mobile_companion_windows

  4. mobile_companion_stop

The host creates a random per-session token and an owned adb forward to a local-abstract socket. Frames are length-prefixed JSON with a 1 MiB maximum. The instrumentation process preserves key downTime, supports explicit press/down/up and repeats, and releases all held keys when a client disconnects.

Platform limits are reported rather than hidden:

  • keys follow Android's focused-display policy because public KeyEvent has no portable display setter;

  • a normal APK cannot hold the signature-only INJECT_EVENTS permission;

  • synthetic gamepad events do not have physical controller identity;

  • accessibility exposes interactive windows and nodes, not secure or inaccessible rendering.

Security

  • Host-side commands are argv arrays (no host /bin/sh -c). On-device async screenrecord uses a bounded sh -c with a validated physical display ID and quoted remote path under /data/local/tmp.

  • Device serials, packages, components, keycodes, tags, permissions, paths, and display IDs are validated.

  • Input/UI mutations are serialized per device. Async record/logcat/focus sessions intentionally bypass that queue so agents can interleave input.

  • Subprocesses have deadlines, cancellation, and output caps.

  • Push paths must remain under the server host root.

  • Pull/push device paths are restricted to shared storage and /data/local/tmp.

  • Raw shell, root, remount, verity, SELinux, partition, credential, and system-process operations are not exposed.

  • Recording stop probes /proc/<pid>/cmdline for this session's screenrecord path (token match). Transient probe failures while the PID is still alive are treated as uncertain and still signaled so a live recording is not pulled mid-write.

See SECURITY.md for reporting and deployment guidance.

Agent interaction benchmark

This benchmark compares PolyScreen's structured Android automation surface with direct adb commands on an Android emulator. It uses Janan's Add medication button and name field, plus the Camera shutter, so the actions exercise navigation, semantic button discovery, text input, and photo capture.

The run targeted emulator-5554, logical display 0 (1280×2856), and typed PolyScreenTest. All four tasks succeeded through both paths; both camera trials produced a JPEG in the emulator's /sdcard/Pictures/ directory.

Token counts use the o200k_base tokenizer and include the agent-visible request/command text plus textual tool results. MCP initialization bytes, model reasoning, and binary image bytes are excluded. The adb path used uiautomator XML for target discovery and verification; hard-coded coordinates would use fewer tokens but would be more brittle.

Task

PolyScreen MCP

Direct adb

Difference

Ease of use

Navigate to Janan

1 call / 230 tokens

2 calls / 169 tokens

adb used 61 fewer tokens

PolyScreen resolves the activity automatically

Click Add medication

3 / 457

3 / 4,241

PolyScreen used 89.2% fewer

PolyScreen returns a semantic match and tap plan

Input PolyScreenTest

2 / 324

2 / 2,710

PolyScreen used 88.0% fewer

PolyScreen provides a dedicated text operation and structured verification

Take a picture

4 / 640

5 / 4,231

PolyScreen used 84.9% fewer

PolyScreen finds the shutter semantically and targets the logical display

Task total

10 / 1,651

12 / 11,351

PolyScreen used 85.5% fewer (9,700 tokens)

PolyScreen was easier overall

The main difference is response shape: mobile_ui_find returns only the matching node and its resolved tap point, while the comparable adb workflow returns the full UI hierarchy for the agent to parse. In this single sequential run, measured call-time totals were about 15.0 seconds for PolyScreen and 14.0 seconds for adb; latency is environment-dependent and was not the primary measure.

PolyScreen has a one-time session cost: the core profile's 33-tool catalog and discovery calls consumed 12,503 text tokens in this run, compared with 113 tokens for the two basic adb setup checks. This catalog cost is separate from the task table and is normally amortized across a session.

Testing and development

pnpm format
pnpm lint
pnpm typecheck
pnpm test
pnpm test:coverage
pnpm build
gradle -p companion :app:assembleDebug :app:assembleDebugAndroidTest

The local suite covers:

  • ADB subprocess timeouts, cancellation, output limits, and argv safety;

  • logical/physical display parsing, virtual displays, capture routing, and launch verification;

  • UI hierarchy parsing and matching;

  • per-device mutation serialization;

  • host/device path confinement and typed command validation;

  • MCP tool-profile registration and Streamable HTTP security;

  • artifact storage and traversal protection;

  • companion framing and request correlation.

Device integration checks are intentionally separate from deterministic unit tests. Multi-display acceptance additionally verifies physical-ID capture and recording, all-display accessibility windows, authenticated companion forwarding, and independent key down/up injection.

Never silently select the first connected device: pass the exact serial returned by mobile_devices_list.

Occasional Logical display N is not available flakes on multi-display handhelds usually clear after re-listing displays, tapping the target display (or waking it), and retrying. The device acceptance scripts recover via runWithDisplay.

Repeatable device acceptance

The generic suite is read-mostly. It captures requested displays and can optionally record, inspect one package, and exercise an already-installed companion:

POLYSCREEN_DEVICE=<serial-from-devices_list> \
POLYSCREEN_DISPLAY_IDS=<ids-from-displays_list> \
POLYSCREEN_RECORD=1 \
POLYSCREEN_TEST_PACKAGE=com.example.app \
POLYSCREEN_COMPANION=1 \
pnpm test:device

The destructive suite uses a dedicated integration-fixture APK, separate from the production companion. It installs and launches the fixture, verifies tap/swipe/drag/text input, grants and revokes CAMERA, force-stops the fixture, and uninstalls it during cleanup:

POLYSCREEN_DEVICE=<serial-from-devices_list> \
POLYSCREEN_DISPLAY_ID=<id-from-displays_list> \
POLYSCREEN_ALLOW_DESTRUCTIVE=1 \
pnpm test:device:destructive

The destructive suite refuses to run without the acknowledgement variable and never chooses a device serial implicitly. Override POLYSCREEN_FIXTURE_APK when testing an externally built fixture.


Branding

The logo wordmark uses Baz (Baz Light) by fakharia (SIL OFL) — the same Arabic typeface as Siglat and Edadat. The face is vendored at assets/fonts/baz-Light.otf; the SVG outlines HarfBuzz-shaped شــاشات (tatweel after ش, not after ا) so GitHub/npm render without loading the font.


License

MPL-2.0 — weak copyleft, commercial use allowed. Modified package files stay under MPL; your app can remain closed-source.

Available Tools

43 tools
mobile_analyze_recordingAnalyze recording for black/dim framesA

Sample mean grayscale over an MP4 (ffmpeg), detect black/dim runs, and classify OEM-agnostic brightness buckets (true_black, near_black_content, system_launcher_idle, dark_app_ui, light_app_ui). Bucket ranges are heuristics—calibrate with exportSampleFrames on the device under test. Use mode='flash' for one-call regressions. Full per-frame timeline is omitted unless includeFullTimeline=true.

Inputs: fps, mode, path, marks, +7 more. Returns: path, marks, width, height, +23 more

mobile_app_foregroundRead the foreground Android appA
Read-onlyIdempotent

Return the focused package and activity for one logical display. Window focus is preferred; activity tops are the fallback.

Inputs: serial (required), displayId. Returns: serial, source, taskId, activity, +3 more

mobile_app_inspectInspect Android appB
Read-onlyIdempotent

Read package state, permissions, components, users, and installation metadata.

Inputs: serial (required), packageName (required). Returns: dump, packageName

mobile_app_installInstall Android APKB

Install one local APK with a bounded timeout and structured ADB result.

Inputs: path (required), serial (required), replace. Returns: data, device, backend, display, +4 more

mobile_app_launchLaunch Android appB

Resolve and launch an activity on a logical display with ActivityManager wait diagnostics.

Inputs: serial (required), displayId (required), packageName (required), userId, +1 more. Returns: data, device, backend, display, +4 more

mobile_app_relaunch_on_displaysStop then launch on displaysA

Force-stop a package, then launch its main (or given) activity on each logical display in order. Use for dual-display cold-start / flash reproduction.

Inputs: serial (required), displayIds (required), packageName (required), userId, +2 more. Returns: stopped, launches, packageName

mobile_app_stopForce-stop Android appB
Idempotent

Force-stop a package, including its processes and pending implicit launches.

Inputs: serial (required), packageName (required), userId. Returns: data, device, backend, display, +4 more

mobile_app_uninstallUninstall Android appB
Destructive

Uninstall a package, optionally retaining its data.

Inputs: serial (required), packageName (required), keepData. Returns: data, device, backend, display, +4 more

mobile_artifacts_listList local artifactsA
Read-onlyIdempotent

List metadata stubs (uri, name, mime, size) for files under the artifact root. Does not embed binary contents.

Inputs: limit. Returns: count, artifacts, artifactRoot

mobile_artifacts_prunePrune local artifactsA
Destructive

Delete old or excess artifact files by maxAgeMs and/or maxCount. Use dryRun to preview.

Inputs: dryRun, maxAgeMs, maxCount. Returns: dryRun, deleted, retained

mobile_batchRun several Android tools in orderA

Call other registered tools in one request. Each step is validated with that tool's schema. Nested mobile_batch, screenshots, and blocking screen records are rejected. stopOnError ends the sequence at the first failure.

Inputs: steps (required), stopOnError. Returns: results

mobile_clipboardRead or write the Android clipboardA

Read dumpsys clipboard, then cmd clipboard get when the dump is redacted. set uses cmd clipboard set. When the shell cannot read or write, an active companion session uses ClipboardManager. Without a companion, the result says the read was denied and names the companion. An empty string is returned only when a read can see that the clipboard is empty. A clip the companion cannot see stays readable false.

Inputs: serial (required), text, action. Returns: set, text, reason, backend, +2 more

mobile_device_foldRead or set fold stateB

List cmd device_state postures and, when state is set, switch to a name such as OPEN or CLOSED or a numeric identifier. Non-foldables return foldable false.

Inputs: serial (required), state. Returns: name, reason, states, foldable, +3 more

mobile_device_inspectInspect Android capabilitiesB
Read-onlyIdempotent

Probe the selected device's actual ADB, input, command, build, and backend capabilities.

Inputs: serial (required), refresh. Returns: capabilities

mobile_devices_listList Android devicesA
Read-onlyIdempotent

List ADB devices with reachability, hardware serial grouping, preferred TCP serial, and aliases when the same device appears under multiple serials.

Inputs: no named parameters. Returns: devices

mobile_displays_listList Android displaysB
Read-onlyIdempotent

List logical displays and correlated physical capture IDs with evidence and focus state.

Inputs: serial (required). Returns: displays

mobile_focus_traceTrace focused package/activity per display (blocking)A

Blocking sample of focused package/activity/taskId for a fixed duration. Prefer mobile_focus_trace_start/stop when interleaving input or recording. Large sample sets are written to an artifact; only a compact prefix is inlined.

Inputs: serial (required), displayIds (required), durationMs (required), maxSamples, +2 more. Returns: serial, changes, samples, truncated, +12 more

mobile_focus_trace_startStart async focus traceA

Start non-blocking focus sampling for logical displays. Optionally bind to a recordId so samples include recordOffsetMs aligned with record marks. Default interval 250ms; samples are ring-buffered (maxSamples).

Inputs: serial (required), displayIds (required), maxSamples, boundRecordId, +1 more. Returns: startedAtIso, focusSessionId

mobile_focus_trace_stopStop async focus traceA

Stop an async focus session and return focus change events plus samples (wallClockIso, tMs, optional recordOffsetMs). Prefer changes for flash/launcher hunting. Large traces write samples JSON to an artifact.

Inputs: serial (required), focusSessionId (required), includeAllSamples. Returns: serial, changes, samples, truncated, +12 more

mobile_input_double_tapDouble-tap Android displayB

Inject two display-targeted taps at the same point in one on-device script, so gapMs is the pause between the taps. Pass ref or x and y.

Inputs: serial (required), displayId (required), x, y, +2 more. Returns: data, device, backend, display, +4 more

mobile_input_dragDrag on Android displayB

Inject a capability-gated long-press drag-and-drop on one logical display. startRef and endRef come from the latest UI read and win over start and end.

Inputs: serial (required), displayId (required), end, start, +3 more. Returns: data, device, backend, display, +4 more

mobile_input_keySend Android keyC

Send symbolic or numeric Android keys with gamepad source and capability-gated press options.

Inputs: key (required), serial (required), displayId (required), action, +2 more. Returns: data, device, backend, display, +4 more

mobile_input_key_combinationSend simultaneous Android keysB

Inject a device-supported key combination with explicit ordering and chord duration.

Inputs: keys (required), serial (required), displayId (required), source, +1 more. Returns: data, device, backend, display, +4 more

mobile_input_key_sequenceSend a timed Android key sequenceA

Send a timed sequence of Android keys and touchscreen taps in one call. Digits between actions are millisecond pauses, not a simultaneous chord. An x,y token is a tap; join points with + for a touch combo with no pause between them. Pass stepped.checks, one entry per action, to run each action on its own and poll for a UI node before continuing.

Inputs: serial (required), sequence (required), displayId (required), gapMs, +2 more. Returns: data, device, backend, display, +4 more

mobile_input_long_pressLong-press Android displayB

Hold a display-targeted touch at one point by swiping from that point to itself. Pass ref or x and y.

Inputs: serial (required), displayId (required), x, y, +2 more. Returns: data, device, backend, display, +4 more

mobile_input_swipeSwipe Android displayB

Inject a display-targeted touchscreen swipe with explicit duration. startRef and endRef come from the latest UI read and win over start and end.

Inputs: serial (required), displayId (required), end, start, +3 more. Returns: data, device, backend, display, +4 more

mobile_input_tapTap Android displayA

Inject a display-targeted touchscreen tap in logical display coordinates. Pass ref from the latest UI read, or x and y. ref wins when both are set. Prefer ref over a node's raw center.

Inputs: serial (required), displayId (required), x, y, +1 more. Returns: data, device, backend, display, +4 more

mobile_input_textType Android textA

Type text through ADB's virtual keyboard. Pass ref to tap that node first. Reports the Unicode limitation instead of hiding it.

Inputs: text (required), serial (required), displayId (required), ref. Returns: data, device, backend, display, +4 more

mobile_locationSet or clear a mock locationA

On an emulator, set uses adb emu geo fix (longitude then latitude). Clearing an emulator fix fails because the console has no clear. On a device, set requires Android 12 (API 31) and injects a gps test provider via cmd location. Fused location clients can ignore that provider. clear removes it.

Inputs: serial (required), action, latitude, longitude, +1 more. Returns: backend, cleared, warning, latitude, +1 more

mobile_open_urlOpen an http(s) URLA

Start ACTION_VIEW for an http or https URL on one logical display. Other schemes are rejected.

Inputs: url (required), serial (required), displayId. Returns: url, output

mobile_orientationGet or lock display rotationA

Read or set user rotation for one logical display. action lock uses rotation 0, 1, 2, or 3. action free lets the accelerometer decide. action get returns free or lock N, plus observedRotation from the live display when it is known.

Inputs: serial (required), action, rotation, displayId. Returns: raw, mode, rotation, displayId, +1 more

mobile_record_markMark async recording timelineB

Attach a labeled timestamp (offsetMs from record start) for later correlation with analyze/focus/logcat.

Inputs: label (required), serial (required), recordId (required). Returns: label, offsetMs, recordId, wallClockIso

mobile_record_startStart async display recordingA

Start a non-blocking screenrecord on one logical display so input tools can run during capture. One active session per (serial, displayId).

Inputs: serial (required), displayId (required). Returns: pathHint, recordId, displayId, startedAtIso, +1 more

mobile_record_stopStop async display recordingB

Stop screenrecord with SIGINT, pull the MP4 into artifacts, and return marks with offsets.

Inputs: serial (required), recordId (required). Returns: path, marks, serial, recordId, +7 more

mobile_screen_captureCapture an Android displayA

Capture one logical display after resolving its SurfaceFlinger physical display ID. Images whose long edge exceeds 1280px are downscaled before they are returned. coordinateSpace.scaleX and scaleY convert those screenshot pixels into the logical coordinates used by mobile_input_tap.

Inputs: serial (required), displayId (required), saveArtifact. Returns: serial, display, mimeType, sizeBytes, +4 more

mobile_screen_capture_pairCapture paired Android displaysB

Capture multiple logical displays as tightly paired same-moment screenshots (parallel screencap after one display resolve).

Inputs: serial (required), displayIds (required), saveArtifact. Returns: serial, skewMs, captures

mobile_screen_recordRecord an Android displayA

Blocking record of one physical-backed display for a bounded duration. Prefer mobile_record_start/mark/stop when input must be interleaved.

Inputs: serial (required), displayId (required), durationSeconds. Returns: path, serial, displayId, sizeBytes, +2 more

mobile_server_infoPolyScreen server infoA
Read-onlyIdempotent

Return package version, active profiles, and the exact registered tool list. Call after reconnect to verify Cursor sees the full core surface.

Inputs: no named parameters. Returns: name, version, profiles, toolCount, +2 more

mobile_sessions_statusList active detective sessionsA
Read-onlyIdempotent

Return active async recording, focus, and logcat sessions (optional serial filter). Use before stop/mark to recover IDs after a reconnect.

Inputs: serial. Returns: logcats, recordings, focusTraces

mobile_theme_flash_reportTheme / night-mode flash reportC

Correlate system night mode with a recording analysis (mode=flash) and marks whose offsets fall inside any reported black/dim run (± markWindowMs).

Inputs: serial (required), path, artifactUri, markWindowMs. Returns: analysis, nightMode, marksNearDim, nightModeRaw, +1 more

mobile_ui_findFind Android UI nodesA
Read-onlyIdempotent

Find accessibility nodes by text, content description, or resource ID. Each match has a ref (@nN) and a tap point that accounts for clickable containers and overlays. Pass ref to mobile_input_tap. This replaces refs from the previous UI read.

Inputs: serial (required), displayId (required), text, exact, +2 more. Returns: count, matches

mobile_ui_snapshotSnapshot Android UIA
Read-onlyIdempotent

Return parsed accessibility nodes, each with a ref (@nN) and the point that actually routes a tap to it. Pass ref to tap, double-tap, long-press, swipe, drag, or text. Refs are shared across HTTP requests until the next UI read on this serial and display. Portable ADB supports only display 0; other displays use the companion hierarchy when a companion session is active.

Inputs: serial (required), displayId (required). Returns: nodes, serial, backend, displayId, +1 more

mobile_ui_waitWait for Android UIC
Read-onlyIdempotent

Poll until a matching accessibility node appears or the bounded timeout expires.

Inputs: serial (required), displayId (required), text, exact, +4 more. Returns: found, matches, attempts, elapsedMs

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.4/5.0

    Scored across 43 tools

    Disambiguation4/5

    Most tools have distinct resource/action targets, and descriptions explicitly distinguish blocking vs async recording/focus and input variants. Some overlap remains among UI node reads (snapshot/find/wait) and capture/record families, but guidance usually clarifies selection.

    Naming Consistency4/5

    All tools use a mobile_ prefix and snake_case, so the convention is broadly readable. However the ordering is mixed: many use noun_verb (mobile_screen_capture, mobile_app_launch) while others use verb_noun (mobile_open_url, mobile_analyze_recording).

    Tool Count2/5

    43 tools is heavy for one device-automation server; many variants (blocking vs async capture/record/focus, several input primitives) could be consolidated or grouped. The broad Android testing scope justifies some breadth, but the surface exceeds the 25-tool threshold in the rubric.

    Completeness4/5

    Coverage is broad across device/display discovery, capture, recording, UI inspection, input, app lifecycle, location, clipboard, and batching. Minor gaps include no direct logcat tool despite session status mentioning logcat, and no generic file/shell operations, but core device-control workflows are well supported.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      D
      maintenance
      A powerful MCP server that provides comprehensive Android device automation capabilities through ADB, enabling AI agents to interact with Android devices for testing, automation, and device control tasks.
      1
      -
    • A
      license
      Not graded
      quality
      B
      maintenance
      An MCP server that provides tools for controlling Android devices using uiautomator2, enabling AI to automate tasks like tapping, swiping, and managing apps.
      186 PyPI
      55
      Apache 2.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      MCP server for controlling Android devices over ADB, using direct commands and semantic accessibility selectors with a Kotlin helper APK.
      444 npm
      MIT