chrome-bridge-mcp 1.17.0 → 1.18.0
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 +35 -5
- package/package.json +1 -1
- package/server/cli.js +5 -2
- package/server/protocol.js +1 -1
- package/server/tools.js +67 -17
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.
|
|
4
|
+
"version": "1.18.0",
|
|
5
5
|
"description": "Bridge your browser to Claude Code: 59 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",
|
|
@@ -683,7 +683,7 @@ async function bitmapToBase64Capped(bitmap, maxSide = MAX_IMAGE_SIDE) {
|
|
|
683
683
|
|
|
684
684
|
// --- Implementazione comandi ---
|
|
685
685
|
|
|
686
|
-
async function cmdGetTabs({ include_windows = false } = {}) {
|
|
686
|
+
async function cmdGetTabs({ include_windows = false, ended } = {}) {
|
|
687
687
|
const tabs = await chrome.tabs.query({});
|
|
688
688
|
const list = tabs.map((t) => ({
|
|
689
689
|
id: t.id,
|
|
@@ -691,8 +691,17 @@ async function cmdGetTabs({ include_windows = false } = {}) {
|
|
|
691
691
|
title: t.title,
|
|
692
692
|
active: t.active,
|
|
693
693
|
windowId: t.windowId,
|
|
694
|
+
// Una scheda scartata da Chrome (memory saver) esiste ancora ma non ha un
|
|
695
|
+
// renderer: execute_js e compagni falliscono finché non torna in primo piano.
|
|
696
|
+
...(t.discarded ? { discarded: true } : {}),
|
|
694
697
|
}));
|
|
695
|
-
|
|
698
|
+
// ended = id che il server sa suoi e non trova più: la forma ad array resta
|
|
699
|
+
// quella di sempre per chi non lo chiede.
|
|
700
|
+
const open = new Set(list.map((t) => t.id));
|
|
701
|
+
const endings = Array.isArray(ended) && ended.length
|
|
702
|
+
? Object.fromEntries(ended.filter((id) => !open.has(id)).map((id) => [id, tabEndings.get(id) ?? { reason: 'unknown' }]))
|
|
703
|
+
: null;
|
|
704
|
+
if (!include_windows) return endings ? { tabs: list, ended: endings } : list;
|
|
696
705
|
|
|
697
706
|
// Senza geometria e stato non si può decidere dove mettere una finestra: si
|
|
698
707
|
// può solo indovinare. `left` in particolare è ciò che identifica il monitor
|
|
@@ -712,7 +721,7 @@ async function cmdGetTabs({ include_windows = false } = {}) {
|
|
|
712
721
|
// provarci.
|
|
713
722
|
scriptable: (w.tabs || []).some((t) => /^(https?|file):/i.test(t.url || '')),
|
|
714
723
|
}));
|
|
715
|
-
return { tabs: list, windows };
|
|
724
|
+
return endings ? { tabs: list, windows, ended: endings } : { tabs: list, windows };
|
|
716
725
|
}
|
|
717
726
|
|
|
718
727
|
async function cmdNavigate({ url, tab_id }) {
|
|
@@ -1189,12 +1198,26 @@ async function cmdMoveTab({ tab_id, window_id, new_window = false, window_type,
|
|
|
1189
1198
|
};
|
|
1190
1199
|
}
|
|
1191
1200
|
|
|
1201
|
+
// Perché una scheda non c'è più: «No tab with id» da solo non distingue una
|
|
1202
|
+
// chiusura a mano da una sostituzione. In memoria e limitato: se il service
|
|
1203
|
+
// worker riparte il registro si svuota e il motivo torna 'unknown'.
|
|
1204
|
+
const tabEndings = new Map();
|
|
1205
|
+
const TAB_ENDINGS_MAX = 200;
|
|
1206
|
+
const bridgeClosing = new Set();
|
|
1207
|
+
|
|
1208
|
+
function recordTabEnding(tabId, ending) {
|
|
1209
|
+
if (bridgeClosing.delete(tabId) && ending.reason === 'closed') ending = { reason: 'closed_by_bridge' };
|
|
1210
|
+
tabEndings.set(tabId, { ...ending, at: new Date().toISOString() });
|
|
1211
|
+
if (tabEndings.size > TAB_ENDINGS_MAX) tabEndings.delete(tabEndings.keys().next().value);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1192
1214
|
async function cmdTabAction({ action, tab_id, bypass_cache = false }) {
|
|
1193
1215
|
if (!action) throw new Error('Missing required parameter: action');
|
|
1194
1216
|
const tabId = await resolveTabId(tab_id);
|
|
1195
1217
|
|
|
1196
1218
|
if (action === 'close') {
|
|
1197
|
-
|
|
1219
|
+
bridgeClosing.add(tabId);
|
|
1220
|
+
try { await chrome.tabs.remove(tabId); } catch (e) { bridgeClosing.delete(tabId); throw e; }
|
|
1198
1221
|
return { action, closed: tabId };
|
|
1199
1222
|
}
|
|
1200
1223
|
if (action === 'discard') {
|
|
@@ -4561,9 +4584,16 @@ async function cmdHttpAuth({ action, username, password }) {
|
|
|
4561
4584
|
|
|
4562
4585
|
// --- Tab lifecycle: cleanup injection state ---
|
|
4563
4586
|
|
|
4564
|
-
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
4587
|
+
chrome.tabs.onRemoved.addListener((tabId, info) => {
|
|
4565
4588
|
browserNetLog.delete(tabId);
|
|
4566
4589
|
mainFrameHeaders.delete(tabId);
|
|
4590
|
+
recordTabEnding(tabId, { reason: info?.isWindowClosing ? 'window_closed' : 'closed' });
|
|
4591
|
+
});
|
|
4592
|
+
|
|
4593
|
+
// Chrome sostituisce l'id quando rimpiazza la scheda (prerender, alcuni
|
|
4594
|
+
// discard): per chi teneva il vecchio id è una scheda sparita senza chiusura.
|
|
4595
|
+
chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => {
|
|
4596
|
+
recordTabEnding(removedTabId, { reason: 'replaced', replaced_by: addedTabId });
|
|
4567
4597
|
});
|
|
4568
4598
|
|
|
4569
4599
|
// --- get_interactives ---
|
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.
|
|
4
|
+
"version": "1.18.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MCP server with 59 tools for browser automation via Chrome extension. Works on ChromeOS/Crostini.",
|
|
7
7
|
"main": "server/index.js",
|
package/server/cli.js
CHANGED
|
@@ -119,10 +119,13 @@ function connect(port) {
|
|
|
119
119
|
sendCommand(type, params = {}) {
|
|
120
120
|
return new Promise((res, rej) => {
|
|
121
121
|
const command = createCommand(type, params);
|
|
122
|
+
// Come ws-manager: un timeout chiesto dal comando alza quello di trasporto.
|
|
123
|
+
const asked = Number(params?.timeout);
|
|
124
|
+
const timeout = Number.isFinite(asked) && asked > 0 ? Math.max(getTimeout(type), asked + 5000) : getTimeout(type);
|
|
122
125
|
const timer = setTimeout(() => {
|
|
123
126
|
pending.delete(command.id);
|
|
124
|
-
rej(new Error(`Command ${type} timed out after ${
|
|
125
|
-
},
|
|
127
|
+
rej(new Error(`Command ${type} timed out after ${timeout}ms`));
|
|
128
|
+
}, timeout);
|
|
126
129
|
pending.set(command.id, { res, rej, timer });
|
|
127
130
|
ws.send(JSON.stringify(command));
|
|
128
131
|
});
|
package/server/protocol.js
CHANGED
package/server/tools.js
CHANGED
|
@@ -113,6 +113,34 @@ function fitArray(items, max, serialize) {
|
|
|
113
113
|
return items.slice(0, lo);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Output di execute_js sotto `max` senza rompere il JSON: tagliare la
|
|
118
|
+
* serializzazione a metà stringa dava «Invalid control character» a chi la
|
|
119
|
+
* parsava. Una stringa si accorcia dentro il suo valore, un array per elementi
|
|
120
|
+
* (jsonText), il resto diventa un prefisso dichiarato come tale.
|
|
121
|
+
*/
|
|
122
|
+
export function jsResultText(data, max) {
|
|
123
|
+
const text = JSON.stringify(data);
|
|
124
|
+
if (text == null || text.length <= max) return text;
|
|
125
|
+
const hint = 'raise max_length, or save_to for the whole result';
|
|
126
|
+
const value = data?.result;
|
|
127
|
+
const wrap = (field, cut, total) => JSON.stringify({ [field]: cut, truncated: true, total_chars: total, hint });
|
|
128
|
+
const fitString = (field, full) => {
|
|
129
|
+
// Gli escape JSON allungano la stringa: il prefisso più lungo che sta nel
|
|
130
|
+
// tetto si cerca sulla serializzazione, non sui caratteri grezzi.
|
|
131
|
+
let lo = 0;
|
|
132
|
+
let hi = Math.min(full.length, max);
|
|
133
|
+
while (lo < hi) {
|
|
134
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
135
|
+
if (wrap(field, full.slice(0, mid), full.length).length <= max) lo = mid; else hi = mid - 1;
|
|
136
|
+
}
|
|
137
|
+
return wrap(field, full.slice(0, lo), full.length);
|
|
138
|
+
};
|
|
139
|
+
if (typeof value === 'string') return fitString('result', value);
|
|
140
|
+
if (Array.isArray(value)) return jsonText(data, max, hint);
|
|
141
|
+
return fitString('result_json_prefix', JSON.stringify(value));
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
/** Una riga (oggetto colonna→valore o array di celle) soddisfa il filtro where. */
|
|
117
145
|
function tableRowMatches(row, where) {
|
|
118
146
|
return Object.entries(where).every(([col, needle]) => {
|
|
@@ -534,21 +562,38 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
534
562
|
server.tool(
|
|
535
563
|
'get_tabs',
|
|
536
564
|
'List every open tab with id, url, title, active flag and mine (created by this session). Read-only. Find a tab_id when the implicit '
|
|
537
|
-
+ 'target is not the one you mean. include_windows adds position, size, state and type of each window — needed before moving or tiling.'
|
|
565
|
+
+ 'target is not the one you mean. include_windows adds position, size, state and type of each window — needed before moving or tiling. '
|
|
566
|
+
+ 'Session tabs gone since the last call come back once in closed_session_tabs with the reason (closed, window_closed, replaced, closed_by_bridge).',
|
|
538
567
|
{
|
|
539
568
|
include_windows: z.boolean().optional().default(false)
|
|
540
569
|
.describe('Also return the windows with bounds, state, type and tab count'),
|
|
541
570
|
},
|
|
542
571
|
async ({ include_windows }) => {
|
|
543
|
-
|
|
572
|
+
// `ended` = le schede che il server sa sue: l'estensione dà il motivo di
|
|
573
|
+
// quelle che non trova più. Senza schede di sessione la risposta resta
|
|
574
|
+
// quella di sempre.
|
|
575
|
+
const data = await send(MessageType.GET_TABS, {
|
|
576
|
+
include_windows: include_windows === true,
|
|
577
|
+
...(ownedTabs.size ? { ended: [...ownedTabs] } : {}),
|
|
578
|
+
});
|
|
544
579
|
const list = Array.isArray(data) ? data : (data?.tabs ?? []);
|
|
580
|
+
const windows = Array.isArray(data) ? null : data?.windows;
|
|
545
581
|
for (const t of list) if (ownedTabs.has(t.id)) t.mine = true;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
};
|
|
582
|
+
const missing = [...ownedTabs].filter((id) => !list.some((t) => t.id === id));
|
|
583
|
+
const shaped = windows ? { tabs: list, windows } : list;
|
|
584
|
+
if (!missing.length) return { content: [{ type: 'text', text: jsonText(shaped) }] };
|
|
585
|
+
|
|
586
|
+
// Un'estensione precedente ignora `ended` e risponde con l'array: motivo ignoto.
|
|
587
|
+
const ended = (!Array.isArray(data) && data?.ended) || {};
|
|
588
|
+
const closed = missing.map((id) => ({ id, ...(ended[id] ?? { reason: 'unknown' }) }));
|
|
589
|
+
for (const c of closed) {
|
|
590
|
+
ownedTabs.delete(c.id);
|
|
591
|
+
const successor = c.replaced_by != null ? list.find((t) => t.id === c.replaced_by) : null;
|
|
592
|
+
// Scheda sostituita da Chrome: resta della sessione con il nuovo id.
|
|
593
|
+
if (successor) { ownedTabs.add(successor.id); successor.mine = true; }
|
|
594
|
+
if (sessionTabId === c.id) sessionTabId = successor?.id ?? null;
|
|
595
|
+
}
|
|
596
|
+
return { content: [{ type: 'text', text: jsonText({ tabs: list, ...(windows ? { windows } : {}), closed_session_tabs: closed }) }] };
|
|
552
597
|
}
|
|
553
598
|
);
|
|
554
599
|
|
|
@@ -645,21 +690,25 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
645
690
|
// --- execute_js ---
|
|
646
691
|
server.tool(
|
|
647
692
|
'execute_js',
|
|
648
|
-
'Run JavaScript in the page (MAIN world). Requires the extension\'s "Allow user scripts" toggle; errors explain setup if disabled.'
|
|
693
|
+
'Run JavaScript in the page (MAIN world). Requires the extension\'s "Allow user scripts" toggle; errors explain setup if disabled. '
|
|
694
|
+
+ 'A promise is awaited up to timeout. A cut result stays valid JSON and says truncated; save_to writes the whole result to disk.',
|
|
649
695
|
{
|
|
650
696
|
code: z.string().describe('JS evaluated in the page; the value of the last expression is returned'),
|
|
651
697
|
tab_id: tabId,
|
|
652
698
|
frame_id: frameId,
|
|
653
699
|
max_length: z.number().optional().default(20000).describe('Max output chars'),
|
|
700
|
+
timeout: z.number().optional().describe('Max ms to wait for the result (default 30000); raise it for long async work'),
|
|
701
|
+
save_to: saveToField('the result (a string as is, anything else as JSON)'),
|
|
654
702
|
},
|
|
655
|
-
async ({ code, tab_id, frame_id, max_length }) => {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
}
|
|
703
|
+
async ({ code, tab_id, frame_id, max_length, timeout, save_to }) => {
|
|
704
|
+
guardWrite(save_to);
|
|
705
|
+
const data = await send(MessageType.EXECUTE_JS, { code, tab_id, frame_id, ...(timeout ? { timeout } : {}) });
|
|
706
|
+
if (save_to) {
|
|
707
|
+
const value = data?.result;
|
|
708
|
+
const isString = typeof value === 'string';
|
|
709
|
+
return savedSummary(save_to, Buffer.from(isString ? value : JSON.stringify(value ?? null), 'utf8'), { format: isString ? 'text' : 'json' });
|
|
710
|
+
}
|
|
711
|
+
return { content: [{ type: 'text', text: jsResultText(data, max_length ?? DEFAULT_MAX_OUTPUT) }] };
|
|
663
712
|
}
|
|
664
713
|
);
|
|
665
714
|
|
|
@@ -1367,6 +1416,7 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1367
1416
|
return { content: [{ type: 'text', text: jsonText({ closed }) }] };
|
|
1368
1417
|
}
|
|
1369
1418
|
const data = await send(MessageType.TAB_ACTION, { action, bypass_cache, tab_id });
|
|
1419
|
+
if (action === 'close') ownedTabs.delete(data?.closed ?? tab_id ?? sessionTabId);
|
|
1370
1420
|
if (action === 'close' && (tab_id == null || tab_id === sessionTabId)) sessionTabId = null;
|
|
1371
1421
|
if (action === 'activate' && tab_id != null) sessionTabId = tab_id;
|
|
1372
1422
|
return { content: [{ type: 'text', text: jsonText(data) }] };
|