From 45ea969b9badfb79c97d734e1e959d3f2264d396 Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Mon, 19 Oct 2026 10:18:24 +0100 Subject: [PATCH 1/5] docs: Agent Devices (hosted MCP device sessions) - mcp/agent-devices.md: what Agent Devices are; connecting Claude Code, Cursor and other MCP clients to https://api.devicecloud.dev/mcp (OAuth sign-in through the console, or an API key, ideally "Device sessions only"); connected apps; how an agent works a device; asking a person for help and handing back; watching and taking control in the console; secrets in env; recordings and artifacts; limits; pricing. - mcp/agent-devices-tools.md: every tool, its description and inputs, generated from the server's own tool list. - api/device-sessions.md: the /device/sessions REST API and its error codes. - notifications/webhook-notifications.md: the device.needs_human event. - README quick link, and SUMMARY entries. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA --- README.md | 1 + SUMMARY.md | 3 + api/device-sessions.md | 73 ++++++++++++ mcp/agent-devices-tools.md | 153 +++++++++++++++++++++++++ mcp/agent-devices.md | 121 +++++++++++++++++++ notifications/webhook-notifications.md | 25 +++- 6 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 api/device-sessions.md create mode 100644 mcp/agent-devices-tools.md create mode 100644 mcp/agent-devices.md diff --git a/README.md b/README.md index e57e4ff..ef9142f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Start with the [Quick Start](getting-started/quickstart.md) to install the CLI a ### Automate with AI & code +* [Agent Devices](mcp/agent-devices.md) - give your AI agent a cloud iOS or Android device to drive, with a person on hand when it gets stuck (beta) * [MCP Server](mcp/overview.md) - let Claude, Cursor and other MCP clients run your tests * [CLI Reference](cli/overview.md) - every `dcd` command and flag * [REST API](api/overview.md) - uploads, results and flows over HTTP diff --git a/SUMMARY.md b/SUMMARY.md index f09f65f..d83b193 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -64,10 +64,13 @@ * [Uploads](api/uploads.md) * [Results](api/results.md) * [Flows](api/flows.md) +* [Device Sessions](api/device-sessions.md) * [IP Addresses](api/ip-addresses.md) ## AI Agents & MCP +* [Agent Devices](mcp/agent-devices.md) +* [Agent Devices Tools](mcp/agent-devices-tools.md) * [MCP Server](mcp/overview.md) * [Tools Reference](mcp/tools.md) diff --git a/api/device-sessions.md b/api/device-sessions.md new file mode 100644 index 0000000..4979732 --- /dev/null +++ b/api/device-sessions.md @@ -0,0 +1,73 @@ +# Device sessions + +The REST API behind [Agent Devices](../mcp/agent-devices.md). The MCP tools call these same endpoints. Authenticate with an [API key](../getting-started/api-keys.md) in `x-app-api-key`. A key limited to **Device sessions only** can reach these endpoints and the build upload endpoints, but nothing else. + +A session is named by its `id` (for example `brave-teal-otter`) in every path. Every response includes a `session` block (see [the tool reference](../mcp/agent-devices-tools.md)). + +## Sessions + +| Method and path | What it does | +|---|---| +| `GET /device/models` | The devices you can start: models, OS versions and the defaults | +| `POST /device/sessions` | Start a session. Body: `platform` (`ios` or `android`), and optionally `model`, `os`, `googlePlay`, `binaryUploadId`, `maxDurationMin`, `clientLabel`, `record` (default `true`) | +| `GET /device/sessions` | Your organization's open sessions. `?all=true` adds those that ended in the last day | +| `GET /device/sessions/:id` | One session | +| `DELETE /device/sessions/:id` | Stop a session and release the device | + +## The device + +| Method and path | What it does | +|---|---| +| `POST /device/sessions/:id/install` | Install a build: `{ "binaryUploadId": "..." }`, or `{ "url": "https://..." }` for an `.apk` or a zipped simulator `.app` at a link (up to 500 MB) | +| `POST /device/sessions/:id/launch` | Launch an app: `{ "appId"?: "...", "clearState"?: true }`. Without `appId`, the installed build | +| `POST /device/sessions/:id/actions` | One action: `tap` (`x`/`y` percent, `text` or `id`), `type` (`text`, and `sensitive: true` to keep it secret), `swipe` (`direction`, or `from`/`to`), `press_key` (`key`) | +| `POST /device/sessions/:id/run` | Run Maestro: `{ "yaml": "..." }` for one flow, or `{ "flowUploadId": "...", "flow"?: "..." }` for an uploaded bundle. `env` passes secrets and variables (see below). Waits about 110 s, then answers `running` | +| `GET /device/sessions/:id/commands/:commandId` | The status and result of a `run` that answered `running` | +| `GET /device/sessions/:id/inspect` | The screen as compact JSON | +| `GET /device/sessions/:id/screenshot` | The latest screenshot, as a data URL | +| `GET /device/sessions/:id/logs?lines=200` | The installed app's recent device log lines | + +## Asking a person + +| Method and path | What it does | +|---|---| +| `POST /device/sessions/:id/request-human` | `{ "reason": "..." }`. The session waits for a person. Its idle timer pauses for up to 30 minutes, and the starter and your [webhook](../notifications/webhook-notifications.md#device-needs-human) are told | +| `GET /device/sessions/:id/human` | Whether it's still waiting, or was handed back, with the person's note | +| `POST /device/sessions/:id/hand-back` | `{ "note"?: "..." }`. Hand the session back to the agent | + +## Artifacts + +| Method and path | What it does | +|---|---| +| `GET /device/sessions/:id/timeline` | Every command and event, newest first: who sent it, its status, and the screenshot after it. Paged with `limit` and `before` | +| `GET /device/sessions/:id/artifacts` | Links to the session's video, device log, and the screenshot and screen dump after each action. Paged the same way | + +## Secrets + +`env` on `run` is an object of names to string values. Names are letters, digits and `_`; names starting with `DCD_` are reserved. Refer to them in the YAML as `${NAME}`. They're encrypted before they're stored, never shown in the timeline, and masked in the command's output. + +## Errors + +Errors are JSON with a `message` and a stable `error` code: + +| Code | Meaning | +|---|---| +| `LIVE_NOT_ENABLED` | Device sessions aren't enabled for your organization | +| `LIVE_CONCURRENCY_LIMIT` | Your organization has as many sessions open as it may | +| `LIVE_SPEND_CAP` | Your organization reached its monthly spend cap | +| `LIVE_NOT_FOUND` | No such session in your organization | +| `LIVE_NOT_READY` | The device is still starting, or installing an app: retry shortly | +| `LIVE_SESSION_ENDED` | The session has ended | +| `LIVE_INVALID_ACTION` | The request is malformed: the message says how | +| `LIVE_INVALID_APP` | The build isn't one this device can install | +| `LIVE_APP_DOWNLOAD_FAILED` | The build at the URL couldn't be downloaded | +| `LIVE_NO_APP` | No app is installed, and none was named | +| `LIVE_COMMAND_NOT_FOUND` | No such command in this session | +| `LIVE_LOGS_UNAVAILABLE` | The device logs couldn't be read | +| `LIVE_SESSION_REQUIRED` | MCP only: more than one session is open, so pass `session_id` | +| `LIVE_RATE_LIMITED` | Too many requests: slow down | +| `LIVE_NOT_WAITING_FOR_HUMAN` | Hand-back on a session that isn't waiting for a person | +| `LIVE_HUMAN_IN_CONTROL` | A person has taken control in the console: wait for them to hand back | +| `LIVE_CONSOLE_ONLY` | Only a person signed in to the console can do this (take control) | +| `LIVE_SECRETS_UNAVAILABLE` | Secrets can't be accepted right now | +| `API_KEY_SCOPE` | The API key isn't allowed to reach this endpoint | diff --git a/mcp/agent-devices-tools.md b/mcp/agent-devices-tools.md new file mode 100644 index 0000000..f1954c4 --- /dev/null +++ b/mcp/agent-devices-tools.md @@ -0,0 +1,153 @@ +# Agent Devices tool reference + +The tools DeviceCloud's hosted MCP server (`https://api.devicecloud.dev/mcp`) offers for [Agent Devices](agent-devices.md). Every tool except `list_devices` and `cheat_sheet` takes an optional `session_id`. You can leave it out when your organization has exactly one open session. + +Every result includes the session block: `id`, `state`, `platform`, `model`, `queue` (position, ETA and reason while queued), `time_remaining_s`, `idle_remaining_s`, `cost_so_far_usd`, `last_crash`, `warnings`, and `monitor_url` (the session's page in the console). + +When a tool fails, the result is an error with a stable `error` code, the same codes as the [REST API](../api/device-sessions.md#errors). + +## `list_devices` + +List the cloud devices you can start (iOS simulators and Android emulators, with their OS versions and the defaults) and your organization's open device sessions. Use this first, before start_device, or to find a session_id. + +Read-only. + +No inputs. + +## `start_device` + +Lease a fresh cloud iOS simulator or Android emulator. Use this when you need a mobile device and none is available locally. Billed per minute from when a device is assigned until the session ends ($0.03/min before any discount); call stop_device when you are done. Sessions end on their own after 5 idle minutes or their max duration. When the fleet is busy the session is "queued": that is not an error, it starts on its own. Waits up to 60s for the device (wait: false returns at once); if it is still queued or provisioning, poll get_device_status until state is "ready". + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `platform` | `ios` or `android` | Yes | ios or android. | +| `model` | string | No | A model from list_devices, e.g. "iphone-16" or "pixel-10". Default: the platform default. | +| `os` | string | No | iOS major (e.g. "26") or Android API level (e.g. "36"). Default: the model default. | +| `google_play` | boolean | No | Android only: an image with Google Play services. | +| `app_binary_id` | string | No | An uploaded build to install when the device is ready. | +| `max_duration_min` | integer | No | End the session after this many minutes. Default and ceiling: your organization's limit. | +| `client_label` | string | No | Who is driving, e.g. "claude-code". Shown to people watching the session. | +| `record` | boolean | No | Record the session on video. Default true. A screenshot after each action and the device log are kept either way. | +| `wait` | boolean | No | Wait up to 60s for the device to be ready. Default true. | + +## `get_device_status` + +A session's state (queued, provisioning, ready, active, needs_human, released, expired, failed), its queue position, time and idle time left, cost so far, the app's last crash and warnings. Poll this after start_device until state is "ready". + +Read-only. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | + +## `stop_device` + +End a device session and release the device. Billing stops. Call this as soon as you are done with a device. + +Ends the session. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | + +## `install_app` + +Install an app on the session's device: an uploaded build (app_binary_id; upload with `dcd upload` or the DeviceCloud console), or a build at an https URL (app_url, up to 500 MB). Android takes an .apk; iOS takes a zipped simulator .app (not an .ipa). Returns the app id. While it installs, commands answer LIVE_NOT_READY (phase "installing_app"); retry them after a few seconds. Then launch it with launch_app. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | +| `app_binary_id` | string | No | An uploaded build's id. Pass this or app_url. | +| `app_url` | string | No | An https link to the build, e.g. a CI artifact or a signed storage URL. Pass this or app_binary_id. | + +## `launch_app` + +Launch an app on the device: the installed build by default, or any app by id (e.g. "com.apple.Preferences" for iOS Settings). clear_state wipes its data first. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | +| `app_id` | string | No | Bundle id or package name. Default: the installed build. | +| `clear_state` | boolean | No | Clear the app's data before launching. | + +## `inspect_screen` + +The current screen as compact JSON: every element with text, an id, a label or a hint, with its bounds. Use it before tapping, and copy txt and rid values exactly into run commands (tapOn text / id). Shows the result of your last command. + +Read-only. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | + +## `take_screenshot` + +A screenshot of the device, showing the result of your last command. Prefer inspect_screen for finding things to tap; use this to see layout, images and colours. + +Read-only. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | + +## `run` + +Run Maestro on the device: `yaml` is one flow (a header naming the app, `---`, then commands), run whole. Or `flow_upload_id` runs an uploaded zip of flows (`flow` names the entry file; omitted, all of them run). Waits up to ~110s; a longer flow answers status "running" with a command_id for get_command_status. See cheat_sheet for syntax. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | +| `yaml` | string | No | One Maestro flow, e.g. "appId: com.example\n---\n- tapOn: \"Log in\"". | +| `flow_upload_id` | string | No | An uploaded flow bundle (zip). | +| `flow` | string | No | The entry flow inside the bundle, e.g. "flows/login.yaml". | +| `env` | object of strings | No | Secrets and variables for the flow, used in the YAML as ${NAME}, e.g. {"PASSWORD": "..."} with inputText: ${PASSWORD}. Stored encrypted, never shown, and masked in the output. Names starting with DCD_ are reserved. | + +## `get_command_status` + +The status and result of a run that answered "running" because it outlasted the wait. + +Read-only. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | +| `command_id` | string | Yes | The command_id run returned. | + +## `get_logs` + +The installed app's recent device log lines (logcat on Android, the unified log on iOS). Use after a failure or when the session warns that the app crashed. + +Read-only. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | +| `lines` | integer | No | How many lines. Default 200. | + +## `request_human` + +Ask a person to do something on the device that you can't: enter a 2FA code, solve a CAPTCHA, approve a payment sheet, log in with credentials you don't have. The session waits for them (its idle timer pauses, for up to 30 minutes by default) and they see your reason in the DeviceCloud console. Then poll get_human_status until handed_back is true, and read their note. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | +| `reason` | string | Yes | What the person should do, e.g. "Enter the 2FA code sent to the test phone, then hand back". | + +## `get_human_status` + +Whether the session is still waiting for a human (waiting, reason, expires_at), or a human has handed it back (handed_back, with their note). Poll this every 30 seconds or so after request_human. + +Read-only. + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | No | The session id from start_device, e.g. "brave-teal-otter". Optional when your organization has exactly one open session. | + +## `cheat_sheet` + +Maestro syntax and how to work with DeviceCloud device sessions. Read it before your first run. + +Read-only. + +No inputs. + diff --git a/mcp/agent-devices.md b/mcp/agent-devices.md new file mode 100644 index 0000000..ffd4394 --- /dev/null +++ b/mcp/agent-devices.md @@ -0,0 +1,121 @@ +# Agent Devices + +Agent Devices give your AI agent a real cloud iOS simulator or Android emulator to work on. Through DeviceCloud's hosted MCP server, an agent such as Claude Code or Cursor can start a device, install your app, look at the screen, tap and type, run Maestro flows, read the device logs, and ask a person for help when it gets stuck. You can watch every session live in the console and take over the device at any time. + +{% hint style="warning" %} +**Agent Devices are in beta** and are billed at **$0.03/min**, excluding Sales Tax, the same as [Live sessions](../cli/dcd-live.md). Access is by request: contact support to have it turned on for your organization. +{% endhint %} + +{% hint style="warning" %} +We accept no liability for anything your agent may read or execute on a device. LLMs are a new technology: use them with care and at your own risk. +{% endhint %} + +## Connect your agent + +The server is at `https://api.devicecloud.dev/mcp` (Streamable HTTP). Your MCP client signs you in through the DeviceCloud console the first time it connects, or you can give it an API key. + +### Claude Code + +```bash +claude mcp add --transport http devicecloud https://api.devicecloud.dev/mcp +``` + +Then run `/mcp` in Claude Code and choose **devicecloud** to sign in. A browser opens on the DeviceCloud console: sign in, pick the organization the agent should use, and click **Connect**. + +### Cursor + +Add the server to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): + +```json +{ + "mcpServers": { + "devicecloud": { + "url": "https://api.devicecloud.dev/mcp" + } + } +} +``` + +Cursor then asks you to sign in to the server, which opens the DeviceCloud console. + +### Other MCP clients + +Any client that supports Streamable HTTP and OAuth can connect to `https://api.devicecloud.dev/mcp`. It discovers the sign-in from the server's `401` response, registers itself, and opens the DeviceCloud console for you to approve it. + +### With an API key instead + +For agents that can't open a browser, such as one running in CI, send an [API key](../getting-started/api-keys.md) as a header: + +```bash +claude mcp add --transport http devicecloud https://api.devicecloud.dev/mcp \ + --header "Authorization: Bearer " +``` + +{% hint style="info" %} +When you create a key for an agent, choose **Device sessions only** under **Access**. Such a key can start and drive devices and upload builds, but it can't start test runs or read results. +{% endhint %} + +## Connected apps + +Every client you sign in shows up under **Settings → Account → Connected apps**, with the organization it uses and when it was last used. **Disconnect** stops it working at once. Sessions it starts run as you, and the timeline shows which app sent each command. + +## How an agent works with a device + +1. `list_devices` shows the devices it can start, and any sessions already open. +2. `start_device` leases a device. If the fleet is busy, the session is queued, which isn't an error: it starts on its own, and the agent waits for it. +3. `install_app` installs a build you uploaded, or one at an HTTPS link. `launch_app` starts it. +4. The agent loops: `inspect_screen` to see what's on screen, then `run` to act with Maestro commands, then `inspect_screen` again. `take_screenshot` shows the screen as an image, and `get_logs` reads the app's device log. +5. `stop_device` releases the device when it's done. + +Every response carries a short **session** block: the session's state, how long it has left, its cost so far, and plain-English warnings, such as an upcoming idle timeout or the app having crashed. The agent never needs a separate call to stay oriented. + +The full list of tools is in the [tool reference](agent-devices-tools.md). + +## When the agent needs a person + +Some things only a person can do: enter a 2FA code, solve a CAPTCHA, approve a payment sheet. The agent calls `request_human` with what it needs, and then: + +- the session waits, and its idle timer pauses, for up to 30 minutes +- whoever started the session gets an email, and the organization's [webhook](../notifications/webhook-notifications.md) receives a `device.needs_human` event +- the session shows under **Needs you** on the Devices page in the console + +Open the session, do what it asks (use **Take control** to tap and type on the device yourself), then click **Hand back**, optionally with a note for the agent. The agent reads the note with `get_human_status` and carries on. + +If nobody hands the session back in time, it ends. + +## Watching and taking over + +The **Devices** page in the console lists your organization's sessions: those that need you, those running, those queued (with their place in the queue), and those that ended in the last day. + +Open one to watch it. The screen is on the left and the timeline of everything the agent did is on the right, with the screen after each action. **Take control** pauses the agent: while you have control, its commands are refused and it waits. Click the screen to tap, use **Back**, **Home** and **Enter**, or type into the focused field. **Hand back** returns control to the agent. + +## Secrets + +Never put a password or token into a flow's YAML. Pass it in `run`'s `env` and refer to it by name: + +```yaml +- tapOn: "Password" +- inputText: ${PASSWORD} +``` + +with `env: { "PASSWORD": "..." }`. Values sent this way are encrypted before they're stored, they never appear in the timeline, and they're masked in the command's output. + +## Recordings and artifacts + +Each session is recorded on video by default (`record: false` on `start_device` turns it off). A screenshot and a screen dump are kept after every action, along with the device log. The screenshots are on the session's timeline in the console. The video, the log and the screen dumps are available from the [REST API](../api/device-sessions.md#artifacts). + +## Limits + +| Limit | Default | +|-------|---------| +| Sessions open at once, per organization | 2 | +| Maximum session length | 120 minutes | +| Idle timeout (no commands) | 5 minutes | +| Waiting for a person | 30 minutes | +| Waiting in the queue | 15 minutes | + +The idle timeout is fixed. Contact support to change the other limits for your organization, or to set a monthly spend cap on device sessions. + +## Pricing + +Device time is billed at $0.03 per minute, in 30-second steps, from when a device is assigned to the session until the session ends. Time spent queued isn't billed. Your organization's discount applies. diff --git a/notifications/webhook-notifications.md b/notifications/webhook-notifications.md index 7d021d7..9829437 100644 --- a/notifications/webhook-notifications.md +++ b/notifications/webhook-notifications.md @@ -71,7 +71,7 @@ User-Agent: DeviceCloud-Webhooks/1.0 | Field | Description | | --- | --- | -| `event` | Always `upload.completed`. | +| `event` | `upload.completed` for a finished run. Device sessions add [`device.needs_human`](#device-needs-human). | | `status` | Overall run outcome: `PASSED` only when every test passed, otherwise `FAILED`. Use this to gate a deploy or set a commit status. | | `device` | Device and runner context for the run: `name`, `osVersion`, `runnerType`, and `maestroVersion`. Individual fields are omitted when unavailable — `name` and `osVersion` are left out when the run's flows used more than one device. | | `summary` | Aggregate counts (`totalTests`, `passed`, `failed`, and optional `cancelled`/`queued`/`pending`/`running`) plus `durationSeconds`, `wallClockDurationSeconds`, and `retryCount`. | @@ -121,6 +121,29 @@ This is on by default, so automations always see the latest outcome. To be calle Your endpoint should therefore treat `upload_id` as the key for a run and keep the delivery with the latest `timestamp`, rather than assuming one request per run. +### Device needs human + +When an [Agent Devices](../mcp/agent-devices.md) session asks for a person's help, your webhook receives a `device.needs_human` event. It's sent once each time a session starts waiting, with the same headers and delivery as `upload.completed`: + +```json +{ + "event": "device.needs_human", + "timestamp": "2026-10-19T10:00:00.000Z", + "session": { + "id": "brave-teal-otter", + "platform": "ios", + "model": "iPhone 16", + "client_label": "claude-code" + }, + "reason": "Enter the 2FA code sent to the test phone", + "since": "2026-10-19T10:00:00.000Z", + "expires_at": "2026-10-19T10:30:00.000Z", + "console_url": "https://console.devicecloud.dev/device/brave-teal-otter" +} +``` + +`reason` is what the agent asked for. `expires_at` is when the session ends unless someone hands it back. `console_url` opens the session in the console, where you can take control and hand it back. + ### Webhook Secrets Every request includes the secret shown on your DeviceCloud console, sent as-is in the `X-DeviceCloud-Secret` header. To verify a request came from DeviceCloud, compare that header with your secret using a constant-time comparison (such as `crypto.timingSafeEqual` in Node.js or `hmac.compare_digest` in Python) and reject the request if they don't match. From 1c63b9fa10d82a823f8afe4a26111304d350cbf8 Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Mon, 19 Oct 2026 11:27:31 +0100 Subject: [PATCH 2/5] docs: dcd device, dcd mcp and flow bundles for Agent Devices - New CLI page for dcd device: every subcommand, the --json envelope, exit codes and the default-session rule. - Agent Devices: connecting through dcd mcp, with local files. - Device sessions API: the flow bundle endpoints. - dcd live is marked deprecated in favour of dcd device. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA --- SUMMARY.md | 1 + api/device-sessions.md | 13 +++- cli/dcd-device.md | 126 +++++++++++++++++++++++++++++++++++++ cli/dcd-live.md | 4 ++ cli/overview.md | 6 +- mcp/agent-devices-tools.md | 2 +- mcp/agent-devices.md | 10 +++ 7 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 cli/dcd-device.md diff --git a/SUMMARY.md b/SUMMARY.md index d83b193..ce493f8 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -56,6 +56,7 @@ * [Status](cli/dcd-status.md) * [List](cli/dcd-list.md) * [Login & Accounts](cli/dcd-login.md) +* [Devices](cli/dcd-device.md) * [Live Sessions](cli/dcd-live.md) ## REST API diff --git a/api/device-sessions.md b/api/device-sessions.md index 4979732..bf3dc70 100644 --- a/api/device-sessions.md +++ b/api/device-sessions.md @@ -21,12 +21,23 @@ A session is named by its `id` (for example `brave-teal-otter`) in every path. E | `POST /device/sessions/:id/install` | Install a build: `{ "binaryUploadId": "..." }`, or `{ "url": "https://..." }` for an `.apk` or a zipped simulator `.app` at a link (up to 500 MB) | | `POST /device/sessions/:id/launch` | Launch an app: `{ "appId"?: "...", "clearState"?: true }`. Without `appId`, the installed build | | `POST /device/sessions/:id/actions` | One action: `tap` (`x`/`y` percent, `text` or `id`), `type` (`text`, and `sensitive: true` to keep it secret), `swipe` (`direction`, or `from`/`to`), `press_key` (`key`) | -| `POST /device/sessions/:id/run` | Run Maestro: `{ "yaml": "..." }` for one flow, or `{ "flowUploadId": "...", "flow"?: "..." }` for an uploaded bundle. `env` passes secrets and variables (see below). Waits about 110 s, then answers `running` | +| `POST /device/sessions/:id/run` | Run Maestro: `{ "yaml": "..." }` for one flow, or `{ "flowUploadId": "...", "flow"?: "..." }` for a [flow bundle](#flow-bundles). `env` passes secrets and variables (see below). Waits about 110 s, then answers `running` | | `GET /device/sessions/:id/commands/:commandId` | The status and result of a `run` that answered `running` | | `GET /device/sessions/:id/inspect` | The screen as compact JSON | | `GET /device/sessions/:id/screenshot` | The latest screenshot, as a data URL | | `GET /device/sessions/:id/logs?lines=200` | The installed app's recent device log lines | +## Flow bundles + +To run several flows, or a flow that runs others, upload them as a zip, then pass its id to `run` as `flowUploadId`. `flow` names the entry file inside the zip; without it, every flow runs. [`dcd device run `](../cli/dcd-device.md) does this for you. + +| Method and path | What it does | +|---|---| +| `POST /device/bundles/upload-url` | `{ "bytes"?: 1234 }`. Returns the bundle's `id`, and signed URLs to `PUT` the zip to: `upload` and `mirror` (either may be `null`). Upload to each one given | +| `POST /device/bundles` | Register it: `{ "id", "sha", "bytes", "supabase": true, "backblaze": true }`, where `sha` is the SHA-256 of the zip in hex, and `supabase` and `backblaze` say which URLs you uploaded to. Returns `{ "bundle" }` | + +A bundle can be up to 50 MB. Bundles are deleted a day after they're uploaded, once no command needs them. + ## Asking a person | Method and path | What it does | diff --git a/cli/dcd-device.md b/cli/dcd-device.md new file mode 100644 index 0000000..85cbed8 --- /dev/null +++ b/cli/dcd-device.md @@ -0,0 +1,126 @@ +# dcd device + +Lease a cloud iOS simulator or Android emulator, drive it, and release it, from your terminal or a script. These are the same device sessions an AI agent uses through [Agent Devices](../mcp/agent-devices.md): you can watch any of them in the console, and they show on the same Devices page. + +{% hint style="warning" %} +**Device sessions are in beta** and are billed at **$0.03/min**, excluding Sales Tax. Access is by request: contact support to have it turned on for your organization. `dcd device` works with a [`dcd login`](dcd-login.md) session or an [API key](../getting-started/api-keys.md). +{% endhint %} + +```bash +dcd device [flags] +``` + +A typical session: + +```bash +dcd device start --platform ios --app ./build/MyApp.app --wait +dcd device launch +dcd device inspect +dcd device run flows/login.yaml +dcd device screenshot -o after-login.jpg +dcd device stop +``` + +## Rules for every subcommand + +- **The session id is optional** when your organization has exactly one open session. With none, or several, the command stops and lists the sessions to choose from. +- **`--json`** prints exactly one JSON object on stdout, on success and on failure: + + ```json + { "ok": true, "session": { "id": "brave-teal-otter", "state": "active", "...": "..." }, "data": { }, "error": null } + ``` + + `session` is the same session block the [REST API](../api/device-sessions.md) returns: state, time and idle time left, cost so far, warnings, and the console link. On failure, `ok` is `false` and `error` is `{ "code", "message" }`, where `code` is the API's error code (such as `LIVE_SPEND_CAP`) or `null`. +- **Exit codes:** + + | Code | Meaning | + |---|---| + | `0` | Done | + | `1` | Called wrongly: a bad flag or value, or no session to pick | + | `2` | It ran and failed: a flow or launch failed, or the API couldn't do it | + | `3` | A limit was hit: concurrent sessions, spend cap, or rate limit | + | `4` | The session has ended, or there is no such session | + +- **Progress** goes to stderr, so stdout holds only the result. There are no prompts, and no spinners unless you are at a terminal. + +Every subcommand also takes `--api-key` and `--api-url`. + +## `dcd device start` + +Start a session. Without `--wait` it returns at once, possibly with the session still queued; it starts on its own. + +| Flag | Description | +|------|-------------| +| `--platform ` | The platform. Optional with `--app`, which implies it | +| `--model ` | A device model, such as `iphone-16` or `pixel-10`. See `dcd device models` | +| `--os ` | The iOS major version or Android API level, such as `18` or `35` | +| `--google-play` | Android only: an image with Google Play services | +| `--app ` | A build to install: a local `.apk`, `.app`, `.zip` or Expo `.tar.gz` (uploaded first), or an HTTPS link to an `.apk` or a zipped `.app` | +| `--app-binary-id ` | A build you already uploaded | +| `--locale ` | The device locale, such as `de_DE` | +| `--name