Skip to content

cua-driver: auto-delegate mcp to daemon for correct TCC context - #1479

Merged
ddupont808 merged 3 commits into
mainfrom
cua-driver-mcp-tcc-auto-delegation
May 12, 2026
Merged

cua-driver: auto-delegate mcp to daemon for correct TCC context#1479
ddupont808 merged 3 commits into
mainfrom
cua-driver-mcp-tcc-auto-delegation

Conversation

@f-trycua

@f-trycua f-trycua commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #1465. cua-driver mcp (stdio MCP server) always failed AXIsProcessTrusted() 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 naively because open -a would 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 mcp detects the wrong TCC context, it auto-launches a cua-driver serve daemon, then runs an in-process stdio 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.

  • Detection: same heuristic as ServeCommand.shouldRelaunchViaOpen() (bare binary path resolves into a CuaDriver.app bundle, ppid != launchd, opt-out via --no-daemon-relaunch or CUA_DRIVER_MCP_NO_RELAUNCH=1).
  • Daemon launch: open -n -g -a CuaDriver --args serve, then poll the socket for up to 10s.
  • Proxy: new CuaDriverMCPServer.makeProxy(...) builds an MCP Server whose handlers call DaemonClient.sendRequest(...) instead of ToolRegistry.default.call(...). Tool list is cached at startup. DaemonResponseCallTool.Result translation preserves isError semantics so MCP clients see the same envelopes they would in-process.
  • Compat mode: --claude-code-computer-use-compat rewrites the client-visible screenshot tool descriptor and translates inbound {pid, window_id} calls into the daemon's native {window_id, format:"jpeg", quality:85} shape.

