Skip to content

feat(mcp): add dcd mcp, one stdio server for cloud tests and cloud devices - #237

Merged
riglar merged 20 commits into
feature/agent-devicesfrom
feat/device-mcp-bridge
Oct 19, 2026
Merged

riglar merged 20 commits into
feature/agent-devicesfrom
feat/device-mcp-bridge

Conversation

@riglar

@riglar riglar commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Stacked on #235. Agent Devices stage 4.4. Needs devicecloud-dev/dcd#1719 for dcd login users, and dcd#1718 for directories.

What

dcd mcp runs one MCP server on stdio with two sets of tools:

  • The dcd_* cloud-test tools. These come from the same in-process server dcd-mcp runs, reached over an in-memory transport, so they behave exactly as before.
  • The device tools (start_device, inspect_screen, run, …). These are proxied from the hosted <api>/mcp, so they are defined and contract-tested once, in the API. They are fetched when the client lists tools.

The bridge adds the inputs a remote server can't serve:

Tool New input What the bridge does
install_app app_path Uploads the local build (like dcd device install), then calls the tool with app_binary_id
run path A file is read and sent as yaml. A directory is uploaded as a flow bundle and sent as flow_upload_id

The two tool descriptions get one sentence about these inputs.

Details

  • Auth is the CLI's (getContext): DEVICE_CLOUD_API_KEY or a dcd login session. The hosted /mcp accepts the session with dcd#1719. When a session is renewed, the remote connection is remade.
  • No hosted endpoint (no credentials yet, or an API without /mcp): it serves the dcd_* tools only and says why on stderr. A device tool call then returns a tool error.
  • Read-only (--read-only / DCD_MCP_READONLY=1): only device tools annotated readOnlyHint are listed, and dcd_run_cloud_test is left out as before.
  • stdout carries only JSON-RPC. Uploads run with their progress output off, and messages go to stderr.
  • Hidden from dcd --help until the device API is live, like dcd device in feat(device): add dcd device start, list, status, stop, install, launch and models #231. The launch PR (feat(device): list dcd device and dcd mcp in help, and deprecate dcd live #233) should unhide it too.
  • dcd-mcp and its dcd_* tools are unchanged.

Tests

test/unit/mcp-bridge.test.ts runs the bridge against a local stateless Streamable HTTP server that stands in for the hosted /mcp. It covers:

  • listing both sets of tools, with the extra inputs
  • forwarding a call with the CLI's credentials
  • sending a local flow file as yaml
  • read-only filtering
  • falling back to dcd_* only when the endpoint is unreachable
  • the input helpers

pnpm lint, pnpm typecheck, pnpm test:unit (400 passing, exits cleanly) and pnpm build pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA

riglar and others added 7 commits October 19, 2026 10:30
…ch and models

The first stages of the Agent Devices CLI (dcd plan 3.1 and 3.2).

- --json prints one envelope, {ok, session, data, error}, on success
  and on failure.
- Exit codes: 0 ok, 1 usage, 2 failed, 3 limit, 4 session ended or
  not found.
- The session id may be left out when exactly one session is open.
- start --app uploads a local build first; an https URL goes to the
  API's install-from-URL.
- Progress goes to stderr, with no spinner unless at a terminal.

Hidden from dcd --help until the API reaches production.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA
Stage 3.3 of the Agent Devices CLI.

- inspect prints the compact screen; screenshot saves the frame to a
  file and reports its path.
- run sends one flow (a file or --yaml) with optional --env KEY=VALUE,
  and polls a flow that outlasts the API's wait. A failed flow exits 2.
- logs prints the app's recent log lines; --follow keeps printing new
  ones until the session ends.
- open opens the monitor at a terminal, and prints its URL otherwise.

Running a directory of flows needs an API change first and is refused
for now. openBrowser moves out of login.ts so device can share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA
Stage 5.3 of Agent Devices. Downloads what a session left behind into
./<session>-artifacts (or -o dir): recording.mp4, device.log, and per
action actions/NNN-screenshot.jpg and NNN-screen.json, numbered oldest
first. Follows every page. --links lists the signed links instead.
Files that cannot be fetched are reported, and the command exits 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA
logs --follow, a long run and start --wait reused the token resolved at
the start. A dcd login access token lasts about an hour, so a poll past
that failed with an auth error. Each poll now calls refreshAuth first;
an API key is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA
dcd device run <dir> zips the directory (without dot-folders and
node_modules), uploads it to both copies through POST
/device/bundles/upload-url, registers it with its SHA-256, and runs it
by id. --entry picks one flow inside it; without it every flow runs.
The zip is encrypted when the org's default or DCD_ENCRYPT says so, as
dcd cloud's flow zips are. Needs dcd#1718.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA
…vices

