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 @@ -17,7 +17,7 @@ jobs:
cache: npm
- run: npm ci
- run: npm test
- run: node --experimental-test-module-mocks --test test/redaction.test.mjs test/remove.test.mjs test/host-profiles.test.mjs test/host-origin.test.mjs test/prepare-page.test.mjs test/load.test.mjs
- run: node --experimental-test-module-mocks --test test/adapters.test.mjs test/redaction.test.mjs test/remove.test.mjs test/host-profiles.test.mjs test/host-origin.test.mjs test/prepare-page.test.mjs test/load.test.mjs
- if: runner.os == 'macOS'
run: swift build --package-path helper/macos -c release
- run: npm audit --omit=dev
73 changes: 34 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,63 +8,57 @@ Initial capabilities: tab discovery, CDP attach/detach, Console/Log capture, Net

This project targets **developers using AI coding agents**. The intended loop is edit -> reload -> inspect Console/Network -> fix -> repeat, including Chrome-extension development.

Client adapters are documented in `docs/CLIENTS.md`. Codex, Cursor, and the local Grok Build CLI use stdio MCP. Chrome Ops never needs to expose its localhost Host ports for these clients.
Supported harnesses are Claude Code, Codex, Cursor, and the local Grok Build CLI. Each launches the stdio MCP server; Chrome Ops never exposes its localhost Host ports to them. See `docs/CLIENTS.md`.

Network event observations are redacted before they cross the extension boundary: Cookie, Set-Cookie, Authorization, proxy authorization, and cookie value fields are replaced with `[REDACTED]`. Response bodies requested explicitly with `network_response_body` are **not** content-scanned for arbitrary secrets; see `SECURITY.md`.

## Development
## Support

```powershell
npm install
npm run build
npm start
```

On Windows, the current developer baseline is PowerShell 7+. `npm run setup:windows` builds the project and registers **Chrome Ops Host** as a per-user logon scheduled task. The Host owns the persistent Chrome connection; short-lived stdio MCP processes connect to it on localhost. It does not modify an MCP client's configuration unless that client is explicitly supported/detected.
| | Host service | Unpacked-extension developer operations |
| --- | --- | --- |
| macOS | LaunchAgent | Swift helper (Accessibility) |
| Windows 11 | logon Scheduled Task | PowerShell 7 helper (UI Automation) |
| Linux (systemd) | systemd user service | not automated yet — use `chrome://extensions` |

On macOS, `npm run setup:macos` builds the Swift helper and installs a per-user LaunchAgent for the same Host. When an unpacked Bridge is already connected, setup also updates it from this installation and confirms that the new worker reconnects. The command is safe to repeat; `npm run uninstall:macos` removes the LaunchAgent and stops its Host. Neither command removes Chrome's bridge extension or client registrations. The Host logs to `~/Library/Logs/ChromeOps/`.
Claude Code, Codex, Cursor, and Grok Build are registered the same way on every OS.

## Quick start (macOS alpha)
## Quick start

Requirements: macOS with the Swift toolchain (`swift`), Chrome, Node.js 22+, and Chrome Developer mode. Give Accessibility permission to the app that runs Chrome Ops when macOS requests it. `npm run doctor` reports that authorization; the Swift helper does not use Screen Recording APIs.
Requirements: Chrome with Developer mode, Node.js 22+, and Git. macOS also needs the Swift toolchain (`swift`) and Accessibility permission for the app that runs Chrome Ops. Windows also needs PowerShell 7+ (`pwsh`). Linux needs a systemd user session.

```sh
git clone https://github.com/kitepon/chrome-ops-mcp.git
cd chrome-ops-mcp
npm ci
npm run setup:macos
npm run doctor
npm run setup
```

Open `chrome://extensions`, enable Developer mode, choose **Load unpacked**, and select this package's `extension/` directory. Keep Chrome open for developer-extension operations. Check that `npm run doctor` reports `host.connected: true` after the bridge connects. Register each installed MCP client you intend to use:
`npm run setup` installs dependencies, builds, and starts the persistent **Chrome Ops Host** as a per-user service for this OS. It is safe to repeat. When an unpacked Bridge is already connected, it also updates the Bridge from this checkout and waits for the new worker to reconnect.

Open `chrome://extensions`, enable Developer mode, choose **Load unpacked**, and select this repository's `extension/` directory. Load it in one Chrome profile only; if several Bridge profiles connect, Chrome Ops reports the ambiguity instead of guessing. Then register the harnesses you use:

```sh
npm run register:codex
npm run register:cursor
npm run register:grok
npm run register # every installed harness
npm run register:claude # or one of: claude, codex, cursor, grok
npm run doctor
```

