computer-use-mcp
This doc is a map, not a tour of every line. It gives you the shape of the system and the load-bearing ideas, with file:path anchors so you can dive where you want. Read §1–4 first for the skeleton; the rest is depth on each moving part.
01What it is
Most "computer use" runs against a sandboxed VM or a remote framebuffer: the agent gets one composited pixel stream and a virtual mouse. This project is different — it's a local, trusted, in-session driver. Because it runs on your real Mac as you, it can reach below the pixels:
Accessibility (AX)
Semantic tree of every window: roles, labels, values, actions, focused element, settable attributes. This is how it "reads" UIs precisely instead of guessing from pixels.ScreenCaptureKit
Captures one specific window — even occluded or on another Space — without foregrounding it. Not a whole-display stream.CoreGraphics event posting
Posts mouse/key events to a process or window (CGEventPostToPid) instead of always moving a global pointer.A shared local daemon
One engine process per user coordinates capture, input, the cursor overlay, and app leases across every concurrent agent session.02The big idea: two operators, no focus fight
The central promise — the thing every subsystem exists to protect — is background control. You keep typing in your foreground app while an agent drives a different app that may be occluded behind other windows. The agent's synthetic clicks and keystrokes never move your real cursor and never steal keyboard focus.
Three principles fall out of that promise, and they're the lens for everything below:
Background-safe by default
Every mutating tool tries the path that doesn't touch the real cursor or foreground first. Focus-changing paths exist but need explicit opt-in (allow_focus_change, allow_global_cursor).Never lie about success
Background event delivery has no OS "it landed" signal. So the server observes the effect and reports a verdict — it will not claim success it can't see.Yield to the human
If you're actively working in the same app, mutating tools pause with a recoverable error rather than interleaving synthetic events with your real ones.03Process model
One binary, launched as different subcommands (see main.swift). Understanding the three-process split is the single most important thing about this codebase.
serve each, talk MCP over stdioCallTool to the daemon over a Unix socket. Holds no engine state. Serve.swift · Dispatch.swift.accessory AppKit process (the server has no run loop). Draws a glyph gliding to targets across all displays. Never moves the real cursor. Overlay/OverlayHelper.swiftWhy a shared daemon?
- No collisions by construction. Capture, AX, and input are single-owner. Multiple agents can't stomp each other's screenshots or event streams.
- Cross-process element ids. Snapshots persist to disk, so an
element_idminted in one process resolves in another (serve, thecallharness, or a restarted server). - App leases. The daemon serializes action tools against the same resolved pid for a short window — two sessions can't interleave actions in one app. Perception is never blocked. Daemon/AppLeases.swift
DAEMON_UNAVAILABLE errors; neither runs in-process. The daemon alone calls the local dispatch funnel and persists buffered runtime metrics.Other subcommands are operational: doctor (permission check/prompt), health_report (identity + permissions + capture + daemon diagnostics), call (invoke one tool from the shell — the dev/self-test harness), version.
04A request's journey
Every entry point (serve, daemon, call) converges on one dispatch funnel, dispatchTool() in Dispatch.swift. That single funnel is why guarantees are uniform — nothing reaches a handler without passing the same gates.
// client → serve → daemon → dispatchTool() 1. catalog lookup // unknown tool → error 2. RateLimiter.acquire() // optional global throttle (off by default) 3. SleepAssertion.noteActivity() // keep the Mac awake while calls flow 4. preflightRefusal(): ├─ screen-lock pause // mutating tools pause when locked ├─ interference yield // yield if the human is active in this app └─ URL policy // browser deny/confirm patterns 5. spec.handler(arguments) // the actual tool logic 6. log + Telemetry.record // per-call stderr line + counters
At the daemon level there are two more gates in front of this: peer auth (same-UID + bearer token, DaemonServer.swift:239) and app-lease check before dispatch. The result comes back carrying _meta blocks — focus telemetry and the outcome verdict (§7) — that generic MCP clients can read to detect regressions.
05Perception & snapshots — live AX identity
How the agent "sees". get_app_state is the canonical surface: it returns one window's AX tree + a ScreenCaptureKit screenshot, with every element boxed in the screenshot's pixel space. Tools/Perception.swift · Core/Screenshot.swift
The stale-reference problem, solved with exact handles
The daemon gives the model an element_id backed by the exact live AXUIElement captured for that node. Before an action, it proves the handle is live, compares its semantic fingerprint, checks the owning PID, and proves attachment to the captured window. Core/Snapshot.swift · Core/Target.swift
Three coordinate spaces (name which one, always)
| Space | Used by | Origin / units |
|---|---|---|
| Screenshot pixels | state boxes, OCR boxes, click/scroll/drag coords | Top-left of latest window screenshot, pixels |
| Global screen points | AX frames, internal delivery, manage_window move | macOS global display space, top-left, points |
| Window-local points | window-routed synthetic events (internal bridge) | Bottom-left AppKit window coords |
Coordinate actions require a prior snapshot (it carries window origin, scale, pixel bounds). Out-of-bounds coordinates are rejected, not clipped. Core/HitTest.swift
Cheap loops: diffs, not re-sends
After a mutating action, the result returns reduced fresh state. If the tree didn't change, it says "existing ids remain valid" instead of resending. If it did, it returns a compact diff — changed (~), appeared (+), disappeared (−) — falling back to the full tree only when >50% churned. For sparse UIs, use ocr:true; for truncated trees, scope get_app_state to a retained container. Core/TreeBuilder.swift · Core/OCR.swift
06The dispatch ladder — how it acts without stealing focus
This is the heart of the background guarantee. For any click/scroll/key, the server tries delivery paths AX-first, event-last, and — critically — never auto-escalates past tier 3, because macOS gives no reliable "event landed" signal. Core/Input.swift
CGWindowID, bridge an NSEvent with that windowNumber and CGEventPostToPid. Routed to a specific window, no cursor movement.SLEventPostToPid SPI, dlopen-resolved at runtime. May break across macOS versions / notarization. COMPUTER_USE_MCP_SKYLIGHT=1.CGEventPostToPid to the process directly. Still background, still no cursor movement. App-dependent: apps needing real key-focus may drop it.allow_global_cursor:true; keyboard additionally needs the app already foreground. Restores the cursor afterward.allow_global_cursor:true + allow_focus_change:true). The caller decides. AX-tier actions fail loudly and get no hint.07Verifier-first outcomes
The design pivot that makes background control trustworthy: move mutating tools from "success = the AX call didn't throw" to "success = the effect was observed." Core/ActionOutcome.swift · docs/outcome-contract.md
Every mutating tool snapshots the target element's fields before dispatch, re-reads them (plus the whole-window change bit) after, diffs the fields that matter for that action family, and reduces to a four-value verdict carried in a computer-use-mcp/outcome _meta block:
| Verdict | Meaning |
|---|---|
| success | Effect observed — or the target was already in the requested state (idempotent no-op counts). |
| effect_not_verified | Dispatched cleanly but no confirming effect seen — the exact case old contracts wrongly called success. |
| verifier_ambiguous | Couldn't read enough state to judge. Never a hard error — it may well have worked. |
| unsupported | The target can't perform this action at all (disabled, no settable value). Retrying won't help. |
Note isError is left unchanged — a verified no-effect is not a throw, it's data. This composes with the focus telemetry block (focus_changed, delivery_tier, ui_changed, frontmost before/after) from Core/FocusTelemetry.swift. A generic client treating focus_changed:true + focus_change_allowed:false as a regression is the intended contract.
08Safety gates the agent can't bypass
These run server-side inside the funnel — no tool argument disables them casually. Core/SafetyPolicy.swift · InterferenceGuard.swift · URLPolicy.swift · SessionPower.swift
Interference yield
If real HID input was seen withininterference_idle_seconds (default 1s) and the target app is the one you're in — or the action uses the global cursor/keyboard — the call returns a recoverable yield error. Read from CGEventSource HID state, which the server's own synthetic per-pid/window events don't update, so the agent can't trip its own guard.Browser URL policy
Before a mutating action on a known browser, it reads the current page URL from AX.url_deny substrings block outright (confirm can't override); url_confirm + built-in payment-page defaults require per-action confirm. Unreadable URL fails closed only if you configured patterns.Screen lock & sleep
A prevent-idle-sleep assertion is held while calls flow (released after a quiet period) so long tasks don't die to sleep. When the screen is actually locked, mutating tools pause with a recoverable error; read-only perception stays live. No auto-unlock — that's your call.Confirmation gating
Destructive labels, confirmation-listed apps, secure password fields, risky URL schemes, and destructive window actions all requireconfirm:true. The caller reads the reason and retries deliberately.09The agent cursor overlay
Purely cosmetic, but it's what makes background control legible to a watching human: you can see where the agent is working without it touching your real pointer. Overlay/OverlayHelper.swift · AgentCursor.swift
- Separate process because the MCP server is a headless async stdio process with no AppKit run loop. The helper runs an
.accessoryNSApplication — no Dock icon, never frontmost. - Singleton — one cursor serves every concurrent server process. Reads target points from a shared FIFO (
move <globalX> <globalY>,pingkeep-alives) and glides a glyph toward them. - Multi-display correct. One click-through borderless panel per display (because "Displays have separate Spaces" clips a window to one screen). The cursor position is modeled once in AppKit global coords and mirrored into every panel; each clips to its own screen, so the glyph appears on whichever display holds it — even straddling a boundary mid-glide.
screencapture shell-out cannot see this overlay. To prove the cursor renders, capture via ScreenCaptureKit through the real server.10Tool catalog — 17 tools
Schemas + model-facing descriptions live in Tools/Catalog.swift; handlers in the sibling Tools/*.swift. Mutating tools (red) pass the full gate stack and fail closed without a daemon; read-only tools (green) can fall back in-process.
Perception (read-only)
Interaction (mutating)
Apps, windows & navigation (mutating)
Exact mutating set: click, type_text, set_value, press_key, scroll, drag, select_text, perform_secondary_action, write_clipboard, open_app, open_url, manage_window — see ToolKit/ToolSpec.swift (mutatingToolNames).
11Source map — where to look
Everything is under Sources/computer-use-mcp/. Four layers:
| Layer | Files | What lives here |
|---|---|---|
| Entry / wiring | main.swift · Serve.swift · Dispatch.swift · Call.swift · Doctor.swift · HealthReport.swift · Version.swift | Subcommand routing, the shared dispatch funnel, rate limiter, per-call logging. |
| ToolKit | ToolKit/ToolSpec.swift · Schema.swift | Tool-spec type, mutating-tool set, typed argument accessors, JSON schema helpers. |
| Tools (the public surface) | Tools/Catalog.swift · Perception.swift · ClickTool.swift · InputTools.swift · TextTools.swift · SystemTools.swift · Handlers.swift · HealthReportTool.swift | Schemas + handlers for the focused 17-tool surface. |
| Core (the engine) | Core/Snapshot.swift · TreeBuilder.swift · AX.swift · Input.swift · SkyLightInput.swift · Screenshot.swift · HitTest.swift · ActionOutcome.swift · FocusTelemetry.swift · MetricsRecorder.swift · InterferenceGuard.swift · URLPolicy.swift · SafetyPolicy.swift · SessionPower.swift · Window.swift · WindowMotion.swift · ScrollRouting.swift · AppResolver.swift · Target.swift · Keymap.swift · OCR.swift · GeometrySanitization.swift · Telemetry.swift · Config.swift · CrossProcessLock.swift · ErrorCodes.swift · PointTarget.swift | Perception, the dispatch ladder, verification, safety, coordinate math, telemetry. This is the real substance of the project. |
| Daemon | Daemon/DaemonServer.swift · DaemonClient.swift · DaemonProtocol.swift · AppLeases.swift | Socket server, auth handshake, framing/limits, per-app leases. |
| Overlay | Overlay/OverlayHelper.swift · AgentCursor.swift · OverlayTransport.swift | The cosmetic agent-cursor helper process + FIFO transport. |
Plus: Sources/ComputerUseFixture/ — a deterministic AppKit GUI fixture with stable AX identifiers, for the end-to-end "truth suite"; Tests/ComputerUseMCPTests/; scripts/ (live/CI eval harnesses like live_background_eval.py); docs/ (the contracts below); packaging/homebrew/.
12The contracts — read these next
This repo is unusually spec-driven: the behaviour is written down as contracts that tests, tool docs, and evals all reference. If you read nothing else after this page, read these two.
Modality Contract
The production bible. A surface-by-surface matrix (observation, capture scope, element identity, dispatch ladder, leases, safety) with default contract + escape hatch + failure language for each. Every claim ends with a source anchor.Background Control Contract
The three delivery modes (background-safe / best-effort / focus-mutating), the focus-telemetry JSON shape, the yield-to-human rule, and the deterministic eval recipe.Outcome Contract
The verifier design + per-tool "trap matrix" behind §7.TESTING.md
The three verification tiers and the hosted-CI-vs-live-GUI safety boundary.Fixture App
The deterministic GUI used as the source of truth for background-control checks.13Mental model recap
- One binary, three roles. N thin
serveshims → onedaemonengine → oneoverlaycursor. The daemon owning all shared state is why concurrent agents can't collide. - One funnel. Every call — from any entry point — passes the same gates in
dispatchTool. Uniform guarantees come from that single chokepoint. - See below the pixels. AX tree for semantics + ScreenCaptureKit for one window + ids backed by validated live AX handles. Same objects retain ids; recreated objects fail stale.
- Act AX-first, event-last, never auto-escalate. The ladder protects your cursor and focus by default; global takeover is always an explicit opt-in.
- Never lie. Background delivery has no OS success signal, so the server verifies the effect and reports a four-value verdict + focus telemetry instead of claiming success.
- Yield to the human. Interference guard, screen-lock pause, URL policy, confirmation gating — all server-side, all recoverable errors, none the agent can casually disable.
click end to end", "how does live-handle validation work in Snapshot.swift", "show me the daemon auth handshake", or "how does scoped state preserve identity" — and we'll go deep from here.