Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,4 @@ jobs:
run: cargo clippy --all-targets -- -D warnings

- name: Stdio handshake smoke test (headless build)
run: cargo test --test e2e_stdio --verbose
run: cargo test --test e2e_stdio --test e2e_managed_mcp --verbose
81 changes: 60 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,16 @@ stdio or Streamable HTTP.
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
> for Nova. The managed `nova mcp` entrypoint and Bamboo plugin use the independent
> Nova.app on macOS. Legacy direct stdio/HTTP can use the host app, terminal, or
> directly launched binary as the permission subject. See
> [Permissions & code signing](#permissions--code-signing-macos).

## Run

```sh
cargo run # stdio transport (default)
cargo run -- mcp # managed MCP: Nova.app on macOS, stdio elsewhere
cargo run -- --http # Streamable HTTP on 127.0.0.1:3100
cargo run -- --http --addr 127.0.0.1:8080
```
Expand Down Expand Up @@ -94,9 +96,9 @@ On Windows, extract the archive for the machine's architecture and invoke
not Authenticode-signed, so SmartScreen may warn on first run.

> `v0.2.1` predates the current AX-first tools (`ax_read`, `read_ui`, and
> `ax_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 the `v0.2.1` binaries.
> `ax_activate`), the managed `mcp` command, and Nova.app. Its archives provide
> the earlier screenshot/mark/input tool set. Build the current source below
> when using this workflow; those features are not in the `v0.2.1` binaries.

To build from source:

Expand Down Expand Up @@ -128,8 +130,18 @@ ditto Nova.app /Applications/Nova.app
open -gj -b com.zenith.nova
```

Configure a stdio MCP client with the bundled executable and `--connect`, as
shown in [Use it from an MCP client](#use-it-from-an-mcp-client) below.
Install the app independently of Bodhi and the plugin's downloaded CLI. Keep it
at `/Applications/Nova.app` (or `~/Applications/Nova.app`), outside Bodhi.app and
the plugin directory. The plugin still downloads the CLI archive and uses it
only as the connector on macOS; installing/updating the plugin does not install
or update Nova.app. Use a CLI and app built from the same current version.

Configure a stdio MCP client with `nova mcp`, as shown in
[Use it from an MCP client](#use-it-from-an-mcp-client) below. If no app archive
has been published for the current code, build both macOS architectures,
combine them into a universal binary, then assemble the app with
[`package-development-app.sh`](packaging/macos/package-development-app.sh),
which requires a universal binary and the matching Cargo version as arguments.

> [!WARNING]
> The app archive is **DEVELOPMENT ONLY**. It is ad-hoc signed, not Developer ID
Expand All @@ -148,32 +160,43 @@ config. Claude Desktop uses
```json
{
"mcpServers": {
"nova": { "command": "/absolute/path/to/nova" }
"nova": { "command": "/absolute/path/to/nova", "args": ["mcp"] }
}
}
```

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:
`mcp` is the cross-platform managed entrypoint used by the Bamboo plugin.
Windows and Linux headless builds serve ordinary stdio MCP. On macOS it only
connects to the independent Nova.app, launching it through LaunchServices when
needed. Install the app separately, open it once, and grant **Accessibility** to
Nova; **Screen Recording** is needed for capture, OCR, and `list_windows` (the
current preview may request it on app startup). The bundled executable can also
be used as the connector:

```json
{
"mcpServers": {
"nova": {
"command": "/Applications/Nova.app/Contents/MacOS/nova",
"args": ["--connect"]
"args": ["mcp"]
}
}
}
```

`--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 explicit `--connect` command remains supported and uses the same transport
as macOS `mcp`. It carries MCP bytes over a private per-user Unix socket. The
connector does not call desktop APIs or request macOS permissions;
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.

If Nova.app is unavailable, the managed command exits with installation and
reconnection guidance. It never falls back to desktop operations inside the MCP
host. `NOVA_APP_SOCKET` is for isolated development/tests; when set it disables
automatic app launch. Unset it for the normal installed-app setup. Unbundled
`nova` with no arguments still offers the legacy direct stdio mode.

### Chrome DevTools MCP sidecar

For routine Chrome page automation and debugging, Nova can launch the official
Expand Down Expand Up @@ -260,7 +283,8 @@ output. On Windows, use an escaped executable path such as
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
Reconnect/reload the Nova MCP server in the client; Bodhi's main window can stay
open. See
[Permissions & code signing](#permissions--code-signing-macos) for legacy
direct-stdio and development-binary cases.

Expand Down Expand Up @@ -289,16 +313,31 @@ 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.
`nova mcp` (or explicit `nova --connect`). The connector never initializes
CoreGraphics or Accessibility, so Bamboo, Claude Desktop, and terminals no
longer need Nova's desktop permissions.

Keep Nova.app installed independently and unchanged when upgrading Bodhi. The
new Bodhi/plugin connector connects to the same app-owned service, so its own
build/signing identity does not become Nova's permission subject. This is an
architectural guarantee about where desktop calls execute; signed installation
and real TCC upgrade acceptance remain separate release gates. Replacing Nova.app
itself, changing its signature, or an OS permission decision can still require
granting permissions again. The development preview is ad-hoc signed.

After granting Nova permissions in System Settings, retry the tool. If macOS
requires a restart for the change, quit/reopen **Nova.app**, then reconnect only
the **Nova MCP server** in the client. Keep Bodhi's main window open. The
connector does not replay interrupted requests or automatically restore an MCP
session after Nova exits. Do not remove/re-add Bodhi's grants to repair this
managed Nova path.

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
attribute a child process to its responsible parent app. For legacy direct
stdio MCP (an empty argument list), 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*.
Expand Down
73 changes: 50 additions & 23 deletions packaging/plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ The `nova` binary itself is **not** in this bundle. `plugin.json`'s
GitHub Releases (built by `.github/workflows/release.yml`); the installer
downloads, sha256-verifies, and unpacks the one matching your OS.

The enabled desktop server runs `nova mcp`. On macOS this binary is only a
connector to the separately installed **Nova.app**; on Windows it runs the
ordinary stdio server. The optional Chrome DevTools server keeps its separate
`chrome-devtools` entrypoint.

## Optional Chrome DevTools server

`nova-chrome-devtools` runs `nova chrome-devtools`, which transparently
Expand Down Expand Up @@ -78,6 +83,24 @@ checksums of that release's archives by

## Installing

On macOS, first install the matching Nova.app development preview independently
of Bodhi and this plugin, at `/Applications/Nova.app` or
`~/Applications/Nova.app`, and open it once. See
[Nova.app installation](https://github.com/bigduu/Nova#novaapp-development-preview)
for checksum verification and installation. The app archive is
`nova-v<version>-universal-apple-darwin-development-app.zip`; the plugin installer
downloads the CLI archive only and does **not** install or update the app.
Keep the app outside Bodhi.app and the plugin directory so a Bodhi/plugin update
does not replace Nova's permission-bearing application.

Use a plugin, CLI, and app from the same current version. `v0.2.1` predates both
the managed `mcp` command and the app package; the current source/development
preview is needed until a release containing them is published. These plugin
defaults apply to the next release containing this change, and do not modify
an already installed `v0.2.1` plugin. An unavailable app causes a clear
connection error, with no fallback to running desktop tools inside Bamboo/Bodhi.
Windows needs no separate Nova.app installation.

A URL install is verified against the bundle's checksum by default — grab the
`.sha256` published next to the bundle on the release page and pass it:

Expand Down Expand Up @@ -132,29 +155,33 @@ Nova needs two TCC-gated macOS permissions:
- **Screen Recording** — for `screenshot`, `ocr`, `list_windows`.
- **Accessibility** — for `ax_read`, semantic activation, and input.

`ax_read` does not require Screen Recording. Grant Screen Recording only when
the workflow reaches capture/OCR.

Nova runs as an unbundled subprocess of bamboo (stdio MCP transport), so the
permission prompt and grant attach to the **launching process — Bamboo**,
not to this plugin's `nova` binary. The plugin system has no post-install
hook to request this today, so:

1. The first time a nova tool actually needs the permission, macOS should
prompt. Grant it, then retry the tool call.
2. If the prompt never appears, or capture keeps failing/returning empty,
add Bamboo manually: **System Settings → Privacy & Security → Screen
Recording** (and **Accessibility**) → `+` → find Bamboo (or ⌘⇧G to enter
its path) → enable it. A headless/backgrounded subprocess often can't
trigger the prompt on its own.
3. If granting Bamboo still doesn't help, add the `nova` binary itself as a
fallback: it lives at `bin/macos/nova` inside this plugin's install
directory (`~/.bamboo/plugins/nova/bin/macos/nova`) once installed —
add that path the same way.
4. The grant is keyed to Bamboo's **code-signing identity**, not just "the
app." If Bamboo is rebuilt/re-signed (dev builds, ad-hoc/unsigned) the
grant can silently stop persisting — if permissions mysteriously stop
working after updating Bamboo, re-grant it.
`ax_read` does not require Screen Recording. The current app preview can
request Screen Recording when it starts; granting it is needed for the capture
tools above. A future permission UI can make that request more contextual.

Grant these permissions to **Nova.app**, which runs independently through macOS
LaunchServices. The plugin's `nova mcp` process only forwards MCP bytes; it does
not initialize desktop APIs or request permissions in Bamboo/Bodhi's process
chain.

1. Open **System Settings → Privacy & Security → Accessibility**, add the
installed Nova.app if needed, and enable it. Do the same under **Screen
Recording** when using capture tools.
2. Retry the Nova tool. If macOS requires the application to restart, quit and
reopen **Nova.app**, then reconnect/reload only the **Nova MCP server** in
the client. **Bodhi's main window can stay open.** Interrupted MCP requests
are not replayed automatically.
3. If the app cannot be found, check its installation path and open it once.
Remove a development `NOVA_APP_SOCKET` override for normal use; an override
deliberately disables automatic app launch. Do not switch the plugin back
to empty arguments or grant permissions to the plugin connector as a repair.

Upgrading only Bodhi or the plugin leaves the independently installed Nova.app
as the desktop permission subject. This change establishes that process
boundary; real signed-upgrade/TCC acceptance remains release work. The app
preview is ad-hoc signed, not notarized: replacing or re-signing Nova.app itself
can still require granting permissions again. Developer ID signing and stable
Nova updates are separate prerequisites for a production upgrade experience.

Windows currently ships no Nova-side automation permission model equivalent
to macOS TCC; the Windows binary is otherwise unsigned (no Authenticode
Expand Down
4 changes: 2 additions & 2 deletions packaging/plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "nova",
"name": "Nova Desktop Automation",
"version": "0.0.0",
"description": "Nova is an AX-first Computer Use MCP server for macOS and Windows: semantic Accessibility/UIA reads and actions, mouse/keyboard, focused screenshots, and OCR fallbacks. On macOS, Accessibility is sufficient for ax_read; Screen Recording is only needed for capture/OCR. Grants commonly attach to the launching Bamboo process; see README.md for details.",
"description": "Nova is an AX-first Computer Use MCP server for macOS and Windows: semantic Accessibility/UIA reads and actions, mouse/keyboard, focused screenshots, and OCR fallbacks. On macOS, install Nova.app separately and grant desktop permissions to Nova; the plugin connects to its independent service. Windows uses the packaged stdio server. See README.md for setup.",
"platforms": [
"macos",
"windows"
Expand All @@ -16,7 +16,7 @@
"transport": {
"type": "stdio",
"command": "${platform_bin}",
"args": []
"args": ["mcp"]
}
},
{
Expand Down
8 changes: 7 additions & 1 deletion scripts/test-release-workflow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,14 @@ if transport != {
desktop = servers.get("nova")
if desktop is None or desktop.get("enabled") is not True:
raise SystemExit("the primary Nova desktop server must remain enabled")
if desktop.get("transport") != {
"type": "stdio",
"command": "${platform_bin}",
"args": ["mcp"],
}:
raise SystemExit("the primary Nova server must use the managed cross-platform mcp entrypoint")

print("Bamboo Chrome DevTools opt-in launcher contract checks passed")
print("Bamboo managed MCP and Chrome DevTools opt-in launcher contract checks passed")
PY

TEST_DIRECTORY="$(mktemp -d)"
Expand Down
61 changes: 60 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

Expand Down Expand Up @@ -147,6 +147,10 @@ struct Cli {

#[derive(Debug, Subcommand)]
enum Commands {
/// Start the managed MCP transport. On macOS, connect to the independent
/// Nova.app (install it separately); on Windows/Linux, serve over stdio.
Mcp,

/// Run the pinned official Chrome DevTools MCP server over transparent
/// stdio, with Nova's privacy-oriented defaults.
///
Expand Down Expand Up @@ -189,6 +193,17 @@ async fn main() -> Result<()> {
return nova::chrome_devtools::run(options);
}

// Managed clients share one cross-platform command. On macOS this must
// return before ANY desktop bootstrap or permission diagnostics: changing
// the MCP host must not move desktop work back into its TCC chain.
if cfg!(target_os = "macos") && matches!(cli.command, Some(Commands::Mcp)) {
return nova::app_service::connect_stdio().await.context(
"Nova MCP requires the independent Nova.app service. Install Nova.app in \
/Applications or ~/Applications and open it once, then reconnect only the \
Nova MCP server; Bodhi can remain open. Grant desktop permissions to Nova.app",
);
}

// The connector must remain a pure transport proxy. In particular, do
// this before CoreGraphics initialization so TCC/Desktop responsibility
// belongs to Nova.app, never Bamboo/Claude/the terminal that spawned the
Expand Down Expand Up @@ -968,3 +983,47 @@ fn run_capture_probe(_app: &str) -> Result<()> {
fn log_platform_permissions() {
tracing::info!("{}", HEADLESS_DIAG);
}

#[cfg(test)]
mod cli_tests {
use super::*;

#[test]
fn managed_mcp_is_an_explicit_subcommand() {
let cli = Cli::try_parse_from(["nova", "mcp"]).unwrap();
assert!(matches!(cli.command, Some(Commands::Mcp)));
assert!(!cli.http && !cli.connect && !cli.app_service);
}

#[test]
fn managed_mcp_rejects_other_transport_and_desktop_modes() {
for arguments in [
vec!["nova", "--http", "mcp"],
vec!["nova", "--connect", "mcp"],
vec!["nova", "--app-service", "mcp"],
vec!["nova", "--capture-daemon", "mcp"],
vec!["nova", "--selftest", "mcp"],
vec!["nova", "mcp", "--http"],
vec!["nova", "mcp", "--connect"],
vec!["nova", "mcp", "--capture-daemon"],
vec!["nova", "mcp", "chrome-devtools"],
] {
assert!(Cli::try_parse_from(&arguments).is_err(), "{arguments:?}");
}
}

#[test]
fn existing_cli_modes_keep_their_meaning() {
assert!(Cli::try_parse_from(["nova"]).unwrap().command.is_none());
assert!(Cli::try_parse_from(["nova", "--connect"]).unwrap().connect);
let http = Cli::try_parse_from(["nova", "--http", "--addr", "127.0.0.1:3210"]).unwrap();
assert!(http.http);
assert_eq!(http.addr, "127.0.0.1:3210");
assert!(matches!(
Cli::try_parse_from(["nova", "chrome-devtools"])
.unwrap()
.command,
Some(Commands::ChromeDevtools(_))
));
}
}
Loading
Loading