From 55f0c43a2fa4cfe00eba516b45b64b2b7ff8720b Mon Sep 17 00:00:00 2001 From: finalerock44 Date: Mon, 9 Nov 2026 18:30:01 +0000 Subject: [PATCH] docs(mcp): dcd mcp for cloud tests and headless use The README's MCP section is now about dcd mcp: setting it up with dcd mcp install, running it headless with DEVICE_CLOUD_API_KEY (for example in CI), setting it up by hand, its dcd_* tools and read-only mode, and the older dcd-mcp. The server's instructions are sent before it knows which hosted tools an account has, so they now name only the dcd_* tools; other tools explain themselves in their own descriptions. --- README.md | 103 ++++++++++------------------------- src/mcp/bridge.ts | 10 +++- test/unit/mcp-bridge.test.ts | 13 +++++ 3 files changed, 50 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 9df9430..646a023 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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": "" }`. 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`): @@ -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 `, for example with `claude mcp add ... --header "Authorization: Bearer "`. +**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 { @@ -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 diff --git a/src/mcp/bridge.ts b/src/mcp/bridge.ts index bc446b7..5614c77 100644 --- a/src/mcp/bridge.ts +++ b/src/mcp/bridge.ts @@ -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 @@ -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 }> = { diff --git a/test/unit/mcp-bridge.test.ts b/test/unit/mcp-bridge.test.ts index c15683e..e946c73 100644 --- a/test/unit/mcp-bridge.test.ts +++ b/test/unit/mcp-bridge.test.ts @@ -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';