Each registration command verifies the Host and preserves a backup before changing an existing client configuration. Restart or reload the client if it does not discover the new MCP server immediately. `npm run doctor` reports the LaunchAgent, Host, native helper permission, and client registration state. Load the Bridge once in the intended Chrome profile. If multiple Bridge profiles connect at the same time, Chrome Ops reports the ambiguity and waits for one profile to remain connected.
Registration backs up the harness configuration before a change and refuses to replace a different `chrome-ops` entry. Restart or reload the harness if it does not discover the new server immediately. `npm run doctor` reports the Host service, the Bridge connection, developer-operation readiness, and the registration state of each harness. `npm run uninstall` stops and removes the Host service; it leaves the Bridge extension and harness registrations in place.

## Quick start (Windows alpha)
Registrations point at the absolute path of the current Node executable and of `dist/index.js` in this checkout. Moving the checkout or changing Node requires registering again.

Requirements: Windows 11, Chrome, Node.js 22+, PowerShell 7+, and Chrome Developer mode. The Windows helper is validated with Japanese and English Chrome UI labels.
Logs: macOS writes the Host log to `~/Library/Logs/ChromeOps/`; Linux uses `journalctl --user -u chrome-ops-host`. Backups of harness configuration go to `~/Library/Application Support/ChromeOps/backups` (macOS), `%LOCALAPPDATA%\ChromeOps\backups` (Windows), or `${XDG_STATE_HOME:-~/.local/state}/chrome-ops/backups` (Linux).

```powershell
npm install
npm run setup:windows
npm run doctor
```
## Code layout

Open `chrome://extensions`, enable Developer mode, choose **Load unpacked**, and select `extension/` from this package/repository. Then register the MCP with one or more detected clients:
Shared code, OS adaptation, and harness adaptation live in separate files.

```powershell
npm run register:codex
npm run register:cursor
npm run register:grok
```
| Area | Shared | OS adaptation | Harness adaptation |
| --- | --- | --- | --- |
| MCP server | `src/index.ts`, `src/bridge.ts`, `src/host.ts`, `src/helper.ts` | `src/os/{macos,windows,linux}.ts` | — |
| Setup | `scripts/chrome-ops.mjs`, `scripts/lib/` | `scripts/os/{macos,windows,linux}.mjs` | `scripts/harness/{claude,codex,cursor,grok}.mjs` |
| Native helper | `helper/contract.ts` | `helper/macos/`, `helper/windows/` | — |

Restart/reload the MCP client after registration if it does not pick up the new server immediately.
See `docs/ARCHITECTURE.md` for the contracts between them.

## Core MCP tools

Expand All @@ -91,16 +85,17 @@ Chrome 116+ keeps extension service workers alive when WebSocket traffic is acti
- Windows helper: Load unpacked, exact-ID reload, Errors extraction, remove + postcondition verification
- macOS helper: Load unpacked, exact-ID reload, Errors extraction, remove + postcondition verification on a disposable fixture
- macOS LaunchAgent: setup, repeat setup, restart, uninstall, and reinstall with stdio MCP reconnection
- v0.3 on macOS 27, Windows 11, and Ubuntu 26.04: `npm run setup`, `npm run register` for Claude Code, Codex, Cursor, and Grok Build, `npm run doctor`, and each harness's own MCP check. Load/reload/errors/remove passed on macOS and Windows with a disposable fixture.

## Known boundary

Chrome's public `chrome.management` extension API can inspect installed extensions but does not expose arbitrary unpacked-directory loading or another extension's developer reload action. Chrome Ops uses deliberately narrow PowerShell 7 + Windows UI Automation and Swift + macOS Accessibility helpers for those developer-mode operations. The helpers are not generic shell/UI automation APIs.

### v0.2 alpha limitations
### v0.3 alpha limitations

- Chrome Developer mode must already be enabled for unpacked-extension operations.
- Developer-management UI automation is currently validated against Japanese and English Chrome labels; other UI languages are not yet guaranteed.
- The Chrome Ops extension itself must be loaded manually once during initial setup. Subsequent `setup:macos` runs update an already connected unpacked Bridge.
- Client registration adapters currently cover Codex, Cursor, and Grok Build. See `docs/CLIENTS.md`.
- On macOS, each unpacked-extension developer operation prepares its own management tab in the connected Bridge profile. Load verifies the new development extension through Chrome's management API before returning `verifiedLoaded: true`. Reload reports UI submission; check an observable version or behavior change. Remove verifies absence through Chrome's management API. The Windows helper contract remains unchanged.
- macOS was verified on one Mac and Chrome installation. A login/logout cycle, revoked Screen Recording permission, and a fresh Windows machine were not tested in this macOS pass.
- The Chrome Ops extension itself must be loaded manually once during initial setup. Later `npm run setup` runs update an already connected unpacked Bridge.
- Linux does not automate Load unpacked, reload, errors, or remove yet. Chrome's extension-management page is not exposed through AT-SPI in the tested GNOME Wayland session; everything else works on Linux.
- Each unpacked-extension developer operation prepares its own management tab in the connected Bridge profile. Load verifies the new development extension through Chrome's management API before returning `verifiedLoaded: true`. Reload reports UI submission; check an observable version or behavior change. Remove verifies absence through Chrome's management API.
- Each OS was verified on one machine and Chrome installation.
29 changes: 28 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,31 @@ Network secrets are redacted before crossing from the extension to the MCP proce

