Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 29 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,11 @@ See full documentation: [Docs](https://docs.devicecloud.dev)

## MCP server

devicecloud.dev has an [MCP](https://modelcontextprotocol.io) server, so AI agents (Claude, Codex, Cursor, VS Code) can run cloud tests and drive cloud devices. You don't run it yourself. You add it to your MCP client's config, and the client starts it.
`dcd mcp` is an [MCP](https://modelcontextprotocol.io) server, so AI agents (Claude, Codex, Cursor, VS Code) can run cloud tests and read their results. You don't run it yourself. You add it to your MCP client's config, and the client starts it. It works the same on your machine and headless, such as in CI.

There are three ways to connect:
### Set it up with `dcd mcp install`

| | Install | Tools | Use it when |
| --- | --- | --- | --- |
| **`dcd mcp`** (recommended) | the CLI | device tools + `dcd_*` cloud-test tools | you want everything, including installing a local build or running local flow files |
| **Hosted** `https://api.devicecloud.dev/mcp` | nothing | device tools | the agent only drives devices, with apps and flows that are already uploaded or at a URL |
| **`dcd-mcp`** | the CLI | `dcd_*` cloud-test tools | you only run cloud tests (the older server; `dcd mcp` covers it) |

### `dcd mcp`

`dcd mcp` connects to the hosted server and passes its device tools through. It adds what only a local process can do: `install_app` takes `app_path` (a build on your machine, uploaded first) and `run` takes `path` (a flow file, or a directory uploaded as a bundle). It also serves the `dcd_*` tools below.

**Set it up with `dcd mcp install`.** Install the CLI (see the top of this page), sign in, then add the server to your AI tool:
Install the CLI (see the top of this page), sign in, then add the server to your AI tool:

```sh-session
$ dcd login
Expand All @@ -59,7 +49,19 @@ $ dcd mcp install cursor # or claude-code, claude-desktop, codex, vscode

Leave out the client name to pick from a list. Add `--read-only` for a server that only offers tools that change nothing, or `--print` to see what would be added without changing anything. For Claude Code, Codex and VS Code it runs `claude mcp add`, `codex mcp add` and `code --add-mcp`. For Claude Desktop and Cursor it adds an entry to their JSON config file and keeps everything else in it. Restart the app afterwards.

**Setting it up by hand.** Start dcd by its **full path**. Desktop apps such as Claude Desktop and Cursor are usually opened from the Dock or Start menu, so they don't see the PATH your shell sets up, and a bare `dcd` gives "command not found". The installer prints the path at the end; it's `~/.dcd/bin/dcd` by default, written out in full because config files don't expand `~`.
### Headless, with an API key

For an agent that runs without you, such as in CI or a script, sign in with an API key instead of `dcd login`. Create one in the console under Settings → API Keys, and give it to the server as `DEVICE_CLOUD_API_KEY` in its environment. For example, for Claude Code in CI:

```sh-session
$ claude mcp add --scope user devicecloud -e DEVICE_CLOUD_API_KEY="$DEVICE_CLOUD_API_KEY" -- dcd mcp
```

In a JSON config, put it in the server's `env`: `"env": { "DEVICE_CLOUD_API_KEY": "<your-api-key>" }`. Store the key as a secret in your CI provider, never in the repository.

### Setting it up by hand

Start dcd by its **full path**. Desktop apps such as Claude Desktop and Cursor are usually opened from the Dock or Start menu, so they don't see the PATH your shell sets up, and a bare `dcd` gives "command not found". The installer prints the path at the end; it's `~/.dcd/bin/dcd` by default, written out in full because config files don't expand `~`.

Claude Desktop (`claude_desktop_config.json`) and Cursor (`~/.cursor/mcp.json`):

Expand Down Expand Up @@ -105,56 +107,25 @@ $ codex mcp add devicecloud -- dcd mcp

If you use the npm package instead of the standalone binary, the command is `npx` with `"args": ["-y", "@devicecloud.dev/dcd", "mcp"]`. That needs Node 22 or newer on the app's PATH, and it checks npm each time it starts.

It signs in the same way as the CLI: run `dcd login` once, or set `DEVICE_CLOUD_API_KEY` in `env`. Add `--read-only` to the args (or set `DCD_MCP_READONLY=1`) to serve only the tools that change nothing. If the hosted server can't be reached, it serves the `dcd_*` tools only and says why on stderr.

**Device tools:** `list_devices`, `start_device`, `get_device_status`, `install_app`, `launch_app`, `inspect_screen`, `take_screenshot`, `run`, `get_command_status`, `get_logs`, `request_human`, `get_human_status`, `stop_device`, and `cheat_sheet` (Maestro syntax for the agent).

### Hosted
It signs in the same way as the CLI: with your `dcd login` session, which it renews as it runs, or with `DEVICE_CLOUD_API_KEY`. Point it at a non-prod environment with `DCD_API_URL`.

Point a client that speaks Streamable HTTP at `https://api.devicecloud.dev/mcp`. It signs you in through the browser (OAuth) the first time it connects, and you can disconnect it later under Settings → Account → Connected apps.
### Tools

Claude Code (then run `/mcp` in Claude Code and choose `devicecloud` to sign in):

```sh-session
$ claude mcp add --scope user --transport http devicecloud https://api.devicecloud.dev/mcp
```

Cursor (`~/.cursor/mcp.json`):

```jsonc
{
"mcpServers": {
"devicecloud": { "url": "https://api.devicecloud.dev/mcp" }
}
}
```

VS Code (`.vscode/mcp.json`, or the user `mcp.json`), or `code --add-mcp '{"name":"devicecloud","type":"http","url":"https://api.devicecloud.dev/mcp"}'`:

```jsonc
{
"servers": {
"devicecloud": { "type": "http", "url": "https://api.devicecloud.dev/mcp" }
}
}
```

Codex:

```sh-session
$ codex mcp add devicecloud --url https://api.devicecloud.dev/mcp
$ codex mcp login devicecloud
```

Claude Desktop: open Settings → Connectors, choose **Add custom connector**, name it DeviceCloud and paste the URL. Its config file only takes local servers.
| Tool | What it does |
| --- | --- |
| `dcd_list_devices` | Discover available devices, OS versions, and Maestro versions |
| `dcd_list_runs` | List recent test runs (filter by name/date, paginated) |
| `dcd_get_status` | Get the status + per-test results of a run |
| `dcd_download_artifacts` | Download a run's artifacts/report to disk |
| `dcd_run_cloud_test` | Submit a flow to run on the cloud (**billable**) |

**Read-only.** `https://api.devicecloud.dev/mcp?read_only=true` serves only the device tools that change nothing (`list_devices`, `get_device_status`, `inspect_screen`, `take_screenshot`, `get_command_status`, `get_logs`, `get_human_status`, `cheat_sheet`). Quote it on a command line, since `?` is a wildcard in zsh: `claude mcp add --scope user --transport http devicecloud 'https://api.devicecloud.dev/mcp?read_only=true'`. Like `--read-only` locally, it keeps an agent from changing anything by mistake; it isn't a permission.
By default `dcd_run_cloud_test` is async: it returns an `uploadId` immediately, which you poll with `dcd_get_status`. Pass `wait: true` (bounded by `waitTimeoutSeconds`) to block until completion, or `dryRun: true` to preview the flows without submitting.

**API key instead of signing in.** For an agent that can't open a browser, such as one in CI, send an API key from the console (Settings → API Keys) as `Authorization: Bearer <your-api-key>`, for example with `claude mcp add ... --header "Authorization: Bearer <your-api-key>"`.
**Read-only mode.** `dcd_run_cloud_test` consumes test minutes, so it is annotated as non-read-only/destructive (clients can prompt before calling it). To hide it entirely, which is recommended for autonomous or untrusted agents, add `--read-only` after `mcp` in the args (or pass it to `dcd mcp install`), or set `DCD_MCP_READONLY=1` in `env`.

### `dcd-mcp`

The older server, with the `dcd_*` tools only. Add it to your MCP client config:
The npm package also ships `dcd-mcp`, an older server with the same `dcd_*` tools. `dcd mcp` covers it, so you only need it for a setup that already uses it:

```jsonc
{
Expand All @@ -168,23 +139,7 @@ The older server, with the `dcd_*` tools only. Add it to your MCP client config:
}
```

`--package` matters: the package's default bin is `dcd`, so `npx @devicecloud.dev/dcd dcd-mcp` would run `dcd dcd-mcp` (an unknown command) instead of the server.

Auth is inherited from the CLI: set `DEVICE_CLOUD_API_KEY` as above, or run `dcd login` once and the server picks up the stored session. Point it at a non-prod environment with `DCD_API_URL`.

**`dcd_*` tools** (served by both `dcd mcp` and `dcd-mcp`)

| Tool | What it does |
| --- | --- |
| `dcd_list_devices` | Discover available devices, OS versions, and Maestro versions |
| `dcd_list_runs` | List recent test runs (filter by name/date, paginated) |
| `dcd_get_status` | Get the status + per-test results of a run |
| `dcd_download_artifacts` | Download a run's artifacts/report to disk |
| `dcd_run_cloud_test` | Submit a flow to run on the cloud (**billable**) |

**Read-only mode.** `dcd_run_cloud_test` consumes test minutes, so it is annotated as non-read-only/destructive (clients can prompt before calling it). To hide it entirely — recommended for autonomous or untrusted agents — pass `--read-only` at the end of `args` (`["-y", "--package=@devicecloud.dev/dcd", "dcd-mcp", "--read-only"]`), or set `DCD_MCP_READONLY=1` in `env`.

By default `dcd_run_cloud_test` is async: it returns an `uploadId` immediately, which you poll with `dcd_get_status`. Pass `wait: true` (bounded by `waitTimeoutSeconds`) to block until completion, or `dryRun: true` to preview the flows without submitting.
`--package` matters: the package's default bin is `dcd`, so `npx @devicecloud.dev/dcd dcd-mcp` would run `dcd dcd-mcp` (an unknown command) instead of the server. It signs in like `dcd mcp`, and takes `--read-only` and `DCD_MCP_READONLY=1` the same way.


## Development
Expand Down
10 changes: 8 additions & 2 deletions src/mcp/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ import { getContext, isReadOnly, logStderr } from './context.js';
import { errorResult, runTool } from './helpers.js';
import { createServer } from './server.js';

/*
* Sent at initialize, before this server knows which hosted tools the
* account has: the hosted server only serves its tools to organizations that
* have them. So these name only the dcd_* tools, and the hosted tools explain
* themselves in their own descriptions.
*/
const INSTRUCTIONS =
'DeviceCloud tools. dcd_* tools run Maestro test suites in the cloud. The device tools lease a cloud iOS or Android device for you to drive: start_device, then inspect_screen / take_screenshot, launch_app, run, and stop_device when done. Call cheat_sheet first for Maestro syntax. Local files: install_app takes app_path and run takes path (a flow file or a directory), which this server uploads for you.';
'DeviceCloud tools. dcd_* tools run Maestro test suites in the cloud: dcd_list_devices shows what you can test on, dcd_run_cloud_test submits flows (billable), and dcd_get_status follows a run. Any other tools describe their own use. Where a tool takes a path to a local file (an app build, a flow file or a directory), this server uploads it for you.';

/**
* How long one forwarded call may take. Longer than dcd_run_cloud_test's
Expand All @@ -51,7 +57,7 @@ const INSTRUCTIONS =
export const CALL_TIMEOUT_MS = 2 * 60 * 60 * 1000;

const READ_ONLY_INSTRUCTIONS =
'DeviceCloud tools, read-only: nothing here starts a device, runs a test or changes anything. dcd_* tools read cloud test runs and their results. The device tools read open device sessions: list_devices, get_device_status, inspect_screen, take_screenshot, get_logs.';
'DeviceCloud tools, read-only: nothing here runs a test or changes anything. dcd_* tools read cloud test runs and their results. Any other tools describe their own use.';

/** The inputs this bridge adds to hosted tools, and what they say. */
const LOCAL_INPUTS: Record<string, { description: string; properties: Record<string, object> }> = {
Expand Down
13 changes: 13 additions & 0 deletions test/unit/mcp-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,19 @@ describe('dcd mcp bridge', () => {
await client.close();
});

it("names only the dcd_* tools in its instructions, since the hosted ones depend on the account", async () => {
for (const readOnly of [false, true]) {
if (readOnly) process.env.DCD_MCP_READONLY = '1';
const client = await connect();
const instructions = client.getInstructions() ?? '';
expect(instructions).to.include('dcd_');
for (const hosted of ['start_device', 'inspect_screen', 'install_app', 'cheat_sheet', 'device session']) {
expect(instructions).to.not.include(hosted);
}
await client.close();
}
});

it('read-only mode still refuses on an API that lists every tool', async () => {
hosted.ignoreReadOnly = true;
process.env.DCD_MCP_READONLY = '1';
Expand Down
Loading