chrome-bridge-mcp 1.30.2 → 1.30.4
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/package.json +1 -1
- package/server/index.js +22 -37
- package/server/instructions.js +48 -0
- package/server/protocol.js +1 -1
- package/server/tools.js +72 -37
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.4",
|
|
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/protocol.js
CHANGED
package/server/tools.js
CHANGED
|
@@ -361,7 +361,7 @@ export const TOOL_ANNOTATIONS = {
|
|
|
361
361
|
get_frames: ro(),
|
|
362
362
|
get_interactives: ro(),
|
|
363
363
|
get_page_info: ro(),
|
|
364
|
-
get_status: ro()
|
|
364
|
+
get_status: rw({ idempotent: true }), // enable accende gruppi di tool per la sessione: ro() lo negava
|
|
365
365
|
get_storage: ro(),
|
|
366
366
|
get_tabs: ro(),
|
|
367
367
|
manage_downloads: rw({ open: true }), // action=download scrive un file sul disco e va in rete
|
|
@@ -379,7 +379,7 @@ export const TOOL_ANNOTATIONS = {
|
|
|
379
379
|
read_page: ro(),
|
|
380
380
|
screenshot: ro(),
|
|
381
381
|
wait_for: ro(),
|
|
382
|
-
watch_dom:
|
|
382
|
+
watch_dom: rw({ destructive: true }), // installa un observer nella pagina; clear e stop cancellano le mutation raccolte
|
|
383
383
|
audit: ro(),
|
|
384
384
|
read_form: ro(),
|
|
385
385
|
watch: rw({ idempotent: true }),
|
|
@@ -838,10 +838,11 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
838
838
|
// --- get_status ---
|
|
839
839
|
server.tool(
|
|
840
840
|
'get_status',
|
|
841
|
-
'
|
|
842
|
-
+ '(false
|
|
843
|
-
+ '
|
|
844
|
-
+ '
|
|
841
|
+
'Status of the bridge itself, not of a page: call it when a tool you expect is missing or browser commands fail '
|
|
842
|
+
+ '(connected=false: the extension is not reachable). Also reports mode (primary/relay), host, port, server and '
|
|
843
|
+
+ 'extension versions, js_evaluation (false under --no-js), write_root, read_root, session_tab_id (the implicit tab), '
|
|
844
|
+
+ 'owned_tabs (created by this session), uptime_sec. caps_active and caps_available: a missing tool lives in an inactive cap. '
|
|
845
|
+
+ 'enable is the only change it makes: it switches caps on for this session, no restart, and lists the tools added. Optional caps: '
|
|
845
846
|
+ `${Object.entries(TOOL_CAPS).map(([g, t]) => `${g} (${t.join(', ')})`).join('; ')}.`,
|
|
846
847
|
{
|
|
847
848
|
enable: z.array(z.enum(['all', ...Object.keys(TOOL_CAPS)])).optional()
|
|
@@ -895,7 +896,7 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
895
896
|
// --- get_tabs ---
|
|
896
897
|
server.tool(
|
|
897
898
|
'get_tabs',
|
|
898
|
-
'List every open tab with id, url, title, active flag and mine (created by this session).
|
|
899
|
+
'List every open tab with id, url, title, active flag and mine (created by this session). Find a tab_id when the implicit '
|
|
899
900
|
+ 'target is not the one you mean. include_windows adds position, size, state and type of each window — needed before moving or tiling. '
|
|
900
901
|
+ 'Session tabs gone since the last call come back once in closed_session_tabs with the reason (closed, window_closed, replaced, closed_by_bridge).',
|
|
901
902
|
{
|
|
@@ -934,7 +935,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
934
935
|
// --- navigate ---
|
|
935
936
|
server.tool(
|
|
936
937
|
'navigate',
|
|
937
|
-
'Navigate a Chrome tab to a URL
|
|
938
|
+
'Navigate a Chrome tab to a URL and wait for the load; DNS and connection errors fail the call. Without tab_id it reuses the tab '
|
|
939
|
+
+ 'this session last navigated or created, else opens a new one rather than taking over the active tab. Returns a preview of '
|
|
940
|
+
+ 'interactive elements with refs usable in click/type_text/hover, plus the first console errors and failed requests, if any.',
|
|
938
941
|
{
|
|
939
942
|
url: z.string().describe('Absolute URL, or a path resolved against the current page'),
|
|
940
943
|
tab_id: tabId,
|
|
@@ -1052,7 +1055,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1052
1055
|
server.tool(
|
|
1053
1056
|
'execute_js',
|
|
1054
1057
|
'Run JavaScript in the page (MAIN world). Requires the extension\'s "Allow user scripts" toggle; errors explain setup if disabled. '
|
|
1055
|
-
+ '
|
|
1058
|
+
+ 'Use it when no dedicated tool fits: extract or extract_table read data, query_dom inspects elements, inject_css restyles, '
|
|
1059
|
+
+ 'and none of them needs the toggle. A promise is awaited up to timeout. A cut result stays valid JSON and says truncated; save_to writes the whole result to disk.',
|
|
1056
1060
|
{
|
|
1057
1061
|
code: z.string().describe('JS evaluated in the page; the value of the last expression is returned'),
|
|
1058
1062
|
tab_id: tabId,
|
|
@@ -1177,7 +1181,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1177
1181
|
'Page metadata: title, url, visibility, doctype, charset, meta tags, scripts, stylesheets, links and forms. visibility=hidden means '
|
|
1178
1182
|
+ 'the window is minimized, covered or in the background: the page does not render (screenshots fail) and its timers slow down. '
|
|
1179
1183
|
+ 'dev.server names a dev server (vite, webpack-dev-server, next, nuxt) and dev.overlay carries the compiler message when an error '
|
|
1180
|
-
+ 'overlay is open: read it before treating the page as valid.'
|
|
1184
|
+
+ 'overlay is open: read it before treating the page as valid. For the text use read_page, for specific elements query_dom, '
|
|
1185
|
+
+ 'for the bridge itself get_status.',
|
|
1181
1186
|
{
|
|
1182
1187
|
tab_id: tabId,
|
|
1183
1188
|
frame_id: frameId,
|
|
@@ -1217,7 +1222,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1217
1222
|
// --- query_dom ---
|
|
1218
1223
|
server.tool(
|
|
1219
1224
|
'query_dom',
|
|
1220
|
-
'Query DOM elements by CSS selector, returning structure, attributes, bounding rect
|
|
1225
|
+
'Query DOM elements by CSS selector, returning structure, attributes, bounding rect and, with properties, computed styles. '
|
|
1226
|
+
+ 'Use it when you already have a selector (no match: count 0); to find one, get_interactives or find_text; '
|
|
1227
|
+
+ 'for the rule that set a style, get_css_styles.',
|
|
1221
1228
|
{
|
|
1222
1229
|
selector: z.string().describe('CSS selector; ">>>" pierces shadow DOM. Matches all, not just the first'),
|
|
1223
1230
|
properties: z.array(z.string()).optional().describe('Computed styles to include, e.g. ["color"]'),
|
|
@@ -1306,7 +1313,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1306
1313
|
// --- read_console ---
|
|
1307
1314
|
server.tool(
|
|
1308
1315
|
'read_console',
|
|
1309
|
-
'Read console messages captured since page load, incl. uncaught errors and unhandled rejections.'
|
|
1316
|
+
'Read console messages captured since page load, incl. uncaught errors and unhandled rejections. '
|
|
1317
|
+
+ 'clear deletes the messages it returns, hence not read-only; since=cursor reads only newer ones without deleting, '
|
|
1318
|
+
+ 'and failed requests are in monitor_network.',
|
|
1310
1319
|
{
|
|
1311
1320
|
clear: z.boolean().optional().default(false).describe('Clear buffer after read'),
|
|
1312
1321
|
level: z.enum(['all', 'log', 'warn', 'error', 'info', 'debug']).optional().default('all').describe('all merges every level in one chronological list'),
|
|
@@ -1351,7 +1360,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1351
1360
|
// --- monitor_network ---
|
|
1352
1361
|
server.tool(
|
|
1353
1362
|
'monitor_network',
|
|
1354
|
-
'Monitor network requests. source=page: XHR/fetch hook (installed on first call); source=browser: all requests incl. static assets; source=websocket: connections and messages. format=har exports HAR 1.2.'
|
|
1363
|
+
'Monitor network requests. source=page: XHR/fetch hook (installed on first call); source=browser: all requests incl. static assets; source=websocket: connections and messages. format=har exports HAR 1.2. '
|
|
1364
|
+
+ 'It only observes: to block, stub or rewrite requests use network_rules, to send one yourself http_request.',
|
|
1355
1365
|
{
|
|
1356
1366
|
clear: z.boolean().optional().default(false).describe('Clear buffer after read'),
|
|
1357
1367
|
source: z.enum(['page', 'browser', 'websocket']).optional().default('page').describe('page: XHR/fetch only; browser: static assets too; websocket: connections and messages'),
|
|
@@ -1467,7 +1477,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1467
1477
|
// --- scroll ---
|
|
1468
1478
|
server.tool(
|
|
1469
1479
|
'scroll',
|
|
1470
|
-
'Scroll. action=to: once to element/coordinates; action=until: repeatedly until element visible, network idle, or no new content (infinite scroll).'
|
|
1480
|
+
'Scroll. action=to: once to element/coordinates; action=until: repeatedly until element visible, network idle, or no new content (infinite scroll). '
|
|
1481
|
+
+ 'It moves the tab\'s scroll position and returns it (to) or stopped_reason and steps (until); full_page_screenshot '
|
|
1482
|
+
+ 'scrolls by itself, wait_for waits without scrolling.',
|
|
1471
1483
|
{
|
|
1472
1484
|
action: z.enum(['to', 'until']).optional().default('to').describe('to jumps to a position or element; until scrolls repeatedly to load more'),
|
|
1473
1485
|
selector: z.string().optional().describe('Target (to) or stop element (until=element)'),
|
|
@@ -1724,7 +1736,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1724
1736
|
// --- watch_dom ---
|
|
1725
1737
|
server.tool(
|
|
1726
1738
|
'watch_dom',
|
|
1727
|
-
'Watch DOM mutations (MutationObserver). First call installs the watcher
|
|
1739
|
+
'Watch DOM mutations (MutationObserver) under selector, to see what an action changed. First call installs the watcher in the '
|
|
1740
|
+
+ 'page; later calls return the latest mutations (type, tag, attribute, nodes added/removed), clear deletes the ones returned, '
|
|
1741
|
+
+ 'stop disconnects and empties the buffer. To wait for one element or text use wait_for.',
|
|
1728
1742
|
{
|
|
1729
1743
|
selector: z.string().optional().default('body').describe('Subtree to observe; ">>>" pierces shadow DOM'),
|
|
1730
1744
|
attributes: z.boolean().optional().default(true).describe('Report attribute changes'),
|
|
@@ -1838,7 +1852,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1838
1852
|
+ 'composited_estimate guesses whether only transform/opacity/filter move (an estimate, not the compositor\'s answer); fade_only marks '
|
|
1839
1853
|
+ 'opacity-only fades, what prefers-reduced-motion should leave. Without duration_ms: what runs now. With duration_ms: records every '
|
|
1840
1854
|
+ 'animation that starts in the window, optionally after an action (hover, click, scroll, key), since a 150 ms transition is over '
|
|
1841
|
-
+ 'before a second call. Refuses on a hidden page. Pair with emulate_media reducedMotion to check the reduced version
|
|
1855
|
+
+ 'before a second call. Refuses on a hidden page. Pair with emulate_media reducedMotion to check the reduced version; '
|
|
1856
|
+
+ 'how smoothly they run is frames.',
|
|
1842
1857
|
{
|
|
1843
1858
|
scope: z.string().optional().describe('CSS selector: only animations on this element or inside it'),
|
|
1844
1859
|
duration_ms: z.number().optional().describe('Record for this many ms (max 30000) instead of a snapshot'),
|
|
@@ -1874,7 +1889,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1874
1889
|
+ '(duration, blocking time, render and style/layout start, the scripts responsible), layout shifts with the elements that moved (CLS '
|
|
1875
1890
|
+ 'for the window only), and the slowest interaction (INP for the window, split into input delay, processing, presentation; needs trusted input, synthetic events have no interaction id). '
|
|
1876
1891
|
+ 'Optionally runs an action (click, hover, scroll, key) right after recording starts. Animations that started are counted too. '
|
|
1877
|
-
+ 'Compositor-only animations can stay smooth while the main thread drops frames. Refuses on a hidden page: rAF and observers stop there.'
|
|
1892
|
+
+ 'Compositor-only animations can stay smooth while the main thread drops frames. Refuses on a hidden page: rAF and observers stop there. '
|
|
1893
|
+
+ 'For load speed use perf_trace; for the settings of each animation, animations.',
|
|
1878
1894
|
{
|
|
1879
1895
|
duration_ms: z.number().optional().default(3000).describe('Recording window in ms (max 30000)'),
|
|
1880
1896
|
action: motionAction,
|
|
@@ -1948,7 +1964,7 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1948
1964
|
+ 'function responsible, long animation frames. The raw trace goes to a JSON file (path and size in the result) that DevTools > '
|
|
1949
1965
|
+ 'Performance can load. action record (default) = start, reload unless reload: false, optional action, wait duration_ms, stop. '
|
|
1950
1966
|
+ 'start/stop bracket anything else done meanwhile. Uses chrome.debugger: Chrome shows its debugging bar while tracing; in launch '
|
|
1951
|
-
+ 'mode nobody sees it. Refuses on a hidden page.',
|
|
1967
|
+
+ 'mode nobody sees it. Refuses on a hidden page. For smoothness during an interaction use frames, for scores lighthouse.',
|
|
1952
1968
|
{
|
|
1953
1969
|
action: z.enum(['record', 'start', 'stop']).optional().default('record').describe('record does start → wait → stop in one call'),
|
|
1954
1970
|
reload: z.boolean().optional().describe('Reload the page right after tracing starts (default true for record, false for start)'),
|
|
@@ -2302,7 +2318,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2302
2318
|
// --- find_text ---
|
|
2303
2319
|
server.tool(
|
|
2304
2320
|
'find_text',
|
|
2305
|
-
'Find text on the page: parent selector, context, visibility, position per match. Attaches nearby interactive elements (with refs for click/type_text/hover) for the first visible match.'
|
|
2321
|
+
'Find text on the page: parent selector, context, visibility, position per match. Attaches nearby interactive elements (with refs for click/type_text/hover) for the first visible match. '
|
|
2322
|
+
+ 'Use it when you know the words on screen but not the selector (query_dom needs one); no match returns count 0.',
|
|
2306
2323
|
{
|
|
2307
2324
|
text: z.string().describe('Literal text to find, not a regex'),
|
|
2308
2325
|
case_sensitive: z.boolean().optional().default(false).describe('Match case exactly'),
|
|
@@ -2341,7 +2358,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2341
2358
|
// --- network_rules ---
|
|
2342
2359
|
server.tool(
|
|
2343
2360
|
'network_rules',
|
|
2344
|
-
'Network interception, browser-wide, survives reloads until cleared: block, redirect, set/remove headers, stub a synthetic body, or record real API responses and replay them with forced errors/latency (local helper; from HTTPS pages the stub host must be trusted).'
|
|
2361
|
+
'Network interception, browser-wide, survives reloads until cleared: block, redirect, set/remove headers, stub a synthetic body, or record real API responses and replay them with forced errors/latency (local helper; from HTTPS pages the stub host must be trusted). '
|
|
2362
|
+
+ 'It changes traffic, while monitor_network only observes it and http_request sends one request; each rule comes back as added, list shows them, clear removes them all.',
|
|
2345
2363
|
{
|
|
2346
2364
|
action: z.enum(['block', 'redirect', 'modify_header', 'stub', 'record', 'replay', 'list', 'clear']).describe('record saves the page\'s API responses (url_filter) as a fixture, replay serves them with overrides; list/clear'),
|
|
2347
2365
|
name: z.string().optional().describe('Fixture name for record/replay'),
|
|
@@ -2607,7 +2625,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2607
2625
|
'watch',
|
|
2608
2626
|
'Keep watching a page in the background — "tell me when the pipeline is green / the Approve button appears / this number changes" — '
|
|
2609
2627
|
+ 'checked every interval_s by the extension. Events are collected, not pushed: action=poll, or a script blocking on '
|
|
2610
|
-
+ '"chrome-bridge watch --wait <name>". Survives extension idling, not a browser restart.'
|
|
2628
|
+
+ '"chrome-bridge watch --wait <name>". Survives extension idling, not a browser restart. '
|
|
2629
|
+
+ 'poll returns one line per event (time, name, kind, url, value, previous); a change due within seconds is wait_for.',
|
|
2611
2630
|
{
|
|
2612
2631
|
action: z.enum(['add', 'poll', 'list', 'remove']).optional().default('add').describe('poll = events since a timestamp'),
|
|
2613
2632
|
name: z.string().optional().describe('Watch id'),
|
|
@@ -2639,7 +2658,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2639
2658
|
server.tool(
|
|
2640
2659
|
'read_form',
|
|
2641
2660
|
'Read a form as the user filled it — label, type, value, checked/selected, required-but-empty, browser validity — to check it against the '
|
|
2642
|
-
+ 'project\'s documents before an irreversible Submit. Password and card values come back [redacted]. Read-only, on request
|
|
2661
|
+
+ 'project\'s documents before an irreversible Submit. Password and card values come back [redacted]. Read-only, on request; '
|
|
2662
|
+
+ 'to change values use fill_form.',
|
|
2643
2663
|
{
|
|
2644
2664
|
selector: z.string().optional().describe('The form or container; omitted = every visible control on the page'),
|
|
2645
2665
|
tab_id: tabId,
|
|
@@ -2657,7 +2677,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2657
2677
|
'audit',
|
|
2658
2678
|
'One-call page audit, pick the kinds: accessibility, keyboard (tab order and focus issues), SEO, security headers, broken links, '
|
|
2659
2679
|
+ 'Core Web Vitals, unused CSS, resources (which plugin/theme/module/host slows the page), cache (is the CDN serving the new version). '
|
|
2660
|
-
+ 'A summary line per kind; with save_to the full Markdown report with every finding. Read-only.'
|
|
2680
|
+
+ 'A summary line per kind; with save_to the full Markdown report with every finding. Read-only. '
|
|
2681
|
+
+ 'For one deep look at load speed use perf_trace, for Lighthouse scores lighthouse.',
|
|
2661
2682
|
{
|
|
2662
2683
|
kinds: z.array(z.enum(AUDIT_KINDS)).optional().default(DEFAULT_KINDS).describe('Default skips keyboard, css (slow, approximate), resources and cache'),
|
|
2663
2684
|
save_to: saveToField('the full Markdown report'),
|
|
@@ -2684,8 +2705,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2684
2705
|
// --- find_setting ---
|
|
2685
2706
|
server.tool(
|
|
2686
2707
|
'find_setting',
|
|
2687
|
-
'Where is a setting in an unknown admin panel: follows the panel\'s menu links (same origin) until a page contains the keyword
|
|
2688
|
-
+ '
|
|
2708
|
+
'Where is a setting in an unknown admin panel: follows the panel\'s menu links (same origin) until a page contains the keyword. '
|
|
2709
|
+
+ 'Returns the URL, the menu path (Settings › Media) and the matching text; when nothing matches, how many pages it tried. '
|
|
2710
|
+
+ 'Navigates the tab and leaves it on the last page visited, at most max_pages.',
|
|
2689
2711
|
{
|
|
2690
2712
|
keyword: z.string().describe('What you are looking for, e.g. "webp", "cron", "maintenance mode"'),
|
|
2691
2713
|
max_pages: z.number().optional().default(25).describe('Pages visited at most'),
|
|
@@ -2736,10 +2758,11 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2736
2758
|
// --- extract_table ---
|
|
2737
2759
|
server.tool(
|
|
2738
2760
|
'extract_table',
|
|
2739
|
-
'Read a real <table> as JSON: thead cells become keys, each row an object.
|
|
2761
|
+
'Read a real <table> as JSON: thead cells become keys, each row an object. '
|
|
2740
2762
|
+ 'Only for tabular markup — `where` filters rows server-side, so one matching row out of 1500 '
|
|
2741
2763
|
+ 'costs a few hundred bytes instead of the whole document. `row_count` is the table total, '
|
|
2742
|
-
+ '`match_count` how many rows passed the filter.'
|
|
2764
|
+
+ '`match_count` how many rows passed the filter. No table at index is an error saying how many the selector found; '
|
|
2765
|
+
+ 'for cards and lists use extract.',
|
|
2743
2766
|
{
|
|
2744
2767
|
selector: z.string().optional().default('table').describe('The table to read; ">>>" pierces shadow DOM'),
|
|
2745
2768
|
index: z.number().optional().default(0).describe('Which table to take when the selector matches several, 0-based'),
|
|
@@ -2760,7 +2783,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2760
2783
|
// --- drag_and_drop ---
|
|
2761
2784
|
server.tool(
|
|
2762
2785
|
'drag_and_drop',
|
|
2763
|
-
'Drag an element onto another. html5 = DragEvent+DataTransfer; pointer = pointer/mouse events (sortable libraries).'
|
|
2786
|
+
'Drag an element onto another. html5 = DragEvent+DataTransfer; pointer = pointer/mouse events (sortable libraries). '
|
|
2787
|
+
+ 'It scrolls the source into view and returns once the events are sent, without knowing whether the app accepted the drop '
|
|
2788
|
+
+ '(check with assert or query_dom); a missing element is an error.',
|
|
2764
2789
|
{
|
|
2765
2790
|
source_selector: z.string().describe('Element to drag; ">>>" pierces shadow DOM'),
|
|
2766
2791
|
target_selector: z.string().describe('Drop target; ">>>" pierces shadow DOM'),
|
|
@@ -2777,8 +2802,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2777
2802
|
// --- clipboard ---
|
|
2778
2803
|
server.tool(
|
|
2779
2804
|
'clipboard',
|
|
2780
|
-
'Read or write the system clipboard (text). Activates the tab first. '
|
|
2781
|
-
+ 'write overwrites whatever the user had copied, which is not recoverable.'
|
|
2805
|
+
'Read or write the system clipboard (text). Activates the tab and focuses its window first. '
|
|
2806
|
+
+ 'write overwrites whatever the user had copied, which is not recoverable. '
|
|
2807
|
+
+ 'To fill a field use type_text instead of write and paste; read checks what a Copy button copied, and an empty clipboard returns text "".',
|
|
2782
2808
|
{
|
|
2783
2809
|
action: z.enum(['read', 'write']).describe('write takes text; read returns the current clipboard contents'),
|
|
2784
2810
|
text: z.string().optional().describe('For write'),
|
|
@@ -2793,7 +2819,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2793
2819
|
// --- set_geolocation ---
|
|
2794
2820
|
server.tool(
|
|
2795
2821
|
'set_geolocation',
|
|
2796
|
-
'Override navigator.geolocation with fixed coordinates (page-level patch). reset restores native.'
|
|
2822
|
+
'Override navigator.geolocation with fixed coordinates (page-level patch). reset restores native. '
|
|
2823
|
+
+ 'For location-based pages (store locators, delivery zones): it answers without a permission prompt, and a reload or '
|
|
2824
|
+
+ 'navigation drops it, so call it after navigate.',
|
|
2797
2825
|
{
|
|
2798
2826
|
latitude: z.number().optional().describe('Decimal degrees, -90 to 90'),
|
|
2799
2827
|
longitude: z.number().optional().describe('Decimal degrees, -180 to 180'),
|
|
@@ -2888,7 +2916,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2888
2916
|
// --- save_page ---
|
|
2889
2917
|
server.tool(
|
|
2890
2918
|
'save_page',
|
|
2891
|
-
'Save the full page (DOM, styles, images) as an MHTML archive file on the server filesystem.'
|
|
2919
|
+
'Save the full page (DOM, styles, images) as an MHTML archive file on the server filesystem. '
|
|
2920
|
+
+ 'An offline copy Chrome reopens as it looked, for evidence or a later comparison (for an image use screenshot, for the text '
|
|
2921
|
+
+ 'read_page); it overwrites output_path and returns path and size.',
|
|
2892
2922
|
{
|
|
2893
2923
|
output_path: z.string().describe('Absolute file path to write (e.g. /tmp/page.mhtml)'),
|
|
2894
2924
|
tab_id: tabId,
|
|
@@ -2991,7 +3021,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2991
3021
|
'tile_windows',
|
|
2992
3022
|
'Tile Chrome windows over one monitor in equal parts with no gap. Only Chrome windows. The monitor is the one of a '
|
|
2993
3023
|
+ 'window already on it, whose work area is read from a page there: at least one target needs a scriptable tab '
|
|
2994
|
-
+ '(not chrome://). Maximized windows are restored first, since maximized ignores bounds.'
|
|
3024
|
+
+ '(not chrome://). Maximized windows are restored first, since maximized ignores bounds. '
|
|
3025
|
+
+ 'To restore a saved arrangement use window_layout; to move one tab into another window, move_tab.',
|
|
2995
3026
|
{
|
|
2996
3027
|
window_ids: z.array(z.number()).optional().describe('Windows to tile; omitted = every normal window on the reference monitor'),
|
|
2997
3028
|
reference_window_id: z.number().optional().describe('Window whose monitor is used; omitted = the focused one'),
|
|
@@ -3114,7 +3145,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
3114
3145
|
// --- get_interactives ---
|
|
3115
3146
|
server.tool(
|
|
3116
3147
|
'get_interactives',
|
|
3117
|
-
'List actionable elements (buttons, links, inputs, [role], [onclick]) with ready-to-use CSS selector, label, position, flags. Prefer this over dumping HTML to discover selectors.'
|
|
3148
|
+
'List actionable elements (buttons, links, inputs, [role], [onclick]) with ready-to-use CSS selector, label, position, flags. Prefer this over dumping HTML to discover selectors. '
|
|
3149
|
+
+ 'Document order, cut at limit (count equal to limit: there may be more), each with a ref (n1, n2…) for click, type_text and hover.',
|
|
3118
3150
|
{
|
|
3119
3151
|
scope: z.string().optional().describe('Limit search (CSS selector)'),
|
|
3120
3152
|
limit: z.number().optional().default(100).describe('Max elements returned; raise it on a dense page'),
|
|
@@ -3163,7 +3195,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
3163
3195
|
'extract',
|
|
3164
3196
|
'Read repeated non-tabular structures — product cards, list items, search results — as one record per item: '
|
|
3165
3197
|
+ 'item_selector matches the repeating block, fields map output names to selectors relative to it. '
|
|
3166
|
-
+ 'Read-only, deterministic, parsed server-side from the main-document HTML.'
|
|
3198
|
+
+ 'Read-only, deterministic, parsed server-side from the main-document HTML. Default output: total and shown, '
|
|
3199
|
+
+ 'then one tab-separated row per item under the field names; a field with no match is empty (null in json).',
|
|
3167
3200
|
{
|
|
3168
3201
|
item_selector: z.string().describe('CSS selector matching each item'),
|
|
3169
3202
|
fields: z.record(z.string(), z.object({
|
|
@@ -3207,7 +3240,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
3207
3240
|
// --- assert ---
|
|
3208
3241
|
server.tool(
|
|
3209
3242
|
'assert',
|
|
3210
|
-
'Assert a page condition, polling until timeout: element exists/visible (optionally with count or containing text), text on page, tab url/title. Patterns: substring, or "/…/" for regex. Recorded flows replay it as a test.'
|
|
3243
|
+
'Assert a page condition, polling until timeout: element exists/visible (optionally with count or containing text), text on page, tab url/title. Patterns: substring, or "/…/" for regex. Recorded flows replay it as a test. '
|
|
3244
|
+
+ 'Unlike wait_for, which returns found: false, a failure is an error naming what did not match; success returns passed and waited_ms.',
|
|
3211
3245
|
{
|
|
3212
3246
|
selector: z.string().optional().describe('Element the assertion is about; ">>>" pierces shadow DOM'),
|
|
3213
3247
|
state: z.enum(['attached', 'visible']).optional().default('attached').describe('attached means present in the DOM, visible also requires a rendered box'),
|
|
@@ -3237,7 +3271,8 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
3237
3271
|
// --- session_record ---
|
|
3238
3272
|
server.tool(
|
|
3239
3273
|
'session_record',
|
|
3240
|
-
'Record the commands of this session as a replayable jsonl (chrome-bridge replay --file <path>); observe what the user does in a tab ("watch how I do it") into the same format plus a readable procedure, never recording sensitive values; export a recording as a Playwright test for CI. Replays target the tab they navigate.'
|
|
3274
|
+
'Record the commands of this session as a replayable jsonl (chrome-bridge replay --file <path>); observe what the user does in a tab ("watch how I do it") into the same format plus a readable procedure, never recording sensitive values; export a recording as a Playwright test for CI. Replays target the tab they navigate. '
|
|
3275
|
+
+ 'Use it for a flow to repeat or a procedure to hand over; for one check now, assert.',
|
|
3241
3276
|
{
|
|
3242
3277
|
action: z.enum(['start', 'stop', 'status', 'list', 'export', 'observe']).describe('start records this session; observe records what the USER does; stop writes the file; export makes a Playwright test'),
|
|
3243
3278
|
name: z.string().optional().describe('Required for start, observe and export (recording name, or a .jsonl path)'),
|