MCP server
How to register the Karagöz MCP server with each client, and what its tools take, return and do.
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 15 commands are its tools, and each tool returns the JSON line the CLI prints.
mcp takes no arguments or options. It exits with 0 when stdin closes, 130 on SIGINT and 143 on SIGTERM.
Registration
Section titled “Registration”Karagöz is not on npm yet, so every example runs the built file by its absolute path. Replace /abs/path/karagoz-mcp with the folder you cloned it into.
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
Section titled “Claude Code”claude mcp add karagoz -- node /abs/path/karagoz-mcp/packages/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
Section titled “Claude Desktop”On macOS in ~/Library/Application Support/Claude/claude_desktop_config.json, on Windows in %APPDATA%\Claude\claude_desktop_config.json:
{ "mcpServers": { "karagoz": { "command": "node", "args": ["/abs/path/karagoz-mcp/packages/karagoz/dist/cli.js", "mcp"], "env": { "ANDROID_HOME": "/Users/me/Library/Android/sdk" } } }}Quit and restart Desktop after editing.
Desktop starts servers with part of your environment and possibly / as the working directory. Set ANDROID_HOME in env when adb is not on the PATH it passes, and use absolute paths.
Cursor
Section titled “Cursor”In .cursor/mcp.json in the project or ~/.cursor/mcp.json:
{ "mcpServers": { "karagoz": { "type": "stdio", "command": "node", "args": ["/abs/path/karagoz-mcp/packages/karagoz/dist/cli.js", "mcp"] } }}VS Code
Section titled “VS Code”In .vscode/mcp.json in the workspace:
{ "servers": { "karagoz": { "type": "stdio", "command": "node", "args": ["/abs/path/karagoz-mcp/packages/karagoz/dist/cli.js", "mcp"] } }}Or from the command line: code --add-mcp '{"name":"karagoz","command":"node","args":["/abs/path/karagoz-mcp/packages/karagoz/dist/cli.js","mcp"]}'.
In ~/.codex/config.toml:
[mcp_servers.karagoz]command = "node"args = ["/abs/path/karagoz-mcp/packages/karagoz/dist/cli.js", "mcp"]env_vars = ["ANDROID_HOME", "ANDROID_SDK_ROOT", "KARAGOZ_DEVICE", "ANDROID_SERIAL", "DEVELOPER_DIR"]tool_timeout_sec = 600- Codex passes a server only a short list of environment variables.
env_varsforwards the ones Karagöz reads. tool_timeout_seccovers longtapwaits and big installs.installanduninstallask for approval.codex execnever asks, so it refuses them withMCP tool call requires approval, but approval policy is never.
| Tool | Command | Arguments | Marked |
|---|---|---|---|
devices | devices | none | read-only |
screenshot | screenshot | out, inline, device | |
ui_tree | ui-tree | device | read-only |
tap | tap | x, y, text, id, duration, timeout, device | |
swipe | swipe | x1, y1, x2, y2 (required), duration, device | |
text | text | text (required), device | |
key | key | key (required), device | |
install | install | apk (required), device | destructive |
launch | launch | package (required), device | |
terminate | terminate | package (required), device | |
uninstall | uninstall | package (required), device | destructive |
logs | logs | package, since, lines, device | read-only |
doctor | doctor | none | read-only |
install_runner | install-runner | team, device | open world |
uninstall_runner | uninstall-runner | device | destructive |
ui_tree is the one renamed tool, because Codex turns - into _.
“Marked” is the tool’s annotation: readOnlyHint, destructiveHint, or openWorldHint for install_runner, whose build talks to Apple’s developer service. The other 8 carry destructiveHint: false, the other 14 openWorldHint: false, and all 15 a title. Clients use these to decide what needs approval and what may run in parallel.
Arguments
Section titled “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. - Values go through the CLI’s own checks, as the text of the value:
12.5is12.5. 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 usesKARAGOZ_DEVICE, thenANDROID_SERIALfrom its own environment, then the only device (Device selection). Codex passes these variables only throughenv_vars.
Results
Section titled “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, 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
Section titled “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.
- A cancelled
installmay leave the app installed.
- It keeps its UiAutomation slot for about 1-2 s, so a
- In Claude Code, Esc cancels the running call.
- stdin closing, SIGINT and SIGTERM cancel every call the same way.
- The server keeps the
adbit found for the whole session. If that file disappears (ENOENT), the next call looks it up again. Anadbthat appears mid-session higher in the lookup order is not picked up until the server restarts.
Token cost
Section titled “Token cost”What the server adds to a Claude Code session: input tokens of the first API call with Karagöz registered, minus the same call with no MCP server. The numbers come from Claude Code 2.1.306 and claude-opus-5-5, on 2026-10-21.
| Case | Tokens |
|---|---|
Every session, tool search on (the default): the 15 tool names and instructions |
370 |
All 15 definitions, loaded by one ToolSearch call |
2,750 |
Every session, tool search off (ENABLE_TOOL_SEARCH=false): every definition and instructions |
2,632 |
The tools/list result is 7,152 characters, instructions 398. One ui_tree of the launcher home screen is 5,383 characters.