# MCP

Source: https://phonebox.dev/docs/interfaces/mcp

> Connect Claude Code, Cursor or any MCP client to the hosted Phonebox server, and use its tools.



Phonebox runs a hosted MCP server at `https://phonebox.dev/mcp`. It speaks Streamable HTTP and authenticates with the same API keys as the REST API. Every tool call is a request to the [REST API](/docs/interfaces/rest) made with your key, so scopes, limits and billing are exactly the same, and the console's activity shows each call as coming from MCP.

## Connect a client [#connect-a-client]

Every request carries your key as a bearer token:

```http
Authorization: Bearer pbx_…
```

The server doesn't use OAuth, so your client has to be able to send this header.

### Claude Code [#claude-code]

```bash
claude mcp add --transport http phonebox https://phonebox.dev/mcp --header "Authorization: Bearer pbx_…"
```

To share the server with a project without sharing the key, commit a `.mcp.json` at the project's root that reads the key from each person's environment:

```json
{
  "mcpServers": {
    "phonebox": {
      "type": "http",
      "url": "https://phonebox.dev/mcp",
      "headers": { "Authorization": "Bearer ${PHONEBOX_API_KEY}" }
    }
  }
}
```

### Cursor [#cursor]

Add the server to `~/.cursor/mcp.json` to use it in every project, or to `.cursor/mcp.json` in one project:

```json
{
  "mcpServers": {
    "phonebox": {
      "url": "https://phonebox.dev/mcp",
      "headers": { "Authorization": "Bearer pbx_…" }
    }
  }
}
```

Keep a file that holds a key out of version control.

### Other clients [#other-clients]

Point any client that supports Streamable HTTP at `https://phonebox.dev/mcp`, and have it send the `Authorization` header. The server is stateless: it accepts only POST requests, holds no session between them, and takes one JSON-RPC message per request, so it refuses JSON-RPC batches.

## Tools [#tools]

| Tool                  | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Arguments                                                                                          |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `list_phones`         | Lists the project's phones, newest first, with their status and when each parks: up to 50 a page, and `next_cursor` for the next page. Without `status`, it leaves out failed and deleted phones.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `status` and `cursor`, both optional.                                                              |
| `get_phone`           | Reads one phone: its status, session and when it parks. With `wait`, waits until it is ready or parked, only reading it, so it is the way to wait for a phone that is still creating or starting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `phone_id`, and optionally `wait`, `"ready"` or `"parked"`.                                        |
| `create_phone`        | Creates a phone and waits until it is ready.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | All optional: `name`, `country`, `metadata`, `idle_timeout`, `max_duration` and `idempotency_key`. |
| `start_phone`         | Starts a parked phone, or renews a running one, and waits until it is ready.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `phone_id`, and optionally `idle_timeout` and `max_duration`.                                      |
| `park_phone`          | Parks a phone to stop billing, and waits until it has stopped.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `phone_id`.                                                                                        |
| `observe`             | Reads the screen, and adds a screenshot 720 pixels wide as an image.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `phone_id`, and `screenshot`, which is `true` by default.                                          |
| `act`                 | Runs up to 20 [actions](/docs/using-phones/actions) in order, stopping at the first failure.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `phone_id`, `actions`, and `observe`, which is `ui` by default.                                    |
| `list_apps`           | Lists the apps installed on the phone: package, label, version, and whether it came with the phone.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `phone_id`.                                                                                        |
| `reboot_phone`        | Restarts a stuck phone, keeping everything on it, and waits until it is ready again. A phone can reboot once every 10 minutes. A reboot the phone service didn't confirm is never sent twice: wait with `get_phone` instead.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `phone_id`.                                                                                        |
| `install_app`         | With `package`, installs an app from Phonebox's app library in the background. It doesn't reach Google Play: a Play Store app installs through the Play Store app on the phone. With `upload`, installs your own APK and waits until the phone lists it, or fails with `install_failed` and what to do. While that upload is still installing on the phone, and for 10 minutes after that install succeeds, calling it again with the same arguments starts nothing: it waits for that install, or reports it. When the phone doesn't confirm an install's start, it follows Phonebox's record of the install to its end. While two installs run, or while the same app is still installing, another answers `install_in_progress`. | `phone_id`, and `package` or `upload`; with `upload`, optionally `replace`.                        |
| `create_app_upload`   | Starts an upload of your own APK. MCP can't carry a file, so it returns a one-off `upload_url` and the exact `curl` command that sends the APK there from your shell.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | None.                                                                                              |
| `complete_app_upload` | Finishes an upload once its file has arrived, and waits while Phonebox reads and stores the APK. Calling it again with the same arguments only waits again.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `upload_id`, and `storage_id`: the `storageId` that `upload_url` answered.                         |
| `live_view_url`       | Creates a [live view link](/docs/using-phones/live-view) for a person to watch and control the phone. Anyone with the link controls the phone until it expires, so share it only with your user, and never paste it into logs or shared chats.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `phone_id`, and `expires_in` in seconds, which is 3600 by default.                                 |

`get_phone` reads one phone. With `wait` set to `"ready"` or `"parked"`, it waits for the phone the way `GET /v1/phones/{id}?wait=…` does, without starting or renewing it, so it is the way to wait for a phone that is still creating or starting.

There is no tool that deletes or resets phones, because both wipe the phone. Delete or reset phones through the API or the CLI with an Admin key.

### Install your own build [#install-your-own-build]

A coding agent with a shell installs the APK it just built in four steps:

1. Call `create_app_upload`. Its result has the upload's `id`, its `upload_url`, and the `curl` command to run.
2. Run that command with your APK's path, such as `app/build/outputs/apk/debug/app-debug.apk`. It answers `{"storageId": "…"}`. Over the REST API, that answer goes to `POST /v1/apps/uploads/{id}/complete` as it is.
3. Call `complete_app_upload` with the upload's ID and the `storageId`. It answers once the upload is `ready`, or with the reason it can't be installed, such as a file that isn't a signed APK.
4. Call `install_app` with the phone's ID and `upload`. It answers once the phone lists the app. If the result is still `running`, with a `note` saying so, call `install_app` again with the same arguments: while that install runs, and for 10 minutes after it succeeds, a repeat only waits for it or reports it, and never starts a second one (or uninstalls the app again for `replace`). After an install fails, calling it again installs again, as its `next` step says. If the phone already has a build of the app signed with another key, or a newer one, install with `replace: true`, which uninstalls the app first and deletes its data.

[Uploads](/docs/api-reference/uploads) describes the upload's limits and statuses.

The tools carry the usual MCP hints for clients that decide what to confirm with you. `list_phones`, `list_apps` and `observe` only read. `act` is marked destructive, because an action can do anything a person can do on a phone, such as clearing an app's data. `park_phone` is safe to repeat.

Unknown arguments are refused, so a misspelled argument fails instead of being ignored.

## Results [#results]

A successful call returns `{"data": …, "request_id": "req_…"}`, both as JSON text and as structured content. `data` is what the REST API returned.

`observe` returns the screen in the form a model reads best. The text starts with the snapshot ID, continues with the [text form](/docs/using-phones/observe#the-text-form), and ends with the screenshot's size and scale. The screenshot itself follows as an image:

```text
snapshot snp_4f2k7m3q6z3a
app com.android.launcher3
keyboard hidden
screen 1080x2400
[1] EditText "Search apps" #search clickable editable (540,250)
[2] TextView "Chrome" #icon clickable (200,1900)
[3] TextView "Settings" #icon clickable (500,1900)
[4] TextView "Play Store" #icon clickable (800,1900)
screenshot 720x1600, scale 0.666667 (device pixels = image pixels / scale)
```

Coordinates in the text are device pixels. To tap a point you found on the image, divide its coordinates by `scale`. `act` returns the batch result as JSON, followed by the screen after the batch in the same form, unless the screen couldn't be read, or not in time. Without the screen, call `observe` before you act again. When the call has a `progressToken`, `act` sends a progress notification for each action as the batch runs, which also keeps the connection active during a long batch.

A tool call makes one REST request, or several when a waiting tool keeps waiting. Its `request_id` names the request whose data it returns, which is the last one, while the HTTP response's `X-Request-Id` names the first. The console's activity has a row for each of them.

## Waiting tools [#waiting-tools]

`create_phone`, `start_phone`, `park_phone`, and `get_phone` with `wait`, wait for the phone; `complete_app_upload` waits for its upload, and `install_app` with `upload` for its install. How long they wait depends on your client:

* A call without a `progressToken` returns within about 50 seconds, the length of one REST wait; `complete_app_upload` and `install_app` within about 45. An upload still processing is waited on again by repeating `complete_app_upload`, and an install still running by repeating `install_app`: both repeats only wait. If the phone is still creating or starting then, the result is the phone as it is, and calling `get_phone` with its ID and `wait` set to `"ready"` waits again, only reading the phone. Calling `start_phone` again would renew a running phone's session, which reserves credit again. A park that hasn't finished can be waited on the same way with `park_phone`.
* A call with a `progressToken` keeps waiting for up to about 100 seconds. The tool sends a progress notification before each wait, which also keeps the connection active.

The limit exists because a response that sends nothing for 60 seconds is closed by many proxies on the way.

Once `create_phone` has created a phone, it always returns that phone. If a later wait fails, for example on a rate limit, the result is the phone as last seen, and only the phone's own failure is reported as an error. If the call itself fails or times out before you see a result, repeat it with the same `idempotency_key` and arguments to get the same phone instead of a second one. A repeat returns the phone as it is now, even one that has failed since, with its `failure`: then create the next phone with a new `idempotency_key`. Without a key, call `list_phones` and use the newest phone with the name you gave it before you create another.

## Errors [#errors]

A refused call is a tool result with `isError` set to `true`. Its text, and its structured content, is the REST API's [error envelope](/docs/interfaces/rest#errors), with the code, the message, whether it is retryable, the next step and the request ID. An `internal_error` from a tool that changes the phone isn't retryable, because the change may have happened: read the phone before you call it again. MCP has no headers, so a `Retry-After` wait appears as `details.retry_after_seconds`. Every message you send counts toward your key's limit of 600 requests a minute, `initialize` and `tools/list` included, and one over the limit is refused with `429 rate_limited` and a `Retry-After` wait. A failed phone's own failure carries no wait, because it isn't retryable: create a new phone, with a new `idempotency_key` if you use one.

A call that ends without a tool result, for example because the connection dropped or timed out, may still have run. After `act` or another tool that changes the phone, call `observe` before you call it again.

Arguments that don't match a tool's schema are refused before any request is made, in the MCP library's own error shape. The text names the tool and each problem:

```json
{
  "content": [
    {
      "type": "text",
      "text": "Input validation error: Invalid arguments for tool observe: phone_id: Invalid string: must match pattern /^ph_[a-z2-7]{12}$/"
    }
  ],
  "isError": true
}
```

The server checks the form of your key on every request, and it answers a missing or malformed key with `401` and a `WWW-Authenticate` header. It checks whether the key exists, has expired or was revoked when a tool runs, so a key that no longer works shows up as a tool error on the first tool call, while the handshake and the tool list still succeed. A request that carries an `Origin` header is refused, as on the REST API.
