Skip to main content
Glama
Rixmerz

android-layout-inspector-mcp

by Rixmerz
README.md
# android-layout-inspector-mcp

Spatial inspection of a **live Android UI** for AI agents. Pulls the real view
hierarchy off a device or emulator over `adb` + `uiautomator`, and reports
layout problems as **measured geometry** — overlap area in px², touch targets
in dp, clipped fractions — not as an opinion about a screenshot.

The Android counterpart of
[layout-inspector-mcp](https://github.com/Rixmerz/layout-inspector-mcp), which
does the same job for web pages via Playwright.

## Why

An agent that changes a layout has no way to know whether the result overlaps,
clips, or leaves a control too small to tap. Screenshots are the obvious answer
and the wrong one: judging a 4px collision by looking at pixels is guesswork,
and legitimate overlays get reported as bugs.

This measures instead. Every node's bounds, cross-referenced for intersections,
with the overlap area in px² and touch targets converted to dp against the
device's real density. Deterministic and reproducible: it tells you *which two
nodes* collide, not "something looks off". Use a screenshot afterwards to
confirm a finding looks wrong to a human, not to find it.

No Android Studio required.

## Requirements

- Python 3.11+
- [`uv`](https://docs.astral.sh/uv/) — used by the plugin launch path
- `adb` on `PATH`, or `ADB_PATH`, or `ANDROID_HOME` pointing at an SDK that
  contains `platform-tools/adb`
- A device with USB debugging on, or a running emulator

## Install as a plugin

```bash
claude plugin install android-layout-inspector --marketplace Rixmerz/claude-plugins
```

## Install as a plain MCP server

```bash
git clone https://github.com/Rixmerz/android-layout-inspector-mcp.git
cd android-layout-inspector-mcp
uv sync
claude mcp add android-layout-inspector -- uv run --project "$PWD" android-layout-inspector-mcp
```

## Tools

| Tool | Purpose |
| --- | --- |
| `detect_issues(device?, xml_path?)` | The main one. Every check below, sorted by severity, each with the geometry that produced it. |
| `inspect_layout(device?, xml_path?, interactive_only?, max_nodes?)` | Full computed hierarchy: bounds in px and dp, class, ids, flags. |
| `element_context(selector, device?, xml_path?)` | Deep dive on one node — ancestry, siblings, children, and what collides with it. Matches a resource-id, then exact text, then a substring. |
| `accessibility_spatial(device?, xml_path?)` | Touch targets under 48dp, interactive nodes covered by something else, clickables a screen reader cannot announce. |
| `compare_orientations(device?)` | Runs the checks in portrait and landscape and diffs them. The Android answer to responsive breakpoints. |
| `list_devices()` | Attached devices and their adb state. |

Every analysis tool accepts `xml_path` instead of a device, so a saved
`uiautomator` dump can be analysed in CI with no hardware attached.

## What it detects

| Check | Fires when |
| --- | --- |
| `overlap` | Two unrelated **leaf** nodes with content collide. Ancestor/descendant overlap is the layout working, not a bug, and is never reported. Sub-4px² and sub-2% intersections are treated as rounding. |
| `clipped` / `offscreen` | A node with content falls partly or wholly outside the viewport. |
| `zero_size` | A node carries text, a description or a click handler but was laid out with no area. |
| `small_touch_target` | An enabled interactive node is under **48dp** on either axis — Material's minimum, not the web's 44px. |
| `occluded_interactive` | A clickable node is fully covered by an unrelated node drawn *after* it, so the tap never reaches it. |
| `unlabeled_interactive` | A clickable node has no text, no `content-desc`, and no labeled descendant, so TalkBack announces nothing. |
| `compose_not_inspectable` | See below. |

## Jetpack Compose: the one thing you must do

By default a Compose screen collapses into a handful of anonymous
`android.view.View` nodes. The geometry is real, but **nothing can be selected
by id**, which makes every finding much harder to act on. Add this to your root
composable:

```kotlin
@OptIn(ExperimentalComposeUiApi::class)
Surface(
    modifier = Modifier
        .fillMaxSize()
        .semantics { testTagsAsResourceId = true }
) { /* ... */ }
```

Then every `Modifier.testTag("boton_sumar")` shows up as
`resource-id="boton_sumar"`. `detect_issues` raises
`compose_not_inspectable` when it sees a Compose tree that skipped this.

## What this deliberately does not do

Three things are measurable on the web and are **not** recoverable from a
`uiautomator` dump. They are left out rather than approximated:

**Text truncation.** `uiautomator` reports the full string even when the view
ellipsized it — verified on both a classic `TextView` with
`android:ellipsize="end"` and a Compose `Text` with `TextOverflow.Ellipsis`.
There is no signal in the dump, so no check pretends to find one.

**Fully off-screen nodes on a live device.** Android drops them from the
hierarchy before `uiautomator` ever sees them. The `offscreen` check still
fires on saved dumps and on partially clipped nodes, which is the case that
actually reaches the dump.

**Real occlusion by opacity.** Draw order and containment are known; alpha and
elevation are not. `occluded_interactive` requires full geometric containment
by a later sibling, which is conservative on purpose.

One more caveat worth knowing: **Compose already expands every clickable to
48dp** for accessibility, so `small_touch_target` mostly catches classic View
layouts and code that overrides `LocalMinimumInteractiveComponentSize`.

## Verified

`uv run pytest` — unit checks on crafted hierarchies plus a fixture captured
from a real emulator.

End to end against a Pixel 8 AVD (API 36, 1080x2400 @ 420dpi):
`overlap` (15211px², 74% of the smaller node), `unlabeled_interactive` on two
Compose buttons, `small_touch_target` on a 24x24dp `Button` in a classic View
layout, and `compare_orientations` correctly reading 1080x2400 in portrait and
2400x1080 in landscape.

## License

MIT

TDQS

B3.2/5.0

Scored across 6 tools

Disambiguation2/5

detect_issues and accessibility_spatial overlap significantly: both detect touch targets under 48dp, covered interactive nodes, and accessibility labeling problems. inspect_layout and element_context also both expose geometry, though one is screen-wide and the other node-specific, creating boundary ambiguity.

Naming Consistency3/5

Four tools follow a verb_noun pattern (list_devices, inspect_layout, detect_issues, compare_orientations), but element_context and accessibility_spatial break the pattern with noun-first names. The mixed conventions are still readable and not chaotic.

Tool Count5/5

Six tools is well-scoped for an Android layout inspector: device enumeration, layout inspection, issue detection, deep node context, accessibility-specific checks, and orientation comparison. Each tool has a clear role in the workflow, and the count is neither thin nor bloated.

Completeness4/5

The tool surface covers the main inspection lifecycle: enumerate devices, dump layout, identify issues, drill into a node, run accessibility checks, and compare orientations. Minor gaps exist, such as no screenshot capture or explicit way to filter issues by type, but agents can accomplish core layout analysis workflows.

Maintenance

ActivityMaintained
ResponsivenessNo issues