@synccalm/agent
v0.1.0
Published
Claude-facing control layer for Android emulators over MCP — build, launch, inspect, click, type, test and verify your app from a conversation.
Maintainers
Readme
@synccalm/agent
Let Claude drive an Android emulator. An MCP server that turns a running emulator into something a model can operate directly — build the app, launch it, read the screen, tap, type, run a flow, and assert the result.
@synccalm/agent
│
└── Claude-facing control layer
├── build gradle assemble → APK
├── launch install + start + report the first screen
├── inspect read the screen as structured elements
├── click tap by label, id or accessibility name
├── type enter text into a field
├── test run a scripted flow, step by step
└── verify assert what is (or isn't) on screenWhy the screen is read, not looked at
inspect returns the accessibility tree, not a picture:
{
"app": { "package": "com.example.app", "activity": ".MainActivity" },
"elements": [
{ "id": "n15", "class": "EditText", "hint": "Email",
"resourceId": "com.example.app:id/email", "editable": true,
"bounds": [126, 63, 1080, 252], "center": [603, 158] },
{ "id": "n22", "class": "Button", "text": "Sign in",
"clickable": true, "center": [540, 1420] }
]
}That matters more than it first looks. It is exact rather than inferred, it costs a fraction of the tokens a screenshot does, it works on a headless emulator, and every element comes with the selector you use to act on it. screenshot is still there for when appearance itself is the question.
Install
npm install -g @synccalm/agentRegister it with Claude Code:
claude mcp add synccalm-agent -- npx -y @synccalm/agent mcpOr add it to .mcp.json by hand:
{
"mcpServers": {
"synccalm-agent": {
"command": "npx",
"args": ["-y", "@synccalm/agent", "mcp"]
}
}
}Then ask Claude to check the setup — it will call doctor, which reports the SDK location, attached devices and the AVDs available to boot.
Requirements
- Node 18+
- Android SDK with
platform-tools(adb) andemulator, found viaANDROID_HOME,ANDROID_SDK_ROOT, or the default install path - At least one AVD, created in Android Studio's Device Manager
Addressing elements
Every screen-facing tool takes the same selector fields, so there is one addressing scheme to learn rather than one per tool:
| Field | Matches |
| --- | --- |
| text | the visible label — exactly first, then case-insensitively |
| textContains | a substring of the label |
| resourceId | pkg:id/name, the bare name, or a substring |
| contentDesc | the accessibility label, for icon-only controls |
| className | a widget class substring, e.g. EditText |
| index | the nth match, when a selector legitimately matches several |
Two behaviours are worth knowing:
Ambiguity is an error, not a guess. A selector matching four elements returns all four and asks you to narrow it, rather than silently tapping the first. Add index when several matches are expected.
Taps route to the clickable ancestor. Matching a TextView inside a list row taps the row, which is what a person would have hit.
Running a flow
test replays a user journey in a single call instead of a dozen round trips, and stops at the first failure — every step after a broken one would be acting on the wrong screen anyway.
{
"name": "sign-in",
"steps": [
{ "action": "launch", "package": "com.example.app" },
{ "action": "type", "className": "EditText", "index": 0, "text": "[email protected]" },
{ "action": "type", "className": "EditText", "index": 1, "text": "hunter2" },
{ "action": "tap", "text": "Sign in" },
{ "action": "verify", "text": "Welcome back", "timeoutMs": 8000 }
]
}The report names the step that failed, how long each took, and what was on screen when it did.
In a type step, text is the content to enter — target the field with resourceId, className or index, not text.
Tools
Environment — doctor, use_device, launch_emulator
App lifecycle — build, install, launch, stop_app, list_packages
Screen — inspect, screenshot
Interaction — click, type, scroll, press_key
Assertions — verify, test
Diagnostics — logs
build detects React Native, Expo-prebuilt and native Android projects, runs the Gradle assemble task, and returns the APKs it produced. On failure it returns the part of the Gradle output that explains why, not the whole log.
CLI
The same code paths are drivable from a terminal, which is the quickest way to check a setup:
synccalm-agent doctor
synccalm-agent emulator Medium_Phone_API_36.0
synccalm-agent launch com.example.app
synccalm-agent inspect
synccalm-agent click "Sign in"
synccalm-agent screenshot shot.pngKnown limitations
Screens that never go idle cannot be dumped. uiautomator waits for the window to stop changing before it captures, and gives up after about twelve seconds. A spinner, a video, a progress bar or live-refreshing text will defeat it — inspect then reports exactly that, and screenshot still works. Booting through launch_emulator turns off window animations, which removes the common transition-related case.
Typing is ASCII-only. Android's input text bridge cannot deliver non-ASCII characters; anything dropped comes back in warnings rather than failing silently.
Canvas-drawn UIs expose no tree. Apps rendering through a SurfaceView — Flutter, games, video players — have nothing for inspect to read. Use screenshot and coordinate taps.
iOS is not implemented yet. The device layer is deliberately separated from the control layer so a simctl-backed driver can slot in behind the same tools.
Programmatic use
const { adb, actions, verify, flow } = require('@synccalm/agent');
const serial = await adb.resolveSerial();
await actions.click(serial, { text: 'Sign in' });
const result = await verify.verify(serial, { text: 'Welcome back', timeoutMs: 8000 });
const report = await flow.runTest(serial, [{ action: 'tap', text: 'Settings' }]);License
MIT
