From 36e37f6283ef7bf5c7b3e2dc2f1501729c103f2b Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Mon, 26 Oct 2026 09:16:04 +0000 Subject: [PATCH 1/2] docs(mcp): say the mcp command is for MCP clients to start Reword the dcd mcp help line so it is clear an MCP client starts it, not a person. Add a README section on dcd mcp and the hosted endpoint, next to the existing dcd-mcp docs. Co-Authored-By: Claude Opus 5.5 --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++--- src/commands/mcp.ts | 2 +- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 04b861f..b5a4cc5 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,56 @@ See full documentation: [Docs](https://docs.devicecloud.dev) ## MCP server -The same npm package ships an [MCP](https://modelcontextprotocol.io) server (`dcd-mcp` bin) so AI agents — Claude, Cursor, VS Code — can drive devicecloud.dev directly. +devicecloud.dev has an [MCP](https://modelcontextprotocol.io) server, so AI agents (Claude, 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. -Add it to your MCP client config: +There are three ways to connect: + +| | 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. + +Claude Code: + +```sh-session +$ claude mcp add devicecloud -- npx -y @devicecloud.dev/dcd mcp +``` + +Other clients: + +```jsonc +{ + "mcpServers": { + "devicecloud": { + "command": "npx", + "args": ["-y", "@devicecloud.dev/dcd", "mcp"] + } + } +} +``` + +If the CLI is installed globally, use `"command": "dcd", "args": ["mcp"]` instead. + +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 + +Point an HTTP-capable client at the URL. It signs you in through the browser (OAuth), and you can disconnect it later under Settings → Connected apps. + +```sh-session +$ claude mcp add --transport http devicecloud https://api.devicecloud.dev/mcp +``` + +### `dcd-mcp` + +The older server, with the `dcd_*` tools only. Add it to your MCP client config: ```jsonc { @@ -56,7 +103,7 @@ Add it to your MCP client config: 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`. -**Tools** +**`dcd_*` tools** (served by both `dcd mcp` and `dcd-mcp`) | Tool | What it does | | --- | --- | diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts index d6c797c..3b9dd56 100644 --- a/src/commands/mcp.ts +++ b/src/commands/mcp.ts @@ -14,7 +14,7 @@ import { telemetry } from '../services/telemetry.service.js'; export const mcpCommand = defineCommand({ meta: { name: 'mcp', - description: 'Run the DeviceCloud MCP server on stdio (cloud tests and cloud devices)', + description: 'MCP server for AI agents, started by your MCP client (Claude, Cursor, VS Code)', }, args: { 'read-only': { From f4fa651df5fe33241470bc4b76c3dddc5ec9604c Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Mon, 26 Oct 2026 09:28:37 +0000 Subject: [PATCH 2/2] fix(device): fit start's next steps to the session state A queued or provisioning session can't be inspected yet, so start now suggests dcd device status first and points at --wait. It suggests install when no app was given, and ends with dcd device --help. Co-Authored-By: Claude Opus 5.5 --- src/commands/device.ts | 37 ++++++++++++++++++++++++-------- test/unit/device-command.test.ts | 20 +++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/commands/device.ts b/src/commands/device.ts index 3f63694..1e7f265 100644 --- a/src/commands/device.ts +++ b/src/commands/device.ts @@ -234,6 +234,28 @@ export function platformOfModel(catalogue: DeviceCatalogue, model: string): 'and throw new CliError(`"${model}" is a model on both iOS and Android. Pass --platform ios or --platform android.`); } +/** + * The "Next steps" `dcd device start` prints. They follow the session: a + * device that isn't ready yet can't be inspected, so that comes first, and + * a session with no app gets the install step. + */ +export function startNextSteps(session: DeviceSession, hasApp: boolean): [string, string][] { + const { id } = session; + const steps: [string, string][] = []; + if (!USABLE_STATES.has(session.state)) { + steps.push(['check it is ready', `dcd device status ${id}`]); + } + + if (!hasApp) steps.push(['install an app', `dcd device install ${id} `]); + steps.push( + ['inspect the screen', `dcd device inspect ${id}`], + ['run a flow', `dcd device run ${id} flow.yaml`], + ['stop', `dcd device stop ${id}`], + ['all commands', 'dcd device --help'], + ); + return steps; +} + const startSub = defineCommand({ meta: { name: 'start', description: 'Start a device session' }, args: { @@ -373,15 +395,12 @@ const startSub = defineCommand({ logger.log(ui.section('Device session')); printSession(session); logger.log(ui.section('Next steps')); - logger.log( - ui.branch( - ui.fields([ - ['inspect the screen', colors.highlight(`dcd device inspect ${session.id}`)], - ['run a flow', colors.highlight(`dcd device run ${session.id} flow.yaml`)], - ['stop', colors.highlight(`dcd device stop ${session.id}`)], - ]), - ), - ); + const hasApp = Boolean(installedId ?? uploadedId); + const steps = startNextSteps(session, hasApp); + logger.log(ui.branch(ui.fields(steps.map(([label, cmd]) => [label, colors.highlight(cmd)])))); + if (!USABLE_STATES.has(session.state)) { + logger.log(ui.info('Next time, `dcd device start --wait` waits until the device is ready.')); + } }, }; }); diff --git a/test/unit/device-command.test.ts b/test/unit/device-command.test.ts index af7d334..62d4869 100644 --- a/test/unit/device-command.test.ts +++ b/test/unit/device-command.test.ts @@ -11,6 +11,7 @@ import { deviceCommand, newLogLines, parseEnvFlags, + startNextSteps, } from '../../src/commands/device.js'; import { DeviceApiError } from '../../src/gateways/device-gateway.js'; import type { DeviceSession } from '../../src/types/domain/device-session.types.js'; @@ -730,6 +731,25 @@ describe('dcd device', () => { }); describe('helpers', () => { + it('startNextSteps fits the session: check readiness first, install only with no app', () => { + const cmds = (s: DeviceSession, hasApp: boolean) => startNextSteps(s, hasApp).map(([, cmd]) => cmd); + + expect(cmds(session('a', { state: 'queued' }), false)).to.deep.equal([ + 'dcd device status a', + 'dcd device install a ', + 'dcd device inspect a', + 'dcd device run a flow.yaml', + 'dcd device stop a', + 'dcd device --help', + ]); + expect(cmds(session('a', { state: 'ready' }), true)).to.deep.equal([ + 'dcd device inspect a', + 'dcd device run a flow.yaml', + 'dcd device stop a', + 'dcd device --help', + ]); + }); + it('formatWhen says how far off a time is', () => { const now = Date.parse('2026-10-19T10:00:00Z'); expect(formatWhen('2026-10-19T10:12:00Z', now)).to.match(/^in 12m \(.+\)$/);