chrome-bridge-mcp 1.30.1 → 1.30.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/extension/manifest.json +1 -1
- package/extension/service-worker.js +12 -2
- package/package.json +1 -1
- package/server/index.js +22 -37
- package/server/instructions.js +48 -0
- package/server/lighthouse.js +18 -1
- package/server/protocol.js +1 -1
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.30.
|
|
4
|
+
"version": "1.30.2",
|
|
5
5
|
"description": "Bridge your browser to Claude Code: 67 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",
|
|
@@ -1507,14 +1507,24 @@ function recordTabEnding(tabId, ending) {
|
|
|
1507
1507
|
if (tabEndings.size > TAB_ENDINGS_MAX) tabEndings.delete(tabEndings.keys().next().value);
|
|
1508
1508
|
}
|
|
1509
1509
|
|
|
1510
|
+
const CLOSE_WAIT_MS = 5000;
|
|
1511
|
+
|
|
1510
1512
|
async function cmdTabAction({ action, tab_id, bypass_cache = false }) {
|
|
1511
1513
|
if (!action) throw new Error('Missing required parameter: action');
|
|
1512
1514
|
const tabId = await resolveTabId(tab_id);
|
|
1513
1515
|
|
|
1514
1516
|
if (action === 'close') {
|
|
1517
|
+
// chrome.tabs.remove aspetta che la pagina lasci andare: con un dialogo
|
|
1518
|
+
// «Vuoi uscire?» aperto o la pagina bloccata in un ciclo resterebbe muta
|
|
1519
|
+
// fino al timeout del comando (30 s, visto il 15/10). Dopo 5 s si risponde
|
|
1520
|
+
// con un errore; la chiusura resta in corso e Chrome la completa quando
|
|
1521
|
+
// la pagina cede (get_tabs la riporta come closed_by_bridge).
|
|
1515
1522
|
bridgeClosing.add(tabId);
|
|
1516
|
-
|
|
1517
|
-
|
|
1523
|
+
const removing = chrome.tabs.remove(tabId).then(() => true, (e) => { bridgeClosing.delete(tabId); throw e; });
|
|
1524
|
+
const done = await Promise.race([removing, new Promise((r) => setTimeout(() => r(false), CLOSE_WAIT_MS))]);
|
|
1525
|
+
if (done) return { action, closed: tabId };
|
|
1526
|
+
removing.catch(() => {});
|
|
1527
|
+
throw new Error(`Tab ${tabId} did not close within ${CLOSE_WAIT_MS / 1000} s: the page is not answering, either a "Leave site?" dialog is open (beforeunload) or the page is stuck in a loop. The close is still pending and completes if the page lets go. Look at the tab: answer the dialog, or close it by hand.`);
|
|
1518
1528
|
}
|
|
1519
1529
|
if (action === 'discard') {
|
|
1520
1530
|
// La scheda resta nella barra e si ricarica al focus: il processo muore, la
|
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.30.
|
|
4
|
+
"version": "1.30.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). 67 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",
|
package/server/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { WSManager } from './ws-manager.js';
|
|
|
14
14
|
import { registerTools } from './tools.js';
|
|
15
15
|
import { createObserver } from './observe.js';
|
|
16
16
|
import { BRANDED_CHROME_HINT, launchBrowser } from './launcher.js';
|
|
17
|
+
import { INSTRUCTIONS } from './instructions.js';
|
|
17
18
|
import { DEFAULT_PORT, VERSION } from './protocol.js';
|
|
18
19
|
|
|
19
20
|
// Launch mode: browser dedicato (profilo effimero + estensione unpacked).
|
|
@@ -50,9 +51,7 @@ function parseCaps() {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
async function main() {
|
|
53
|
-
// 1. Crea il server MCP
|
|
54
|
-
// instructions: dette una volta qui invece che ripetute in ogni descrizione
|
|
55
|
-
// tool — pesano ~1 volta nel contesto del client anziché ~50.
|
|
54
|
+
// 1. Crea il server MCP (istruzioni e loro limiti in instructions.js)
|
|
56
55
|
const mcpServer = new McpServer({
|
|
57
56
|
name: 'chrome-bridge',
|
|
58
57
|
version: VERSION,
|
|
@@ -60,36 +59,7 @@ async function main() {
|
|
|
60
59
|
// get_status({enable}) accende N tool uno per uno: una sola notifica,
|
|
61
60
|
// non N (un ciclo di list_changed costava CPU a Claude Code, 2.1.271).
|
|
62
61
|
debouncedNotificationMethods: ['notifications/tools/list_changed'],
|
|
63
|
-
instructions:
|
|
64
|
-
// Eval del 29/09: con Claude in Chrome richiesto e assente, il modello
|
|
65
|
-
// ripiegava su chrome-bridge senza chiedere (2 casi su 2).
|
|
66
|
-
'If the user names another browser tool (e.g. Claude in Chrome) and it is not available, say so and ask; do not switch to chrome-bridge on your own.',
|
|
67
|
-
'Selectors on DOM tools pierce shadow DOM with ">>>" ("my-app >>> button.save").',
|
|
68
|
-
'tab_id omitted = the tab last navigated/created in this session, else the active tab. frame_id omitted = main frame (list frames with get_frames).',
|
|
69
|
-
'To find targets use get_interactives, not read_page(html); its refs (n1, n2…) are the ref param of click/type_text/hover and of fill_form fields, and navigate already returns them.',
|
|
70
|
-
// Il costo dominante sono i TURNI, non i byte: un turno vale ~15-30 volte
|
|
71
|
-
// un KB di output risparmiato. Queste due clausole si pagano una volta
|
|
72
|
-
// qui e valgono più di qualunque ottimizzazione di schema.
|
|
73
|
-
'Several fields: one fill_form (submit_selector submits in the same call), not repeated type_text: one turn instead of N.',
|
|
74
|
-
'Tables: extract_table (where/columns filtered server-side) or extract, never read_page: a big table through read_page costs tens of thousands of tokens.',
|
|
75
|
-
// Debug del 29/09: in 7 run su 20 il modello caricava la skill prima di
|
|
76
|
-
// agire e pagava ~2 turni. La regola che serve sta qui, già in contesto.
|
|
77
|
-
'Debugging a page: navigate already reports the first console error mapped to its source line and the failed requests with status; get_css_styles names the rule behind a style and flags undefined_vars. Start there; no skill or recipe needed.',
|
|
78
|
-
'Detail in a screenshot: element_screenshot (selector or region, scale), not another full one. To check an outcome: assert or wait_for poll for you, no screenshot.',
|
|
79
|
-
// Il blocco più frequente sul campo (Meta Ads Manager, 23/09/2026): tre
|
|
80
|
-
// attese scadute e due screenshot falliti prima di capire che la
|
|
81
|
-
// finestra era dietro un'altra.
|
|
82
|
-
// Il 42% delle sessioni reali caricava i tool con 2+ ToolSearch (media 2,0).
|
|
83
|
-
'When tools are deferred, batch every tool needed for the next workflow into ONE ToolSearch call using exact names (select:name1,name2,...); do not search one tool at a time.',
|
|
84
|
-
'Missing optional tools: call get_status({enable:["group",...]}) once for all required groups (audits, visual, network, storage, dom, files, perf; "all" enables all). After tools/list_changed, use them next turn; if still deferred, batch-search their names once. No restart or user configuration needed.',
|
|
85
|
-
// 14/10/2026: nei flussi di lavoro Claude restava fermo sulla finestra
|
|
86
|
-
// «Salva con nome», di sistema e fuori portata dell'estensione. Su ChromeOS
|
|
87
|
-
// con «Chiedi dove salvare ciascun file» si apre anche con saveAs:false.
|
|
88
|
-
'Saving a file: never click a Download/Export button and wait for the browser download, the OS "Save as" dialog can open and no tool can press it. '
|
|
89
|
-
+ 'A file with a URL: http_request with save_to (browser cookies, bytes written to disk, no dialog). '
|
|
90
|
-
+ 'A file the page builds itself (blob, data URL, showSaveFilePicker, a download the page starts): capture_downloads arm, then click, then capture_downloads wait.',
|
|
91
|
-
'A tab in a minimized, covered or background window does not render: screenshots fail, timers slow down. get_page_info reports visibility (page_hidden in a result means the same); bring the window on screen, or create_tab new_window with bounds.',
|
|
92
|
-
].join(' '),
|
|
62
|
+
instructions: INSTRUCTIONS,
|
|
93
63
|
});
|
|
94
64
|
|
|
95
65
|
// 2. Avvia il WebSocket server
|
|
@@ -104,16 +74,31 @@ async function main() {
|
|
|
104
74
|
// durante l'avvio (osservate 3 directory residue, una da 122 MB).
|
|
105
75
|
let browser = null;
|
|
106
76
|
const session = { tools: null };
|
|
107
|
-
|
|
77
|
+
// Una sola chiusura anche quando fine di stdin, chiusura del trasporto e
|
|
78
|
+
// segnale arrivano insieme.
|
|
79
|
+
let closing = null;
|
|
80
|
+
const shutdown = () => (closing ??= (async () => {
|
|
108
81
|
console.error('[chrome-bridge] Shutting down...');
|
|
109
|
-
|
|
110
|
-
|
|
82
|
+
// Il browser di --launch per primo: su Windows Claude Code chiude stdin e
|
|
83
|
+
// dopo 300 ms uccide l'albero dei processi (2.1.296). Le sue schede
|
|
84
|
+
// muoiono con lui; nel Chrome dell'utente si chiudono quelle rimaste vuote.
|
|
85
|
+
if (browser) {
|
|
86
|
+
try { await browser.stop(); } catch {}
|
|
87
|
+
} else {
|
|
88
|
+
try { await Promise.race([session.tools?.closeEmptyOwnedTabs(), new Promise((r) => setTimeout(r, 2000))]); } catch {}
|
|
89
|
+
}
|
|
111
90
|
try { await wsManager.stop(); } catch {}
|
|
112
91
|
try { await mcpServer.close(); } catch {}
|
|
113
92
|
process.exit(0);
|
|
114
|
-
};
|
|
93
|
+
})());
|
|
115
94
|
process.on('SIGINT', shutdown);
|
|
116
95
|
process.on('SIGTERM', shutdown);
|
|
96
|
+
// Su Windows SIGINT e SIGTERM non arrivano, e un client terminato di colpo
|
|
97
|
+
// non manda segnali: in entrambi i casi resta solo la fine di stdin. L'SDK
|
|
98
|
+
// non la ascolta, e il WebSocket teneva vivo il processo.
|
|
99
|
+
process.stdin.once('end', shutdown);
|
|
100
|
+
process.stdin.once('close', shutdown);
|
|
101
|
+
mcpServer.server.onclose = shutdown;
|
|
117
102
|
process.on('uncaughtException', (err) => {
|
|
118
103
|
console.error('[chrome-bridge] Uncaught exception:', err);
|
|
119
104
|
shutdown().catch(() => process.exit(1));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Istruzioni del server MCP: dette una volta all'initialize invece che
|
|
3
|
+
* ripetute in ogni descrizione tool, pesano ~1 volta nel contesto del client
|
|
4
|
+
* anziché ~50.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code le taglia: 4.096 caratteri dalla 2.1.296, 2.048 fino alla
|
|
7
|
+
* 2.1.295. RULES deve entrare nei primi 2.048; TAIL ripete quello che dicono
|
|
8
|
+
* già le descrizioni dei tool, quindi un client vecchio la può perdere.
|
|
9
|
+
* test/unit/instructions.test.js tiene i due limiti.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const RULES = [
|
|
13
|
+
// Eval del 29/09: con Claude in Chrome richiesto e assente, il modello
|
|
14
|
+
// ripiegava su chrome-bridge senza chiedere (2 casi su 2).
|
|
15
|
+
'If the user names another browser tool (e.g. Claude in Chrome) and it is not available, say so and ask; do not switch to chrome-bridge on your own.',
|
|
16
|
+
// Il costo dominante sono i TURNI, non i byte: un turno vale ~15-30 volte
|
|
17
|
+
// un KB di output risparmiato. Queste due clausole si pagano una volta
|
|
18
|
+
// qui e valgono più di qualunque ottimizzazione di schema.
|
|
19
|
+
'Several fields: one fill_form (submit_selector submits in the same call), not repeated type_text: one turn instead of N.',
|
|
20
|
+
'Tables: extract_table (where/columns filtered server-side) or extract, never read_page: a big table through read_page costs tens of thousands of tokens.',
|
|
21
|
+
// Debug del 29/09: in 7 run su 20 il modello caricava la skill prima di
|
|
22
|
+
// agire e pagava ~2 turni. La regola che serve sta qui, già in contesto.
|
|
23
|
+
'Debugging a page: navigate already reports the first console error mapped to its source line and the failed requests with status; get_css_styles names the rule behind a style and flags undefined_vars. Start there; no skill or recipe needed.',
|
|
24
|
+
'Detail in a screenshot: element_screenshot (selector or region, scale), not another full one. To check an outcome: assert or wait_for poll for you, no screenshot.',
|
|
25
|
+
// Il 42% delle sessioni reali caricava i tool con 2+ ToolSearch (media 2,0).
|
|
26
|
+
'When tools are deferred, batch every tool needed for the next workflow into ONE ToolSearch call using exact names (select:name1,name2,...); do not search one tool at a time.',
|
|
27
|
+
'Missing optional tools: call get_status({enable:["group",...]}) once for all required groups (audits, visual, network, storage, dom, files, perf; "all" enables all). After tools/list_changed, use them next turn; if still deferred, batch-search their names once. No restart or user configuration needed.',
|
|
28
|
+
// 14/10/2026: nei flussi di lavoro Claude restava fermo sulla finestra
|
|
29
|
+
// «Salva con nome», di sistema e fuori portata dell'estensione. Su ChromeOS
|
|
30
|
+
// con «Chiedi dove salvare ciascun file» si apre anche con saveAs:false.
|
|
31
|
+
'Saving a file: never click a Download/Export button and wait for the browser download, the OS "Save as" dialog can open and no tool can press it. '
|
|
32
|
+
+ 'A file with a URL: http_request with save_to (browser cookies, bytes written to disk, no dialog). '
|
|
33
|
+
+ 'A file the page builds itself (blob, data URL, showSaveFilePicker, a download the page starts): capture_downloads arm, then click, then capture_downloads wait.',
|
|
34
|
+
// Il blocco più frequente sul campo (Meta Ads Manager, 23/09/2026): tre
|
|
35
|
+
// attese scadute e due screenshot falliti prima di capire che la
|
|
36
|
+
// finestra era dietro un'altra.
|
|
37
|
+
'A tab in a minimized, covered or background window does not render: screenshots fail, timers slow down. get_page_info reports visibility (page_hidden in a result means the same); bring the window on screen, or create_tab new_window with bounds.',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// Le ripetono le descrizioni di read_page, get_interactives, navigate e
|
|
41
|
+
// fill_form, e dei parametri ref, tab_id, frame_id e selector.
|
|
42
|
+
export const TAIL = [
|
|
43
|
+
'To find targets use get_interactives, not read_page(html); its refs (n1, n2…) are the ref param of click/type_text/hover and of fill_form fields, and navigate already returns them.',
|
|
44
|
+
'tab_id omitted = the tab last navigated/created in this session, else the active tab. frame_id omitted = main frame (list frames with get_frames).',
|
|
45
|
+
'Selectors on DOM tools pierce shadow DOM with ">>>" ("my-app >>> button.save").',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export const INSTRUCTIONS = [...RULES, ...TAIL].join(' ');
|
package/server/lighthouse.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* chrome-bridge gira da Node 18: sotto si usa la 12.8.2 (Node >= 18.16).
|
|
10
10
|
*/
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { win32 } from 'node:path';
|
|
12
14
|
|
|
13
15
|
export function lighthouseVersion(node = process.versions.node) {
|
|
14
16
|
const [maj, min] = node.split('.').map(Number);
|
|
@@ -21,9 +23,24 @@ export function lighthouseArgs(url, port, outBase, { categories, formFactor = 'm
|
|
|
21
23
|
'--quiet', ...(categories?.length ? [`--only-categories=${categories.join(',')}`] : []), ...(formFactor === 'desktop' ? ['--preset=desktop'] : [])];
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Come lanciare npx. Su Windows npx è npx.cmd, che spawn senza shell non trova
|
|
28
|
+
* (ENOENT): si usa npx-cli.js accanto a node.exe con il node del server, senza
|
|
29
|
+
* shell. Se manca, npx.cmd con la shell e gli argomenti fra virgolette.
|
|
30
|
+
*/
|
|
31
|
+
export function npxCommand({ platform = process.platform, execPath = process.execPath, env = process.env, exists = existsSync } = {}) {
|
|
32
|
+
if (env.CHROME_BRIDGE_NPX) return { cmd: env.CHROME_BRIDGE_NPX, pre: [], shell: false };
|
|
33
|
+
if (platform !== 'win32') return { cmd: 'npx', pre: [], shell: false };
|
|
34
|
+
const cli = win32.join(win32.dirname(execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js');
|
|
35
|
+
if (exists(cli)) return { cmd: execPath, pre: [cli], shell: false };
|
|
36
|
+
return { cmd: 'npx.cmd', pre: [], shell: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
export function runLighthouse(args, { timeoutMs = 240000 } = {}) {
|
|
25
40
|
return new Promise((resolve, reject) => {
|
|
26
|
-
const
|
|
41
|
+
const { cmd, pre, shell } = npxCommand();
|
|
42
|
+
const argv = [...pre, ...args].map((a) => (shell ? `"${a}"` : a));
|
|
43
|
+
const p = spawn(cmd, argv, { stdio: ['ignore', 'ignore', 'pipe'], shell });
|
|
27
44
|
let err = '';
|
|
28
45
|
const timer = setTimeout(() => { p.kill('SIGKILL'); reject(new Error(`Lighthouse did not finish within ${timeoutMs / 1000} s`)); }, timeoutMs);
|
|
29
46
|
p.stderr.on('data', (d) => { err += d; });
|