Chrome MCP
by crimSun-dev
README.md
# Chrome MCP
Connect AI coding assistants (Cursor, Claude Code, and any MCP-compatible client) to your **live Google Chrome** browser over the Model Context Protocol. Your agent can list tabs, navigate, snapshot the page, click, type, screenshot, and evaluate JavaScript in your real profile — cookies, logins, and extensions intact.
Unlike headless-browser MCPs, this drives the Chrome you already use, via a Manifest V3 extension and `chrome.debugger` (CDP). Multiple MCP clients (e.g. Cursor **and** Claude Code) share one browser connection through a per-machine Connection Hub.
## Architecture
```
Cursor / Claude Code ──stdio MCP──▶ chrome-mcp (per session)
│ IPC (Unix socket / Windows named pipe)
▼
Connection Hub (chrome-mcp-hub, one per machine)
│ WebSocket ws://127.0.0.1:17321
▼
Chrome Extension (MV3 service worker)
│ chrome.debugger (CDP)
▼
Your Google Chrome
```
- **`packages/shared`** — wire protocol v1, error codes, IPC transport, config, policy.
- **`packages/hub`** — `chrome-mcp-hub`: holds the single WebSocket to the extension and multiplexes MCP sessions.
- **`packages/mcp-server`** — `chrome-mcp`: the stdio MCP server that registers the 10 browser tools.
- **`apps/chrome-extension`** — the MV3 extension (CDP adapter + pairing popup).
## Tools
`browser_list_tabs`, `browser_activate_tab`, `browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_screenshot`, `browser_evaluate`, `browser_wait_for`, `browser_subscribe`.
## Quickstart
### 1. Build
```bash
pnpm install
pnpm build
```
> **exFAT / removable drives:** pnpm symlinks fail on exFAT. This repo's `.npmrc`
> already sets `node-linker=hoisted`, `inject-workspace-packages=true`, and
> `package-import-method=copy`. `pnpm build` runs `scripts/sync-workspace-deps.mjs`
> to copy the built `@chrome-mcp/shared` into `node_modules` (no symlinks).
### 2. Load the Chrome extension
1. Open `chrome://extensions`.
2. Enable **Developer mode** (top right).
3. Click **Load unpacked** and select `apps/chrome-extension/dist`.
### 3. Start the hub
```bash
node packages/hub/dist/cli.js start
# or, once published: npx chrome-mcp-hub start
```
It prints a **pairing token** and listens on `ws://127.0.0.1:17321` + the platform IPC socket. Leave it running.
### 4. Configure your MCP client
Copy the matching example from [`examples/`](./examples):
- **Claude Code** → `.mcp.json` (project) — see `examples/claude-code.mcp.json`
- **Cursor** → `.cursor/mcp.json` — see `examples/cursor.mcp.json`
- **Windows** → use the `cmd /c` wrapper — see `examples/windows-cmd-wrapper.json`
- **Local (pre-publish)** → point at the built CLI — see `examples/local-dev.mcp.json`
### 5. Pair the extension
1. Click the **Chrome MCP** extension icon.
2. Paste the pairing token from the hub output.
3. Check **Approve this connection** and click **Save & Connect**.
4. The status dot turns green when connected.
Your agent can now control Chrome. Try: *"list my tabs, navigate to http://localhost:3000, and take a snapshot."*
## Security
- **Localhost only.** All listeners bind to `127.0.0.1`. Nothing is exposed externally.
- **Pairing required** by default; the extension must present the hub's token (rejected with close code 4001 otherwise).
- **URL allowlist**, enforced on *both* the server and the extension. Defaults permit only `localhost` / `127.0.0.1`. `file://`, `chrome://`, and `chrome-extension://` are always blocked unless explicitly allowlisted.
- **New-origin prompt.** When an agent hits an unlisted origin, the popup offers **Allow / Deny** (configurable via `promptForNewOrigins`).
- **Incognito tabs** are excluded by default.
Configure via `~/.chrome-mcp/config.json` (hot-reloaded within ~2s) or the extension popup. Example:
```json
{
"policy": {
"allowedUrlPatterns": ["http://localhost:*", "https://*.mycompany.dev/*"],
"enforceAllowlist": true,
"promptForNewOrigins": true
}
}
```
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Every tool returns `NOT_CONNECTED` | Hub not running, or extension not paired | Run `chrome-mcp-hub start`; open the popup and confirm the status dot is green |
| `DEBUGGER_ATTACH_FAILED` on a tab | DevTools is open on that tab (only one debugger per tab) | Close DevTools on the tab, or target a different tab |
| Extension shows "Disconnected" and won't reconnect | Wrong/rotated pairing token | Copy the current token from the hub output into the popup and re-approve |
| `INVALID_REF` when clicking/typing | The page changed or navigated; refs are ephemeral | Re-run `browser_snapshot` to get fresh refs |
| `POLICY_DENIED` with a URL | Origin not in the allowlist | Approve it in the popup, or add a pattern to `~/.chrome-mcp/config.json` |
| `spawn ENOENT` on Windows when the client starts the server | npx not resolved directly | Use the `cmd /c npx` wrapper (`examples/windows-cmd-wrapper.json`) |
| MCP client shows protocol/parse errors | Something wrote to stdout | The server logs only to stderr by design; check for a stray `console.log` in custom forks |
| Hub `status` says NOT RUNNING but you started it | Stale socket after a crash (POSIX) | Delete `/tmp/chrome-mcp-hub.sock` and restart the hub |
## Development
```bash
pnpm test # run the full Vitest suite (shared, hub, mcp-server, extension)
pnpm typecheck # tsc --noEmit across packages
pnpm lint # ESLint 9 (bans stdout writes in the MCP server)
```
## Scope (MVP)
Chrome stable only; stdio MCP transport only; `chrome.debugger` (no Puppeteer/Playwright at runtime); sideloaded extension. HTTP/SSE transport, cross-origin iframe targeting, and Chrome Web Store publishing are deferred. See `openspec/changes/chrome-mcp/` for the full spec and `TDD.md` for the technical design.
This server cannot be deployed