When mcp IS launched with the right TCC context (from CuaDriver.app directly via the bundle's main executable, or any process where Bundle.main.bundlePath ends in .app), the existing in-process path runs unchanged — same PermissionsGate, same in-process ToolRegistry.call, same AppKit bootstrap.

Docs regenerated from source via the existing dump-docs pipeline. The installation.mdx guide and faq.mdx were updated by hand to document the new behavior; mcp-tools.mdx and cli-reference.mdx are auto-generated.

Files changed

  • libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swiftMCPCommand gains --no-daemon-relaunch / --socket; new runViaDaemonProxy() / launchDaemonViaOpen() / waitForDaemon() helpers.
  • libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift — new makeProxy(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 of mcp-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 for cua-driver mcp from an IDE terminal.
  • docs/content/docs/cua-driver/reference/cli-reference.mdx, mcp-tools.mdx — regenerated.

Test plan

  • TCC-denied path (the bug we're fixing):
    • Fresh terminal, no Accessibility grant on the shell/IDE.
    • Run cua-driver mcp-config --client claude | sh to register, then launch Claude Code.
    • Confirm stderr shows the "auto-launching the daemon" notice.
    • Inside Claude Code, call cua-driver:check_permissions and confirm Accessibility reads granted.
    • Call a few AX tools (list_apps, list_windows, get_window_state, screenshot) and confirm they return real data.
  • TCC-granted path (the regression risk):
    • Launch CuaDriver.app directly so it owns the TCC context.
    • From inside that process, cua-driver mcp should stay in-process — verified by absence of the proxy stderr notice and presence of the PermissionsGate window when grants are missing.
    • Equivalent: open -a CuaDriver → app stays running; subsequent cua-driver mcp invocations from /Applications/CuaDriver.app/Contents/MacOS/cua-driver exercise the same path.
  • Explicit opt-out:
    • cua-driver mcp --no-daemon-relaunch from a fresh terminal still pops the in-process PermissionsGate (and reads NOT granted against the calling shell, as before).
    • CUA_DRIVER_MCP_NO_RELAUNCH=1 cua-driver mcp behaves the same.
  • Compat mode end-to-end:
    • cua-driver mcp --claude-code-computer-use-compat from a fresh terminal proxies through the daemon; screenshot calls with {pid, window_id} return a JPEG.
  • Builds clean: swift build succeeds. swift test --skip integration passes.
  • Existing integration tests unaffected: they spawn the binary from .build/release/cua-driver (raw bare binary, not inside any CuaDriver.app), so resolvedExecutableIsInsideCuaDriverApp() returns false and the in-process path runs — same behavior as before.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Version bumped to 0.1.6.
    • MCP now auto-launches a daemon to resolve macOS accessibility attribution issues when invoked from IDE terminals.
    • Added --no-daemon-relaunch flag and environment variable to disable auto-daemon behavior.
    • Added --socket option to override daemon socket path.
  • Documentation

    • Expanded tool documentation with improved hotkey, launch_app, press_key, and screenshot descriptions.
    • Added guidance on TCC auto-delegation behavior and opt-out mechanisms.

Review Change Stack

`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>
@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 12, 2026 1:39pm

Request Review

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b64f8206-7d5a-4ad6-89a5-147310ea2b4e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds TCC-aware daemon relaunch and Unix socket proxying to the MCP subcommand. When cua-driver mcp is launched from an IDE terminal lacking Accessibility attribution, it auto-detects the condition, ensures a daemon is running via LaunchServices, and proxies all MCP tool calls through the daemon's Unix socket so Accessibility checks succeed under CuaDriver.app identity. Users can opt out via CLI flag or environment variable.

Changes

MCP TCC auto-delegation daemon proxy

Layer / File(s) Summary
Documentation and CLI interface
docs/content/docs/cua-driver/guide/getting-started/faq.mdx, docs/content/docs/cua-driver/guide/getting-started/installation.mdx, docs/content/docs/cua-driver/reference/cli-reference.mdx, docs/content/docs/cua-driver/reference/mcp-tools.mdx, libs/cua-driver/Sources/CuaDriverCLI/Docs/CLIDocExtractor.swift, scripts/docs-generators/cua-driver.ts
User-facing documentation across guides and reference pages explains TCC auto-delegation behavior, the new --no-daemon-relaunch and --socket CLI options, and tool documentation updates (hotkey, launch_app, press_key, screenshot) with optional window_id support for background app interaction. Version bumped to 0.1.6.
MCP server proxy factory
libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift
CuaDriverMCPServer.makeProxy constructs an MCP server that forwards ListTools and CallTool requests through a Unix socket to a running daemon. Tool discovery is cached; call forwarding constructs daemon requests and maps daemon responses to MCP results. Claude Code compatibility mode rewrites screenshot tool schema and arguments to daemon-native expectations.
MCP command daemon relaunch orchestration
libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift
MCPCommand adds --no-daemon-relaunch flag and --socket option. The run() method detects whether a daemon proxy is needed via heuristic checks (executable location, bundle attribution, environment overrides). When proxying is required, ensures the daemon is running (launching via open -n -g -a CuaDriver --args serve if necessary), waits for socket readiness, then starts the MCP stdio server using makeProxy under AppKit runtime.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • trycua/cua#1424: Both PRs introduce and extend Claude Code "computer-use" compatibility mode in the MCP server/CLI, with the main PR adding daemon-proxy path that also applies screenshot tool rewrites when compat mode is enabled.
  • trycua/cua#1368: Both PRs address macOS TCC attribution by relaunching or ensuring the CuaDriver daemon runs under CuaDriver.app identity via open -n -g -a CuaDriver --args serve so Accessibility permissions are correctly attributed.
  • trycua/cua#1418: The main PR modifies and extends the doc-generation machinery introduced in #1418, updating CLIDocExtractor, cua-driver docs generator, and generated MCP/CLI reference documentation.

Poem

🐰 A daemon's gift, through socket's thread,
Where TCC attribution spread,
IDE terminals now see AX true,
With LaunchServices blessing askew,
Access granted, automation flows free!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'cua-driver: auto-delegate mcp to daemon for correct TCC context' accurately summarizes the main change: implementing automatic delegation of the mcp command to the daemon to resolve TCC context issues.
Linked Issues check ✅ Passed The PR fully addresses issue #1465 by implementing auto-detection and delegation of mcp to the daemon, ensuring correct TCC context when invoked from IDE terminals without requiring external bridges.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the stated objective of adding TCC-aware delegation to mcp: implementation in CuaDriverCommand.swift and CuaDriverMCPServer.swift, documentation updates, and version bumps.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cua-driver-mcp-tcc-auto-delegation

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift (1)

582-591: ⚖️ Poor tradeoff

Code duplication: resolvedExecutableIsInsideCuaDriverApp maintenance 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 TCCContext or BundleHelpers utility) 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 win

Verify empty tool list fallback behavior on daemon fetch failure.

When fetchProxyToolList cannot reach the daemon at startup, it returns an empty array (line 131). While the comment explains that subsequent CallTool requests will surface the connection error, this could lead to a confusing UX where ListTools returns success with zero tools, but then every CallTool fails with "daemon not reachable."

Consider surfacing the connection failure earlier by throwing from makeProxy when 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

📥 Commits

Reviewing files that changed from the base of the PR and between 31bc4f8 and fd2e4fa.

📒 Files selected for processing (8)
  • docs/content/docs/cua-driver/guide/getting-started/faq.mdx
  • docs/content/docs/cua-driver/guide/getting-started/installation.mdx
  • docs/content/docs/cua-driver/reference/cli-reference.mdx
  • docs/content/docs/cua-driver/reference/mcp-tools.mdx
  • libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/Docs/CLIDocExtractor.swift
  • libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift
  • scripts/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>
@f-trycua

Copy link
Copy Markdown
Collaborator Author

Addressed both CodeRabbit nits in df4de8c:

  • Extracted the CuaDriver.app/Contents/MacOS/ heuristic into a shared isExecutableInsideCuaDriverApp() helper in CuaDriverCLI/BundleHelpers.swift. Both ServeCommand and MCPCommand now call it; behavior is byte-identical to the prior duplicated implementations.
  • fetchProxyToolList / makeProxy are now throwing. A missing/unreachable daemon (or unexpected list response) raises MCPError.internalError with the socket path and the open -n -g -a CuaDriver --args serve recovery hint, so MCP clients see a clear startup failure instead of a successful handshake advertising zero tools followed by per-CallTool errors. The error propagates through runViaDaemonProxy and gets logged to stderr by AppKitBootstrap's existing catch path before exit.

swift build and swift test (26 tests) both green locally.

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>
@ddupont808
ddupont808 merged commit 41c6afd into main May 12, 2026
9 of 10 checks passed
f-trycua added a commit that referenced this pull request May 16, 2026
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>
f-trycua added a commit that referenced this pull request May 16, 2026
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>
f-trycua added a commit that referenced this pull request May 16, 2026
…#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>
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.

mcp subcommand lacks TCC relaunch — Accessibility always false from IDE terminals

2 participants