Initial `Load unpacked` is a helper operation because Chrome's public extension APIs do not expose arbitrary unpacked-directory loading.

On macOS, the Bridge first prepares a fixed management tab in its own Chrome profile. The helper matches that exact tab before using developer controls. The installer has one internal, fixed Bridge-update action for an already connected unpacked Bridge; it is not an MCP tool and does not accept arbitrary URLs or input commands.
On every OS with a helper, the Bridge first prepares a fixed management tab in its own Chrome profile. The helper matches that exact tab before using developer controls. The installer has one internal, fixed Bridge-update action for an already connected unpacked Bridge; it is not an MCP tool and does not accept arbitrary URLs or input commands.

## Code layout: shared, OS, harness

Shared code, OS adaptation, and harness adaptation are separated at the file level. Shared files do not branch on `process.platform`; OS files do not know about harnesses; harness files do not know about OSes.

### MCP server (`src/`)

- Shared: `index.ts` (tools), `bridge.ts` (Host client), `host.ts` (persistent Host), `helper.ts` (runs the OS helper and parses its JSON), `protocol.ts`.
- OS: `os/macos.ts`, `os/windows.ts`, `os/linux.ts`, selected by `os/index.ts`. Contract in `os/types.ts`:
- `unsupportedReason` — `null` when the OS has a developer-operation helper.
- `reportsLoadedId` — whether the helper reads the new id after Load unpacked. Otherwise the server takes the one new development extension.
- `helper(operation, value, token)` — the helper command line.

### Setup (`scripts/`)

- Shared: `chrome-ops.mjs` (entry: `setup`, `uninstall`, `doctor`, `register`), `lib/host-client.mjs`, `lib/bridge-update.mjs`, `lib/registration.mjs`, `lib/process.mjs`.
- OS: `os/macos.mjs` (LaunchAgent, Swift build), `os/windows.mjs` (Scheduled Task, npm-shim resolution), `os/linux.mjs` (systemd user service). Each exports:
- `name`, `stateDir` (backups), `exec(program, args)` (runs a harness CLI), `prepare()`,
- `installService()` → `{ changed, service }` (idempotent, rolls back on failure), `uninstallService()`, `serviceStatus()`,
- `developerOperations()` (doctor), `nativeBridgeUpdate(id, activeTabHashes)` or `null`.
- Harness: `harness/claude.mjs`, `harness/codex.mjs`, `harness/cursor.mjs`, `harness/grok.mjs`. See `docs/CLIENTS.md`.

`test/adapters.test.mjs` checks that every OS and harness adapter fills the same contract.

### Native helpers (`helper/`)

`helper/contract.ts` is shared. `helper/macos/` (Swift, Accessibility) and `helper/windows/` (PowerShell 7, UI Automation) implement it. Linux has no helper yet.
49 changes: 36 additions & 13 deletions docs/CLIENTS.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
# MCP client support
# Harness support

Chrome Ops keeps client-specific installation outside the core runtime.
Chrome Ops supports four harnesses: Claude Code, Codex, Cursor, and the local Grok Build CLI. Each launches the stdio MCP server (`node dist/index.js`); that short-lived process talks to the persistent Chrome Ops Host on `127.0.0.1:32146`.

## Local stdio clients
Topology: `harness -> Chrome Ops stdio MCP -> persistent Host -> Chrome Bridge extension -> Chrome`.

- Codex — local MCP configuration/CLI when detected.
- Cursor — global `~/.cursor/mcp.json`.
- Grok Build — local MCP configuration/CLI when detected.
- Claude clients — adapter to be implemented after current local configuration is detected/verified.
## Registration

These clients launch `dist/index.js`; that short-lived stdio process talks to the persistent Chrome Ops Host on `127.0.0.1:32146`.
```sh
npm run register # every installed harness
npm run register:<harness> # claude, codex, cursor, or grok
```

On macOS, run `npm run setup:macos` first, then `npm run register:codex`, `npm run register:cursor`, or `npm run register:grok` for each installed client. Each registration command also checks Host setup. Existing client settings are backed up before a change, and a conflicting `chrome-ops` entry is reported instead of replaced. Run `npm run doctor` to inspect the exact registration state. Setup reloads an already connected unpacked Bridge when its worker source changes; it requires a uniquely connected Chrome profile and Accessibility authorization. Client configuration uses the current absolute Node executable and built MCP server path; relocation or a Node change requires deliberately updating that entry.
Registration runs `npm run setup`'s Host step first, so the Host is running before a harness can start the server. The flow is the same on every OS and for every harness:

