Karagöz
Allows automating Android emulators and physical devices via adb, including device selection, screenshots, accessibility UI tree inspection, touch and text input, app lifecycle management, and log capture.
Allows automating iOS simulators and physical devices via simctl and WebDriverAgent/XCTest, including device selection, screenshots, accessibility UI tree inspection, touch and text input, app lifecycle management, and log capture.
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., "@KaragözTake a screenshot of the Android emulator and show the UI tree."
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.
Karagöz
Device automation for mobile apps. One tool for four targets: Android emulator, Android physical device, iOS simulator, iOS physical device. It is a CLI, and karagoz mcp serves the same commands to an AI agent as an MCP server.
Status: early development. Twelve commands work on the Android emulator,
devicesalso works on a physical Android device,doctorreports theadbthey use, and the MCP server offers all thirteen to an AI agent. The other three targets are not written yet. Nothing is published to npm. See Status.
Contents
Related MCP server: Android-MCP
Status
Command | Android emulator | Android device | iOS simulator | iOS device |
done | done | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | planned | planned | planned | |
done | done | planned | planned | |
done |
"Done" means tested against a live emulator: macOS, an API 36 image (Android 16), 1080x2400 at 420 dpi. devices was tested on a physical Samsung phone (Android 14) over USB; no other command has been run on one yet. doctor touches no device; its row means tested on the same Mac with fake and real adb binaries. For the MCP server, done means smoke/M-mcp.sh passes against the live emulator, and Claude Code and Codex called its tools. Windows and Linux have not been run.
Requirements
Node 22 or newer.
adbfrom Android platform-tools, for Android targets. karagoz does not bundle it; it looks for it on every run, in this order:$ANDROID_HOME/platform-tools/adb$ANDROID_SDK_ROOT/platform-tools/adbadbonPATHAndroid Studio's default SDK:
~/Library/Android/sdkon macOS,~/Android/Sdkon Linux,%LOCALAPPDATA%\Android\Sdkon Windows
An empty variable is skipped. The first
adbthat starts is used for the rest of the run, so a brokenadbunderANDROID_HOMEdoes not fall back toPATH. The exception is anadbthat fails to start withENOENT(a missing interpreter or a broken link): it counts as not there, and the next one is tried. Underkaragoz mcpthe run is the whole server session, and if theadbin use disappears (ENOENT), the next call looks it up again. Anadbthat appears mid-session higher in the list is not picked up until the server restarts.karagoz doctorshows which one is used and why.installneeds platform-tools 30.0.0 or newer. When none is found:{"error":{"code":"ADB_NOT_FOUND","message":"adb not found (tried ..., PATH). Install platform-tools (brew install --cask android-platform-tools, or download https://developer.android.com/tools/releases/platform-tools and add it to PATH) or set ANDROID_HOME to your Android SDK."}}A running emulator (
emulator -avd <name>), or fordevices, a phone with USB debugging on, in statedevice. karagoz installs nothing on a device by itself;installinstalls only the APK you pass it. A phone that adb cannot see is missing from the list: on Windows without the phone maker's USB driver, on a Mac laptop where "Allow accessory to connect" was refused, or in fastboot mode.For the
smoke/1.5-app-lifecycle.shscript only: a JDK and Android SDK build-tools with one platform. The smoke builds its own test APK,dev.karagoz.smoke, and removes it at the end. See Development.For
ui-treeandtap --text/--id: the screen is on, and no other UiAutomation client is connected (Appium, Maestro,uiautomator events). For input to reach apps, the screen is also unlocked.
Install
Not on npm yet. From source:
git clone https://github.com/yagizdo/karagoz.git
cd karagoz
npm install
npm run build # writes dist/: cli.js and two chunks
node dist/cli.js devicesnpm link puts karagoz on your PATH. The examples below use karagoz.
Quick start
$ karagoz devices
{"devices":[{"id":"emulator-5554","platform":"android","kind":"emulator","state":"device","name":"Medium_Phone_API_36.1"}]}
$ karagoz tap --text Chrome
{"device":"emulator-5554","x":667.5,"y":2002.5,"element":{"class":"android.widget.TextView","text":"Chrome","contentDesc":"Chrome","clickable":true,"longClickable":true,"focusable":true,"bounds":[581,1905,754,2100]}}
$ karagoz key HOME
{"device":"emulator-5554","key":"KEYCODE_HOME","code":3}
$ karagoz screenshot --out home.png
{"path":"/Users/me/home.png","device":"emulator-5554","pixels":{"width":1080,"height":2400},"logical":{"width":411.42857142857144,"height":914.2857142857143},"scale":2.625,"safeArea":{"top":63,"right":0,"bottom":63,"left":0},"rotation":0}karagoz ui-tree prints the screen's accessibility tree; tap --text above found its target in that tree.
Concepts
Device selection
Every command except devices works on one device, picked in this order:
--device <id>the
ANDROID_SERIALenvironment variable (ignored when--deviceis given; empty counts as unset)the only listed device
The value is matched against adb serials first (emulator-5554), exactly. If no serial matches, it is matched against every name that devices prints: an emulator's AVD name (Medium_Phone_API_36.1) or a phone's model (SM-S908N), exactly and case-sensitively.
Situation | Error |
A value is given and nothing matches |
|
No value, nothing connected |
|
No value and two or more devices listed (in any state), or a name that matches two entries: the same AVD listed as |
|
The picked device is not in state |
|
karagoz never guesses between devices. There is no config file and no other environment variable.
A phone connected over Wi-Fi as well as USB is listed twice, once per serial, and its model matches both: pick it by serial then. Quote a --device value that contains spaces, as some mDNS serials do.
Coordinates
screenshot pixels, ui-tree bounds and the tap / swipe coordinates share one space: physical pixels of the current screen orientation, origin top left. A node's bounds of [581,1905,754,2100] can be tapped at its center, (667.5, 2002.5), and that point is the same pixel in the screenshot. Decimals are allowed.
screenshot reports scale (physical pixels per density-independent pixel) and logical size so a caller can convert to dp.
Accessibility tree first
To see the screen, read ui-tree before taking a screenshot. The tree is text a program can search, costs a few hundred to a few thousand tokens, and gives the bounds needed to tap. A screenshot is written to disk and returned as a path; the CLI never prints the image.
Output and errors
Every command prints exactly one line of JSON on stdout and exits, except mcp, which speaks JSON-RPC on stdout until stdin closes (MCP server).
Success: the command's result object, exit code
0.Failure: an error object, exit code
1:{"error":{"code":"DEVICE_NOT_FOUND","message":"device 'nosuch' from --device matches no serial or device name. Listed: emulator-5554 (Medium_Phone_API_36.1)."}}The same message goes to stderr as
karagoz: <message>. It can span more than one line when it quotes adb or Node output; the stdout line never does.INSTALL_FAILEDandUNINSTALL_FAILEDadd areasonfield with Android's own code for the failure:{"error":{"code":"INSTALL_FAILED","message":"adb: failed to install /Users/me/app-v1.apk: Failure [INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade detected: Update version code 1 is older than current 2]","reason":"INSTALL_FAILED_VERSION_DOWNGRADE"}}No other code has it.
Branch on error.code, not on the message. Messages are for people and can change.
stderr also carries adb's own notices on success, such as * daemon started successfully when the adb server was not running. That first call takes about 3 s longer.
karagoz --version prints the version as plain text (0.0.0) and exits 0. It wins over any command: karagoz devices --version prints the version.
Arguments
Options can appear anywhere, before or after the command name. A repeated option keeps its last value.
There are no short flags. A value that starts with
-is read as an option:--device=-xpasses it as a value, and a positional that starts with-goes after--, with options before it:karagoz text --device emulator-5554 -- -5.An empty value (
--out=) is refused.An option the command does not take is refused:
'devices' does not take the option '--device'.
All of these fail with INVALID_ARGS before any device is contacted.
Error codes
The set is closed. Any failure without a code of its own is reported as INTERNAL.
Code | Meaning | From |
| No command given. The message lists the commands. | all |
| The command name is not known. | all |
| Missing, extra, malformed or unknown arguments, an unknown key name, an APK path that is not an existing | all |
| No | all that reach adb, except |
| adb did not answer in time: 10 s per call, 20 s for a | all that reach adb, except |
| adb exited with an error. The message is adb's stderr, or Node's error when adb printed nothing. For | all that reach adb, except |
| No device connected and none named. | all but |
| The named device is not connected. | all but |
| More than one device and none named, or a name that matches more than one. | all but |
| The device is | all but |
| The screen could not be read: bad screenshot data, missing display info, or a uiautomator failure. The message says which. |
|
| Another UiAutomation client holds the device. |
|
| The screenshot file could not be written. |
|
| The text has a character Android's |
|
| No node matches. |
|
| More than one node matches. |
|
| The node's center is under the on-screen keyboard. |
|
| Android refused the APK. |
|
| Android refused to remove the app. |
|
| The package is not installed. |
|
| The package is installed but has no activity a launcher can start. |
|
| A bug in karagoz. Please report it with the message. | all |
Commands
Command | Does |
List connected devices | |
Save a full-resolution PNG and return its path with scale metadata | |
Return the accessibility tree of the focused window | |
Tap or long-press a point, or a node found by text or id | |
Swipe between two points | |
Type text into the focused field | |
Press a key | |
Install or replace an app from an APK | |
Start an app as its launcher icon does | |
Stop every process of an app | |
Remove an app | |
Read the device log | |
Report which adb karagoz uses | |
Start the MCP server on stdio |
devices
karagoz devicesLists what adb devices lists. Takes no options.
Output
{"devices":[{"id":"emulator-5554","platform":"android","kind":"emulator","state":"device","name":"Medium_Phone_API_36.1"}]}A phone on USB next to the emulator (serial masked):
{"devices":[{"id":"XXXXXXXXXXX","platform":"android","kind":"physical","state":"device","name":"SM-S908N"},{"id":"emulator-5554","platform":"android","kind":"emulator","state":"device","name":"Medium_Phone_API_36.1"}]}Field | Type | Meaning |
| string | The adb serial. Pass it to |
|
| |
|
|
|
| string | adb's state, unchanged: |
| string or | The AVD name for emulators, the model ( |
No devices is not an error: {"devices":[]}, exit 0. Devices keep adb's order.
Errors: INVALID_ARGS, ADB_NOT_FOUND, ADB_TIMEOUT, ADB_FAILED.
Notes
About 55 ms, plus about 50 ms per emulator for its name, asked in parallel. A device whose id is not
emulator-<port>costs one more call in parallel, a chainedgetpropof about 135 ms. With a phone on USB and one emulator, the wholekaragoz devicesrun took 257 to 289 ms (three runs, macOS).A device listed as
(no serial number), or two devices that share one serial, cannot be targeted: adb's-scannot tell them apart.karagoz does not pair or connect over Wi-Fi. Use
adb pairandadb connect; a paired Android 11+ phone reconnects by itself.If an emulator's name comes back
null, check thatHOMEpoints to your home directory; the emulator console reads a token from there.
screenshot
karagoz screenshot [--out <path>] [--device <id>]Saves the screen as a PNG at full device resolution, never scaled, and prints where it went with the metadata needed to measure against it.
Option | Default | Meaning |
| a new file under the temp directory | Where to write the PNG; must end in |
| see Device selection |
Output
{"path":"/Users/me/home.png","device":"emulator-5554","pixels":{"width":1080,"height":2400},"logical":{"width":411.42857142857144,"height":914.2857142857143},"scale":2.625,"safeArea":{"top":63,"right":0,"bottom":63,"left":0},"rotation":0}Field | Type | Meaning |
| string | Absolute path of the PNG. |
| string | Serial of the device captured. |
|
| Size of the PNG, read from the file itself. |
|
|
|
| number | Density / 160, using the override density if one is set ( |
|
| Pixels of this PNG covered by the status bar, navigation bar, caption bar or display cutout. A hidden bar counts as 0. The keyboard and gesture areas are not included. |
|
| Screen rotation in degrees. |
File location
Without
--out:<temp dir>/karagoz/<device id>-<UTC timestamp>.png, for example/var/folders/.../T/karagoz/emulator-5554-20260926T101530123Z.png. The temp directory followsTMPDIR. Thekaragozdirectory is created private to your user (mode 0700) and must be a real directory you own; the file is created with mode 0600 and never overwrites. Characters other than letters, digits,.,_and-in the device id become_.With
--out: the path must end in.png, in any case, or the command fails withINVALID_ARGSbefore any device call. Relative paths resolve against the current directory, missing parent directories are created, and an existing.pngis overwritten.karagoz never deletes screenshots. One 1080x2400 capture is about 1.4 MB.
Errors
CAPTURE_FAILED: screencap returned something other than a whole PNG (its message is included); the density, display size, rotation or insets could not be read (cannot read <value> from <command>); or the screen rotated or resized during the capture (display is WxH but the screenshot is WxH). With more than one display, screencap's warning comes back asCAPTURE_FAILED.WRITE_FAILED: the file could not be written, or the default directory is not yours (pass --out). Two captures of one device in the same millisecond without--out: the second fails.INVALID_ARGS:'<absolute path>' is not a .png filefor an--outthat does not end in.png, before any device call.Plus the device selection errors, the other
INVALID_ARGScases and the adb errors.
Notes
About 1 s.
A screen that is off, or an app that sets
FLAG_SECURE, gives a black PNG and exit0.If the density changes during a capture,
scaleandsafeAreacan disagree for that one capture.
ui-tree
karagoz ui-tree [--device <id>]Returns the accessibility tree of the focused window, read with uiautomator dump.
Output (children cut short here; the real tree nests the Chrome icon several levels deep)
{"device":"emulator-5554","rotation":0,"root":{"class":"android.widget.FrameLayout","package":"com.google.android.apps.nexuslauncher","bounds":[0,0,1080,2400],"children":[...]}}A node from the same tree:
{"class":"android.widget.TextView","text":"Chrome","contentDesc":"Chrome","clickable":true,"longClickable":true,"focusable":true,"bounds":[581,1905,754,2100]}Field | Present | Meaning |
| always | Serial. |
| always |
|
| always | The top node. |
Node fields, in this order:
Field | Present | Meaning |
| always | Android class, e.g. |
| on the root, and on any node whose package differs from its parent's | App package. |
| when not empty |
|
| only when | |
| only when | |
| always |
|
| when the node has any | Nodes, same shape. |
Other attributes that uiautomator prints (index, drawing-order, NAF) are dropped. Every node uiautomator returns is kept; there is no filtering.
What the tree covers is what uiautomator covers: the focused window, and nodes visible to the user. Where a framework puts its labels differs: Jetpack Compose puts text in text, Flutter puts it in contentDesc.
WebView: a WebView's content often arrives only on the second read. When the tree has a WebView with no children, karagoz reads once more and returns the second tree, or the first if the second read fails.
Errors
CAPTURE_FAILED, with the reason in the message:the screen kept changing for uiautomator's 10 s idle wait (an animation or live content);
no focused window (the screen is off, or an app is still starting);
uiautomator was killed (the message names
adb -s <id> logcat -b crash); a lone UTF-16 surrogate in on-screen text does this;the output could not be parsed.
AUTOMATION_BUSY: another UiAutomation client is connected. Only one can be at a time.ADB_TIMEOUTafter 20 s.Plus the device selection errors,
INVALID_ARGSand the adb errors.
Notes
One read takes 2.4 to 3.3 s; a screen with a fresh WebView about 5.3 s. The idle failure arrives after about 12 s.
Output size on real screens was 560 to 3,900 tokens.
While a read runs, accessibility services such as TalkBack are unbound, and apps see accessibility as enabled.
tap
karagoz tap <x> <y> [--duration <ms>] [--device <id>]
karagoz tap --text <label> [--timeout <ms>] [--duration <ms>] [--device <id>]
karagoz tap --id <resource-id> [--timeout <ms>] [--duration <ms>] [--device <id>]Taps a point, or finds one node in the tree and taps its center. Give exactly one of <x> <y>, --text or --id.
Option | Default | Meaning |
| A point in screen pixels. Non-negative, decimals allowed ( | |
| A node whose | |
| A node whose | |
| none | Hold for this long (a long press). Whole milliseconds, 0 to 999999999. |
|
| Keep reading the tree until a node matches or this much time has passed. Only with |
| see Device selection |
Output
Point:
{"device":"emulator-5554","x":540,"y":1200}With --duration 10 a "duration":10 field follows y. Node:
{"device":"emulator-5554","x":667.5,"y":2002.5,"element":{"class":"android.widget.TextView","text":"Chrome","contentDesc":"Chrome","clickable":true,"longClickable":true,"focusable":true,"bounds":[581,1905,754,2100]}}element is the matched node as ui-tree returns it, without children. x and y are its center.
How a node is found
The tap goes to the center of the node that matched, not to a clickable parent. Measured on View, Compose and Flutter test apps, the center of the matched node receives the click.
A parent and a child that both match are two matches:
ELEMENT_AMBIGUOUS. Pick one and tap its coordinates.Nodes with zero-size bounds never match.
If the keyboard is visible and the node's center is inside it, the tap is refused with
ELEMENT_COVERED. Close the keyboard withkaragoz key BACK. Only the keyboard is checked; bubbles and picture-in-picture windows are not.With
--timeout, only "no match" triggers another read, and it starts right away. The last read can end up to one read (about 3 s) past the timeout. The tap itself is never repeated.The screen can change between the read and the tap; the tap then lands on the old point.
A long press with
--durationis sent as a swipe that does not move. Long-press thresholds measured: 400 ms on View and Compose, 500 ms on Flutter.
Errors
INVALID_ARGS: no target or more than one ('tap' takes <x> <y>, --text or --id), a single coordinate ('tap' needs <x> <y>),--timeoutwith a point, or a malformed number.ELEMENT_NOT_FOUND:no node with text or contentDesc 'Save' in com.example (1 read in 2.6 s).ELEMENT_AMBIGUOUS: lists up to five matches with class and bounds.ELEMENT_COVERED: the node is under the keyboard.Node taps also get every
ui-treeerror. A point tap does not read the tree, so it works while another UiAutomation client is connected.Plus the device selection errors and the adb errors.
Notes
A point tap takes about 0.2 s. A node tap adds one tree read, 2.5 to 3.3 s.
Points are not checked against the screen size.
Exit
0means Android accepted the event, not that the app reacted to it. Read the tree again to check.
swipe
karagoz swipe <x1> <y1> <x2> <y2> [--duration <ms>] [--device <id>]Moves one finger from (x1, y1) to (x2, y2).
Option | Default | Meaning |
| Screen pixels, same rules as | |
|
| Time from start to end. Whole milliseconds, 0 to 999999999. |
| see Device selection |
Output
{"device":"emulator-5554","x1":540,"y1":1800,"x2":540,"y2":600,"duration":300}duration is always present, including the default.
Duration matters. A fast swipe flings a list and a slow one drags it. Measured on a 1200 px drag: 300 ms scrolled a further 1059 px after the finger lifted, 1000 ms scrolled 131 px further, 3000 ms 19 px.
Errors: INVALID_ARGS, the device selection errors and the adb errors.
text
karagoz text <text> [--device <id>]
karagoz text [--device <id>] -- <text starting with ->Types into whatever has focus, as Android's input text does.
Output
{"device":"emulator-5554","text":"hello"}Characters. Printable ASCII, newline (sent as Enter), tab (sent as Tab), ç, Ç and ß. Anything else fails before anything is typed:
{"error":{"code":"TEXT_UNSUPPORTED","message":"cannot type 'ü' (U+00FC): Android's input text types only printable ASCII, newline, tab, ç, Ç and ß; nothing was typed"}}Quotes, spaces and %s are typed as they are; karagoz handles the escaping.
Errors
INVALID_ARGS: empty text.TEXT_UNSUPPORTED: see above.Long text is sent in pieces of up to 100 characters. If a piece fails, the error keeps its code and the message ends with
; <n> of <total> characters were typed before this.Plus the device selection errors and the adb errors.
Notes
Characters typed right after a field appears can be lost. Waiting about 2 s after the field shows up fixed it in testing.
100 characters take about 0.9 s.
key
karagoz key <key> [--device <id>]Presses one key.
<key> is an Android KeyEvent name or code:
A name, case-insensitive, with or without
KEYCODE_:HOME,back,KEYCODE_ENTER,VOLUME_UP.A number from 1 to 340 is a key code:
key 3isHOME. To press the digit 7, useKEYCODE_7, becausekey 7is code 7, which isKEYCODE_0.
Output
{"device":"emulator-5554","key":"KEYCODE_HOME","code":3}key is the resolved name and code its number.
Errors
INVALID_ARGS:unknown key 'FOO'; use a KeyEvent name such as HOME, BACK or ENTER, or a code from 1 to 340. Checked before any device call.Plus the device selection errors and the adb errors.
Notes
Codes 338 to 340 exist in the name table but Android 16 sends them as
KEYCODE_UNKNOWN, still with exit0.Some keys act on the whole device: code 312 opens Recents, 318 saves a screenshot.
Exit
0means Android accepted the key, not that the app reacted to it.
install
karagoz install <apk> [--device <id>]Installs an app from one APK file, or replaces the installed version of the same app.
Option | Default | Meaning |
| Path to an | |
| see Device selection |
Output
{"device":"emulator-5554","path":"/Users/me/app/build/outputs/apk/debug/app-debug.apk"}path is the absolute path of the APK, as given (symlinks are not followed). The package name is not reported.
Errors
INVALID_ARGS:'<path>' is not an .apk file,no file at '<path>'or'<path>' is not a file. Checked before any device call.INSTALL_FAILED: Android refused the APK. The message is adb's, andreasonis Android's code, as in the example above. Codes seen on the emulator:INSTALL_FAILED_VERSION_DOWNGRADE(a lowerversionCodethan the installed app),INSTALL_FAILED_UPDATE_INCOMPATIBLE(signed with another key),INSTALL_PARSE_FAILED_NOT_APK,INSTALL_FAILED_DEPRECATED_SDK_VERSION(targetSdkVersionbelow 24 on Android 16).A failure without an Android code, such as
Error: device is still booting., staysADB_FAILED.ADB_TIMEOUTafter 10 s plus 1 s for every started MB of the APK.Plus the device selection errors and the adb errors.
Notes
Runs
adb install -r --no-incremental. Since Android 9 a reinstall replaces the app without-r; it stays for older devices.--no-incrementalmatters when an.idsigfile sits next to the APK (apksignerwrites one by default): adb would then install incrementally, through a backgroundadb inc-serverprocess.One
.apkonly. Split APKs,.apksand.aabare not supported. There is no way to pass-g(grant runtime permissions),-d(allow a downgrade) or-t: an APK markedtestOnly, which Android Studio's Run button can produce, fails withINSTALL_FAILED_TEST_ONLY.An 8.5 KB APK took 0.7 s, a 100 MB one 2.5 to 3.4 s.
launch
karagoz launch <package> [--device <id>]Starts an app the way tapping its launcher icon does.
Option | Default | Meaning |
| The package name, such as | |
| see Device selection |
Output
{"device":"emulator-5554","package":"com.android.settings","activity":"com.android.settings/.homepage.SettingsHomepageActivity"}activity is the activity Android reports as started, in its package/.Class short form, or null when Android names none. It is not always the launcher activity:
Settings' launcher activity
.Settingshands off to.homepage.SettingsHomepageActivity, and that one is reported.If the app's task is already running, it comes to the front as it is, and its top activity is reported, even one from another package. With the Settings search open,
launch com.android.settingsreportscom.google.android.settings.intelligence/.modules.search.activity.SearchActivity.
Which activity is started. Android's own rule for launch intents: the first activity with the MAIN action and the INFO category, otherwise the first with MAIN and LAUNCHER. With two launcher activities the first one Android lists is started; no chooser appears.
Errors
INVALID_ARGS:'<value>' is not a package name. A package name is letters, digits and_, in dot-separated parts that each start with a letter. Checked before any device call.APP_NOT_FOUND:package 'dev.karagoz.nope' is not installed on emulator-5554.APP_NOT_LAUNCHABLE:package 'com.android.shell' has no launcher activity.ADB_FAILED: Android refused the start. The message is Android'sError:line, such asError: Activity class {com.example/com.example.Main} does not exist.ADB_TIMEOUTafter 30 s. Android itself gives up after 10 s for a process to start and 10 s for the activity to settle.Plus the device selection errors and the adb errors.
Notes
Exit
0means Android started the activity, not that the app is up. An app that crashes at start, and a start with the screen off, also exit0. Read the tree to check.Nothing is restarted or cleared. For a fresh start, run
terminatefirst.A cold start of a small app took 1.7 to 2.0 s.
terminate
karagoz terminate <package> [--device <id>]Stops an app with am force-stop: every process of the package is killed before the command returns, and Android removes its alarms, scheduled jobs and notifications.
Option | Default | Meaning |
| The package name. | |
| see Device selection |
Output
{"device":"emulator-5554","package":"com.android.settings"}An app that is installed but not running gives the same output.
Errors
INVALID_ARGS:'<value>' is not a package name, as forlaunch.APP_NOT_FOUND: the package is not installed.am force-stopalone says nothing in that case, so karagoz checks first.ADB_FAILED:am force-stopprinted an error; the message is that error.Plus the device selection errors and the adb errors.
Notes
Only the package's own processes stop. An activity from another package in the same task stays on screen: after
terminate com.android.settingswith the Settings search open, the search is still in front.Took 0.8 to 1.3 s.
uninstall
karagoz uninstall <package> [--device <id>]Removes an app and its data.
Option | Default | Meaning |
| The package name. | |
| see Device selection |
Output
{"device":"emulator-5554","package":"dev.karagoz.smoke"}Errors
INVALID_ARGS:'<value>' is not a package name, as forlaunch.APP_NOT_FOUND: the package is not installed. Checked first, because Android answers a missing package withDELETE_FAILED_INTERNAL_ERROR, the same code it gives for a package it will not remove.UNINSTALL_FAILED: Android refused. The message is Android'sFailure [...]line andreasonits code, such asDELETE_FAILED_INTERNAL_ERRORfor a system app with no updates.Plus the device selection errors and the adb errors.
Notes
A system app with installed updates goes back to its factory version. Android reports that as success, and so does karagoz.
The app's data is always removed;
adb uninstall -k(keep data) is not offered.Took 0.6 to 0.8 s.
logs
karagoz logs [--package <package>] [--since <seconds>] [--lines <n>] [--device <id>]Reads the device log once and prints the newest records as JSON. Each record is whole: a stack trace is one record with newlines in its message, not one entry per line.
Option | Default | Meaning |
| all records | Keep only the records written under this package's Linux user id (uid). |
| the whole log | Unix time in seconds on the device clock, up to 9 decimals and at most 4294967295. Only records stamped later are read. |
|
| How many of the newest matching records to return, 1 to 999999999. |
| see Device selection |
Output
{"device":"emulator-5554","package":"com.android.shell","uid":2000,"records":[{"time":1790521762.474251,"pid":24931,"tid":24931,"level":"I","tag":"KaragozManual","message":"hello from karagoz"}],"omitted":0}That record was written with adb shell log -t KaragozManual "hello from karagoz"; adb shell runs as com.android.shell.
Field | Type | Meaning |
| string | Serial of the device read. |
| string | Only with |
| number | Only with |
| array | The matching records, at most |
| number | When the record was written: seconds since the Unix epoch on the device clock, to the microsecond, rounded up. |
| number | Process and thread that wrote it. |
| string |
|
| string | The log tag. |
| string | The message, newlines kept. |
| number | How many older matching records |
An empty records with omitted 0 means nothing matched. It is not an error.
Reading since your last call. Pass the largest time of one result as the next --since to get only newer records. The rounding up makes this exact: no record comes back in the next call. After the call above:
karagoz logs --package com.android.shell --since 1790521762.474251{"device":"emulator-5554","package":"com.android.shell","uid":2000,"records":[],"omitted":0}Take the largest time, not the last record's: records are in arrival order, and times can step back by a few milliseconds.
Errors
INVALID_ARGS:'<value>' is not a package name, as forlaunch;--since must be Unix time in seconds (got '<value>');--lines must be a whole number from 1 to 999999999 (got '<value>'). Checked before any device call.APP_NOT_FOUND:package 'dev.karagoz.missing' is not installed on emulator-5554.ADB_FAILED: logcat printed its own error instead of records, such asFailed to wait for logd.ready to become true. logd not running?; the output was not whole log records; orpm list packagesprinted an error while karagoz looked up the uid. The message is that output, cut at 300 characters.ADB_TIMEOUTafter 10 s.Plus the device selection errors and the adb errors.
Notes
Logs read: main, system and crash, plus kernel from Android 11, logcat's defaults. The events log is not read.
The log is never cleared, resized or reconfigured, so other tools and the user keep their history. A call reads what is there and exits; nothing streams. To wait for a line, call again with
--since.The whole window is read from the device and filtered on the host, because logcat counts records before it filters by uid. A full log on the test emulator was 26 MB, about 133,000 records:
logs --lines 1took 0.7 s, 0.8 s with--package. The default 30 records come to about 7 KB of JSON on average; over every 30-record window of a 141,000-record log, the largest was 47 KB.--packagematches the uid, not a process: every process of the app, every restart, and its Java and native crash lines. Lines the system server writes about the app, such asStart procandANR in, have uid 1000 and are not included. A package that shares a system uid, such as Settings, gets the other processes of that uid as well.A record stamped at or before
--sincethat the log daemon receives after the previous read is returned by neither call. Records arrived up to 8.4 ms late on the test emulator.--sinceis on the device clock. The emulator keeps it in step with the host (within 55 ms here), so a host timestamp works there too.Android cuts a record's tag and message at 4068 bytes together when it is written. Bytes that are not valid UTF-8 become U+FFFD.
From Android 15 the device ends a read after 5 s without data and still exits
0, so a read cut short looks complete.
doctor
karagoz doctorReports which adb karagoz uses, its version, every other adb it could use, and how to install platform-tools when adb is missing or too old. It changes nothing. No options.
Output
{"adb":{"status":"ok","source":"PATH","path":"/opt/homebrew/bin/adb","version":"37.0.0-14910828","candidates":[{"source":"ANDROID_HOME","status":"unset"},{"source":"ANDROID_SDK_ROOT","status":"unset"},{"source":"PATH","status":"ok","path":"/opt/homebrew/bin/adb","version":"37.0.0-14910828"},{"source":"default","status":"ok","path":"/Users/me/Library/Android/sdk/platform-tools/adb","version":"36.0.2-14143358"}]}}Field | Type | Meaning |
| string |
|
| string | Copied from the candidate karagoz uses, when it has them. |
| string | Only when |
| array | The four places karagoz looks, in lookup order (see Requirements). |
| string |
|
| string | See below. |
| string | When known: the file karagoz would run. For |
| string | With |
| string | With |
Status | Means |
|
|
|
|
| Something is there but cannot be used: it did not start, exited non-zero, did not answer within 10 s, or printed no |
| Nothing at that location. For |
| No location to check: the variable is unset or empty. For |
The exit code is 0 whenever the report is printed, with adb missing or broken as well. Check adb.status.
Errors: INVALID_ARGS for any argument or option ('doctor' does not take the option '--device'), INTERNAL.
Notes
Only
adb versionruns, on every candidate at once. It never connects to the adb server, so server trouble does not show up here; the other commands report it asADB_TIMEOUT.Nothing is installed.
outdatedmeans below platform-tools 30.0.0, whereinstallfails: it passes--no-incremental, which older versions forward to the device, and the device rejects it. Debian 12 ships 29.0.6. The other commands still work.versionkeeps the builder's suffix: digits from Google,-debian,-android-tools. Debian's 8.1.0 package prints its package version,1:8.1.0+r23-8, and isoutdated.1.0.41inAndroid Debug Bridge version 1.0.41is adb's protocol number and is not shown.For
PATH,pathis the symlink as invoked on macOS (/opt/homebrew/bin/adb) and the resolved file on Linux.A candidate that is there but fails to start with
ENOENT(a script whose interpreter is missing, a broken link) is shownfailed, and karagoz moves on to the next one, as every command does. Any other failure stops the lookup at that candidate. The exception isPATHon macOS and Linux:adbruns by name there, the error does not say which file failed, and the candidate is shownmissing.An invalid
ANDROID_ADB_SERVER_PORTmakesadb versionitself fail, so everyadbfound isfailedwith adb's message:adb: $ANDROID_ADB_SERVER_PORT must be a positive number less than 65535: got "abc".Took 0.1 s with two adbs; 1.4 s on a cold first run. A candidate that hangs costs 10 s, and the others run meanwhile.
Windows and Linux were not run.
MCP server
karagoz mcpStarts an MCP server for an AI agent on stdin and stdout. It opens no port: the client starts the process and talks to it over stdio. The 13 commands are its tools, and each tool returns the JSON line the CLI prints. The server code loads only when mcp runs, so the other commands start as fast as before.
mcp takes no arguments or options. It exits when stdin closes, or on SIGINT or SIGTERM.
Registration
karagoz is not on npm yet, so every example runs the built file by its absolute path. After the first release, npx -y karagoz mcp replaces node /abs/path/karagoz/dist/cli.js mcp.
Claude Code and Codex were run against this server. The Claude Desktop, Cursor and VS Code entries follow their documentation and were not run.
Claude Code
claude mcp add karagoz -- node /abs/path/karagoz/dist/cli.js mcpThe server starts in the directory claude started in, with the shell's environment. A call still running after 2 minutes becomes a background task.
Claude Desktop, in ~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%\Claude\claude_desktop_config.json on Windows; quit and restart Desktop after editing:
{"mcpServers":{"karagoz":{"command":"node","args":["/abs/path/karagoz/dist/cli.js","mcp"],"env":{"ANDROID_HOME":"/Users/me/Library/Android/sdk"}}}}Desktop starts servers with part of your environment and possibly / as the working directory, so set ANDROID_HOME in env when adb is not on the PATH it passes, and use absolute paths.
Cursor, in .cursor/mcp.json in the project or ~/.cursor/mcp.json:
{"mcpServers":{"karagoz":{"type":"stdio","command":"node","args":["/abs/path/karagoz/dist/cli.js","mcp"]}}}VS Code, in .vscode/mcp.json in the workspace:
{"servers":{"karagoz":{"type":"stdio","command":"node","args":["/abs/path/karagoz/dist/cli.js","mcp"]}}}or from the command line: code --add-mcp '{"name":"karagoz","command":"node","args":["/abs/path/karagoz/dist/cli.js","mcp"]}'.
Codex, in ~/.codex/config.toml:
[mcp_servers.karagoz]
command = "node"
args = ["/abs/path/karagoz/dist/cli.js", "mcp"]
env_vars = ["ANDROID_HOME", "ANDROID_SDK_ROOT", "ANDROID_SERIAL"]
tool_timeout_sec = 600Codex passes a server only a short list of environment variables; env_vars forwards the ones karagoz reads. tool_timeout_sec covers long tap waits and big installs. install and uninstall ask for approval; codex exec, which never asks, refuses them with MCP tool call requires approval, but approval policy is never.
Tools
Tool | Command | Arguments | Marked |
| none | read-only | |
|
| ||
|
| read-only | |
|
| ||
|
| ||
|
| ||
|
| ||
|
| destructive | |
|
| ||
|
| ||
|
| destructive | |
|
| read-only | |
| none | read-only |
"Marked" is the tool's annotation: readOnlyHint or destructiveHint. The other seven carry destructiveHint: false, and all 13 carry openWorldHint: false and a title. Clients use these to decide what needs approval and what may run in parallel.
Arguments
The names are the CLI's option and positional names:
tap {"x": 540, "y": 1200}iskaragoz tap 540 1200, andlogs {"lines": 5}iskaragoz logs --lines 5.ui_treeis the one renamed tool, because Codex turns-into_.Values go through the CLI's own checks, as the text of the value (
12.5is12.5), and the messages keep CLI syntax:tap {"x": 1, "y": 2, "duration": 1.5}fails with--duration must be a whole number of milliseconds (got '1.5').nullcounts as not given. An argument the tool does not take is refused:'devices' does not take the option '--foo'.inlineexists only here:trueorfalse, elseINVALID_ARGSinline must be true or false (got 'yes').outandapkshould be absolute. A relative path resolves against the server's working directory, which the client picks.devicepicks one of several devices. Without it the server usesANDROID_SERIALfrom its own environment, then the only device (Device selection). Codex passesANDROID_SERIALonly throughenv_vars.
Results
Success: one text block with the command's JSON line.
Failure:
isError: trueand one text block with the CLI's error envelope.karagoz: <message>goes to stderr, as in the CLI.An unknown tool name is a JSON-RPC error,
-32602Unknown tool: <name>.screenshotwithinline: trueadds the full-resolution PNG as animageblock. Clients scale it before the model sees it (Claude Code sent a 1080x2400 capture as a JPEG), so take coordinates fromui_treebounds, or frompixelsandscale, never from the image.
A real exchange, one line per message, --> sent and <-- received. The data string is cut.
--> {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"devices","arguments":{}}}
<-- {"result":{"content":[{"type":"text","text":"{\"devices\":[{\"id\":\"emulator-5554\",\"platform\":\"android\",\"kind\":\"emulator\",\"state\":\"device\",\"name\":\"Medium_Phone_API_36.1\"}]}"}]},"jsonrpc":"2.0","id":2}
--> {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"tap","arguments":{"x":1}}}
<-- {"result":{"content":[{"type":"text","text":"{\"error\":{\"code\":\"INVALID_ARGS\",\"message\":\"'tap' needs <x> <y>\"}}"}],"isError":true},"jsonrpc":"2.0","id":3}
--> {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"screenshot","arguments":{"out":"/Users/me/home.png","inline":true}}}
<-- {"result":{"content":[{"type":"text","text":"{\"path\":\"/Users/me/home.png\",\"device\":\"emulator-5554\",\"pixels\":{\"width\":1080,\"height\":2400},\"logical\":{\"width\":411.42857142857144,\"height\":914.2857142857143},\"scale\":2.625,\"safeArea\":{\"top\":63,\"right\":0,\"bottom\":63,\"left\":0},\"rotation\":0}"},{"type":"image","data":"iVBORw0KGgoAAAANSUhEUgAABDgAAAlgCAYAAABt...","mimeType":"image/png"}]},"jsonrpc":"2.0","id":4}
--> {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"ui-tree","arguments":{}}}
<-- {"jsonrpc":"2.0","id":5,"error":{"code":-32602,"message":"Unknown tool: ui-tree"}}Behavior
Calls run in parallel, except UiAutomation reads (
ui_tree, andtapwithtextorid), which take turns per device inside one server. Another process that holds UiAutomation still causesAUTOMATION_BUSY.A cancelled call kills its adb child at once and gets no response. The device does not stop with it: it keeps its UiAutomation slot for about 1-2 s, so a
ui_treeright after can getAUTOMATION_BUSY; a gesture already sent finishes on the device; and a cancelledinstallmay leave the app installed. Esc in Claude Code cancels the running call: the adb dump was gone within 0.3 s and the server kept running.stdin closing, SIGINT and SIGTERM cancel every call the same way; the process exits with
0,130and143.The server keeps the
adbit found for the whole session. If that file disappears (ENOENT), the next call looks it up again.
Token cost
What the server adds to a Claude Code session, measured on this build on 2026-09-29 with Claude Code 2.1.284 and claude-opus-5-5: input tokens of the first API call with karagoz registered, minus the same call with no MCP server.
Case | Tokens |
Every session, tool search on (the default): the 13 tool names and | 331 |
All 13 definitions, loaded by one | 2,247 |
Every session, tool search off ( | 2,129 |
The tools/list result is 5,682 characters and instructions 383. One ui_tree of the launcher home screen is 5,383 characters.
Scope
karagoz is the primitive layer. Each command does one thing and exits.
Not a test framework. No assertions, no test runner, no recorded flows, no retry policy, no reports.
tap --timeoutwaits for a node to appear; it never repeats an action.Not a design comparison tool. Figma diffing, measurement and reporting belong a layer above. karagoz produces the raw input, and the metadata to measure it with, but does not interpret it.
Development
npm run build # empty dist/, then bundle to dist/cli.js and two chunks
npm run typecheck
npm run lint # oxlint and the Prettier check; npm run format fixes formattingEach step has one smoke script in smoke/. Each builds first and needs a running emulator, smoke/M-mcp.sh included, except smoke/0b-version.sh and smoke/1.7-doctor.sh: 1.7 uses fake adb scripts only and never runs the real adb. The header of each script lists its preconditions:
sh smoke/1.4-input.shsmoke/1.5-app-lifecycle.sh also builds a test APK on every run, from the manifest in smoke/fixtures/app-lifecycle/, and installs and removes it as dev.karagoz.smoke. No APK is kept in the repository. It needs a JDK (java and keytool on PATH) and, from the Android SDK, build-tools with aapt2 and apksigner plus one platform. The SDK is the first of $ANDROID_HOME, $ANDROID_SDK_ROOT, ~/Library/Android/sdk and ~/Android/Sdk that has both build-tools/ and platforms/. When something is missing the smoke fails and says what, rather than skipping.
The full check, with the emulator running:
npm run typecheck && npm run lint && for s in smoke/*.sh; do sh "$s" || exit 1; doneName
Karagöz is Turkish shadow puppet theatre. Figures are driven with rods from behind a lit screen while the audience watches the movement on the screen. That is what this tool does.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Control real Android and iOS devices with LLM agents — tap, swipe, type, automate flows.
Drive real devices from your AI Coding tool. Embed a client SDK (Unity, Godot, Flutter, iOS/macOS, Android, React Native, Web) in your app, then capture screenshots, traverse the UI tree, inject taps and key events, and run automated test tasks on the physical device over a secure relay.
Disposable cloud Android emulators for coding agents: run an APK or PR build, tap, type, screenshot.
remote debug iOS/Android/Unity/Godot/Flutter/RN/Web on real-device.ui-tree/screenshots/taps,tests.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables AI agents to interact with Android devices through UI manipulation, screen capture, touch gestures, text input, and app management via ADB. Provides comprehensive mobile automation capabilities including element detection, navigation, and application control for Android device testing and interaction.94-
- AlicenseBqualityBmaintenanceEnables AI agents to control Android devices and emulators through direct UI interaction, allowing app navigation, automated testing, and real-world task execution via ADB without computer vision or scripts.182MIT
- AlicenseAqualityBmaintenanceEnables AI assistants to control mobile and desktop devices with natural language, including running automation tasks, taking screenshots, and managing devices.65 npmMIT
- AlicenseAqualityCmaintenanceEnables AI agents to control, inspect, and automate Android devices, Waydroid containers, and AVD emulators over ADB. Provides tools for screenshots, UI-hierarchy text-based tapping, gestures, key presses, text input, app management, and raw shell commands.12MIT