MCP Server
Purpose
Describe the first-party stdio MCP server exposed by androperator mcp serve: how to launch it, how to configure long-running MCP clients, which tools ship today, and what behavior to expect when device state changes under a running client.
For the post-install decision of whether you should start with MCP or with
androperator skills, read Host Agent Orientation first.
This page assumes you have already decided that MCP is the correct front door.
Sources
- CLI command registration:
apps/node/src/cli/registry.ts - MCP bootstrap:
apps/node/src/cli/commands/mcp.ts,apps/node/src/mcp/server.ts - Shared MCP tool helpers:
apps/node/src/mcp/tools/common.ts,apps/node/src/mcp/selectors.ts - Core tools:
apps/node/src/mcp/tools/core.ts - Named tools:
apps/node/src/mcp/tools/named.ts - MCP session defaults:
apps/node/src/mcp/session.ts - Installer-generated MCP snippet:
install.sh - Execution contract:
apps/node/src/contracts/execution.ts - Error codes:
apps/node/src/contracts/errors.ts - Selector contract:
apps/node/src/contracts/selectors.ts - Result envelope:
apps/node/src/contracts/result.ts - Package Node requirement:
apps/node/package.json
What It Is
androperator mcp serve starts a local stdio MCP server for MCP clients such as Claude Desktop. Execution-backed tools may return the shared result envelope in tool output. The server is transport-only:
- it speaks MCP over stdin/stdout
- it does not expose HTTP or SSE
- it uses the same canonical execution engine as the CLI and
serve - it starts even when no Android device is connected
If no device is connected at startup, the process still boots normally. Tool calls that need a device then return structured Androperator errors such as NO_DEVICES or ADB_NOT_FOUND.
Start The Server
Installed package command:
androperator mcp serve
Development validation command:
npm --prefix apps/node run build
androperator mcp serve
Notes:
mcp serveis a long-running stdio transport. Do not wrap it in another CLI command that also writes to stdout.- The MCP path is detected before the normal CLI formatter runs, so stdout is reserved for MCP protocol messages only.
- Global CLI flags such as
--device,--device-id, and--operator-packageare not accepted on themcp serveargv line. PassdeviceIdandoperatorPackageper MCP tool call instead, or store them for the current MCP session withconfigure. - Node.js
24+is required.
Installer-Generated Snippet
install.sh writes a host-specific MCP snippet to:
~/.androperator/mcp-config-snippet.json
That file is generated from the installer's current binary path, detected adb
path, DEFAULT_OPERATOR_PACKAGE (com.androperator.operator), and
~/.androperator/logs. It includes:
claudeDesktop.entry.androperatorcodex.entryTomlgenericStdioConsumer.server
When the installer can resolve the packaged CLI JS entrypoint, the generated
snippet uses the installer's current absolute Node executable path instead of a
bare node command so GUI MCP clients do not depend on shell PATH.
The installer does not auto-register the MCP server with any client. The file is a paste-ready bridge, not an automatic configuration mutation.
Claude Desktop Example
Example mcpServers entry:
{
"mcpServers": {
"androperator": {
"command": "node",
"args": [
"<installed_androperator_path>/dist/cli/index.js",
"mcp",
"serve"
],
"env": {
"ADB_PATH": "<adb_path>",
"ANDROPERATOR_OPERATOR_PACKAGE": "com.androperator.operator.dev",
"ANDROPERATOR_LOG_DIR": "<log_dir>",
"ANDROPERATOR_LOG_LEVEL": "info"
}
}
}
}
Why node is the command:
- the npm package ships
dist/cli/index.js - MCP desktop clients usually want an explicit executable plus argument list
- using
nodeplus the installed CLI entrypoint avoids relying on shell wrappers
When To Use MCP Versus androperator skills
Use androperator skills when:
- your host can shell out to the CLI directly
- you want to discover installed runtime skills by app, keyword, or id
- you want the runtime-skill wrapper semantics from
skills getandskills run
Use MCP when:
- your host already supports stdio MCP
- you want a long-running registered tool surface instead of repeated CLI process launches
- you need general device tools such as
devices,snapshot,execute, andconfigure, not only runtime-skill discovery
These surfaces are complementary:
androperator skillsis the primary runtime-skill discovery and wrapper surfaceandroperator mcp serveis the primary tool-registration surface for MCP-capable hosts- Host Agent Orientation is the canonical post-install route for choosing between them
Environment For Long-Running MCP Clients
These environment variables matter most for MCP use:
| Variable | Default | Why MCP users care |
|---|---|---|
ADB_PATH |
adb from PATH |
MCP clients like Claude Desktop usually do not inherit your interactive shell PATH. Set this explicitly in the MCP client config. |
ANDROPERATOR_OPERATOR_PACKAGE |
com.androperator.operator |
Use com.androperator.operator.dev for local branch testing against the debug APK. |
ANDROPERATOR_LOG_DIR |
~/.androperator/logs |
Primary diagnostics path for MCP users because Claude Desktop does not surface stderr. |
ANDROPERATOR_LOG_LEVEL |
info |
Raise to debug when diagnosing tool failures. |
Important diagnostics rule:
- stderr is not visible in Claude Desktop
- when an MCP session fails, check the log file under
ANDROPERATOR_LOG_DIR ANDROPERATOR_LOG_LEVEL=debugis the main way to get more runtime detail from a GUI MCP client
For the full environment-variable contract, see Environment Variables.
Selector Shape
Selector-taking MCP tools accept this object shape:
| Field | Maps to NodeMatcher |
Meaning |
|---|---|---|
id |
resourceId |
Exact Android resource id |
role |
role |
Exact node role |
text |
textEquals |
Exact visible text |
textContains |
textContains |
Substring match on visible text |
desc |
contentDescEquals |
Exact content description |
descContains |
contentDescContains |
Substring match on content description |
ancestor |
ancestor |
Non-empty canonical NodePredicate for a strict ancestor |
descendant |
descendant |
Non-empty canonical NodePredicate for an eligible strict descendant |
Rules:
- at least one selector field must be present and non-empty
- an all-empty selector is rejected at the MCP boundary
- selector objects are used by
click,type,read,wait, andscroll_until
For the underlying selector contract, see Selectors.
Tool Summary
| Tool | Purpose |
|---|---|
devices |
List connected Android devices visible to adb. |
snapshot |
Capture the current Android UI hierarchy as XML, or bounded JSON with compact: true. maxNodes and maxTextChars require compact: true. Example: {"compact":true,"maxNodes":100}. Limits affect returned output, not capture or transfer cost. |
execute |
Run a validated Androperator execution payload over the canonical execution engine. |
configure |
Store per-session defaults for deviceId, operatorPackage, and timeoutMs. |
query_ui |
Inspect fresh UI nodes, including blank labels and state. Omit matcher for all eligible nodes. Paths are observation-local, never action handles; visibility is not an occlusion guarantee. Does not wait for navigation; use wait for the destination before querying. |
open |
Open an Android application by package id or launch a URI. |
swipe |
Swipe in a straight line between screen pixels, then release. Duration is required. Completion does not prove an app-specific effect. |
drag |
Hold at the start, move in a straight line without lifting, then release. Requires Android API 26. Both durations are required. Completion does not prove a successful drop. |
click |
Click a matching node or absolute screen coordinate. |
type |
Type text into a matching field, optionally clearing first or submitting after. Step data reports text_entry, submission, and submit_method separately; accepted submission does not verify navigation. Observe the destination before declaring success. |
read |
Read text from a matching node, optionally returning all matches. Supports regex validation via validator and validatorPattern. |
press |
Press one of the supported Android navigation keys. |
wait |
Wait until a matching node appears. |
scroll |
Perform one scroll in a freshly resolved container. Strict mode requires unique selection. |
scroll_until |
Scroll in the given direction until a matching node is visible, optionally clicking it afterward. |
scroll_and_click |
Scroll in the given direction until a matching node is visible, optionally clicking it afterward. |
evidence_capture |
Capture a local screenshot and raw hierarchy bundle with correlated metadata and explicit partial failures. Complete screenshot artifacts include original PNG pixel dimensions and a top-left coordinate origin; resized preview coordinates must be mapped back before use. |
evidence_video_start |
Start a bounded screen recording in a managed bundle; recording is startup confirmation, not verified media. Requires separately installed scrcpy 3.0+, ffprobe, and ffmpeg 6.1+ with libx264 and passthrough/demux timing support on the server PATH. Unmet dependencies return HOST_DEPENDENCY_MISSING with dependency details and recovery instructions. |
evidence_video_status |
Read a managed video session and detect unavailable ownership. |
evidence_video_stop |
Request owned video finalization and wait up to 15 seconds; pending is not success. |
Tool Details
All execution-backed tools accept these common options unless noted otherwise:
| Field | Required | Meaning |
|---|---|---|
deviceId |
no | Explicit adb device serial. Strongly recommended when multiple devices are connected. |
operatorPackage |
no | Operator package override. Blank string is rejected. |
timeoutMs |
no | Execution timeout override. Values must stay within the normal execution timeout bounds or the MCP boundary rejects them with InvalidParams. For wait, this is the wait duration and the execution timeout is derived from it. |
You can also store deviceId, operatorPackage, and timeoutMs once per MCP server process with configure. When both are present, per-call values win over session defaults for each field independently.
devices
List adb-visible devices.
Input:
{}
Success payload shape:
{
"devices": [
{
"serial": "emulator-5554",
"state": "device"
}
]
}
Notes:
- this is observational output only
- it does not apply execution-time device resolution rules
snapshot
Capture the current UI hierarchy XML.
Parameters:
| Field | Required | Notes |
|---|---|---|
deviceId |
no | Explicit target device |
operatorPackage |
no | Explicit operator package |
timeoutMs |
no | Execution timeout; defaults to 30000 |
maxChars |
no | Raw mode only; truncate XML to this many characters. Minimum 1. |
compact |
no | Return a bounded structural JSON projection; default false |
maxNodes |
no | Requires compact; integer 1..1000, default 100 |
maxTextChars |
no | Requires compact; integer 1..4096 Unicode code points per field, default 256 |
saveRaw |
no | Save exact XML in a new runtime-owned temporary file; default false |
Example call:
{
"deviceId": "<device_serial>",
"operatorPackage": "com.androperator.operator.dev",
"maxChars": 2000
}
Minimal compact call:
{"compact": true, "maxNodes": 100}
Set compact: true whenever supplying maxNodes or maxTextChars.
Omitting compact, or setting it to false, selects raw XML and rejects those
limits. Omit maxChars in compact mode. Invalid combinations return MCP
-32602 before device dispatch, with guidance for correcting the arguments.
The published tool schema also describes and encodes these dependencies.
Compact limits bound returned output only; they do not reduce hierarchy capture
or transfer cost.
Success payload includes:
snapshot: XML string fromsnapshottruncated: present astrueonly whenmaxCharsshortened the XMLdeviceIdterminalSourceenvelope
The fields above describe raw mode. With compact: true, compact replaces the
top-level snapshot and snapshot data.text in the envelope copy. Node paths,
state fields, counts, and errors follow the compact snapshot contract.
compact and maxChars cannot be combined. saveRaw: true returns
rawArtifactPath inside compact in compact mode, or top-level in raw mode.
Caller-provided rawPath is rejected; MCP cannot choose arbitrary host paths.
When maxChars is applied, the returned envelope is truncated consistently with the top-level snapshot field, so MCP clients do not receive a second full-copy XML payload through content or structuredContent.
evidence_capture
Capture a local screenshot/raw-hierarchy bundle. Accepts common deviceId,
operatorPackage, and timeoutMs arguments, plus optional label (at most
2048 UTF-16 code units) and context (JSON object, at most 16 KiB UTF-8).
outputDir and other caller-chosen host paths are rejected.
The server creates a unique bundle under ~/.androperator/evidence/bundles and
returns {ok,status,manifestPath,evidenceId} in both content forms. Only complete
capture has ok: true. Partial/failed captures set isError: true and retain
manifestPath when writable. The opaque caller context remains separate from
capture status. See Still Evidence Bundles for metadata, artifact
validation, partial files, deadlines, and the schema.
execute
Run a caller-supplied action list through the canonical execution validator and runtime.
Parameters:
| Field | Required | Notes |
|---|---|---|
actions |
yes | Array of action objects. Each action must include id and type; params is optional passthrough data. |
deviceId |
no | Explicit target device |
operatorPackage |
no | Explicit operator package. Blank string is rejected before runtime dispatch. |
timeoutMs |
no | Top-level execution timeout. Defaults to 30000. |
Example call:
{
"deviceId": "<device_serial>",
"operatorPackage": "com.androperator.operator.dev",
"actions": [
{
"id": "sleep-1",
"type": "sleep",
"params": {
"durationMs": 1000
}
}
]
}
Validation boundary:
- MCP only enforces
actionspresence plusidandtypeon each element - the full action contract is enforced later by
validateExecution() - MCP rejects caller-controlled
take_screenshotpathvalues so an MCP client cannot choose arbitrary host write locations - exact lower-case
on_screen_log_setandon_screen_log_clearaliases reach the canonical executor and normalize toset_on_screen_logandclear_on_screen_log; MCP does not expose separate panel tools params.templatecarries live overlay templates unchanged to Android; supply exactly one oftextortemplate, with the same styling fields- case or whitespace variants and on-screen log parameter aliases are rejected
Use execute for show_toast and cancel_toast; there are no separate toast tools.
See Toast actions for parameters and submission semantics.
Use Actions for canonical action types and params, and
On-screen logs for the panel-specific raw contract.
The on-screen-log set and clear conveniences are CLI commands; MCP clients
continue to use execute with the same canonical action parameters.
configure
Store per-session defaults for execution-backed MCP tools.
Parameters:
| Field | Required | Notes |
|---|---|---|
deviceId |
no | Session default adb device serial |
operatorPackage |
no | Session default operator package |
timeoutMs |
no | Session default execution timeout. Must stay within the normal execution timeout bounds or configure rejects it with InvalidParams. |
Rules:
- all fields are optional
- blank or whitespace-only
deviceIdandoperatorPackageare rejected withInvalidParams timeoutMsmust stay within the normal execution timeout bounds before it is stored- the stored state is scoped to the current
createMcpServer()instance only - the response shape is always
{ "session": { ...currentValues } } - unset fields are omitted from
session - per-call tool arguments override session defaults field-by-field
Example call:
{
"deviceId": "<device_serial>",
"operatorPackage": "com.androperator.operator.dev",
"timeoutMs": 15000
}
Example success payload:
{
"session": {
"deviceId": "<device_serial>",
"operatorPackage": "com.androperator.operator.dev",
"timeoutMs": 15000
}
}
open
Open an app or URI.
Parameters:
| Field | Required | Notes |
|---|---|---|
appId |
conditional | Android package id |
uri |
conditional | URI to open |
deviceId |
no | Explicit target device |
operatorPackage |
no | Explicit operator package |
timeoutMs |
no | Execution timeout. Defaults to 15000. |
Rule:
- provide exactly one of
appIdoruri
Example app launch:
{
"appId": "com.android.settings",
"deviceId": "<device_serial>"
}
Example URI launch:
{
"uri": "https://androperator.com",
"deviceId": "<device_serial>"
}
swipe
Swipe between absolute screen coordinates, then release. start, end, and
durationMs are all required; there is no duration default. See the
swipe action contract for bounds, timing, results,
and failures. Common deviceId, operatorPackage, and timeoutMs options apply.
{
"start": { "x": 100, "y": 500 },
"end": { "x": 800, "y": 500 },
"durationMs": 300,
"deviceId": "<device_serial>"
}
drag
Hold at start, move to end without lifting, then release. start, end,
holdDurationMs, and moveDurationMs are required. Requires Android API 26.
See the drag action contract for validation, evidence,
and cancellation semantics. Common deviceId, operatorPackage, and timeoutMs
options apply. Use a fresh snapshot to verify the application's drop result.
Call the named drag tool with these arguments, not an execution action wrapper:
{
"start": { "x": 600, "y": 1600 },
"end": { "x": 200, "y": 1000 },
"holdDurationMs": 1200,
"moveDurationMs": 800,
"deviceId": "<device_serial>",
"timeoutMs": 30000
}
Replace the example coordinates with bounds from the target's current snapshot.
For a local development Operator, include
"operatorPackage": "com.androperator.operator.dev".
click
Click a node or an absolute coordinate.
Parameters:
| Field | Required | Notes |
|---|---|---|
selector |
conditional | Selector object |
coordinate |
conditional | { "x": <int>, "y": <int> } |
clickType |
no | One of default, long_click, focus |
deviceId |
no | Explicit target device |
operatorPackage |
no | Explicit operator package |
timeoutMs |
no | Execution timeout. Defaults to 30000. |
Rule:
- provide exactly one of
selectororcoordinate
Example selector click:
{
"selector": {
"text": "Network & internet"
},
"deviceId": "<device_serial>"
}
Example coordinate click:
{
"coordinate": {
"x": 540,
"y": 1180
},
"deviceId": "<device_serial>"
}
type
Type text into a matching field.
Parameters:
| Field | Required | Notes |
|---|---|---|
selector |
yes | Target field selector |
text |
yes | Text to enter |
submit |
no | When true, request best-effort submit after entry |
clear |
no | Replace-style entry remains the goal. On Android ACTION_SET_TEXT targets, true first dispatches ACTION_SET_TEXT(""); on Android 13+ custom editors the runtime can use the API 33 input-connection fallback while still replacing existing content |
deviceId |
no | Explicit target device |
operatorPackage |
no | Explicit operator package |
timeoutMs |
no | Execution timeout. Defaults to 30000. |
Behavior notes:
- the MCP
typetool builds a normalenter_textexecution and returns the standard execution payload - The returned step separates
data.text_entry,data.submission, anddata.submit_method;data.submitremains the requested flag. Accepted submission does not prove navigation. Observe the destination separately. - Android prefers a real editor action for
submit=truewhen one is available - if submit is requested but no truthful submit action exists after text entry, the text-entry step still succeeds
- for the full action contract and runtime details, see
Actions -
enter_text
Example call:
{
"selector": {
"id": "com.android.settings:id/search_src_text"
},
"text": "battery",
"clear": true,
"deviceId": "<device_serial>"
}
query_ui
Inspect structured node state with optional matcher, visibility, and limit,
plus the common deviceId, operatorPackage, and timeoutMs options. matcher
uses the canonical NodeMatcher keys (resourceId, textEquals, and so on),
including canonical ancestor and descendant predicates. Omit it to match all
eligible nodes. Visibility defaults to on_screen; limit defaults to 100 and
accepts integers from 1 through 1000.
{
"matcher": {"resourceId": "row", "descendant": {"textEquals": "Display"}},
"visibility": "all",
"limit": 25
}
Success includes a parsed query object and the original correlated result
envelope whose data.query remains a JSON string. Zero matches succeed. See
query_ui for NodeSummary fields, null state,
visibility, truncation, the 256 KiB response limit, and observation-local paths.
The parsed result preserves per-node accessibilityDataSensitive, including
null or an absent field. Treat either as unknown. The field reports Android
node metadata, not a private-mode verdict. The tool does not convert paths into action targets.
read
Read text from one node or all matches. Supports regex validation to filter results at the runtime boundary.
Parameters:
| Field | Required | Notes |
|---|---|---|
selector |
yes | Target selector |
all |
no | When true, returns a JSON array payload instead of a single string |
container |
no | Optional container selector |
validator |
no | Validation mode. Currently only "regex" is supported. |
validatorPattern |
no | Required when validator is "regex". A valid regex pattern the matched text must satisfy. |
deviceId |
no | Explicit target device |
operatorPackage |
no | Explicit operator package |
timeoutMs |
no | Execution timeout. Defaults to 30000. |
Example single-value read:
{
"selector": {
"text": "Network & internet"
},
"deviceId": "<device_serial>"
}
Example all-values read:
{
"selector": {
"textContains": "Wi"
},
"all": true,
"deviceId": "<device_serial>"
}
Example regex-validated read:
{
"selector": {
"id": "com.example:id/version_text"
},
"validator": "regex",
"validatorPattern": "^\\d+\\.\\d+\\.\\d+$",
"deviceId": "<device_serial>"
}
press
Press a supported Android navigation key.
Parameters:
| Field | Required | Notes |
|---|---|---|
key |
yes | One of back, home, recents |
deviceId |
no | Explicit target device |
operatorPackage |
no | Explicit operator package |
timeoutMs |
no | Execution timeout. Defaults to 10000. |
Example call:
{
"key": "back",
"deviceId": "<device_serial>"
}
wait
Wait until a matching node appears.
Parameters:
| Field | Required | Notes |
|---|---|---|
selector |
yes | Target selector |
deviceId |
no | Explicit target device |
operatorPackage |
no | Explicit operator package |
timeoutMs |
no | Wait duration in milliseconds. The execution timeout becomes max(timeoutMs + 5000, 30000). |
Example call:
{
"selector": {
"text": "Settings"
},
"timeoutMs": 8000,
"deviceId": "<device_serial>"
}
scroll_until
Scroll in the given direction until a matching node appears, optionally clicking it afterward.
Parameters:
| Field | Required | Notes |
|---|---|---|
selector |
yes | Target selector |
direction |
yes | Scroll direction: "down", "up", "left", or "right" |
container |
no | Optional container selector |
clickAfter |
no | When true, the runtime uses action type scroll_and_click |
deviceId |
no | Explicit target device |
operatorPackage |
no | Explicit operator package |
timeoutMs |
no | Execution timeout. Defaults to 30000. |
Example scroll only:
{
"selector": {
"text": "About phone"
},
"direction": "down",
"deviceId": "<device_serial>"
}
Example scroll then click:
{
"selector": {
"text": "About phone"
},
"direction": "up",
"clickAfter": true,
"deviceId": "<device_serial>"
}
Result And Error Behavior
Tool responses use normal MCP tools/call results:
- success responses serialize JSON in the text content and, for object payloads, also expose
structuredContent - execution failures return
isError: true - unknown exceptions are caught and returned as MCP tool errors instead of crashing the server
Common failure cases:
NO_DEVICES: no usable Android target is connectedADB_NOT_FOUND:adbcould not be resolved or executedMULTIPLE_DEVICES_DEVICE_ID_REQUIRED: more than one device is connected and nodeviceIdwas suppliedEXECUTION_CONFLICT_IN_FLIGHT: another execution-backed tool is already running on the same device and operator packageEXECUTION_VALIDATION_FAILED: invalid MCP arguments or invalid action payload
For the full error-code catalog, see Errors.
Device And Concurrency Caveats
Device targeting rules:
- if one device is connected, omitting
deviceIdis usually fine - if multiple devices are connected, pass
deviceId - for local branch testing, prefer
com.androperator.operator.dev
Concurrency rules:
devicesis observational and does not use the execution lock- execution-backed tools share the same in-flight protection as the CLI
- concurrent calls against the same device and operator package may return
EXECUTION_CONFLICT_IN_FLIGHT - this is expected behavior, not a transport bug
Smoke-Test Flow
One reproducible terminal smoke path:
npm --prefix apps/node run build
node validation/test_mcp_stdio_smoke.mjs
What the smoke script proves:
- it starts the MCP stdio server command
- it completes the MCP initialize handshake over stdio
- it verifies
devices - it opens Android Settings on a real device or emulator
- it captures a snapshot and confirms the XML contains node elements
- it performs a selector-driven read using text discovered from the live snapshot
The smoke script prefers a physical device when both a physical device and an emulator are connected. Override with ANDROPERATOR_SMOKE_DEVICE=<device_serial> if needed.
Strict selectors
Named click, type, read, wait, scroll, scroll_until, and
scroll_and_click tools accept optional boolean strict and optional container
using the same shorthand selector shape as selector. scroll performs one
scroll; scroll_and_click defaults clickAfter to true. Raw execute accepts the
canonical params.strict and params.container fields. Query predicates remain
unchanged. See strict selection for the
counts, scope, failure data, and matching-Operator requirement.
Non-strict duplicate selections include a discovery hint for --strict in
data.selection_warning. Named read keeps its existing scalar/list value in
the first content item and adds a second JSON text item containing
selection_warning when needed. Unique reads and intentional multi-target
read-all results keep their existing output unless the explicit container is
ambiguous. See duplicate-selection hints.
Managed video tools
evidence_video_start starts bounded screen recording with required
durationSeconds (1..180), optional size, label, context, and device/Operator
selection. An explicit configured session target can supply the device. It
allocates a managed bundle and returns sessionId; it never accepts output paths.
evidence_video_status and evidence_video_stop accept only sessionId, without
path or target overrides. Recording/startup confirmation is distinct from verified
media completion; stop succeeds only when finalization is complete. Pending or
failed stops and partial/failed captures set isError: true.
See Managed video for lifecycle, prerequisites,
artifacts, ownership and recovery contracts.