## Grok Build
1. Read the existing `chrome-ops` entry through the harness adapter.
2. If it already launches this Node with this checkout's `dist/index.js`, do nothing.
3. If a different `chrome-ops` entry exists, stop without changing anything.
4. Otherwise back up the harness configuration file, add the entry, and read it back.

Grok Build supports local stdio MCP servers natively. Register Chrome Ops with Grok's own `grok mcp add` command. Grok Build also supports user/project TOML MCP configuration and compatibility imports from Cursor/Claude MCP files.
| Harness | Adapter | How it is registered | Configuration |
| --- | --- | --- | --- |
| Claude Code | `scripts/harness/claude.mjs` | `claude mcp add-json --scope user` | `~/.claude.json` |
| Codex | `scripts/harness/codex.mjs` | `codex mcp add` | `~/.codex/config.toml` |
| Cursor | `scripts/harness/cursor.mjs` | writes `mcpServers.chrome-ops` | `~/.cursor/mcp.json` (editor and `cursor-agent`) |
| Grok Build | `scripts/harness/grok.mjs` | `grok mcp add`, then `grok mcp doctor` | `~/.grok/config.toml` |

Topology: `Grok Build -> Chrome Ops stdio MCP -> persistent Host -> Chrome`.
On Windows the harness CLIs are often npm shims. The Windows OS adapter runs the program the shim points at, so arguments are never re-parsed by `cmd.exe` or PowerShell.

This support is for the local **Grok Build CLI**, not the grok.com Custom MCP connector product.
`npm run doctor` shows `detected`, `registered`, and any error per harness.

## Checking a harness

| Harness | Check |
| --- | --- |
| Claude Code | `claude mcp list` shows `chrome-ops ... ✔ Connected` |
| Codex | ask it to call `chrome_status` |
| Cursor | `cursor-agent mcp list-tools chrome-ops` lists the tools |
| Grok Build | `grok mcp doctor chrome-ops` reports it healthy |

## Adding a harness

Add `scripts/harness/<name>.mjs` exporting `{ name, configFile, detect(os), read(os), add(os, server) }` and list it in `scripts/harness/index.mjs`. Run harness CLIs through `os.exec(program, args)` so each OS can resolve them. Do not put OS checks in a harness adapter.

Grok support is for the local **Grok Build CLI**, not the grok.com Custom MCP connector product.
4 changes: 2 additions & 2 deletions extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"manifest_version": 3,
"name": "Chrome Ops MCP Bridge",
"version": "0.2.0",
"version": "0.2.3",
"description": "Expose Chrome management and DevTools operations to a local MCP server.",
"permissions": ["debugger", "management", "tabs", "contentSettings", "privacy"],
"permissions": ["debugger", "management", "tabs", "contentSettings", "privacy", "alarms"],
"host_permissions": ["<all_urls>"],
"background": { "service_worker": "service-worker.js" },
"action": { "default_title": "Chrome Ops MCP" }
Expand Down
11 changes: 10 additions & 1 deletion extension/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ function respond(socket, id, ok, result, error) { if (socket.readyState === WebS
chrome.runtime.onStartup.addListener(connect);
chrome.runtime.onInstalled.addListener(connect);
chrome.action.onClicked.addListener(connect);
// While the Host is down the worker goes idle and its retry timer dies with it; an alarm wakes it to reconnect.
chrome.alarms.create("reconnect", { periodInMinutes: 0.5 });
chrome.alarms.onAlarm.addListener(alarm => { if (alarm.name === "reconnect") connect(); });
connect();

chrome.debugger.onDetach.addListener(source => { if (source.tabId != null) attached.delete(source.tabId); });
Expand Down Expand Up @@ -97,7 +100,13 @@ async function dispatch(method, p) {
const digest = await crypto.subtle.digest("SHA-256", bytes);
return [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, "0")).join("");
};
return { preparePage:true, sourceHash:await hash("service-worker.js"), manifestHash:await hash("manifest.json") };
// getURL reads the files on disk, which change before the running worker is reloaded; the in-memory manifest does not.
return { preparePage:true, reloadSelf:true, sourceHash:await hash("service-worker.js"), manifestHash:await hash("manifest.json"), runningVersion:chrome.runtime.getManifest().version };
}
case "extensions.reloadSelf": {
// Answer first; the reload ends this worker and its Host connection.
setTimeout(() => chrome.runtime.reload(), 200);
return { reloading:true };
}
case "extensions.preparePage": {
if (chrome.extension.inIncognitoContext) throw new Error("Chrome Ops Bridge is running in an incognito profile");
Expand Down
Loading
Loading