# MCP server

How to register the Karagöz MCP server with each client, and what its tools take, return and do.

```console title="~/app"
$ karagoz mcp
```

Starts 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

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

```console title="~/app"
$ claude mcp add karagoz -- node /abs/path/karagoz-mcp/packages/karagoz/dist/cli.js mcp
```

The 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

On macOS in `~/Library/Application Support/Claude/claude_desktop_config.json`, on Windows in `%APPDATA%\Claude\claude_desktop_config.json`:

```json title="~/Library/Application Support/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

In `.cursor/mcp.json` in the project or `~/.cursor/mcp.json`:

```json title=".cursor/mcp.json"
{
  "mcpServers": {
    "karagoz": {
      "type": "stdio",
      "command": "node",
      "args": ["/abs/path/karagoz-mcp/packages/karagoz/dist/cli.js", "mcp"]
    }
  }
}
```

### VS Code

In `.vscode/mcp.json` in the workspace:

```json title=".vscode/mcp.json"
{
  "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"]}'`.

### Codex

In `~/.codex/config.toml`:

```toml title="~/.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_vars` forwards the ones Karagöz reads.
- `tool_timeout_sec` covers long `tap` waits and big installs.
- `install` and `uninstall` ask for approval. `codex exec` never asks, so it refuses them with `MCP tool call requires approval, but approval policy is never`.

## Tools

| Tool               | Command                                                                      | Arguments                                               | Marked      |
| ------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------- | ----------- |
| `devices`          | [`devices`](https://karagozmcp.com/docs/commands/devices/)                   | none                                                    | read-only   |
| `screenshot`       | [`screenshot`](https://karagozmcp.com/docs/commands/screenshot/)             | `out`, `inline`, `device`                               |             |
| `ui_tree`          | [`ui-tree`](https://karagozmcp.com/docs/commands/ui-tree/)                   | `device`                                                | read-only   |
| `tap`              | [`tap`](https://karagozmcp.com/docs/commands/tap/)                           | `x`, `y`, `text`, `id`, `duration`, `timeout`, `device` |             |
| `swipe`            | [`swipe`](https://karagozmcp.com/docs/commands/swipe/)                       | `x1`, `y1`, `x2`, `y2` (required), `duration`, `device` |             |
| `text`             | [`text`](https://karagozmcp.com/docs/commands/text/)                         | `text` (required), `device`                             |             |
| `key`              | [`key`](https://karagozmcp.com/docs/commands/key/)                           | `key` (required), `device`                              |             |
| `install`          | [`install`](https://karagozmcp.com/docs/commands/install/)                   | `apk` (required), `device`                              | destructive |
| `launch`           | [`launch`](https://karagozmcp.com/docs/commands/launch/)                     | `package` (required), `device`                          |             |
| `terminate`        | [`terminate`](https://karagozmcp.com/docs/commands/terminate/)               | `package` (required), `device`                          |             |
| `uninstall`        | [`uninstall`](https://karagozmcp.com/docs/commands/uninstall/)               | `package` (required), `device`                          | destructive |
| `logs`             | [`logs`](https://karagozmcp.com/docs/commands/logs/)                         | `package`, `since`, `lines`, `device`                   | read-only   |
| `doctor`           | [`doctor`](https://karagozmcp.com/docs/commands/doctor/)                     | none                                                    | read-only   |
| `install_runner`   | [`install-runner`](https://karagozmcp.com/docs/commands/install-runner/)     | `team`, `device`                                        | open world  |
| `uninstall_runner` | [`uninstall-runner`](https://karagozmcp.com/docs/commands/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

- The names are the CLI’s option and positional names: `tap {"x": 540, "y": 1200}` is `karagoz tap 540 1200`, and `logs {"lines": 5}` is `karagoz logs --lines 5`.
- Values go through the CLI’s own checks, as the text of the value: `12.5` is `12.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')`.
- `null` counts as not given.
- An argument the tool does not take is refused: `'devices' does not take the option '--foo'`.
- `inline` exists only here: `true` or `false`, else [`INVALID_ARGS`](https://karagozmcp.com/docs/error-codes/#invalid-args) `inline must be true or false (got 'yes')`.
- `out` and `apk` should be absolute. A relative path resolves against the server’s working directory, which the client picks.
- `device` picks one of several devices. Without it the server uses `KARAGOZ_DEVICE`, then `ANDROID_SERIAL` from its own environment, then the only device ([Device selection](https://karagozmcp.com/docs/device-selection/)). Codex passes these variables only through `env_vars`.

## Results

- Success: one text block with the command’s JSON line.
- Failure: `isError: true` and 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, `-32602` `Unknown tool: <name>`.
- `screenshot` with `inline: true` adds the full-resolution PNG as an `image` block. Clients scale it before the model sees it, so take coordinates from `ui_tree` bounds, or from `pixels` and `scale`, never from the image.

A real exchange, one line per message, `-->` sent and `<--` received. The `data` string is cut.

```txt
--> {"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`, and `tap` with `text` or `id`), which take turns per device inside one server. Another process that holds UiAutomation still causes [`AUTOMATION_BUSY`](https://karagozmcp.com/docs/error-codes/#automation-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_tree` right after can get `AUTOMATION_BUSY`.
  - A gesture already sent finishes on the device.
  - A cancelled `install` may leave the app installed.

- In Claude Code, Esc cancels the running call.

- stdin closing, SIGINT and SIGTERM cancel every call the same way.

- The server keeps the `adb` it found for the whole session. If that file disappears (`ENOENT`), the next call looks it up again. An `adb` that appears mid-session higher in the [lookup order](https://karagozmcp.com/docs/requirements/#adb) is not picked up until the server restarts.

## 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.
