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
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

The CLI is citty-native: `src/index.ts` has a `#!/usr/bin/env node` shebang (preserved by tsc), and `package.json` points `bin.dcd` directly at `dist/index.js`. There is no `bin/` wrapper directory — don't add one.

The package ships a **second bin, `dcd-mcp`** (`bin.dcd-mcp` → `dist/mcp/index.js`, also shebanged + chmod'd by `pnpm build`). It's the MCP server — see the MCP section under Architecture.

## Architecture

Top-level `defineCommand` in `src/index.ts` wires ten subcommands (`cloud`, `upload`, `list`, `status`, `artifacts`, `live`, plus the auth-related `login`, `logout`, `whoami`, `switch-org`). `cloud` is the primary command and replicates `maestro cloud`.
Expand All @@ -39,3 +41,5 @@ Top-level `defineCommand` in `src/index.ts` wires ten subcommands (`cloud`, `upl
**Cross-repo auth surface.** The dcd API's `ApiKeyGuard` accepts either `x-app-api-key` (existing) or `Authorization: Bearer <jwt>` + `x-dcd-org: <id>`. For Bearer it verifies the JWT, checks `user_org_profile` membership, and injects the org's api_key back into the request headers so existing `@Headers(APP_API_KEY_HEADER)` controller code keeps working unchanged. `dcd switch-org` calls `GET /me/orgs`, a JWT-only endpoint at `../dcd/api/src/apps/me/me.controller.ts`.

**Telemetry.** `src/services/telemetry.service.ts` ships lifecycle (`command started` / `command completed` / `command failed`) and error events to the dcd API's `/cli/logs` proxy → Axiom `cli-dev` / `cli-prod`. Wired in at three points: `src/index.ts` replicates citty's `runMain` (which would otherwise swallow errors and exit 1) to record start/success/failure and honor `CliError.exitCode`; `src/utils/auth.ts` calls `telemetry.configure({ auth })` from `resolveAuth` so the token never has to be re-derived; `src/utils/cli.ts` `logger.error` calls `telemetry.flushSync()` (which shells out to `curl` because `process.exit` bypasses `beforeExit`) before exiting. Unauthenticated invocations (`--help`, `--version`, `dcd login` pre-success) buffer in memory and drop on exit — by design, since there's no identity to attach. Opt out per-invocation with `DCD_TELEMETRY_DISABLED=1`.

**MCP server.** `src/mcp/` is a third front-end onto the same service layer (a sibling to `src/commands/`), shipped as the `dcd-mcp` bin over stdio transport (`@modelcontextprotocol/sdk`, `zod` schemas). `index.ts` boots the server; `server.ts` registers tools; `context.ts` resolves auth + API URL **lazily and once** (so `tools/list` works unauthenticated and auth errors surface as tool errors, not a boot crash) via the same `resolveAuth`/`resolveApiUrl` as the CLI — `DEVICE_CLOUD_API_KEY` env or stored `dcd login` session, with `DCD_API_URL` to override. **Critical invariant: stdout is the JSON-RPC channel** — tools must never call the `src/commands/*` layer or `utils/cli` `logger` (both write to stdout / can `process.exit`); they call services/gateways directly with `logStderr` and return data via `helpers.ts` `jsonResult`/`errorResult`. The `runTool` wrapper records `mcp tool …` telemetry and converts thrown errors to `isError` results. Tools: `dcd_list_devices`, `dcd_list_runs`, `dcd_get_status`, `dcd_download_artifacts` (read-only), and `dcd_run_cloud_test` (billable — gated out by `--read-only` / `DCD_MCP_READONLY=1`, annotated destructive, async-by-default). `dcd_run_cloud_test` reuses `computeCommonRoot`/`buildTestMetadataMap` from `src/services/flow-paths.ts` (extracted from `cloud.ts` so both build identical server-side paths). Registry manifest: `server.json` at repo root.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,41 @@ $ dcd cloud --apiKey <apiKey> <appFile> .myFlows/
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.

Add it to your MCP client config:

```jsonc
{
"mcpServers": {
"devicecloud": {
"command": "npx",
"args": ["-y", "@devicecloud.dev/dcd", "dcd-mcp"],
"env": { "DEVICE_CLOUD_API_KEY": "<your-api-key>" }
}
}
}
```

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**

| 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` in `args`, 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.


## Development

Requires Node 22+ and [pnpm](https://pnpm.io). `pnpm install` builds the CLI and installs the git hooks automatically.
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"author": "devicecloud.dev",
"bin": {
"dcd": "dist/index.js"
"dcd": "dist/index.js",
"dcd-mcp": "dist/mcp/index.js"
},
"dependencies": {
"@clack/prompts": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@supabase/supabase-js": "^2.99.1",
"bplist-parser": "^0.3.2",
"chalk": "4.1.2",
Expand All @@ -15,7 +17,8 @@
"plist": "^3.1.0",
"tar": "^7.5.11",
"tus-js-client": "^4.3.1",
"yazl": "^3.3.1"
"yazl": "^3.3.1",
"zod": "^4.4.3"
},
"description": "Better cloud maestro testing",
"devDependencies": {
Expand Down Expand Up @@ -59,7 +62,7 @@
},
"scripts": {
"dcd": "tsx src/index.ts",
"build": "shx rm -rf dist && tsc -b && shx chmod +x dist/index.js",
"build": "shx rm -rf dist && tsc -b && shx chmod +x dist/index.js dist/mcp/index.js",
"build:binaries": "node scripts/build-binaries.mjs",
"lint": "eslint src test --ext .ts",
"prepare": "pnpm build && husky",
Expand Down
Loading
Loading