polyscreen-mcp
Polyscreen-mcp is an Android-first MCP server for multi-display automation on real devices via ADB. Core capabilities include:
Device & display management: list connected devices with reachability and hardware info, inspect capabilities, list logical displays with physical IDs.
Screen capture & recording: single/paired screenshots, blocking or async recording with timeline marks.
Visual analysis: detect black/dim flashes, brightness bucketing, theme-flash reports.
Focus tracing: monitor focused package/activity per display (blocking or async).
UI automation: snapshot UI hierarchy, find/wait for accessibility nodes.
Input injection: display-targeted tap, swipe, drag, key events, key combos, text input.
App management: inspect, launch, force-stop, relaunch on displays, install, uninstall.
Session & artifact management: manage recording/focus/logcat sessions, list/prune artifacts.
Profile-based extensibility: enable additional profiles for diagnostics, files, performance, device-admin, companion, etc.
Security: validated inputs, argument arrays (no raw shell), restricted file ops, loopback binding with optional token.
Provides tools for automating and interacting with Android devices, including screen capture, input injection, app management, performance monitoring, and multi-display support via ADB.
Click on "Install 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., "@polyscreen-mcplist all connected devices"
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.
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 dumpas 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 |
UI | Snapshot / find / wait without treating |
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 |
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
adbonPATH, orADB_PATH=/absolute/path/to/adbAndroid 11 or newer for the supported multi-display baseline
ffmpegandffprobeonPATHformobile_analyze_recordingJDK 17 and Android SDK only when building the optional companion
Install and build
pnpm install
pnpm check
pnpm buildStdio 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 |
| Server info, devices, displays, screenshots, async record/analyze, async focus traces, artifacts list/prune, UI, input, apps |
| Packages, default-app roles, notifications (list/get/post), and scoped broadcasts |
| Dumpsys slices, logcat snapshot/start/stop, wake, night mode, debuggable shared_prefs read |
| Constrained push/pull under approved roots |
| CPU, power, battery, memory, and frame snapshots |
| Explicit runtime permission grant/revoke |
| All-display accessibility windows and explicit key press/down/up |
| 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.6.1 --profile core diagnosticsdiagnostics 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 |
|
|
Claude Desktop | macOS |
|
Claude Code |
|
|
VS Code / Copilot |
|
|
Windsurf |
|
|
Continue |
|
|
Zed |
|
|
Gemini CLI |
|
|
Cline / Roo | MCP Servers panel → Edit Configuration |
|
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.6.1", "--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.6.1", "--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.6.1
schema: v1
mcpServers:
- name: polyscreen
type: stdio
command: npx
args:
- -y
- polyscreen-mcp@0.6.1
- --profile
- core
- diagnosticsZed
~/.config/zed/settings.json (flat command + args — not a nested command.path object):
{
"context_servers": {
"polyscreen": {
"command": "npx",
"args": ["-y", "polyscreen-mcp@0.6.1", "--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_infomobile_devices_listmobile_device_inspectmobile_displays_listmobile_screen_capturemobile_screen_capture_pairmobile_screen_recordmobile_sessions_statusmobile_record_start/mobile_record_mark/mobile_record_stopmobile_analyze_recording(mode: "flash"for regression hunting)mobile_theme_flash_reportmobile_focus_trace(blocking) /mobile_focus_trace_start/mobile_focus_trace_stopmobile_artifacts_list/mobile_artifacts_prunemobile_ui_snapshot/mobile_ui_find/mobile_ui_waitmobile_input_tap/mobile_input_swipe/mobile_input_dragmobile_input_key/mobile_input_key_combination/mobile_input_textmobile_app_inspect/mobile_app_launch/mobile_app_stop/mobile_app_relaunch_on_displaysmobile_app_install/mobile_app_uninstall
Every display-sensitive tool takes a framework logical displayId. Screenshot and recording implementations resolve that to a SurfaceFlinger physical ID internally.
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— shortget-stateprobe (stale wireless/mDNS entries often fail here);hardwareSerial—ro.serialnowhen the probe succeeds;preferred/preferredSerial— when the same hardware appears under multiple serials (for example TCPIP:portand anadb-tlsmDNS name), the list prefers a reachable TCP serial;aliases— other serials in the same hardware group (only whenhardwareSerialmatches; 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 |
| List active record / focus / logcat sessions (recover IDs after reconnect). |
| Start on-device |
| Label a point with |
| Finalize MP4 + marks sidecar. |
| Quantify black/dim frames from |
Async sessions do not hold the mutation queue.
mobile_analyze_recording options:
Option | Default | Meaning |
|
|
|
|
| Sample rate |
|
| Mean gray below this = black ( |
|
| Mean gray below this = dim |
|
| PNGs at first/last black and each mark |
|
| Include full per-frame |
|
| Step for compact |
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 |
| ~0–15 | Empty / compositor black |
| ~16–29 | Near-black splash/content (not empty) |
| ~30–55 | Mid-gray idle chrome / wallpaper-like band |
| ~56–119 | Dark theme / dark splash |
| ~190–230 | Light app UI |
| 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 |
|
Focus samples |
|
Logcat session | Host |
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 |
| Blocking sample for a fixed |
| Non-blocking; bind with |
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 |
| Metadata stubs only |
|
|
Diagnostics extras (diagnostics profile)
mobile_logcat/mobile_logcat_start/mobile_logcat_stopmobile_diagnostics_collect/mobile_diagnostics_activity_tops/mobile_diagnostics_layer_hintsmobile_power_wake/mobile_uimode_get/mobile_uimode_setmobile_app_prefs_read(debuggablerun-asshared_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 |
| RoleManager holders for common defaults (home, browser, dialer, sms, …) via |
| Holders for one role (short name or |
|
|
|
|
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 |
| Active keys via |
| Details for one key ( |
| Post as |
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.
mobile_devices_list→ pick areachable/preferredserialmobile_displays_list→ note the logicaldisplayId(s) under testmobile_server_info— confirmversionand detective tools after reconnectmobile_record_starton the logical display under testOptional (diagnostics):
mobile_logcat_startwithboundRecordIdmobile_focus_trace_startfor the relevantdisplayIds(optionally sameboundRecordId)mobile_record_mark→ input (tap / key / gamepad / HOME) →mobile_record_markStop focus/logcat →
mobile_record_stopmobile_analyze_recordingwithmode: "flash"and/ormobile_theme_flash_reportAssert: no unexpected
hasBlackFlash/true_blackruns; use focuschangesto see if another package briefly owned the display. Treatsystem_launcher_idleas 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 -dandscreenrecord --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 displaydumpsys SurfaceFlinger --display-iddumpsys inputdumpsys window displaysdumpsys 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.
Optional companion
Build:
gradle -p companion :app:assembleDebug :app:assembleDebugAndroidTest :fixture:assembleDebugOutputs:
companion/app/build/outputs/apk/debug/app-debug.apkcompanion/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
Enable the companion profile, then call:
mobile_companion_installmobile_companion_startmobile_companion_keyormobile_companion_windowsmobile_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
KeyEventhas no portable display setter;a normal APK cannot hold the signature-only
INJECT_EVENTSpermission;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 asyncscreenrecorduses a boundedsh -cwith 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>/cmdlinefor this session'sscreenrecordpath (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.
Testing and development
pnpm format
pnpm lint
pnpm typecheck
pnpm test
pnpm test:coverage
pnpm build
gradle -p companion :app:assembleDebug :app:assembleDebugAndroidTestThe 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:deviceThe 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:destructiveThe 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.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseBqualityDmaintenanceMCP server for Android device automation via ADB, enabling screen control, phone functions, app management, and device control.301
- Flicense-qualityDmaintenanceA 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
- Alicense-qualityAmaintenanceAn MCP server that provides tools for controlling Android devices using uiautomator2, enabling AI to automate tasks like tapping, swiping, and managing apps.34Apache 2.0
- Alicense-qualityBmaintenanceMCP server that enables LLMs to control Android devices via ADB, providing tools for screen interaction and UI inspection.1MIT
Related MCP Connectors
Remote MCP for Android CLI agent build gate, structured receipts, audit logs, and reviewer-ready evi
MCP server for Appcircle mobile CI/CD platform.
The MCP server for Azure DevOps, bringing the power of Azure DevOps directly to your agents.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Zyzto/polyscreen-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server