apkscope
# apkscope
See and drive a running Android app **as text**, for an agent whose model
rejects image input.
Most LLM-based coding agents cannot look at a screenshot: the model refuses
image content, and the usual "describe this image" helpers call a hosted vision
API that bills per request or rate-limits you to nothing. This MCP server exists
so such an agent can still work on an Android UI — reading what is on screen,
measuring it, and tapping it.
Everything is converted into characters the model can read.
## Why it exists
Developed while building a Flutter app (SahayakAI) with an agent that had no
image input at all. Two capabilities made UI work possible:
- **Reading the semantics tree.** Flutter publishes its accessibility tree only
while an accessibility service is bound. With none bound, `uiautomator dump`
against a Flutter screen returns a bare `FrameLayout` with no children.
`apkscope` enables a stock system service for the duration of the read and
restores the previous setting afterwards.
- **Rendering pixels as glyphs.** Enough to judge layout, spacing, colour and
gross breakage — not a substitute for vision, but far better than nothing.
## Install
```bash
pip install -e .
```
Requires `adb` on `PATH`, or `$ANDROID_HOME` / `$ANDROID_SDK_ROOT` set, or
`$APKSCOPE_ADB` pointing at the binary.
## MCP setup
```jsonc
{
"mcp": {
"apkscope": {
"type": "local",
"command": ["apkscope-mcp"],
"environment": { "ANDROID_HOME": "C:\\Users\\you\\AppData\\Local\\Android\\Sdk" }
}
}
}
```
Or run it directly to check it starts:
```bash
apkscope-mcp
```
## Tools
| tool | mutates device | what it gives you |
|---|---|---|
| `screen_tree` | no | every labelled element: class, flags, pixel bounds, centre, label |
| `audit_layout` | no | tap targets under 48dp, overlapping controls, off-screen nodes |
| `screenshot` | no | the screen rendered as text, in one of three modes |
| `tap` | **yes** | taps the element whose label matches |
### `screen_tree` — start here
The primary way to read a Flutter UI. Labels, roles and exact bounds are
precise and cheap; a pixel render is approximate and token-heavy.
```
View [-FE--] 530x98 @( 540, 842) Good morning
View [CFE--] 477x283 @( 283, 1750) Lesson Plan
Button [CFE--] 126x126 @( 765, 202) Network
```
Flags: `C` clickable, `F` focusable, `E` enabled, `k` checked, `s` selected.
**Only `C` means tappable.** Flutter also marks plain text focusable, so `F`
alone is not a control — that distinction is what keeps headings out of the
tap list.
Pass `tappable=True` for just the tap-target table.
### `audit_layout` — turn observation into findings
Reports `TAP-SMALL` (below Material's 48dp guidance, converted using the
device's real density), `OVERLAP` (two controls sharing more than a third of the
smaller one) and, with `check_clip=True`, `CLIPPED` off-screen nodes.
Full-bleed wrappers are excluded. Flutter routinely wraps a whole screen in one
`GestureDetector` (a "tap anywhere to speak" surface); without that exclusion
every other control is reported as overlapping it.
### `screenshot` — pixels as text
Three modes:
| mode | output | cost | good for |
|---|---|---|---|
| `palette` (default) | one glyph per pixel, plus a legend mapping glyph → true RGB | ~2k tokens | layout, colour, shape |
| `ramp` | luminance ASCII (`@%#*+=-:. `) | cheap | structure only |
| `ansi` | truecolour half-blocks `▀`, two pixels per character | very high (~40 chars/pixel) | detail crops only |
**Keep `colors` at 8 or below.** On a light-themed app a large palette gets
spent on imperceptible differences between near-identical backgrounds
(`#f3f1ec` vs `#f7f5f0`) and the one meaningful hue is lost. Fewer bins force
the quantiser to group the near-whites.
The legend always reports RGB from the unaltered image, so colour is never
misrepresented.
`crop` accepts `all`, `top`, `mid`, `bot`, or `'a,b'` fractions of height. A
phone screen is tall and mostly empty near the bottom; cropping a band is the
difference between a legible render and a hundred rows of background.
### `tap` — drive the UI
Resolves a label to coordinates through the semantics tree, which survives
layout changes and screen-size differences in a way hardcoded pixels do not.
Refuses an ambiguous label and lists the candidates, because a silent wrong tap
navigates somewhere unexpected and costs more to debug than asking. Pass
`first=True` when you know what you want.
## CLI
The same operations without an MCP host:
```bash
apkscope tree --taps
apkscope audit --clip
apkscope shot --cols 72 --colors 8 --crop 0.3,0.6
apkscope shot --mode ramp --crop top
apkscope tap "Continue with Google"
```
`audit` exits non-zero when it finds something, so it works in a CI check.
## Honest limits
A glyph render is **not** vision. It conveys structure, colour, alignment,
spacing and obvious breakage. It does not reliably convey small body text,
subtle typography, or whether a screen matches a design intent. For fine visual
judgement a human still has to look.
`screen_tree` and `audit_layout` are the higher-value tools for UI work —
exact, cheap, directly actionable. Reach for `screenshot` to catch layout
problems, not to proofread.
The tree only shows what Flutter marks semantic. Text painted directly to a
canvas is invisible to it, and to `uiautomator` generally.
## Tests
```bash
pip install -e ".[dev]"
pytest
```
97 tests, no device required. They pin down the bugs this tool actually hit
while being built: an inverted luminance ramp, a palette legend that crashed on
low-colour crops, a whole-screen overlap false positive from Flutter's
full-bleed wrapper, and unlabelled icon buttons being invisible to the
tap-target filter.
## License
MIT
TDQS
Scored across 4 tools
Each tool has a clearly distinct role: screenshot renders pixels visually, screen_tree extracts semantic structure, audit_layout checks geometry heuristics, and tap performs an action. The descriptions explicitly cross-reference each other (e.g. screenshot points to screen_tree for exact content), eliminating overlap.
All names are lowercase snake_case, but the grammatical pattern is mixed: 'screenshot' and 'screen_tree' are output nouns, 'audit_layout' is verb_noun, and 'tap' is a bare verb. Readable and non-chaotic, but not a predictable single convention.
Four tools is a lean, well-scoped set covering the read/audit/interact loop for a Flutter screen. It is slightly thin (no gesture, input, or navigation helpers), but nothing feels redundant or out of place.
Read and audit coverage is solid, and tap handles basic interaction, but the surface lacks text input, scrolling/swiping, back navigation, and app launch/wait operations. These are notable gaps for a UI inspection/automation domain that will force agents to work around them.