A Computer Use implementation in Rust. Nova is a Model Context Protocol server that gives an LLM agent AX-first control of macOS and Windows: semantic UI reads/actions, screenshots, mouse, keyboard, scrolling, window/app introspection, OCR, and the clipboard — the "computer use" capability, built natively in Rust rather than wrapping a Python/JS automation stack.
Built directly on native platform APIs (macOS Accessibility/ScreenCaptureKit/ CoreGraphics and Windows UI Automation/Win32) — a single self-contained binary, no runtime to install. Connect it to any MCP client (Claude Desktop, an agent runtime, your own) over stdio or Streamable HTTP.
| Tool | What it does |
|---|---|
ax_read |
Canonical ax:read: read semantic labels, text, values, roles, actions, state, and optional bounds through macOS Accessibility or Windows UIA, without a screenshot. Returns an ephemeral snapshot/node protocol and explicit coverage/status. |
read_ui |
Compatibility alias backed by the same ax_read traversal and cache generation. |
ax_activate |
Activate an exact actionable node from a fresh ax_read; rejects stale snapshot IDs and reports route=ax|uia|web_dom|element_center. Every attempt consumes its generation before provider dispatch. |
screenshot |
Capture the whole display or a single window= — use for layout, icons, colors, images, canvas, and visual verification after semantic/OCR paths. |
zoom_region |
Magnify a rectangle of the last screenshot at native resolution — reads small targets on surfaces with no Accessibility tree. |
ocr |
Recognize on-screen text with Apple Vision on macOS or Windows Media OCR on Windows. mode=auto uses Fast first with confidence-based Accurate fallback; mode=fast|accurate forces either policy. An optional strict roi={x,y,width,height} re-captures a rectangle from the current image through the native region path. Returns each line with a clickable center. |
click_mark |
Compatibility action for the latest numbered mark; prefer generation-safe ax_activate. |
left_click / right_click / double_click / mouse_move / scroll |
Pointer input in the pixel space of the last screenshot. |
cursor_position |
Read the cursor in OS-global logical coordinates; it is not converted into the last screenshot's pixel space. |
type_text / key_combo |
Keyboard input (full Unicode, incl. CJK + emoji). |
list_windows / list_applications / open_application |
Window & app introspection. |
read_clipboard / write_clipboard |
Clipboard access. |
ax_click / ax_set_value / ax_focus |
Drive controls by Accessibility role/label. |
dump_ax |
Read the raw AX/UIA tree for diagnostics and coverage debugging. |
batch_actions |
Run a sequence of input actions in one call. |
wait |
Pause for a specified number of seconds. |
- macOS 14+ for the macOS desktop backend. The release archive is universal and runs on Apple Silicon and Intel Macs.
- Windows x86_64 or ARM64 for the Windows desktop backend. GitHub Releases provide a native archive for each architecture.
- On Windows,
ocruses installed Windows OCR language packs. Usenova --ocr-langsto inspect available languages; install the needed pack if recognition reports that it is unavailable. - Building on macOS requires the macOS 15 SDK / Xcode 16+ because of a
transitive
apple-metalbuild dependency; that is a build-time requirement, not Nova's minimum macOS runtime version. - On macOS, Screen Recording permission is required for
screenshot,ocr, andlist_windows; Accessibility is required forax_read, semantic activation, and input.
macOS grants these permissions to the process it identifies as responsible for Nova. For stdio/plugin use that is usually the host app or terminal; for a directly launched Nova process it can be the binary itself. See Permissions & code signing.
cargo run # stdio transport (default)
cargo run -- --http # Streamable HTTP on 127.0.0.1:3100
cargo run -- --http --addr 127.0.0.1:8080The Swift runtime that ScreenCaptureKit links is located via an
LC_RPATHbaked in bybuild.rs, so noDYLD_*environment variable is needed forcargo run/cargo testor the standalone binary.
The supported public installation paths are a source build and the verified
prebuilt archives on the v0.2.1 GitHub
Release. That release
provides:
| Platform | Archive |
|---|---|
| macOS, Apple Silicon + Intel | nova-v<version>-universal-apple-darwin.tar.gz |
| Windows x86_64 | nova-v<version>-x86_64-pc-windows-msvc.zip |
| Windows ARM64 | nova-v<version>-aarch64-pc-windows-msvc.zip |
Download the matching .sha256 file from the same release and verify the
archive before extracting it. On macOS:
tar -xzf nova-v*-universal-apple-darwin.tar.gz
xattr -dr com.apple.quarantine ./nova # only if Gatekeeper blocks the download
sudo install -m 0755 nova /usr/local/bin/novaOn Windows, extract the archive for the machine's architecture and invoke
nova.exe directly or place its directory on PATH. The Windows binaries are
not Authenticode-signed, so SmartScreen may warn on first run.
v0.2.1predates the current AX-first tools (ax_read,read_ui, andax_activate). Its archives provide the earlier screenshot/mark/input tool set. Build the current source below when using the AX-first workflow in this README; do not expect those tool names from thev0.2.1binaries.
To build from source:
git clone https://github.com/bigduu/Nova.git
cd Nova
cargo build --release --lockedThe result is target/release/nova on macOS or
target/release/nova.exe on Windows. The macOS release binary is ad-hoc signed,
not notarized.
Releases cut from a revision containing the app packaging workflow also attach:
nova-v<version>-universal-apple-darwin-development-app.zip
This archive contains a universal Nova.app that runs Nova's per-user app
service without a Dock icon. It gives Screen Recording and Accessibility a Nova
application identity instead of making the MCP host (for example, Bodhi) the
permission subject. Install and start it with:
shasum -a 256 -c nova-v*-universal-apple-darwin-development-app.zip.sha256
unzip nova-v*-universal-apple-darwin-development-app.zip
ditto Nova.app /Applications/Nova.app
open -gj -b com.zenith.novaConfigure a stdio MCP client with the bundled executable and --connect, as
shown in Use it from an MCP client below.
Warning
The app archive is DEVELOPMENT ONLY. It is ad-hoc signed, not Developer ID
signed, not notarized, and not stapled. Gatekeeper can block it, and replacing
it with a differently signed build can require granting TCC permissions again.
The existing universal CLI .tar.gz remains the supported artifact consumed
by Homebrew, npm, and Bamboo; the app .zip does not replace it.
Claude Desktop (or any stdio MCP client) — add Nova to the client's MCP
config. Claude Desktop uses
~/Library/Application Support/Claude/claude_desktop_config.json on macOS and
%APPDATA%\Claude\claude_desktop_config.json on Windows:
{
"mcpServers": {
"nova": { "command": "/absolute/path/to/nova" }
}
}On macOS, the recommended setup is the independent Nova.app. Put the app in
/Applications, open it once, grant Screen Recording and Accessibility
to Nova, and configure the bundled executable as a connector:
{
"mcpServers": {
"nova": {
"command": "/Applications/Nova.app/Contents/MacOS/nova",
"args": ["--connect"]
}
}
}--connect carries MCP bytes over a private per-user Unix socket and starts the
app through LaunchServices when necessary. It does not call desktop APIs itself;
the app process owns the MCP handlers and TCC responsibility. The socket lives
under /tmp/nova-app-<uid>/ with a mode-0700 directory, mode-0600 socket, and a
same-UID peer check.
For routine Chrome page automation and debugging, Nova can launch the official
Chrome DevTools MCP
next to the desktop server. This is a transparent stdio sidecar, not a second
browser implementation inside Nova. It requires npm/npx, Node.js
^20.19.0, ^22.12.0, or >=23, and current stable Chrome (or newer). Nova
pins the reviewed upstream package to chrome-devtools-mcp@1.8.0.
On macOS, a recommended two-server configuration is:
{
"mcpServers": {
"nova": {
"command": "/Applications/Nova.app/Contents/MacOS/nova",
"args": ["--connect"]
},
"nova-chrome-devtools": {
"command": "/Applications/Nova.app/Contents/MacOS/nova",
"args": ["chrome-devtools"]
}
}
}For a standalone source/release binary, use the same binary path and
["chrome-devtools"]. If a GUI client cannot find npx, add
"--npx", "/absolute/path/to/npx" after the subcommand.
The default launches a new temporary, isolated Chrome profile. Usage
statistics, package update checks, CrUX URL lookups, and sensitive network
headers are disabled/redacted by default. Requests made by attached DevTools
targets can be guarded by repeating --allowed-url-pattern, for example:
"args": [
"chrome-devtools",
"--allowed-url-pattern", "https://example.com/*",
"--allowed-url-pattern", "https://*.example.net/*"
]URL allow patterns require Chrome 149+. They apply only to DevTools targets while the MCP server is attached and are not a complete network sandbox; use an OS/VM sandbox when full network isolation is required, as described by the upstream security policy.
To work with an already running signed-in Chrome profile instead, first open
chrome://inspect/#remote-debugging in Chrome and enable remote debugging,
then configure:
"args": ["chrome-devtools", "--profile", "existing"]Automatic connection requires Chrome 144+. If several Chrome profiles are active, Chrome chooses the profile it considers the default; select and verify the connected pages before acting.
Warning
Existing-profile mode can inspect and control every open window in the selected Chrome profile, including authenticated pages. Enable it only for a trusted local MCP client, and disable remote debugging when finished.
Use --enable-webmcp to expose upstream's experimental WebMCP tools. Nova adds
Chrome's required --enable-features=WebMCP launch argument in isolated mode;
for an existing profile, Chrome itself must already have been started with that
feature enabled. WebMCP requires Chrome 150+. --expose-network-headers and
--enable-performance-crux are explicit privacy opt-ins. The pinned 1.8.0
package does not support a
--disable-javascript-evaluation option, so Nova does not advertise or pass it.
The sidecar and Nova's optional Secure Chrome Bridge serve different trust models: DevTools MCP is the broad, full-featured choice for normal browser automation, DOM/network inspection, and performance debugging; the Secure Chrome Bridge requires explicit per-page pairing and is preferable when least-privilege page scoping matters. Nova's desktop tools remain the path for browser chrome, native dialogs, and non-web UI.
Use the absolute path to the extracted release binary or the source-build
output. On Windows, use an escaped executable path such as
"C:\\absolute\\path\\nova.exe". Use the source-build output for the
AX-first workflow below. If its directory is already on PATH, the command can
be "nova".
Restart the client; the Nova tools then appear to the agent. See Permissions & code signing for legacy direct-stdio and development-binary cases.
HTTP clients — run Nova as a server and connect over Streamable HTTP:
nova --http # 127.0.0.1:3100/mcp
nova --http --addr 127.0.0.1:8080 # custom loopback portHTTP mode is currently a local transport: it keeps rmcp's default loopback Host allowlist and does not configure remote-access authentication. Binding all interfaces is not a supported LAN setup.
First calls (current source build). Call ax_read (optionally
ax_read(window="<name>", mode="all")) for semantic content and controls, then
ax_activate(snapshot_id, node_id) on an exact actionable node. Re-run
ax_read after the action to verify semantic state. If AX/UIA coverage is
absent or partial, use focused-window ocr for rendered text; use
screenshot(window=...) / zoom_region only when pixels are necessary
(layout, icon, color, image, canvas, or visual verification). All pointer tools
use the pixel space of the most recent screenshot; cursor_position instead
reports OS-global logical coordinates.
The independent app transport is the preferred permission model: grant
Screen Recording and Accessibility to Nova.app, then use
nova --connect. The connector never initializes CoreGraphics or
Accessibility, so Bamboo, Claude Desktop, and terminals no longer need Nova's
desktop permissions.
Two details still matter for direct stdio/HTTP and source-development modes:
Grant the responsible process for the way Nova is launched. macOS TCC may
attribute a child process to its responsible parent app. For stdio MCP or the
Bamboo plugin, grant Claude Desktop, Bamboo, or the terminal/IDE that launches
Nova. For a directly launched CLI/HTTP process, macOS may instead use the Nova
binary. If granting the expected host does not work, add the installed nova
binary (or target/release/nova) as a fallback under System Settings → Privacy
& Security → Screen Recording and Accessibility.
Keep the identity of whichever process receives the grant stable. If Nova
itself is the permission subject, cargo build produces an ad-hoc,
linker-signed binary whose code-signing identity is a content hash
(nova-<hash>). It changes every build, so a direct binary grant stops applying.
Sign Nova with a stable self-signed identity when developing in that mode:
cargo build --release
./scripts/dev-codesign.sh --release # re-sign after EVERY buildThe first run creates a Zenith Nova Code Signing identity in your login keychain
(click Always Allow once if codesign prompts) and signs the binary with a
fixed identifier (com.zenith.nova). A direct Nova grant then survives rebuilds
that are re-signed with the same certificate. Host-app grants likewise depend on
the host keeping a stable signing identity.
Troubleshooting —
screenshotfails with a "wedged" / "busy" capture error. All captures (and window enumeration) run in ONE shared per-user daemon (nova --capture-daemon, flock-elected, socket/tmp/nova-capture-<uid>-<hash>.sock), becausereplaydkeys clients by executable path — two same-binary ScreenCaptureKit clients evict each other's XPC identity and wedge every new stream start. The daemon kills itself if a capture exceeds its 8s watchdog, and the client auto-recovers: kill+respawn the daemon, then (second failure) SIGKILL all nova capture processes andkillall -9 replayd— wedges self-heal without manual action. If they don't:nova --selftest(probes ScreenCaptureKit in a sacrificial subprocess, then the daemon path) and read/tmp/nova-capture-worker.log(step trace) +/tmp/nova-capture-daemon.log(daemon stderr). Manual remedy = kill the processes holding streams (pkill -f -- --capture-daemon), NOT replayd: plainkillall replaydis a no-op (replayd ignores SIGTERM), and evenkillall -9 replayddoesn't cure a wedge while a stream-holding client survives — it just reconnects and re-wedges the fresh replayd.
A general LLM judging pixel coordinates off a downscaled screenshot is the main source of mis-clicks — so the primary path avoids pixels entirely.
ax_readfirst (no image) — returns actionable controls and non-actionable readable content in deterministic tree order. A successful macOS read requires Accessibility but does not contact ScreenCaptureKit.permission_deniedmeans fix that grant; it is not an instruction to take a screenshot.- Fresh semantic action — call
ax_activatewith the returned snapshot and node IDs. Native AX/UIA and the browser DOM bridge are tried before a freshly revalidated element-center click. Stale generations fail closed; every activation attempt consumes its generation before provider dispatch, so read again after any result. - OCR second — when coverage is absent/partial and the missing information
is rendered text, use focused-window OCR and its returned text center with
left_click(..., source="ocr_center"). - Screenshot/zoom last — use pixels for visual-only state or a surface with
no semantic/text representation; coordinate clicks report
route=visual_coordinate. Screenshot marks andclick_markremain available for compatibility.
When a screenshot is needed, all click/move/scroll tools work in the pixel space of the last screenshot — the server remembers that frame and maps clicks back to the real screen, so the model just "clicks what it sees":
screenshot(window: "<name>")— capture a single window (substring of its title or app name) instead of the whole display. Smaller, sharper image → less context and far less downscaling → better precision. Later clicks map into that window.zoom_region(x, y, w, h)— magnify a rectangle of the last screenshot at native resolution (capturing only that rectangle). For reading small targets on surfaces that expose no Accessibility tree (canvas, games, custom views), where coordinates are the only option. A labeled coordinate grid is overlaid so the model reads positions straight off the axes.
The suite is split into fast, hermetic tests (run by default) and side-effecting
end-to-end tests (opt-in, #[ignore]d).
cargo testRuns everything that has no side effects and needs no special permission:
- unit tests for coordinate scaling, the key/char keystroke maps, combo parsing, batch (de)serialization, and MCP tool registration;
tests/e2e_interaction.rs— screenshot→logical coordinate mapping (viaCGDisplay, no permission needed) and a non-destructive clipboard round-trip (snapshots and restores the clipboard).
This is what the macOS test job runs in CI (see .github/workflows/ci.yml;
the workflow also has Windows cross-check and Linux headless jobs).
These either post real input events (they move the cursor, click, scroll, or
type into the focused window) or require Screen Recording permission, so they
are excluded from cargo test and must be opted into. Run them on a desktop
session where that's acceptable:
# all of them
cargo test -- --include-ignored
# or a single one
cargo test --test e2e_input mouse_move_roundtrips_through_cursor_position -- --ignored| Test (file) | What it does | Needs |
|---|---|---|
semantic_snapshot_reads… (e2e_ax_read) |
Resolves and reads a focused or NOVA_AX_WINDOW app through AX/UIA without pixel capture |
Accessibility / logged-in UIA desktop |
mouse_move_roundtrips… (e2e_input) |
Moves the cursor, reads it back via cursor_position, asserts the position — restores the cursor |
Accessibility |
click_events_post… (e2e_input) |
Left/right/double click on the empty desktop corner (Esc dismisses the menu) | Accessibility |
scroll_events_post… (e2e_input) |
Posts vertical scroll events | Accessibility |
type_text_posts… (e2e_input) |
Types into the focused window | Accessibility |
open_application_launches… (e2e_input) |
Launches/focuses System Settings | — |
list_windows_returns… (e2e_input) |
Enumerates on-screen windows | Screen Recording |
e2e_capture_display_returns_valid_jpeg (e2e_screenshot) |
Captures the display, checks the JPEG | Screen Recording |
e2e_capture_dims_match_target_dims_contract (e2e_screenshot) |
Asserts capture dims match the click-coordinate mapping | Screen Recording |
e2e_window_screenshot_produces_view_frame (e2e_screenshot) |
Captures a window and validates its view-frame metadata | Screen Recording |
ocr_recognizes_text_on_the_display (e2e_ocr) |
Runs Apple Vision OCR on a live capture; asserts text + in-bounds line centers | Screen Recording |
daemon_* / client_* / concurrent_* (e2e_capture_worker) |
Shared capture daemon: capture, kill→respawn recovery, concurrent clients, clean-error survival | Screen Recording |
legacy_pipe_protocol_still_served (e2e_worker) |
Old --capture-worker pipe protocol, proxied into the daemon |
Screen Recording |
stdio_server_completes_handshake_and_lists_tools (e2e_stdio) |
Exercises the stdio (JSON-RPC) transport end-to-end | — |
safari_opens_google_and_nova_reads_the_homepage (e2e_safari_google) |
Launches Safari, opens Google, and reads the page through Nova | Network + Screen Recording + Accessibility |
mouse_move_roundtrips…proves the macOS pointer post and cursor read-back round-trip using logical coordinates. The non-ignored interaction test covers screenshot→logical coordinate arithmetic, while the live screenshot tests cover captured-dimension contracts.Run
e2e_capture_workersingle-threaded (-- --ignored --test-threads=1): the tests share one daemon/socket.
list_applications_returns_app_bundles (in e2e_input) is not ignored — it
only reads Spotlight and is tolerant of a Spotlight-less CI host.
cargo fmt --all -- --check
cargo clippy --all-targetsA version tag drives everything via .github/workflows/release.yml. The
workflow resolves the tag once, verifies it against Cargo.toml and the event
commit, and makes every source-building job check out that immutable commit. It
builds and smoke-tests the universal macOS CLI and development-only Nova.app,
creates the Release with those assets, then sequenced jobs attach native Windows
x86_64/ARM64 archives and the Bamboo plugin bundle. The CLI .tar.gz name and
checksum outputs stay unchanged for Homebrew, npm, and the Bamboo plugin
manifest.
Run the hermetic release checks before tagging:
scripts/test-release-workflow.shThe current crate version is already published as v0.2.1; bump it before
creating the next release tag. Release tags must be protected from force updates;
the workflow also serializes runs by tag and re-verifies the tag before its first
upload. The Nova.app asset must remain labeled
DEVELOPMENT ONLY until all production distribution gates are complete:
- sign nested code and the outer app, in that order, with a Developer ID Application identity and the hardened runtime;
- submit the distribution artifact to Apple's notary service and verify the accepted ticket;
- staple the ticket to the app and validate it with
codesignandspctl; - authenticate local MCP and Chrome bridge peers with macOS audit tokens and designated code requirements, rather than relying on same-UID sockets alone;
- run the packaged native host and extension against a real Chrome install, including pairing, navigation revocation, stale snapshots, and disconnects;
- pin third-party GitHub Actions by full commit SHA before treating the release workflow as a production supply-chain boundary;
- smoke-test launch, upgrade,
nova --connect, Screen Recording, and Accessibility grants on clean Apple Silicon and Intel macOS 14+ machines.
Do not describe the ad-hoc-signed app preview as a production-ready macOS app.
MIT © bigduu