Skip to content
Open
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
39 changes: 39 additions & 0 deletions libs/cua-driver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,42 @@ claude mcp add --transport stdio cua-computer-use -- cua-driver mcp --claude-cod
This keeps CuaDriver's normal MCP tools and changes only `screenshot`, which requires `pid` and `window_id` and captures that window only.

Use MCP for this Claude Code vision/computer-use-style path. CLI screenshots still work as CuaDriver calls, but they do not expose the `mcp__cua-computer-use__screenshot` tool name that Claude Code appears to use as the image-grounding cue.

## Experimental OAuth connector bridge

`cua-driver mcp` remains the recommended local stdio MCP entry point. For
OAuth-only MCP clients, such as custom connector flows that require OAuth
metadata and Dynamic Client Registration before they will call an MCP endpoint,
`cua-driver` also has an experimental HTTP front door:

```bash
cua-driver mcp-oauth --public-url https://your-tunnel.example \
--mcp-upstream http://127.0.0.1:7677/mcp
```

The bridge listens on `127.0.0.1:7676` by default and expects you to put a
trusted HTTPS tunnel in front of it. It exposes OAuth discovery, Dynamic Client
Registration, authorization-code + PKCE, token exchange, and a Bearer-protected
`/mcp` endpoint that transparently forwards to a local MCP Streamable HTTP
upstream. The upstream defaults to `http://127.0.0.1:7677/mcp`; start it
separately, for example with an MCP HTTP proxy in front of `cua-driver mcp`.
Configure OAuth MCP clients with the full MCP endpoint, for example
`https://your-tunnel.example/mcp`, not just the tunnel root.

Keep this entry point opt-in and temporary:

- Use an HTTPS public URL; the command rejects plain `http://` public URLs.
- Keep the listener on loopback unless you know exactly why you need otherwise.
- Keep `--mcp-upstream` on loopback; the command rejects non-loopback upstream
hosts because forwarded requests can operate the desktop.
- Stop the tunnel and the `mcp-oauth` process when the connector is not in use.
- Delete the OAuth data directory to remove registered clients and issued tokens:
the configured `--storage-dir` when one was supplied, otherwise
`~/.cua-driver/oauth`.

The OAuth front door checks Bearer tokens, verifies the token audience matches
`<public-url>/mcp`, then passes method, body, protocol/session headers, and
streaming responses through to the upstream. The only payload-level compatibility
shim is for `tools/list`: the front door returns a connector-friendly view of
tool descriptors while leaving `tools/call` and other MCP JSON-RPC methods
untouched.
1 change: 1 addition & 0 deletions libs/cua-driver/rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions libs/cua-driver/rust/crates/cua-driver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ cursor-overlay = { path = "../cursor-overlay" }
pip-preview = { path = "../pip-preview" }
async-trait = "0.1"
base64 = { workspace = true }
ring = "0.17"
uuid = { workspace = true }
# Telemetry HTTP client. `ureq` over `reqwest` for the small dep footprint:
# PostHog ingest is a single fire-and-forget POST with a 3s timeout. Uses
Expand Down
78 changes: 76 additions & 2 deletions libs/cua-driver/rust/crates/cua-driver/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ pub enum Command {
/// Code, where this is the documented best-practice install.
claude_code_compat: bool,
},
McpOauth {
public_url: String,
listen: Option<String>,
mcp_upstream: Option<String>,
storage_dir: Option<String>,
token_ttl_seconds: Option<u64>,
code_ttl_seconds: Option<u64>,
require_user_consent: bool,
},
ListTools,
Describe(String),
Call {
Expand Down Expand Up @@ -140,6 +149,8 @@ const VALUE_FLAGS: &[&str] = &[
"--cursor-icon", "--cursor-id", "--cursor-palette", "--cursor-shape",
"--glide-ms", "--dwell-ms", "--idle-hide-ms",
"--screenshot-out-file", "--client", "--socket", "--pid-file", "--type",
"--public-url", "--listen", "--mcp-upstream", "--storage-dir",
"--token-ttl-seconds", "--code-ttl-seconds",
// Experimental PiP preview — value flag for the optional geometry
// override (--experimental-pip itself is a bare flag and doesn't
// need to be listed here).
Expand All @@ -161,7 +172,7 @@ pub fn parse_command() -> Command {
if args.iter().any(|a| a == "--help" || a == "-h") {
println!("cua-driver {} — cross-platform computer-use automation driver", env!("CARGO_PKG_VERSION"));
println!("Usage: cua-driver [SUBCOMMAND] [OPTIONS]");
println!("Subcommands: mcp, list-tools, describe, call, serve, stop, status, config, recording, update, check-update, doctor, diagnose, permissions, autostart, skills, manifest");
println!("Subcommands: mcp, mcp-oauth, list-tools, describe, call, serve, stop, status, config, recording, update, check-update, doctor, diagnose, permissions, autostart, skills, manifest");
println!();
println!("permissions options (macOS):");
println!(" cua-driver permissions status Report Accessibility + Screen Recording status. Read-only (no prompt).");
Expand Down Expand Up @@ -211,6 +222,18 @@ pub fn parse_command() -> Command {
println!(" has no tool-surface effect today; the wiring is in place");
println!(" for any future compat-gated tool.");
println!();
println!("mcp-oauth options (experimental):");
println!(" cua-driver mcp-oauth --public-url <https-url> [--mcp-upstream <url>]");
println!(" Run an OAuth + DCR HTTP front door for a local MCP HTTP endpoint.");
println!(" The listener defaults to 127.0.0.1:7676; expose it through");
println!(" your own trusted HTTPS tunnel when needed.");
println!(" --listen <addr:port> Bind address (default: 127.0.0.1:7676).");
println!(" --mcp-upstream <url> Local MCP HTTP upstream (default: http://127.0.0.1:7677/mcp).");
println!(" --storage-dir <path> OAuth client/code/token JSON store.");
println!(" --token-ttl-seconds <n> Access-token TTL (default: 86400).");
println!(" --code-ttl-seconds <n> Authorization-code TTL (default: 300).");
println!(" --no-consent-page Auto-approve /authorize requests. Local testing only.");
println!();
println!("agent cursor overlay (serve / mcp only — needs the daemon UI runloop):");
println!(" The overlay is ON by default: every MCP session automatically gets its own");
println!(" cursor (keyed by session id) that shows where the agent acts without moving the");
Expand Down Expand Up @@ -305,6 +328,21 @@ pub fn parse_command() -> Command {
socket: socket.clone(),
claude_code_compat,
},
Some("mcp-oauth") => {
let public_url = flag_value(&args, "--public-url").unwrap_or_else(|| {
eprintln!("cua-driver mcp-oauth requires --public-url <https-url>");
std::process::exit(2);
});
Command::McpOauth {
public_url,
listen: flag_value(&args, "--listen"),
mcp_upstream: flag_value(&args, "--mcp-upstream"),
storage_dir: flag_value(&args, "--storage-dir"),
token_ttl_seconds: flag_u64(&args, "--token-ttl-seconds"),
code_ttl_seconds: flag_u64(&args, "--code-ttl-seconds"),
require_user_consent: !args.iter().any(|a| a == "--no-consent-page"),
}
},
Some("list-tools") => Command::ListTools,
Some("mcp-config") => Command::McpConfig { client: mcp_client },
Some("serve") => Command::Serve {
Expand Down Expand Up @@ -477,6 +515,23 @@ fn flag_value(args: &[String], flag: &str) -> Option<String> {
None
}

fn flag_u64(args: &[String], flag: &str) -> Option<u64> {
flag_value(args, flag).and_then(|v| match parse_positive_u64(&v, flag) {
Ok(n) => Some(n),
Err(_) => {
eprintln!("{flag} must be a positive integer");
std::process::exit(2);
}
})
}

fn parse_positive_u64(raw: &str, _flag: &str) -> Result<u64, ()> {
match raw.parse::<u64>() {
Ok(n) if n > 0 => Ok(n),
_ => Err(()),
}
}

/// Print all tools in the registry, one per line: `name: first sentence`.
pub fn run_list_tools(registry: &ToolRegistry) {
// Sort alphabetically by name to match Swift's
Expand Down Expand Up @@ -846,6 +901,17 @@ pub fn build_manifest() -> serde_json::Value {
{ "name": "--socket", "type": "string", "description": "Override the daemon proxy UDS path." },
{ "name": "--claude-code-computer-use-compat", "type": "flag", "description": "Select the Claude Code computer-use compat tool surface." }
] },
{ "name": "mcp-oauth",
"description": "Run an experimental OAuth + DCR HTTP front door for a local MCP HTTP endpoint.",
"args": [
{ "name": "--public-url", "type": "string", "description": "Externally reachable HTTPS base URL." },
{ "name": "--listen", "type": "string", "description": "Bind address. Defaults to 127.0.0.1:7676." },
{ "name": "--mcp-upstream", "type": "string", "description": "Local MCP HTTP upstream. Defaults to http://127.0.0.1:7677/mcp." },
{ "name": "--storage-dir", "type": "string", "description": "OAuth client/code/token JSON store." },
{ "name": "--token-ttl-seconds", "type": "integer", "description": "Access-token lifetime." },
{ "name": "--code-ttl-seconds", "type": "integer", "description": "Authorization-code lifetime." },
{ "name": "--no-consent-page", "type": "flag", "description": "Auto-approve /authorize requests for local testing." }
] },
{ "name": "serve",
"description": "Run the long-lived daemon — backs the proxy/auto-relaunch path on macOS and the autostart Session 1+ daemon on Windows.",
"args": [
Expand Down Expand Up @@ -2799,6 +2865,7 @@ pub fn telemetry_entry_event(cmd: &Command) -> Option<String> {
use crate::telemetry::event;
let name = match cmd {
Command::Mcp { .. } => event::MCP.to_owned(),
Command::McpOauth { .. } => "cua_driver_mcp_oauth".to_owned(),
Command::Serve { .. } => event::SERVE.to_owned(),
Command::Stop { .. } => event::STOP.to_owned(),
Command::Status { .. } => event::STATUS.to_owned(),
Expand Down Expand Up @@ -2922,6 +2989,13 @@ mod tests {
assert!(sanitized.chars().all(|c| c == 'a'));
}

#[test]
fn parse_positive_u64_accepts_positive_and_rejects_zero_or_invalid() {
assert_eq!(parse_positive_u64("1", "--token-ttl-seconds"), Ok(1));
assert_eq!(parse_positive_u64("0", "--token-ttl-seconds"), Err(()));
assert_eq!(parse_positive_u64("abc", "--token-ttl-seconds"), Err(()));
}

// ── Surface 8: manifest shape ───────────────────────────────────────────

/// The manifest must carry the four documented top-level keys so a
Expand Down Expand Up @@ -2956,7 +3030,7 @@ mod tests {
let names: Vec<&str> = subs.iter()
.filter_map(|s| s.get("name").and_then(|v| v.as_str()))
.collect();
for need in ["mcp", "list-tools", "describe", "call", "serve",
for need in ["mcp", "mcp-oauth", "list-tools", "describe", "call", "serve",
"stop", "status", "mcp-config", "manifest"] {
assert!(names.contains(&need), "missing subcommand '{need}'");
}
Expand Down
33 changes: 33 additions & 0 deletions libs/cua-driver/rust/crates/cua-driver/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod bundle;
mod cli;
mod doctor;
mod mcp_http;
mod mcp_oauth;
mod proxy;
mod responsibility;
mod serve;
Expand Down Expand Up @@ -203,6 +204,22 @@ fn main() {
cli::run_manifest(pretty);
return;
}
cli::Command::McpOauth { public_url, listen, mcp_upstream, storage_dir, token_ttl_seconds, code_ttl_seconds, require_user_consent } => {
let opts = mcp_oauth::Options::new(
public_url,
listen,
mcp_upstream,
storage_dir,
token_ttl_seconds,
code_ttl_seconds,
require_user_consent,
);
if let Err(e) = mcp_oauth::run(opts) {
eprintln!("cua-driver mcp-oauth error: {e}");
std::process::exit(1);
}
return;
}
cli::Command::Call { tool, json_args, screenshot_out_file, socket } => {
// Register callbacks (needed if the tool does screenshots/recording).
cua_driver_core::recording::set_screenshot_fn(|window_id, pid| {
Expand Down Expand Up @@ -575,6 +592,22 @@ fn main() -> anyhow::Result<()> {
cli::run_manifest(pretty);
return Ok(());
}
cli::Command::McpOauth { public_url, listen, mcp_upstream, storage_dir, token_ttl_seconds, code_ttl_seconds, require_user_consent } => {
let opts = mcp_oauth::Options::new(
public_url,
listen,
mcp_upstream,
storage_dir,
token_ttl_seconds,
code_ttl_seconds,
require_user_consent,
);
if let Err(e) = mcp_oauth::run(opts) {
eprintln!("cua-driver mcp-oauth error: {e}");
std::process::exit(1);
}
return Ok(());
}
cli::Command::Call { tool, json_args, screenshot_out_file, socket } => {
let reg = Arc::new(build_registry_no_cursor());
reg.init_self_weak();
Expand Down
Loading