chrome-bridge-mcp 1.32.1 → 1.32.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -36
- package/extension/lib/command-outcome.js +35 -1
- package/extension/lib/trusted-input.js +23 -0
- package/extension/manifest.json +1 -1
- package/extension/popup.css +1 -0
- package/extension/service-worker.js +5 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/protocol.js +1 -1
package/README.md
CHANGED
|
@@ -38,17 +38,19 @@ and find the Sign Up button"*: Claude Code calls `navigate`, `audit` and
|
|
|
38
38
|
`find_text`. Because `navigate` already returns element refs,
|
|
39
39
|
`click(ref="n1")` follows with no discovery turn in between.
|
|
40
40
|
|
|
41
|
-
For `execute_js`, enable **Allow user scripts** in `chrome://extensions` →
|
|
42
|
-
Chrome Bridge → Details.
|
|
43
|
-
|
|
44
41
|
The plugin registers the MCP server from npm together with the recipes skill.
|
|
45
|
-
|
|
42
|
+
|
|
43
|
+
| Requirement | For | Notes |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Node.js 18+ | the MCP server | |
|
|
46
|
+
| Chrome 135+ | the extension | `execute_js` needs **Allow user scripts** in `chrome://extensions` → Chrome Bridge → Details; on Chrome 135-137, Developer Mode instead |
|
|
47
|
+
| Claude Code 2.1.275+ in a session, 2.1.292+ from a shell | the one-command install | Older versions: see below |
|
|
48
|
+
| Python 3.8+ (optional) | the plugin's error log hooks | As `python3`, `python` or `py` (the Microsoft Store `python3` alias is skipped). Without it they stay silent and the tools work the same |
|
|
46
49
|
|
|
47
50
|
<details>
|
|
48
|
-
<summary>Older Claude Code, from source, Windows,
|
|
51
|
+
<summary>Older Claude Code, from source, Windows, one path only</summary>
|
|
49
52
|
|
|
50
|
-
**Older Claude Code.**
|
|
51
|
-
session and 2.1.292+ from a shell. Before that, add the marketplace first:
|
|
53
|
+
**Older Claude Code.** Add the marketplace first:
|
|
52
54
|
`/plugin marketplace add frsorrentino/chrome-bridge`, then
|
|
53
55
|
`/plugin install chrome-bridge@chrome-bridge` (from a shell:
|
|
54
56
|
`claude plugin marketplace add frsorrentino/chrome-bridge`, then
|
|
@@ -71,14 +73,9 @@ starts.
|
|
|
71
73
|
|
|
72
74
|
**Windows.** `install.sh` does not run from PowerShell or cmd: use the plugin
|
|
73
75
|
with the Web Store extension, or run `install.sh` from Git Bash. The plugin's
|
|
74
|
-
error log hooks run in Git Bash, as Claude Code runs every hook
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
launch mode see `CHROME_BRIDGE_BROWSER` below: Google Chrome cannot load the
|
|
78
|
-
extension there, Microsoft Edge can.
|
|
79
|
-
|
|
80
|
-
**Chrome 135-137.** `execute_js` needs Developer Mode instead of the **Allow
|
|
81
|
-
user scripts** toggle.
|
|
76
|
+
error log hooks run in Git Bash, as Claude Code runs every hook. For launch
|
|
77
|
+
mode see `CHROME_BRIDGE_BROWSER` below: Google Chrome cannot load the extension
|
|
78
|
+
there, Microsoft Edge can.
|
|
82
79
|
|
|
83
80
|
**One path only.** The plugin and `install.sh` would register the same server
|
|
84
81
|
twice, and so would a plugin added from the [Anthropic
|
|
@@ -203,10 +200,8 @@ user-script toggle isn't available.
|
|
|
203
200
|
## Tools
|
|
204
201
|
|
|
205
202
|
69 in total. `core` (46 tools: every tool used in 101 real sessions, plus the
|
|
206
|
-
two WebMCP tools) loads by default
|
|
207
|
-
|
|
208
|
-
`get_status({enable: ["visual"]})`, with no restart (`--caps` still sets them
|
|
209
|
-
at startup).
|
|
203
|
+
two WebMCP tools) loads by default; the other 23 sit in seven optional caps,
|
|
204
|
+
switched on with `get_status({enable: ["visual"]})` or at startup with `--caps`.
|
|
210
205
|
|
|
211
206
|

|
|
212
207
|
|
|
@@ -251,7 +246,7 @@ executes commands through Chrome APIs. User scripts (`execute_js`) run via
|
|
|
251
246
|
| `CHROME_BRIDGE_ALWAYS_LOAD` / `--always-load` | 22 most used tools | Tools kept in context when the client loads the server whole; the others carry `_meta['anthropic/alwaysLoad'] = false` and wait behind tool search (Claude Code 2.1.285+). `all` loads every tool; a comma list picks them |
|
|
252
247
|
| `CHROME_BRIDGE_NO_JS` / `--no-js` | unset | No arbitrary JavaScript in the page: `execute_js` and `modify_dom` leave the schema, `wait_for(condition=function)` and `javascript:`/`data:` URLs are refused. `get_status` reports `js_evaluation` |
|
|
253
248
|
| `CHROME_BRIDGE_READ_ROOT` / `--read-root` | unset | Where `upload_file` may read from. Unset: any file except keys and credentials (`~/.ssh`, `~/.aws`, `~/.gnupg`, `.env*`, `id_*`, `*.pem`, `*.key`, …), symlinks resolved first. Set: only files under this directory, whatever their name — the way to upload a certificate key on purpose |
|
|
254
|
-
| `CHROME_BRIDGE_BROWSER` | unset | Launch mode only: the browser binary, e.g. `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`. Unset: the first of Chromium, Edge, Brave, Google Chrome found in the standard paths (on Linux Chromium, then Google Chrome)
|
|
249
|
+
| `CHROME_BRIDGE_BROWSER` | unset | Launch mode only: the browser binary, e.g. `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`. Unset: the first of Chromium, Edge, Brave, Google Chrome found in the standard paths (on Linux Chromium, then Google Chrome) |
|
|
255
250
|
| `CHROME_BRIDGE_OBSERVE` | unset | `off` stops the local error log below. The plugin sets `hook`: its hooks keep the log and the server stays out of it |
|
|
256
251
|
| `CHROME_BRIDGE_WRITE_ROOT` / `--write-root` | unset | Every path the model chooses (`save_to`, `output_path`, exports) must be under this directory, checked before the browser does any work; the server's own state under `~/.config/chrome-bridge` stays writable. The CLI is your shell and is not restricted |
|
|
257
252
|
|
|
@@ -279,8 +274,6 @@ private page data (`get_storage`, `get_tabs`). Those are the product, not bugs.
|
|
|
279
274
|
What matters is who can reach them and what a hostile page can make the agent
|
|
280
275
|
do with them.
|
|
281
276
|
|
|
282
|
-
- **Who can connect:** see above — loopback bind, `chrome-extension://` origin
|
|
283
|
-
check, `CHROME_BRIDGE_TOKEN` on both handshakes.
|
|
284
277
|
- **Launch mode also opens a DevTools port** on 127.0.0.1 (chosen by the
|
|
285
278
|
system), for `lighthouse` and `heap_snapshot`. CDP has no authentication:
|
|
286
279
|
any local process can drive that dedicated browser while it runs. Its profile
|
|
@@ -293,11 +286,9 @@ do with them.
|
|
|
293
286
|
JavaScript, send a request with your cookies, submit a form or upload a file.
|
|
294
287
|
Treat every page you automate as untrusted input, and review what the agent
|
|
295
288
|
proposes on sites that hold money or credentials.
|
|
296
|
-
- **Reduce the surface:** `--caps core` (the default) exposes the smallest set
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
fences it to one directory. For sessions that matter, automate in a separate
|
|
300
|
-
Chrome profile.
|
|
289
|
+
- **Reduce the surface:** `--caps core` (the default) exposes the smallest set,
|
|
290
|
+
and see [Limits you can set](#your-browser-your-login). For sessions that
|
|
291
|
+
matter, automate in a separate Chrome profile.
|
|
301
292
|
- **`--no-js` is not a sandbox:** `inject_css` stays, and CSS attribute
|
|
302
293
|
selectors with `url()` can leak attribute values to a remote host.
|
|
303
294
|
- **What leaves your machine** is what the browser sends on the agent's behalf.
|
|
@@ -309,9 +300,9 @@ do with them.
|
|
|
309
300
|
When one of its own tools fails, chrome-bridge adds a line to a **local** file,
|
|
310
301
|
`${XDG_STATE_HOME:-~/.local/state}/claude-observe/chrome-bridge.jsonl` (mode
|
|
311
302
|
0600, in a 0700 folder), in the [claude-observe](https://github.com/frsorrentino/claude-observe)
|
|
312
|
-
format (the plugin's copy is in `observe/`).
|
|
313
|
-
|
|
314
|
-
count.
|
|
303
|
+
format (the plugin's copy is in `observe/`). A `PostToolUseFailure` hook writes
|
|
304
|
+
it, or the server when used without the plugin. A repeated error is one line
|
|
305
|
+
with a count.
|
|
315
306
|
|
|
316
307
|
- **Kept:** the tool name, the error text after scrubbing (your home folder
|
|
317
308
|
becomes `~`; emails, URL queries, secrets and typed values are removed), the
|
|
@@ -323,12 +314,11 @@ count.
|
|
|
323
314
|
content.
|
|
324
315
|
- **Nothing leaves your computer** unless you say yes. With the plugin, once
|
|
325
316
|
a few errors have piled up (or one has waited a few days, or one is marked
|
|
326
|
-
as a defect), a line at the end of a turn tells you so, and Claude may offer
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
and
|
|
330
|
-
|
|
331
|
-
alone never sends or offers anything.
|
|
317
|
+
as a defect), a line at the end of a turn tells you so, and Claude may offer
|
|
318
|
+
to send them as one GitHub issue: it shows the anonymized text first and
|
|
319
|
+
sends only what you choose. `/chrome-bridge:observe send` does the same on
|
|
320
|
+
demand; `list`, `mark` and `add` read and triage the log. The server alone
|
|
321
|
+
never sends or offers anything.
|
|
332
322
|
- **Security observations** (a read or write outside the perimeter, a secret
|
|
333
323
|
exposed, unwanted code execution, data leaving the computer) never enter a
|
|
334
324
|
public issue: `/chrome-bridge:observe send --security` prepares a private
|
|
@@ -23,7 +23,41 @@ export function elementOutcome(results, selector) {
|
|
|
23
23
|
if (first?.result != null) return first.result;
|
|
24
24
|
if (first?.error?.message) throw new Error(first.error.message);
|
|
25
25
|
if (!selector) throw new Error('The page returned no result for the command');
|
|
26
|
-
throw
|
|
26
|
+
throw notFoundError(selector);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sintassi di Playwright, jQuery o XPath dentro un selettore CSS: querySelector
|
|
31
|
+
* la rifiuta con un SyntaxError, che executeScript riduce a un risultato vuoto.
|
|
32
|
+
* «no element matches» faceva credere a un selettore valido senza riscontri
|
|
33
|
+
* (note observe chrome-bridg-a1255c1b, chrome-bridg-5a9d399e, 1.32.1). Le
|
|
34
|
+
* stringhe tra virgolette si tolgono prima: `[title=":visible"]` è CSS valido.
|
|
35
|
+
*/
|
|
36
|
+
export function nonCssToken(selector) {
|
|
37
|
+
const s = String(selector).replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '""');
|
|
38
|
+
if (/^\s*\(?\/\//.test(s) || /(^|[\s,])xpath=/.test(s)) return 'XPath';
|
|
39
|
+
const engine = /(^|[\s,])(text|css|id|role|data-testid|internal:[\w-]+)=/.exec(s);
|
|
40
|
+
if (engine) return `${engine[2]}=`;
|
|
41
|
+
if (/(^|[^>])>>(?!>)/.test(s)) return '>>';
|
|
42
|
+
const pseudo = /:(has-text|text-is|text-matches|text|visible|hidden|nth-match|left-of|right-of|above|below|near|contains|eq|gt|lt|first|last|even|odd)(?![\w-])/.exec(s);
|
|
43
|
+
return pseudo ? `:${pseudo[1]}` : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function notFoundError(selector) {
|
|
47
|
+
const token = nonCssToken(selector);
|
|
48
|
+
if (!token && String(selector).includes('>>>')) {
|
|
49
|
+
// ">>>" entra nello shadowRoot del primo elemento che la parte prima trova:
|
|
50
|
+
// un antenato senza shadowRoot («main >>> span») non trova niente anche
|
|
51
|
+
// se lo span c'è (note observe chrome-bridg-6379570f, chrome-bridg-f262acc4).
|
|
52
|
+
const host = String(selector).split('>>>')[0].trim();
|
|
53
|
+
return new Error(`Element not found: ${selector} (no element matches; ">>>" enters the shadowRoot of the first element "${host}" matches, `
|
|
54
|
+
+ 'so that part must be the shadow host itself, e.g. the custom element\'s tag, not an ancestor of it. '
|
|
55
|
+
+ 'Check it with query_dom, or locate the target by its visible text with find_text, which searches inside shadow roots)');
|
|
56
|
+
}
|
|
57
|
+
if (!token) return new Error(`Element not found: ${selector} (no element matches; check it with query_dom)`);
|
|
58
|
+
const what = token === 'XPath' ? 'XPath is not CSS' : `"${token}" is Playwright/jQuery syntax, not CSS`;
|
|
59
|
+
return new Error(`Invalid selector: ${selector} — ${what}: the page rejects the whole selector, every item of a comma list included. `
|
|
60
|
+
+ 'Use plain CSS (">>>" pierces shadow DOM), a ref from get_interactives, or find_text to locate by visible text.');
|
|
27
61
|
}
|
|
28
62
|
|
|
29
63
|
/**
|
|
@@ -100,10 +100,33 @@ async function attach(tabId, purpose) {
|
|
|
100
100
|
} catch (e) {
|
|
101
101
|
const msg = String(e?.message ?? e);
|
|
102
102
|
if (/already attached/i.test(msg)) throw new Error(`${purpose} unavailable: another debugger (DevTools?) is attached to this tab — close it, or retry without it`);
|
|
103
|
+
if (/chrome-extension:\/\/ URL of different extension/i.test(msg)) throw new Error(`${purpose} unavailable: ${await foreignExtensionCause(tabId, purpose)}`);
|
|
103
104
|
throw new Error(`${purpose} unavailable: ${msg}`);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
// Chrome rifiuta chrome.debugger.attach su una scheda che contiene anche un
|
|
109
|
+
// solo frame di un'altra estensione (password manager, traduttore, correttore
|
|
110
|
+
// che iniettano un iframe chrome-extension://). Il messaggio grezzo non dice
|
|
111
|
+
// quale frame né che riprovare è inutile: un agente ha ripetuto lo stesso
|
|
112
|
+
// click 7 volte (nota observe chrome-bridg-8f18b944, 1.32.1).
|
|
113
|
+
async function foreignExtensionCause(tabId, purpose) {
|
|
114
|
+
let ids = [];
|
|
115
|
+
try {
|
|
116
|
+
const frames = await chrome.webNavigation.getAllFrames({ tabId });
|
|
117
|
+
const self = chrome.runtime?.id;
|
|
118
|
+
ids = [...new Set((frames || [])
|
|
119
|
+
.map((f) => /^chrome-extension:\/\/([^/]+)/.exec(f.url || '')?.[1])
|
|
120
|
+
.filter((id) => id && id !== self))];
|
|
121
|
+
} catch { /* scheda chiusa o frame illeggibili: resta la causa senza id */ }
|
|
122
|
+
const which = ids.length ? ` (chrome-extension://${ids.join(', chrome-extension://')})` : '';
|
|
123
|
+
const remedy = purpose === 'trusted input'
|
|
124
|
+
? 'Retry without trusted (synthetic events)'
|
|
125
|
+
: 'Use the options that work without the debugger';
|
|
126
|
+
return `another extension has a frame in this page${which}, and Chrome does not let chrome.debugger attach to a tab that holds another extension's frame: retrying the same call fails the same way. `
|
|
127
|
+
+ `${remedy}, or turn that extension off for this site (chrome://extensions → Details → Site access) and reload the tab.`;
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
/** Tiene il debugger agganciato alla scheda per un motivo, finché releaseDebugger. */
|
|
108
131
|
export async function holdDebugger(tabId, why, purpose = why) {
|
|
109
132
|
listenDetach();
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Chrome Bridge for Claude Code",
|
|
4
|
-
"version": "1.32.
|
|
4
|
+
"version": "1.32.3",
|
|
5
5
|
"description": "Bridge your browser to Claude Code: 69 token-efficient web-dev automation tools over a local WebSocket. ChromeOS included.",
|
|
6
6
|
"homepage_url": "https://github.com/frsorrentino/chrome-bridge",
|
|
7
7
|
"minimum_chrome_version": "135",
|
package/extension/popup.css
CHANGED
|
@@ -13,7 +13,7 @@ import { computeTiles } from './lib/tile-layout.js';
|
|
|
13
13
|
import { classifyDownload } from './lib/download-state.js';
|
|
14
14
|
import { downloadName } from './lib/capture-names.js';
|
|
15
15
|
import { findTextInPage } from './lib/find-text.js';
|
|
16
|
-
import { elementOutcome, watchNavErrors, navErrorMessage } from './lib/command-outcome.js';
|
|
16
|
+
import { elementOutcome, notFoundError, watchNavErrors, navErrorMessage } from './lib/command-outcome.js';
|
|
17
17
|
import { withDebugger, keyEvents, mouseClickEvents, holdDebugger, releaseDebugger, debuggerHeld } from './lib/trusted-input.js';
|
|
18
18
|
import { emulationCommands } from './lib/emulation.js';
|
|
19
19
|
const { pushError } = globalThis.__cbTelemetry;
|
|
@@ -1021,7 +1021,7 @@ async function trustedTarget(tabId, frame_id, selector, what) {
|
|
|
1021
1021
|
world: 'MAIN',
|
|
1022
1022
|
});
|
|
1023
1023
|
const out = results?.[0]?.result;
|
|
1024
|
-
if (!out || out.missing) throw
|
|
1024
|
+
if (!out || out.missing) throw notFoundError(selector);
|
|
1025
1025
|
return out;
|
|
1026
1026
|
}
|
|
1027
1027
|
|
|
@@ -2556,7 +2556,7 @@ function observeContentScript(recordValues) {
|
|
|
2556
2556
|
send({ command: 'type_text', params: { selector: selectorOf(el), text: value }, human: { label: labelOf(el), sensitive, placeholder: sensitive || !recordValues } });
|
|
2557
2557
|
}, true);
|
|
2558
2558
|
document.addEventListener('keydown', (e) => {
|
|
2559
|
-
if (e.key === 'Enter' && e.target && /^(INPUT)$/.test(e.target.tagName)) send({ command: 'press_key', params: { key: 'Enter', selector: selectorOf(e.target) }, human: { label: labelOf(e.target) } });
|
|
2559
|
+
if (e.key === 'Enter' && !e.isComposing && e.keyCode !== 229 && e.target && /^(INPUT)$/.test(e.target.tagName)) send({ command: 'press_key', params: { key: 'Enter', selector: selectorOf(e.target) }, human: { label: labelOf(e.target) } });
|
|
2560
2560
|
}, true);
|
|
2561
2561
|
return true;
|
|
2562
2562
|
}
|
|
@@ -2821,7 +2821,7 @@ function handoffBanner(id, message, pick, ask, pickMax) {
|
|
|
2821
2821
|
root.querySelector('span').textContent = `${message} — ${picks.length}/${pickMax} picked, click more or Done`;
|
|
2822
2822
|
};
|
|
2823
2823
|
root.querySelector('.done').onclick = done;
|
|
2824
|
-
if (askInput) { askInput.onkeydown = (e) => { if (e.key === 'Enter') done(); }; setTimeout(() => askInput.focus(), 0); }
|
|
2824
|
+
if (askInput) { askInput.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing && e.keyCode !== 229) done(); }; setTimeout(() => askInput.focus(), 0); }
|
|
2825
2825
|
root.querySelector('.cancel').onclick = () => finish({ done: false, action: 'cancel' });
|
|
2826
2826
|
let box = null; let onMove = null; let onClick = null;
|
|
2827
2827
|
function cleanupPick() {
|
|
@@ -3458,7 +3458,7 @@ async function cmdWatchDom({ selector = 'body', attributes = true, childList = t
|
|
|
3458
3458
|
args: [selector, { attributes, childList, characterData, subtree }],
|
|
3459
3459
|
world: 'MAIN',
|
|
3460
3460
|
});
|
|
3461
|
-
if (install?.[0]?.result?.missing) throw
|
|
3461
|
+
if (install?.[0]?.result?.missing) throw notFoundError(selector);
|
|
3462
3462
|
|
|
3463
3463
|
// Read mutations
|
|
3464
3464
|
const results = await chrome.scripting.executeScript({
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-bridge-mcp",
|
|
3
|
-
"version": "1.32.
|
|
3
|
+
"version": "1.32.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "chrome-bridge-mcp",
|
|
9
|
-
"version": "1.32.
|
|
9
|
+
"version": "1.32.3",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@jridgewell/trace-mapping": "^0.3.31",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-bridge-mcp",
|
|
3
3
|
"mcpName": "io.github.frsorrentino/chrome-bridge",
|
|
4
|
-
"version": "1.32.
|
|
4
|
+
"version": "1.32.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Chrome Bridge by frsorrentino: MCP server that lets Claude Code borrow a tab from your own logged-in Chrome and hand it back — logins, 2FA and CAPTCHAs stay with you (handoff). 69 tools; 2.8-4.3× cheaper and up to 9.3× fewer turns than Claude in Chrome, measured; ChromeOS included.",
|
|
7
7
|
"main": "server/index.js",
|