Agent Devices 4.4. dcd mcp serves the dcd_* tools (the same in-process
server dcd-mcp runs) and the device tools, proxied from the hosted
<api>/mcp so they are defined once. It adds what only a local process
can do: install_app takes app_path (uploaded first) and run takes path
(a flow file sent as yaml, or a directory uploaded as a bundle).

Uses the CLI's credentials; the hosted /mcp accepts a dcd login session
with dcd#1719. Without the hosted endpoint it serves the dcd_* tools
only. Read-only mode keeps only read-only device tools. Hidden until the
device API is live; dcd-mcp is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA
Comment thread src/mcp/bridge.ts
riglar and others added 5 commits October 19, 2026 11:59
Read-only mode hid them from the tool list, but a call made anyway was
still forwarded to the hosted /mcp, which knows nothing of the mode. The
bridge now refuses any device tool not annotated readOnlyHint, and the
server instructions in read-only mode name only the reading tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA
…/device-inspect-run

# Conflicts:
#	src/commands/device.ts
#	src/gateways/device-gateway.ts
#	src/types/domain/device-session.types.ts
#	test/unit/device-command.test.ts
Comment thread src/mcp/bridge.ts Outdated
const { name } = request.params;
const args = (request.params.arguments ?? {}) as Record<string, unknown>;
if (localNames.has(name)) {
return (await local.callTool({ name, arguments: args })) as CallToolResult;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: dcd_* calls over 60s time out under dcd mcp. The PR says these tools "behave exactly as before", but that doesn't hold for long calls.

  • Cause: local.callTool(...) is called without request options, so the MCP SDK client applies its default 60s request timeout (DEFAULT_REQUEST_TIMEOUT_MSEC).
  • Example: dcd_run_cloud_test with wait: true polls every 10s for up to waitTimeoutSeconds, which defaults to 600 and accepts up to 3600 (run-cloud-test.ts). Nothing sends progress notifications while it waits.
  • What the agent sees: after 60s, McpError -32001 Request timed out as a raw JSON-RPC error, because this path isn't wrapped in runTool. By then the test may already be submitted and billing, and the agent never gets the upload id. The in-process handler ignores the cancel and keeps polling.
  • Under dcd-mcp: the same call works.
  • Also affected: a large binary upload without wait, a slow dcd_download_artifacts, and the remote client.callTool on line 216 (a long device run).

Suggested fix:

  1. Take the handler's extra argument.
  2. Pass request options to both callTool calls, e.g. local.callTool({ name, arguments: args }, undefined, { signal: extra.signal, timeout: <≥ 3600s + upload time>, resetTimeoutOnProgress: true }).

Alternatively, call the dcd_* handlers directly rather than through a Client.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9958924. Both forwarded calls (the in-process dcd_* tools and the hosted device tools) now take the handler's extra and pass { signal: extra.signal, timeout: 2h, resetTimeoutOnProgress: true }. So the calling client's own timeout is what limits a call, and its cancel is passed through. 2h covers dcd_run_cloud_test's 3600s maximum wait plus an upload. I didn't add a test, because it would need a call slower than 60s.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test in e677477: createBridgeServer takes callTimeoutMs (default 2h), and the test shows a forwarded call that outlasts it times out while a faster one doesn't, even when the agent's own client allows longer.

riglar and others added 4 commits October 19, 2026 12:37
…/device-artifacts

# Conflicts:
#	src/commands/device.ts
#	src/gateways/device-gateway.ts
#	src/types/domain/device-session.types.ts
dcd mcp forwarded every call through an MCP Client with no request
options, so the SDK's 60s default applied: dcd_run_cloud_test with wait
(up to an hour), a large upload, or a long device run failed with
'Request timed out' while the work carried on. Both forwarded calls now
pass the caller's cancel signal and a 2h timeout that resets on progress,
so the calling client's own timeout is what limits them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA
@claude

claude Bot commented Oct 19, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

…/device-run-bundles

# Conflicts:
#	src/commands/device.ts
#	src/gateways/device-gateway.ts
#	src/types/domain/device-session.types.ts
Base automatically changed from feat/device-run-bundles to feature/agent-devices October 19, 2026 11:47
riglar and others added 2 commits October 19, 2026 12:47
createBridgeServer takes callTimeoutMs (default 2h), so a test can show
a forwarded call outlasting it times out and a faster one doesn't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tQfgpTsA9UhcopKoDezUA
@riglar
riglar merged commit c68ddc6 into feature/agent-devices Oct 19, 2026
5 checks passed
@riglar
riglar deleted the feat/device-mcp-bridge branch October 19, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant