cua-driver: auto-delegate mcp to daemon for correct TCC context - #1479
Conversation
`cua-driver mcp` (stdio MCP server) always failed Accessibility
permission when launched from an IDE terminal (Claude Code, Cursor,
VS Code, Warp), because macOS TCC attributes the subprocess to the
parent terminal — not to CuaDriver.app. `serve` already side-stepped
this by relaunching itself via `open -n -g -a CuaDriver --args serve`,
but `mcp` couldn't take the same path without disconnecting the stdio
pipes its MCP client owns.
Fix: when `mcp` detects it's running in the wrong TCC context (bare
binary symlink resolving into a CuaDriver.app bundle, ppid != launchd),
it auto-launches a `cua-driver serve` daemon and then runs an
in-process MCP server whose ListTools / CallTool handlers forward
every request through the daemon's existing Unix socket at
~/Library/Caches/cua-driver/cua-driver.sock. Tool semantics are
identical to the in-process path; the MCP client just sees an
ordinary stdio server. No Python bridge required.
When mcp IS launched with the right TCC context (from CuaDriver.app
directly, or with ppid == launchd post-LaunchServices), the
in-process path runs unchanged. Pass `--no-daemon-relaunch` (or
CUA_DRIVER_MCP_NO_RELAUNCH=1) to force in-process behavior.
Compat-mode (`--claude-code-computer-use-compat`) rewrites the
client-visible `screenshot` tool descriptor and translates inbound
`{pid, window_id}` screenshot calls into the daemon's native
`{window_id, format:"jpeg", quality:85}` shape.
Docs regenerated from source via the existing dump-docs pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR adds TCC-aware daemon relaunch and Unix socket proxying to the MCP subcommand. When ChangesMCP TCC auto-delegation daemon proxy
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift (1)
582-591: ⚖️ Poor tradeoffCode duplication:
resolvedExecutableIsInsideCuaDriverAppmaintenance risk.Lines 582-591 duplicate the path-resolution logic from
ServeCommand(acknowledged in comment at lines 579-581). While the comment explains the rationale ("each subcommand owns its own relaunch heuristic"), this creates maintenance risk: if the path-checking logic needs to evolve (e.g., to handle new bundle layouts or symlink edge cases), it must be updated in both places.Consider extracting this to a shared helper in a common location (e.g., a new
TCCContextorBundleHelpersutility) so the heuristic stays consistent. If the subcommands truly need independent logic, document the specific divergence points.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift` around lines 582 - 591, Extract the duplicated path-resolution heuristic from resolvedExecutableIsInsideCuaDriverApp into a shared helper (e.g., BundleHelpers.isExecutableInsideCuaDriverApp or a new method on TCCContext) and replace the local implementation in CuaDriverCommand and the ServeCommand with calls to that helper; ensure the helper preserves the existing behavior (use Bundle.main.executablePath, fallback to CommandLine.arguments.first, realpath into a buffer and check for "/CuaDriver.app/Contents/MacOS/") and add a short doc comment on the helper noting where subcommands may diverge if they need different heuristics.libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift (1)
121-132: ⚡ Quick winVerify empty tool list fallback behavior on daemon fetch failure.
When
fetchProxyToolListcannot reach the daemon at startup, it returns an empty array (line 131). While the comment explains that subsequentCallToolrequests will surface the connection error, this could lead to a confusing UX whereListToolsreturns success with zero tools, but then everyCallToolfails with "daemon not reachable."Consider surfacing the connection failure earlier by throwing from
makeProxywhen the initial tool-list fetch fails, so clients see a clear initialization error instead of an empty tool list.Alternative: fail fast on daemon unreachable
private static func fetchProxyToolList( socketPath: String, claudeCodeComputerUseCompat: Bool -) async -> [Tool] { +) async throws -> [Tool] { let request = DaemonRequest(method: "list") let result = DaemonClient.sendRequest(request, socketPath: socketPath) guard case let .ok(response) = result, response.ok, case let .list(tools) = response.result else { - return [] + throw MCPError.internalError( + "cua-driver daemon not reachable on \(socketPath). " + + "Start it with `open -n -g -a CuaDriver --args serve` and retry." + ) }Then update the caller in
makeProxy:-let cachedToolList = await fetchProxyToolList( +let cachedToolList = try await fetchProxyToolList( socketPath: socketPath, claudeCodeComputerUseCompat: claudeCodeComputerUseCompat )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift` around lines 121 - 132, The fetchProxyToolList currently swallows daemon connection failures and returns an empty array; change its signature to async throws (function fetchProxyToolList) and throw a descriptive error when DaemonClient.sendRequest fails or the response is not .ok/.list, then update makeProxy to await try fetchProxyToolList(...) and propagate that error so makeProxy fails fast (throwing an initialization error) instead of returning a proxy with an empty tool list; ensure thrown error includes context like "daemon not reachable" and preserve original response/error details for logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift`:
- Around line 582-591: Extract the duplicated path-resolution heuristic from
resolvedExecutableIsInsideCuaDriverApp into a shared helper (e.g.,
BundleHelpers.isExecutableInsideCuaDriverApp or a new method on TCCContext) and
replace the local implementation in CuaDriverCommand and the ServeCommand with
calls to that helper; ensure the helper preserves the existing behavior (use
Bundle.main.executablePath, fallback to CommandLine.arguments.first, realpath
into a buffer and check for "/CuaDriver.app/Contents/MacOS/") and add a short
doc comment on the helper noting where subcommands may diverge if they need
different heuristics.
In `@libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift`:
- Around line 121-132: The fetchProxyToolList currently swallows daemon
connection failures and returns an empty array; change its signature to async
throws (function fetchProxyToolList) and throw a descriptive error when
DaemonClient.sendRequest fails or the response is not .ok/.list, then update
makeProxy to await try fetchProxyToolList(...) and propagate that error so
makeProxy fails fast (throwing an initialization error) instead of returning a
proxy with an empty tool list; ensure thrown error includes context like "daemon
not reachable" and preserve original response/error details for logging.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 33c2fe24-2f50-4609-a907-d23a8a2bb67d
📒 Files selected for processing (8)
docs/content/docs/cua-driver/guide/getting-started/faq.mdxdocs/content/docs/cua-driver/guide/getting-started/installation.mdxdocs/content/docs/cua-driver/reference/cli-reference.mdxdocs/content/docs/cua-driver/reference/mcp-tools.mdxlibs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swiftlibs/cua-driver/Sources/CuaDriverCLI/Docs/CLIDocExtractor.swiftlibs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swiftscripts/docs-generators/cua-driver.ts
- Extract the "is this binary running from inside an installed CuaDriver.app bundle?" heuristic into a single shared `isExecutableInsideCuaDriverApp()` helper in `CuaDriverCLI/BundleHelpers.swift`. `ServeCommand` and `MCPCommand` both call into it now, instead of carrying byte-identical local copies. Subcommands can still wrap it with extra env/flag/ppid gating where their relaunch heuristics diverge. - Make `CuaDriverMCPServer.fetchProxyToolList` (and therefore `makeProxy`) throwing. Previously a missing/unhealthy daemon silently returned an empty tool list, so the MCP client saw a "successful" handshake advertising zero tools and then errored on every subsequent `CallTool`. Now it throws a descriptive `MCPError.internalError` pointing at the socket path and the `open -n -g -a CuaDriver --args serve` recovery, which surfaces during proxy init and gets logged to stderr by `AppKitBootstrap`'s catch path before the process exits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed both CodeRabbit nits in df4de8c:
|
Add a dedicated `process-model` guide page covering the two `cua-driver mcp` runtime modes introduced by the TCC auto-delegation work in this PR: - In-process mode (default when spawned by CuaDriver.app) and daemon-proxy mode (auto-engaged from IDE terminals), with ASCII diagrams. - How to tell which mode is active (the stderr log line, plus the heuristic the code uses). - Daemon lifecycle — spawned once via `open -n -g -a CuaDriver --args serve`, survives mcp-client restarts, exits only on user `stop` / app quit / reboot / permissions denial. - mcp-client lifecycle in proxy mode (exits when stdio closes; does NOT terminate the daemon). - Failure modes — `open` fails, daemon doesn't appear within 10s, daemon refuses the initial ListTools (fail-fast per CodeRabbit nit 2), daemon dies mid-session (next CallTool raises MCPError.internalError with the daemon-restart hint). - Forcing flags — `--no-daemon-relaunch`, `CUA_DRIVER_MCP_NO_RELAUNCH=1`, `--socket`, launching from CuaDriver.app directly. - Recommendation for wrapper authors (Hermes, custom MCP shims): don't manage the daemon yourself, treat the stdio MCP transport as the contract, handle disconnects via MCP-level reconnect (matches trycua/hermes#22821's pattern). Cross-link from the autogenerated `mcp-tools.mdx` TCC-auto-delegation callout, and update the generator script so the link survives the next regen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds crates/cua-driver/src/proxy.rs — a stdio MCP server whose `tools/list` and `tools/call` handlers forward through a running `cua-driver-rs serve` daemon over its Unix socket. This is the runtime half of the TCC auto-relaunch path (issue #1525, mirror of Swift PR #1479's `CuaDriverMCPServer.makeProxy`). The proxy lives in `cua-driver` (not `mcp-server`) because the daemon protocol is owned by `crate::serve` — `mcp-server` already speaks JSON-RPC against an in-process registry, the proxy speaks the same protocol on the client side but the server side is the daemon's UDS protocol. Putting it here avoids `mcp-server → cua-driver` reverse coupling. Behavior: - Fails fast at startup if the daemon isn't reachable, so MCP clients see a clear error rather than a successful handshake that advertises zero tools (matches Swift `fetchProxyToolList`). - Caches the daemon's tool list once at startup (registry is static for the daemon's lifetime). - Forwards `tools/call` via `tokio::task::spawn_blocking` so the sync UDS client doesn't block the reactor during AX-heavy calls like `screenshot` / `get_window_state`. - Reshapes the daemon's `{name, description, input_schema, ...}` envelope into MCP's `{name, description, inputSchema, annotations: {...}}` shape, identical to `ToolDef::to_list_entry`'s in-process output. Drive-by: extend the daemon's `list` handler (both Unix + Windows paths) to include `input_schema` + annotation hints so proxy callers can build a complete `tools/list` from one round-trip instead of N+1 list+describe calls. Backwards compatible — older clients that only read name/description still work. Wired into `MCPCommand::run` in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires up the TCC auto-relaunch path so `cua-driver-rs mcp` invoked from an IDE terminal (Claude Code, Cursor, VS Code, Warp) transparently delegates to a daemon running under CuaDriverRs.app's TCC attribution. This is the user-facing payoff for issue #1525 — the equivalent of Swift PR #1479's `MCPCommand` for the Rust port. Changes: - `cli::Command::Mcp` becomes a struct variant carrying `no_daemon_relaunch: bool` and `socket: Option<String>` (new CLI flags `--no-daemon-relaunch` and `--socket <path>`). - `cli::should_use_daemon_proxy()` — Rust mirror of Swift's `shouldUseDaemonProxy`: returns true only when (1) opt-out flag/env not set, (2) bundle-context detection fires, (3) ppid != 1. - `cli::launch_daemon_and_wait()` — `Command::new("/usr/bin/open") .args(["-n", "-g", "-a", "CuaDriverRs", "--args", "serve"])` then poll the socket up to 10s. Same flags Swift uses; -n forces a new instance, -g keeps it backgrounded. - `cli::run_mcp_via_daemon_proxy()` — orchestrate: ensure daemon is up, then `proxy::run_proxy` against its socket on a fresh tokio runtime. - `main.rs` (macOS): dispatch through the proxy path when `should_use_daemon_proxy` is true, otherwise fall through to the in-process MCP server exactly as before. Non-macOS targets parse the flags cleanly so cross-platform MCP configs work, but ignore them (no TCC, no proxy). - Drop the `#[allow(dead_code)]` shims from bundle.rs and proxy.rs now that the helpers are wired up. Escape hatches: `--no-daemon-relaunch` flag or `CUA_DRIVER_RS_MCP_NO_RELAUNCH=1` env var. Build verified clean; existing tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#1530) * feat(cua-driver-rs): package as macOS .app bundle for TCC attribution Adds the .app bundle skeleton needed for issue #1525's TCC auto-relaunch path. The Rust port currently ships as a bare binary at ~/.local/bin/ cua-driver, which inherits the calling shell/IDE-terminal's TCC responsibility when invoked as an MCP stdio server — the same pathology the Swift driver hit before #1479. The fix mirrors the Swift approach: ship a minimal .app bundle (CuaDriverRs.app, bundle id com.trycua.cuadriverrs) wrapping the same universal binary, and resolve the bare CLI symlink into it. Future commits wire up the detection + relaunch + proxy logic. This commit does not change runtime behavior yet. It only: - Adds libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/Info.plist with the bundle id, LSUIElement=true (headless), distinct from the Swift driver's com.trycua.driver so both installs coexist. - Updates scripts/install.sh on macOS to download the directory tarball (which carries the .app), ditto it to /Applications/ CuaDriverRs.app, and symlink ~/.local/bin/cua-driver into the bundle (matching the Swift install layout). - Updates .github/workflows/cd-rust-cua-driver.yml to assemble the .app at release time, drop it into every macOS directory tarball, and keep the existing bare-binary tarball untouched (so users who explicitly want only the binary can still grab it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(platform-macos): bundle-context detection helper Adds crates/cua-driver/src/bundle.rs with three small helpers used by the upcoming TCC auto-relaunch path: - `is_executable_inside_cuadriverrs_app()` — Rust mirror of Swift's `isExecutableInsideCuaDriverApp()`. Resolves `current_exe()` through symlinks via `canonicalize` and substring-matches `/CuaDriverRs.app/Contents/MacOS/`. False for raw `cargo run` / dev invocations, true for the installed `~/.local/bin/cua-driver` symlink resolving into `/Applications/CuaDriverRs.app/Contents/MacOS/`. - `parent_is_not_launchd()` — `unsafe { libc::getppid() } != 1`. When the parent is launchd, TCC attribution is already correct (we're the daemon LaunchServices spawned). Otherwise we're shell-spawned and need to relaunch. - `is_env_truthy(name)` — recognizes `1|true|yes|on`. Used for `CUA_DRIVER_RS_MCP_NO_RELAUNCH=1` escape hatch. Unit tests cover all three. The dead-code allow at the top of the file silences warnings until commit 4 wires the helpers into `MCPCommand::run`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mcp): daemon-proxy mode for stdio MCP Adds crates/cua-driver/src/proxy.rs — a stdio MCP server whose `tools/list` and `tools/call` handlers forward through a running `cua-driver-rs serve` daemon over its Unix socket. This is the runtime half of the TCC auto-relaunch path (issue #1525, mirror of Swift PR #1479's `CuaDriverMCPServer.makeProxy`). The proxy lives in `cua-driver` (not `mcp-server`) because the daemon protocol is owned by `crate::serve` — `mcp-server` already speaks JSON-RPC against an in-process registry, the proxy speaks the same protocol on the client side but the server side is the daemon's UDS protocol. Putting it here avoids `mcp-server → cua-driver` reverse coupling. Behavior: - Fails fast at startup if the daemon isn't reachable, so MCP clients see a clear error rather than a successful handshake that advertises zero tools (matches Swift `fetchProxyToolList`). - Caches the daemon's tool list once at startup (registry is static for the daemon's lifetime). - Forwards `tools/call` via `tokio::task::spawn_blocking` so the sync UDS client doesn't block the reactor during AX-heavy calls like `screenshot` / `get_window_state`. - Reshapes the daemon's `{name, description, input_schema, ...}` envelope into MCP's `{name, description, inputSchema, annotations: {...}}` shape, identical to `ToolDef::to_list_entry`'s in-process output. Drive-by: extend the daemon's `list` handler (both Unix + Windows paths) to include `input_schema` + annotation hints so proxy callers can build a complete `tools/list` from one round-trip instead of N+1 list+describe calls. Backwards compatible — older clients that only read name/description still work. Wired into `MCPCommand::run` in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): auto-relaunch mcp from IDE terminal context Wires up the TCC auto-relaunch path so `cua-driver-rs mcp` invoked from an IDE terminal (Claude Code, Cursor, VS Code, Warp) transparently delegates to a daemon running under CuaDriverRs.app's TCC attribution. This is the user-facing payoff for issue #1525 — the equivalent of Swift PR #1479's `MCPCommand` for the Rust port. Changes: - `cli::Command::Mcp` becomes a struct variant carrying `no_daemon_relaunch: bool` and `socket: Option<String>` (new CLI flags `--no-daemon-relaunch` and `--socket <path>`). - `cli::should_use_daemon_proxy()` — Rust mirror of Swift's `shouldUseDaemonProxy`: returns true only when (1) opt-out flag/env not set, (2) bundle-context detection fires, (3) ppid != 1. - `cli::launch_daemon_and_wait()` — `Command::new("/usr/bin/open") .args(["-n", "-g", "-a", "CuaDriverRs", "--args", "serve"])` then poll the socket up to 10s. Same flags Swift uses; -n forces a new instance, -g keeps it backgrounded. - `cli::run_mcp_via_daemon_proxy()` — orchestrate: ensure daemon is up, then `proxy::run_proxy` against its socket on a fresh tokio runtime. - `main.rs` (macOS): dispatch through the proxy path when `should_use_daemon_proxy` is true, otherwise fall through to the in-process MCP server exactly as before. Non-macOS targets parse the flags cleanly so cross-platform MCP configs work, but ignore them (no TCC, no proxy). - Drop the `#[allow(dead_code)]` shims from bundle.rs and proxy.rs now that the helpers are wired up. Escape hatches: `--no-daemon-relaunch` flag or `CUA_DRIVER_RS_MCP_NO_RELAUNCH=1` env var. Build verified clean; existing tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(parity): document TCC auto-relaunch / daemon-proxy path for mcp Adds a PARITY.md entry under the lifecycle/process-model section linking Swift's `MCPCommand` (in CuaDriverCommand.swift) + Bundle helpers + `CuaDriverMCPServer.makeProxy` to the new Rust modules (`bundle.rs`, `cli.rs::{should_use_daemon_proxy, launch_daemon_and_wait, run_mcp_via_daemon_proxy}`, `proxy.rs`). Documents: - Why the bundle id intentionally diverges from Swift (`com.trycua.cuadriverrs` vs `com.trycua.driver`) so the two installs coexist in TCC. - All four escape hatches: `--no-daemon-relaunch` flag, `CUA_DRIVER_RS_MCP_NO_RELAUNCH=1`, `--socket <path>`, and the Rust-only `CUA_DRIVER_RS_MCP_FORCE_PROXY=1` for users who've wrapped the binary in a custom bundle or want to smoke-test the proxy against a manually-started daemon. - The daemon `list` protocol extension (now returns full ToolDef so the proxy can build `tools/list` in one round-trip). - A manual smoke-test recipe to verify the path end-to-end on macOS. Also wires up the `CUA_DRIVER_RS_MCP_FORCE_PROXY=1` knob in `cli::should_use_daemon_proxy` + `cli::run_mcp_via_daemon_proxy` so the proxy path can be exercised without an installed `.app` bundle (skips both the bundle-context check and the `open -a` daemon spawn — caller must supply a daemon on `--socket`). Integration test deferred per coordinator request — the substantive detection + proxy + relaunch logic ships in commits 1–4; this PR will be smoke-tested manually before merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): forward --socket to relaunched daemon (CodeRabbit #1) When the caller passed `cua-driver mcp --socket /custom/path`, the auto-relaunched daemon was still listening on `default_socket_path()`, so the proxy would block waiting for a daemon that never came up on the user-supplied path. Append `--socket <path>` to the `open -n -g -a CuaDriverRs --args serve` argv when the socket differs from the default. Keep the common case (default socket) byte-for-byte identical to Swift's invocation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(proxy): wrap daemon tool failures as MCP CallTool.Result (CodeRabbit #2) When the daemon returned `!resp.ok`, the proxy was building a `Response::error(...)` (JSON-RPC envelope error). MCP separates two failure modes: - JSON-RPC errors → transport / protocol failures (unreachable socket, decode error, unknown method). - Tool-level errors → tool ran but returned `isError: true` with the error text in `content[]`. JSON-RPC envelope stays success. A non-`ok` daemon response means the tool reached the daemon and the daemon reported the tool returned an error. That's tool-level, so `Response::ok(...)` with `isError: true` is the right shape — same envelope the in-process `mcp_server::server` path returns. Transport failures (UDS gone, decode error, join panic) still surface as JSON-RPC `-32603` errors, since the client really does need to distinguish "tool said no" from "I couldn't reach the tool." Adds two unit tests pinning the serialized shape of the tool-error envelope so a regression to `Response::error` fails fast in CI on every platform. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(install): fail fast on Darwin if .app bundle is missing (CodeRabbit #3) When SRC_APP is unset or not a directory on macOS, the installer was falling through to the bare-binary `install -m 0755` branch — leaving a working CLI but no /Applications/CuaDriverRs.app, which silently breaks the TCC auto-relaunch path in `cua-driver-rs mcp`. Now exits 1 with a diagnostic before touching BIN_DIR. Linux / WSL path is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): stamp release version into CuaDriverRs.app Info.plist (CodeRabbit #4) The in-tree Info.plist's CFBundleShortVersionString / CFBundleVersion drifted from the release tag on every cut because the workflow just copied the skeleton verbatim. Use `plutil -replace` to stamp ${{ steps.version.outputs.version }} into both keys after copying the skeleton. Echoes the resulting values back for build-log auditing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(parity): show expected smoke-test outputs for daemon proxy (CodeRabbit #5) Adds: - representative tools/list response envelope so the reader knows what "identical envelope shape to the in-process path" looks like in practice - tools/call get_screen_size request + response showing the structuredContent + text mirror the proxy passes through - exact stderr text both `main.rs` (unreachable) and `cli.rs` (CUA_DRIVER_RS_MCP_FORCE_PROXY) emit when no daemon is up, plus the exit status Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes #1465.
cua-driver mcp(stdio MCP server) always failedAXIsProcessTrusted()when launched from an IDE terminal (Claude Code, Cursor, VS Code, Warp), because macOS TCC attributes the subprocess to the parent terminal — not toCuaDriver.app.servealready side-stepped this by relaunching itself viaopen -n -g -a CuaDriver --args serve, butmcpcouldn't take the same path naively becauseopen -awould disconnect the stdin/stdout pipes the MCP client owns.This PR implements Option 1 from the issue (the preferred option per the issue author): when
mcpdetects the wrong TCC context, it auto-launches acua-driver servedaemon, then runs an in-process stdio MCP server whoseListTools/CallToolhandlers forward every request through the daemon's existing Unix socket at~/Library/Caches/cua-driver/cua-driver.sock. Tool semantics are identical to the in-process path — the MCP client just sees an ordinary stdio server.ServeCommand.shouldRelaunchViaOpen()(bare binary path resolves into aCuaDriver.appbundle,ppid != launchd, opt-out via--no-daemon-relaunchorCUA_DRIVER_MCP_NO_RELAUNCH=1).open -n -g -a CuaDriver --args serve, then poll the socket for up to 10s.CuaDriverMCPServer.makeProxy(...)builds an MCPServerwhose handlers callDaemonClient.sendRequest(...)instead ofToolRegistry.default.call(...). Tool list is cached at startup.DaemonResponse→CallTool.Resulttranslation preservesisErrorsemantics so MCP clients see the same envelopes they would in-process.--claude-code-computer-use-compatrewrites the client-visiblescreenshottool descriptor and translates inbound{pid, window_id}calls into the daemon's native{window_id, format:"jpeg", quality:85}shape.When
mcpIS launched with the right TCC context (fromCuaDriver.appdirectly via the bundle's main executable, or any process whereBundle.main.bundlePathends in.app), the existing in-process path runs unchanged — samePermissionsGate, same in-processToolRegistry.call, same AppKit bootstrap.Docs regenerated from source via the existing
dump-docspipeline. Theinstallation.mdxguide andfaq.mdxwere updated by hand to document the new behavior;mcp-tools.mdxandcli-reference.mdxare auto-generated.Files changed
libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift—MCPCommandgains--no-daemon-relaunch/--socket; newrunViaDaemonProxy()/launchDaemonViaOpen()/waitForDaemon()helpers.libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift— newmakeProxy(serverName:socketPath:claudeCodeComputerUseCompat:)builds a stdio MCP server that forwards every call through the daemon socket.libs/cua-driver/Sources/CuaDriverCLI/Docs/CLIDocExtractor.swift— adds the new flag/option to the dump-docs output, so the regenerated CLI reference picks it up automatically.scripts/docs-generators/cua-driver.ts— emits a TCC-auto-delegation<Callout>at the top ofmcp-tools.mdx.docs/content/docs/cua-driver/guide/getting-started/installation.mdx— adds the auto-delegation callout under the MCP registration section.docs/content/docs/cua-driver/guide/getting-started/faq.mdx— adds a Q&A forcua-driver mcpfrom an IDE terminal.docs/content/docs/cua-driver/reference/cli-reference.mdx,mcp-tools.mdx— regenerated.Test plan
cua-driver mcp-config --client claude | shto register, then launch Claude Code.cua-driver:check_permissionsand confirm Accessibility readsgranted.list_apps,list_windows,get_window_state,screenshot) and confirm they return real data.CuaDriver.appdirectly so it owns the TCC context.cua-driver mcpshould stay in-process — verified by absence of the proxy stderr notice and presence of thePermissionsGatewindow when grants are missing.open -a CuaDriver→ app stays running; subsequentcua-driver mcpinvocations from/Applications/CuaDriver.app/Contents/MacOS/cua-driverexercise the same path.cua-driver mcp --no-daemon-relaunchfrom a fresh terminal still pops the in-processPermissionsGate(and readsNOT grantedagainst the calling shell, as before).CUA_DRIVER_MCP_NO_RELAUNCH=1 cua-driver mcpbehaves the same.cua-driver mcp --claude-code-computer-use-compatfrom a fresh terminal proxies through the daemon;screenshotcalls with{pid, window_id}return a JPEG.swift buildsucceeds.swift test --skip integrationpasses..build/release/cua-driver(raw bare binary, not inside anyCuaDriver.app), soresolvedExecutableIsInsideCuaDriverApp()returns false and the in-process path runs — same behavior as before.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
--no-daemon-relaunchflag and environment variable to disable auto-daemon behavior.--socketoption to override daemon socket path.Documentation
hotkey,launch_app,press_key, andscreenshotdescriptions.