android-phone-control
Control an Android phone over ADB via MCP tools: inspect the screen, act on UI, manage apps/system, and automate goals with Jev.
Inspect phone/state:
statusreports model, Android version, foreground app, screen lock, battery;read_screenlists on-screen controls by number/label;take_screenshotcaptures visuals.Interact with UI:
tap,type_text,scroll,swipe,press_key.Launch and wait:
open_app,open_url,list_apps,wait_for.System/data access:
read_notifications,clipboard,run_shell,transfer_file.Jev decision helpers:
decide_next_actionfor next control,ask_jevfor typed screen judgments.Full automation:
run_tasktakes a plain-language goal, reads/decides/acts/checks until done or stopped.
Controls an Android phone over adb by reading the device screen and performing actions to complete tasks like opening apps, toggling settings, and taking photos.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@android-phone-controlOpen the Play Store and install Spotify"
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.
android-jev
Control your Android phone through Jev.
You say what you want done — open the Play Store, turn on aeroplane mode, take a
photo — and this does it. It plugs into any MCP client as a tool called run_task;
inside, it reads the phone's own screen, asks
Jev what to do next,
does it, checks the result, and tells you honestly whether it worked.
Everything runs over adb. Nothing is installed on the phone.
run_task("open the Play Store") → achieved: true done, 3 steps
run_task("search for YouTube") → achieved: true done, 5 steps
run_task("open YouTube") → achieved: true done, 3 stepsQuick start
What you need
A phone attached by USB, with a data-capable cable. It stays plugged in while it works.
USB debugging on — one setting, below.
macOS with Python 3.11+, and
uv. The picture route uses macOS Vision, which installs on macOS as a dependency; on Linux the loop works and reads the accessibility tree, and leaves the fallback out.Verified on a Pixel 8a running Android 17. The project has only been measured on that one, so treat other phones as untested rather than unsupported.
1. Connect the phone
Plug it in, and on the phone:
Settings → About phone → tap "Build number" seven times. Developer options appear.
Settings → System → Developer options → USB debugging → on.
Replug the cable. The phone shows Allow USB debugging? — tick Always allow and tap Allow. Nothing works until that tap happens, and it is deliberate: a computer cannot authorise itself.
Check the connection:
adb devices # the phone should say "device", not "unauthorized"2. Install the server
git clone https://github.com/FZ2000/android-jev ~/src/android-jev
cd ~/src/android-jev
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e .The distribution is android-jev, the import is phone_control and the command is
phone-control: three names, each read somewhere different. The command also answers
to android-jev, so that uvx android-jev — the spelling a registry client derives
from the distribution name — starts the server.
No adb? PHONE_CONTROL_ADB points at one, and the usual SDK locations are
searched:
curl -sSL -o /tmp/pt.zip \
https://dl.google.com/android/repository/platform-tools-latest-darwin.zip
unzip -q /tmp/pt.zip -d ~/.local/opt3. Give it a decision model
run_task works without one, using a keyword fallback, and is markedly better with
Jev: goals that do not name a control — turn off Bluetooth, open the settings
app — need it.
Either of two keys works, and the key's own shape says which gateway serves it: an
OpenRouter key beginning sk-or-, or a TypeSafe key beginning apikey_. Nothing in
the configuration says which you are using, because sending one gateway's model to the
other is a 400.
The server reads it from JEV_API_KEY — or OPENROUTER_API_KEY, if that is the name
already in use here — or from a file named jev-api-key or openrouter-api-key in the
secrets directory of whichever harness starts it. In DeepSeek Harness that is
$DSH_HOME/secrets/:
printf %s "$JEV_API_KEY" > "$DSH_HOME/secrets/jev-api-key"
chmod 600 "$DSH_HOME/secrets/jev-api-key"Whichever way it reaches the server, the key never appears in a state, a log, a run folder or a transcript.
4. Mount it in your agent
In Claude Code, this repository is also a plugin, which mounts the server and installs the skill in one step — steps 4 and 5 both:
claude plugin marketplace add FZ2000/android-jev
claude plugin install android-jev@android-jevIt asks for the Jev key once and keeps it in the system's credential store; leave it
empty to use JEV_API_KEY or OPENROUTER_API_KEY from your environment instead. The
server runs from the plugin's own copy of this repository through uv, so what it
does always matches the skill that describes it. It still needs uv and adb, and
the phone set up as in step 1.
For any other client, or to run from your own clone:
Any MCP client can mount this server; it speaks MCP over stdio. Send the agent this, with the path changed to wherever you cloned the repository — it says what the repository is and where its own documentation lives, because an agent that has to guess either of those will guess wrong. A prompt rather than a snippet per client: each client keeps its MCP configuration somewhere different, and changes the format between versions.
The repository at
~/src/android-jevis an MCP server that drives an Android phone attached by USB, together with the instructions for using it. Mount it for me, and tell me when you have.
Start it as a local stdio server: the absolute path
~/src/android-jev/.venv/bin/python, the single argument-m phone_control, run from that directory.Call it
android.It needs the Jev key in its environment, under
JEV_API_KEY— orOPENROUTER_API_KEYif that is the name already in use here. Either an OpenRoutersk-or-…key or a TypeSafeapikey_…one: the shape of the key decides which gateway serves it. If your MCP bridge scrubsKEY,PASSWORD,SECRETorTOKENout of a child process's environment, name the variable in the server's ownenvrather than expecting it to be inherited.Give it a generous per-call timeout, a couple of minutes: one of its tools waits for the phone by design.
Read
README.mdat the repository root first. Its quick start covers connecting the phone, enabling USB debugging, installing the server and supplying the key;docs/has the longer explanations. If the phone is not attached or the server will not start, read that rather than guessing.The instructions for using it are
skills/android-phone-control/SKILL.md, with every tool and argument inreferences/tools-reference.mdbeside it.When it is mounted, list its tools and tell me whether I need to start a new session before you can see them.
Three things catch people out, and the prompt is written the way it is to survive all three:
The interpreter path has to be absolute. A client starts the server itself and does not inherit this project's virtualenv, so a bare
python -m phone_controlresolves to the wrong interpreter or to none at all.The key cannot always be inherited. DeepSeek Harness, for one, strips every variable whose name matches
/KEY|PASSWORD|SECRET|TOKEN/ifrom a stdio child, so the key has to be named in the server's ownenvor it arrives empty.A mounted server is not a visible one. Most clients need a new session, or a restart, before a model sees the tools at all — which is why the prompt asks.
The tools surface under whatever namespace your client uses — mcp__android__run_task
in DeepSeek Harness, something else elsewhere. Nothing in these instructions depends on
the exact spelling, and neither should you.
5. Teach your agent how to use it
The skill is one body of text, skills/android-phone-control/SKILL.md, and this
repository already ships it rendered for every harness that reads instructions from the
working directory — which file each one reads is in
The skill, for any agent below. If you work in this
repository, your agent has it already and there is nothing to install.
For any other harness, send it this:
Install the phone-control skill in
~/src/android-jevfor yourself, so that you can drive my phone in any session. The instructions areskills/android-phone-control/SKILL.md, with one reference file beside it atreferences/tools-reference.md. Put both wherever you read skills from, and tell me whether you need to be restarted before it takes effect.
If your harness has nowhere to keep a skill — a hosted assistant with an instructions box and nothing else — paste this instead, which is the part that matters:
The phone is driven by an MCP server mounted as
android. To do anything on it, callrun_taskwith the outcome you want in plain language — "open the Play Store", "turn on aeroplane mode". Ask for one thing at a time and wait for each result. Readachievedandreasonfrom the reply, and relay them rather than retrying.
6. Sanity test: take a photograph
With the phone unlocked and the screen on, ask your agent:
run_task("open the camera app and take a photo")Then check the phone's own evidence rather than the report:
adb shell ls -t /sdcard/DCIM/Camera | head -3 # a new file at the topA new file and a report that it could not confirm the goal are both correct here, and this is the most useful thing the quick start can show you. A photograph leaves no trace on the screen — measured, the tree is byte-identical before and after the shutter — so the run can see that the camera opened and cannot see the picture. It says so instead of claiming a success it did not verify.
Run against the phone this was written on, through a fresh clone of this repository, that is what it looks like: five steps, three of them pressing the shutter, and then
"achieved": false, "outcome": "stalled",
"reason": "3 actions in a row left the screen exactly as it was, so the run was
going round rather than on"and three new files in DCIM/Camera. It took the photograph and could not know it
had, so it stopped rather than pressing the shutter forever. That is the honest
answer to a goal whose result is off-screen, and it is why the last line of this
test is one whose result is on the screen:
run_task("open the calculator app") # achieved: true, done, 3 steps, ~13sRelated MCP server: android-mcp
Where to go next
docs/engineering-decisions.md— why the code is the way it is, with the measurement that forced each decision.docs/code-map.md— every module, what it is for, and how they depend on each other.docs/android-apis.md— what this phone will and will not tell you, measured, including the reliable ways to prove an end state.docs/debugging.md— one run failing, and what to read.docs/tool-contract.md— the tool surface as a contract, including what is deliberately not built.docs/what-we-got-wrong.md— what was tried and did not work.CONTRIBUTING.md— the rules the tests live by.
Why it is built this way
Most adb wrappers expose tap(x, y), which is useless to an agent that cannot
see. This one exposes what is actually on screen:
#5 ImageButton labeled "Send message" at (970,2170) [tappable]
#4 EditText labeled "Type a message" at (450,2170) [text field, focused]Four decisions follow from that, and each cost something to learn.
The reading is structured, not a screenshot. read_screen reports every
window with its type, whether the keyboard is up, and a typed description of every
control — its category, label, bounds and a point to tap. An image is the fallback
for genuinely visual screens, not the default. See
docs/android-apis.md for what is extractable and what is
not.
It reports what has focus, not what is merely running. When a system window
such as the lock screen or the notification shade is in front, status says so
and reports no foreground app — rather than naming the app behind it, which is what
mFocusedApp would have said.
A control is addressed by name, then by number, and only then by coordinate. Every action tool re-reads the screen before it acts, so a name or a number is resolved against the screen as it is now; a coordinate is not. Read the reply — it names exactly what was acted on.
The agent states the goal; the server owns the loop. run_task("open the email app") is one call. The server reads its own screen, decides each step,
checks its own work, and repeats until the goal is reached or cannot be — then
reports what it did and why it stopped. An agent needs to know nothing about
accessibility trees, windows or coordinates to use it.
Each step is several typed questions, asked together. Inside that loop, Jev, TypeSafe's System One decision model, answers four questions in one request and only the answer belonging to the chosen operation is used:
question | what it settles |
| which kind of action, from what this screen can actually do |
| which control, if the operation taps one |
| which installed app, if the operation opens one |
| whether a fresh reading shows the goal met |
Splitting them is the part that mattered. One flat list holding operations, app
names and screen controls made the decider unsure on a launcher screen — it
answered give_up at 0.63 — and the reason is mechanical: confidence measures
how concentrated a choice is, so overlapping options always read as doubt,
whichever of them is right. Once the operations had a question of their own,
open_app won the same screen at 0.74.
Only what can be carried out is offered. Typing needs a focused field, scrolling needs something that scrolls, a website needs a value the device can resolve, a tap needs a control to tap. An option the loop cannot execute is a guaranteed dead step and worse than that — it reads as the model being unsure when the model was never at fault. A web address with no scheme was offered once; Android could not resolve it, the action did nothing, and the 0.41 confidence on that step was read as doubt for days.
The state is facts computed from the phone, not a dump to interpret. What the focused field holds, where each control sits in coarse terms, what tells two identical labels apart, what has already been tried on this screen. This is what makes a threshold unnecessary rather than merely wrong: asked whether a goal was complete, Jev scored 0.42 to 0.47 on a screen where it genuinely was and 0.51 on one where it was not, because in neither case did the state say whether the query had been sent or was still in the box. No number separates those two; a state that says what is on the screen does.
A low number is not a reason to stop, and the loop does not decide. Every rule that used to refuse an answer for scoring low is gone, because each one was measured ending a run that was working. What is left is a stop rule about the phone — three actions in a row that left the screen as it was, or two already taken on the same screen, means the run is going round rather than on — and both err toward running on, because a futile run costs a minute and a working run cut short costs the task.
One exception, and it is about safety rather than sureness. An action that would have a material effect — sending, deleting, paying, granting a permission — is not carried out unless the goal explicitly asks for it. That gate exists because a real run tapped Allow Chrome to record audio while pursuing an unrelated goal.
When the loop stops short, a reader takes over. With no reader configured a stop ends the run as it always did. With one, it reads the screen and says whether the goal is met, and may set one next move for the decider to aim at — never choosing the action itself. The reference implementation measured 0.39 becoming 0.92 on the same capture once a focus was set, which is the argument in one number: the fix belongs in what the decider is told, not in a threshold.
The phone is read once per step. Reading it was 84% of a step, half of that a second read whose only job was to see what the action had done. The post-action observation is now the next step's reading:
before step 7.29s reading 2.91s watching_the_result 3.18s deciding 0.26s
after step 3.75s reading 2.18s watching_the_result 0.00s deciding 0.24sWhat is sent is the source, not a summary of it. Jev's state carries the
accessibility tree's own field names and values, and each option is a word
(open_app) rather than our numbering. The
measured prompting evidence
is blunt: the same programs scored 0.894 as source, 0.598 as a syntax tree
and 0.530 as a control-flow graph, even though the graph carried more
information for 2.5× the tokens.
Running it directly
.venv/bin/python -m phone_controlIt speaks MCP over stdio and waits. Nothing to configure; the tools appear as
mcp__android__* once a client mounts it, as described in the quick start.
The tools
Seeing |
|
Acting |
|
System |
|
Doing the whole job |
|
Stepping by hand |
|
Full argument reference:
skills/android-phone-control/references/tools-reference.md.
The skill, for any agent
skills/android-phone-control/ teaches the
loop: start with the goal rather than the taps, when to read the tree rather than
take a screenshot, and how to read Jev's confidence. The body names no agent, so
the same instructions work anywhere.
Each tool reads a different file, so the skill is rendered into all of them from
one canonical source — skills/android-phone-control/SKILL.md:
File | Read by |
| Codex, Cursor, Copilot agent mode, and the wider AGENTS.md convention |
| Gemini CLI |
| GitHub Copilot |
| Cursor |
| Claude Code |
Edit the canonical skill and re-render; a test fails if any copy has drifted:
.venv/bin/python scripts/render_skill_for_agents.pyFor DeepSeek Harness, symlink or copy skills/android-phone-control/ into a
scanned skill root — $DSH_HOME/skills/ for every session, or .dsh/skills/ in a
project. Discovery is live; no restart.
Environment
Variable | Effect |
| Which device to drive when several are attached. |
| The Jev key, enabling the two decision tools. An OpenRouter |
| The same key under its older name, still read. |
| Path to |
Known limits
Whether a screen can be read at all varies moment to moment. This is the limit to know about, because it produces failures that look like the model's fault and are not. Measured on one page, read twice in a row: 2.9 seconds and 29 controls, then 73.8 seconds and a failure. The page cannot be dumped while it is doing something, and a page carrying a live figure is doing something most of the time and not all of it. So the same goal can succeed on one attempt and fail on the next, with nothing changed but the timing. Everything else below is a consequence or a neighbour of this one.
A screen hosting embedded Android Views inside Compose can read as almost empty. The interop wrapper is marked
NO_HIDE_DESCENDANTS, which hides its whole subtree from every accessibility client — no flag or setting overrides it, and Appium's workaround exists only because it ships its own dumper inside an installed APK. Measured, this is the common case on this device's Settings pages rather than the exception.When the tree will not describe a page, a photograph of it is read instead. That is the second route, and it reaches the tools as well as the loop: a page that cannot be dumped still comes back with its words. What it cannot do is give controls — the reading is lines of text with no tappable targets on them — so on such a page a run can conclude the goal and leave, and cannot act further. A page whose dump fails and whose photograph says nothing either is reported as a failure, not as an empty screen.
dumpsys activity topis not a foreground oracle. It reads as though it should be, and a probe built on it will silently read another app's state: asked while the Settings Battery page was in front, it reported fragments belonging to Photos.dumpsys windownames the page that is actually in front, and for a Settings page reached by tapping that is the genericSubSettings— the page's own title, read from the tree, is the reliable identity.A sleeping screen reads as empty. The display going off leaves one bare node.
read_screensays which of asleep, locked or canvas-only it is.Non-ASCII typing.
input textgoes through the virtual keyboard's character map, which returns null for anything it cannot generate — AOSP's own comment says "For robust text entry, do not use this function." Non-ASCII is routed through the clipboard and a paste instead. The proper fix isACTION_SET_TEXT.dumpsysoutput is internal and drifts between releases. Parsing is tested against dump shapes captured from real reports and exercised on a real device;docs/android-apis.mdseparates what was confirmed from what the device corrected.
Documentation
What each route to an Android phone offers and costs, the parsing traps, and what a real device confirmed | |
Where the tool surface is going, with the measurements — failures now raise, so | |
A diagnosis against a reference implementation of the same loop: the state and the questions are structurally wrong, and tuning will not fix them | |
Start here when a run goes wrong — what to read, in what order, and how to re-decide a recorded step without a phone | |
Every belief this code holds about Android, proven against the phone. A fake can only repeat what its author believed; these cannot | |
What protects |
Testing
scripts/check.sh --as-ci # what CI runs: no phone, no key
scripts/check.sh # everything, with the 95% coverage gate
.venv/bin/python -m pytest -q -m "not device" # everything that needs no phone
.venv/bin/python -m pytest -m device -q # everything that needs a real phone--as-ci is the one to run before pushing. It runs the fast suite on a machine that
has never been set up — no adb, no decision-model key, no home directory holding
either — because that is what the runner is, and the difference is not obvious from
a machine that has both. The suite passed here for weeks while main was red for
exactly that reason.
The default suite runs without a device; device tests skip themselves when none is attached, and when the phone is asleep or locked — that is a precondition, not a code property. The same is true of three other things the suite needs and cannot provide: a Jev key, a network that holds for the length of a run, and macOS's Vision framework, which is the only way to read a picture of a screen. A test that needs any of them stands itself aside with the reason rather than failing, because a dropped connection is not a fact about this code.
That last one is worth knowing when reading a build: on the Linux runner, every test
that photographs a screen stands aside, so the picture route is verified on a Mac
rather than in CI. scripts/check.sh --as-ci reproduces the missing adb and the
missing key, because it runs on a Mac it cannot reproduce the missing framework.
The counts are deliberately left out of this list. They change with every commit that adds a test, and a number in a README is stale the moment after it is written — these commands print the real ones. What does not change is the shape: the majority of the suite needs no phone, and the device suite is the part that cannot be faked.
Coverage has to be read from both suites together, against a gate of 95%: most of
this package only executes with a phone attached, so the fast suite alone fails a gate
on code it had no way to reach. scripts/check.sh runs both. CI runs the fast suite
only, because a hosted runner has no phone.
The usage scenarios
tests/scenarios/ is the specification: all 47 scenarios, each a thing a person asks
a phone to do, from one launch to goals no phone can reach. Each carries its own
starting state, its own ground truth read from the phone, and a screenshot at every
stage.
scripts/run_scenarios.py --tier 1 # a tier at a time
scripts/run_scenarios.py --id bluetooth-off # or one, by name
scripts/run_scenarios.py --all # about half an hourArtefacts land in artifacts/scenarios/<id>/. See
docs/debugging.md for how to read them.
The rules that keep the tests honest
scripts/install_git_hooks.sh # once: refuse a commit whose suite is red
.venv/bin/python -m pytest tests/test_no_xfail.py -q # no test may be allowed to failFour times in this project a fake agreed with its author and the belief was wrong:
that a bare domain opens a browser, that am start -d resolves it, that
cmd clipboard set places something, that a settings toggle is checkable. Each cost
real work. Hence two rules rather than good intentions — a test allowed to fail
reports green while the behaviour it describes does not work, and a belief about
Android belongs in
tests/test_what_the_phone_actually_does.py,
where the phone gets asked.
Watching a run afterwards
An MCP reply is one shot and a run leaves nothing behind unless you ask it to:
export PHONE_CONTROL_RUNS=~/.phone-control/runsEvery run then writes a folder — the report, the exact request and answer for every step, and where the seconds went — and the tool's reply names it. A recorded step can be re-decided offline, without the phone:
.venv/bin/python scripts/replay_run.py ~/.phone-control/runs/<folder> --step 0Status
Working and device-validated: opening apps, reading and acting on screens, scrolling, typing, window and keyboard state, screenshots.
Measured, on a Pixel 8a running Android 17
tests/test_jev_does_the_job.py asks for things a person would ask for and then
checks the phone rather than the report. Every goal below was independently
confirmed against the focused window, which the decision never sees:
Goal | Reported | Verified | Steps | Time |
"open the settings app" | achieved |
| 2 | 9.6 s |
"open the clock app" | achieved |
| 2 | 8.5 s |
"open the calculator app" | achieved |
| 2 | 8.3 s |
No false successes: nothing claimed a result the phone did not reach. The completion check agreed with the chosen action every time, at confidences of 0.97, 1.00 and 0.99.
One honest failure. Asked to "order a large pepperoni pizza", which no phone can do, it correctly reported the goal unmet — but it spent all ten steps exploring Chrome's interface first, including tapping "Allow Chrome to record audio". It explores before it concedes, and some of that exploration is consequential. Bounding that is open work.
Not yet done, and named rather than implied: the tool contract migration described
in docs/tool-contract.md, and any device run of the Jev
decision path, which needs a Jev key.
Available Tools
19 toolsask_jevA
Ask Jev a typed question about what is on the phone screen.
Use this for judgements that are not "what do I tap next": whether an action worked, whether the screen is an error state, or which of several visible items the user meant.
Jev reads text only — the control listing, plus whatever state you give
it. It cannot see the screenshot, so keep the question about what the
controls say and do, not about how they look.
Args: instructions: The question, in plain language. question_type: "choice" to pick one option, "yes_or_no" for the probability that something holds, or "scale" to place the screen on an ordered scale. options: For "choice", the alternatives. For "scale", the levels from lowest to highest. Write each as "short_key: what it means" to name the answer, or give just the meaning to key it by position. state: Extra context to judge against. The current screen reading is always included; use this for the goal or instruction being checked.
| Name | Required | Description | Default |
|---|---|---|---|
| state | No | Extra context to judge against. The current screen is always included. | |
| options | No | For 'choice' and 'scale', the alternatives. For 'choice' write each as 'short_key: what it means'. | |
| instructions | Yes | The question, in plain language. | |
| question_type | No | Which shape of answer: 'choice' picks one option, 'yes_or_no' gives a probability, 'scale' places the state on a range. | choice |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
clipboardA
Read or replace the phone's clipboard.
Useful for moving text between the phone and this conversation, and for putting non-ASCII text where a paste will pick it up, since adb can only type ASCII as keystrokes.
Args: action: "get" to read the clipboard, "set" to replace it. text: The text to place on the clipboard when setting.
| Name | Required | Description | Default |
|---|---|---|---|
| text | No | The text to type, or the text to replace the clipboard with. | |
| action | No | What to do: 'get' to read the clipboard, 'set' to replace it. | get |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
decide_next_actionA
Ask Jev which single control on screen best advances a goal.
Turns "send the message" or "log in" into one concrete action. Jev is a System One decision model: it answers with a typed choice drawn from the controls actually on screen, so it cannot invent a control that is not there, and it returns a calibrated confidence alongside the answer.
Jev reads text only. It is sent the numbered listing of on-screen
controls plus your goal, and it never sees a screenshot — so ask it what to
tap, not what the screen looks like. Anything about appearance (colour,
layout, what a photo shows) is yours to judge from take_screenshot.
Read the confidence before acting. It describes how far the leading option stands from the others — it is not whether Jev could answer, and not whether the action is safe:
0.70 and above: carry out the returned action.
0.45 to 0.70: carry it out, then confirm with
read_screen.below 0.45: the leading options are near a coin flip. Do not act on it; call
take_screenshotand decide from the image yourself.
Args: goal: What the user wants, in plain language. For example "reply to the most recent message saying I will be late". max_candidates: The most controls to offer in one choice. The default leaves room for the six built-in actions inside Jev's 255-option limit, so every control on screen is normally offered: measured over paired problems, accuracy held from 2 options to 255, and what costs accuracy is options that resemble each other, not how many there are. Lower this only to reduce token cost.
| Name | Required | Description | Default |
|---|---|---|---|
| goal | Yes | What you want done, in plain language. Phrase it as the outcome, not the steps. | |
| max_candidates | No | The most controls to offer at once. Lower only to reduce token cost. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
list_appsA
List the apps installed on the phone, as a readable name and package id.
Use this when you are unsure what an app is called on this phone, or when
open_app could not find the name you tried.
Args: query: Optional filter, matched against the package id.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Only list what matches this text. Omit to list everything. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
open_appA
Bring an app to the front, by the name a person would say.
Accepts "YouTube", "youtube", "play store", or an exact package id. The name is checked against what is actually installed before anything is launched, and it waits for the app to reach the foreground before returning.
Args: name: The app to open. wait_seconds: How long to wait for it to reach the foreground.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | The app, by the name a person would say: 'settings', 'Play Store', 'com.android.chrome'. | |
| wait_seconds | No | How long to wait for it to reach the foreground before giving up. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
open_urlA
Open a web address or deep link on the phone.
Args: url: A URL such as "https://example.com", or a deep link such as "geo:37.8,-122.4", "tel:+15551234567", or "sms:+15551234567".
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | A web address such as 'https://example.com', or a deep link such as 'geo:37.8,-122.4'. | |
| wait_seconds | No | How long to wait for it to reach the foreground before giving up. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
press_keyB
Press a navigation, hardware, or system key.
Args: key: One of back, home, recents, enter, delete, escape, tab, space, move_home, move_end, volume_up, volume_down, power, wake, sleep, camera, play_pause, next_track, previous_track, copy, cut, paste, select_all, search, menu, page_up, page_down, notification_shade, quick_settings, collapse_shade.
| Name | Required | Description | Default |
|---|---|---|---|
| key | Yes | Which key, from the list in the description above. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
read_notificationsA
Open the notification shade, read what is in it, then close it again.
Args: close_after: Set False to leave the shade open, when you intend to tap one of the notifications.
| Name | Required | Description | Default |
|---|---|---|---|
| close_after | No | Set false to leave the shade open, when you intend to tap a notification. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
read_screenA
List the controls currently on the phone's screen, by number and name.
This is the main way to see what is on screen, and it is far cheaper than a
screenshot. Each control gets a number you can hand to tap, and a label
you can match on.
Every action tool re-reads the screen before it acts, so both forms are resolved against the screen as it is at that moment. A number is precise -- it disambiguates two controls carrying the same text. A label is stable -- it survives the screen being redrawn. Use the number for a control you have just seen and are about to act on, and the label when the screen may have changed since you read it.
Args: query: Optional filter. Only controls whose text, description, or type contains this text are listed, best match first. Use it to ask "is the Send button up yet?" in one call.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Only list controls whose text or description contains this. Omit to list everything on the screen. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
run_shellA
Run a shell command on the phone and return its output.
The escape hatch for whatever the named tools do not cover: reading a setting, listing files, checking what is installed, granting a permission. Commands run as the shell user, which can read most of a stock phone but cannot change protected settings.
Args: command: The shell command line to run on the device.
| Name | Required | Description | Default |
|---|---|---|---|
| command | Yes | The shell command, run on the phone as the shell user. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
run_taskA
Give the phone a goal and let it work the whole thing out itself.
Use this instead of driving the phone a step at a time. It reads the screen, decides what to do, does it, and reads again, until the goal is reported done or unreachable — so "open the email app" is one call here rather than ten round trips through you.
Every step reports the action, what came of it, and the two numbers Jev gave
for the choice: probability is how likely that option was the best one and
confidence is how sure it was of its own ranking. They are reported rather
than gated on, so a run that succeeded on a thin lead says so, and a run that
stopped for a reason other than reaching the goal says that instead.
achieved is what the run claimed. Read the steps for what actually
happened. If a run needs looking at afterwards, set PHONE_CONTROL_RUNS to a
directory and each run leaves a folder there with the full record of what it
asked Jev and what came back; the reply then names the folder.
Prefer the step-by-step tools when you already know exactly what to tap, when the screen is visual rather than textual, or when you need to inspect something part-way through.
Args: goal: What to achieve, in plain language. For example "open the email app" or "turn on airplane mode". max_steps: How many actions to allow before giving up. Reaching it ends the run honestly as unfinished rather than as a failure of the goal.
| Name | Required | Description | Default |
|---|---|---|---|
| goal | Yes | What you want done, in plain language. Phrase it as the outcome, not the steps. | |
| max_steps | No | How many actions to allow before giving up. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
scrollA
Scroll the content on screen.
Args: direction: Where you want to look, not which way the finger moves. "down" reveals content further down the page, "up" goes back toward the top, and "left" and "right" move sideways. distance: "small" nudges, "page" moves about one screenful (the default), "large" jumps most of the way. target: Optional number or label of the region to scroll, when the screen has more than one scrollable area.
| Name | Required | Description | Default |
|---|---|---|---|
| target | No | Which control, by its number from read_screen or by its visible label. | |
| distance | No | How far: a nudge, about one screenful, or most of the way. | page |
| direction | No | Which way to move, or which way the copy goes for a file. | down |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
statusA
Check the phone connection and see what it is currently doing.
Call this first when a command fails, and before starting anything multi-step. It reports the model, Android version, screen size, foreground app, whether the screen is on or locked, and the battery level. It changes nothing on the phone.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
swipeA
Drag between two points, for gestures no named control covers.
Use scroll for ordinary page movement. Reach for this to dismiss a card,
pull to refresh, draw a pattern lock, or drag a slider.
Args: start_x: Horizontal pixel where the finger lands. start_y: Vertical pixel where the finger lands. end_x: Horizontal pixel where the finger lifts. end_y: Vertical pixel where the finger lifts. duration_ms: How long the drag takes. Longer is slower and more likely to be read as a drag rather than a fling.
| Name | Required | Description | Default |
|---|---|---|---|
| end_x | Yes | Horizontal pixel where the drag ends. | |
| end_y | Yes | Vertical pixel where the drag ends. | |
| start_x | Yes | Horizontal pixel where the drag starts. | |
| start_y | Yes | Vertical pixel where the drag starts. | |
| duration_ms | No | How long the drag takes. Slower is more likely to be read as a drag than a fling. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
take_screenshotA
Capture the phone's screen and look at it.
Try read_screen first: it names things so you can act on the result, and
it costs far less. Take a screenshot when the screen is genuinely visual --
a photo, a map, a game, a CAPTCHA, an icon-only toolbar -- when read_screen
returns nothing useful, or when a decision came back with low confidence.
Args: max_width: Downscale the image to this width in pixels before returning it. Lower it to spend fewer tokens; 0 keeps full resolution.
| Name | Required | Description | Default |
|---|---|---|---|
| max_width | No | Downscale the image to this width in pixels. Lower costs fewer tokens to look at. |
tapA
Tap, or long-press, something on the phone screen.
Args:
target: The number from read_screen (for example 7), or the visible
label of the control ("Send", "Sign in"). A label is matched against
the control's text and description, and the best match wins. The
screen is re-read first, so the target is resolved against what is
on screen now; the reply names exactly what was tapped.
x: Horizontal pixel coordinate. Use only when nothing on screen is
nameable, such as a spot on a map or photo.
y: Vertical pixel coordinate, used together with x.
long_press: Hold instead of tapping, for context menus and drag handles.
| Name | Required | Description | Default |
|---|---|---|---|
| x | No | Horizontal pixel coordinate, for tapping a point instead of a control. | |
| y | No | Vertical pixel coordinate, for tapping a point instead of a control. | |
| target | No | Which control, by its number from read_screen or by its visible label. | |
| long_press | No | Hold instead of tapping, for a context menu or a drag handle. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
transfer_fileA
Copy a file between this computer and the phone.
Args: direction: "to_phone" or "from_phone". computer_path: Path on this computer. "~" is expanded. phone_path: Path on the phone, for example /sdcard/Download/report.pdf.
| Name | Required | Description | Default |
|---|---|---|---|
| direction | Yes | Which way to move, or which way the copy goes for a file. | |
| phone_path | Yes | Path on the phone, for example /sdcard/Download/report.pdf. | |
| computer_path | Yes | Path on this computer. '~' is expanded. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
type_textA
Type text into a field on the phone.
Args:
text: The text to type. Use "\n" for a line break.
target: The field to type into, as a number or label from read_screen.
When omitted, the text goes to whatever already has focus.
replace: Clear the field first instead of appending to it.
submit: Press Enter afterwards, for search bars and message boxes.
verify: Re-read the screen afterwards and report whether the text
actually landed, which catches a tap that missed the field.
| Name | Required | Description | Default |
|---|---|---|---|
| text | Yes | The text to type, or the text to replace the clipboard with. | |
| submit | No | Press Enter afterwards, for search bars and message boxes. | |
| target | No | Which control, by its number from read_screen or by its visible label. | |
| verify | No | Read the screen back afterwards and report whether the text landed. | |
| replace | No | Clear the field first instead of adding to what it holds. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
wait_forA
Wait until something becomes true, instead of sleeping a fixed time.
Use this after an action that triggers loading, so the next step reads a settled screen instead of one mid-animation. With no arguments it waits for the screen to stop changing.
Args: text: Wait until this text appears anywhere on screen. app: Wait until this app (a name or a package id) is in the foreground. timeout_seconds: Give up and report the timeout after this long.
| Name | Required | Description | Default |
|---|---|---|---|
| app | No | Wait until this app is in the foreground. | |
| text | No | The text to type, or the text to replace the clipboard with. | |
| timeout_seconds | No | Give up and report the timeout after this long. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
19 tool updates
v0.1.0- First observed
ask_jev - First observed
clipboard - First observed
decide_next_action - First observed
list_apps - First observed
open_app - First observed
open_url - First observed
press_key - First observed
read_notifications - First observed
read_screen - First observed
run_shell - First observed
run_task - First observed
scroll - First observed
status - First observed
swipe - First observed
take_screenshot - First observed
tap - First observed
transfer_file - First observed
type_text - First observed
wait_for
TDQS
Scored across 19 tools
Each tool targets a distinct capability: screen reading (read_screen, take_screenshot), interaction (tap, type_text, scroll, swipe, press_key), app handling (list_apps, open_app), navigation (open_url), waiting (wait_for), notifications, clipboard, shell, file transfer, and three Jev-driven decision tools that are clearly separated by purpose (next action, autonomous task, general question). No two tools could be easily confused.
All tool names follow a consistent snake_case verb_noun/verb_phrase pattern (read_screen, take_screenshot, type_text, open_app, press_key, wait_for, run_shell, etc.). The style is uniform and predictable, making it easy for an agent to infer function from the name.
With 19 tools, the server is slightly over the typical well-scoped range (3-15), but the scope of a phone-control server is broad, covering screen interaction, system controls, app management, and advanced decision-making. Each tool earns its place, though a few (e.g., separate Jev tools) could arguably be merged; still, the count is reasonable and not bloated.
The tool surface provides comprehensive coverage for phone control: reading the screen (text and visual), acting on screen content, navigating the system, managing apps and URLs, waiting for state changes, handling notifications, clipboard, shell escape hatch, file transfer, and high-level autonomous decision-making. No significant gaps are apparent for the stated purpose.
Related MCP Connectors
Give your AI agent a real cloud Android phone: push APKs, drive it, get screenshots and logs.
Cloud Android phones for AI agents: create a phone, read the screen, tap, type, install APKs, park.
Cloud Android phones for AI agents: read the screen, tap, type, install APKs, park and resume.
Launch and verify high-fidelity mobile simulations from an AI agent.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables an agent to inspect and interact with Android emulators or physical devices via ADB, capturing UI snapshots, tapping nodes, typing text, and reading app logs.10MIT
- AlicenseBqualityDmaintenanceEnables AI agents to control Android devices via ADB, supporting gestures, input, screenshots, UI analysis, and app management.1916 npm1ISC
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to see and control Android devices through ADB and screen mirroring, supporting screen capture, touch input, app management, UI element finding, file transfer, shell commands, clipboard access, and audio/video streaming.374 npmMIT
- AlicenseAqualityBmaintenanceEnables AI agents to see and drive a real Android phone over plain ADB: screenshots and numbered UI trees, gestures, typing with accents and emoji, SMS/RCS reading and prepared replies, notifications, files, and settings, with no root and no app installed on the phone. Safeguards mask security codes and require explicit confirmation before sending, paying, deleting or confirming anything.182MIT