chrome-bridge-mcp 1.8.0 → 1.9.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/README.md +9 -9
- package/extension/manifest.json +2 -2
- package/extension/service-worker.js +71 -3
- package/package.json +2 -2
- package/server/protocol.js +3 -1
- package/server/tools.js +222 -23
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Chrome Bridge
|
|
2
2
|
|
|
3
|
-
       [](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb)
|
|
4
4
|
|
|
5
5
|
**Chrome Bridge is an MCP server that connects Claude Code to your real, logged-in Chrome browser — measured 2.75× fewer turns and 2.28× lower cost than the official "Claude in Chrome" extension on a form-filling task, with ~3× the toolset and no paid plan.**
|
|
6
6
|
|
|
7
|
-
By using a local WebSocket bridge and a specialized Chrome extension, Chrome Bridge provides
|
|
7
|
+
By using a local WebSocket bridge and a specialized Chrome extension, Chrome Bridge provides 60 web-development tools (navigation, DOM inspection, visual regression, audits, network mocking) and a dedicated headless instance for CI. It is self-hosted, local-only, and requires no paid plan.
|
|
8
8
|
|
|
9
9
|
## Why Chrome Bridge?
|
|
10
10
|
|
|
@@ -31,12 +31,12 @@ unfavourable ones) and the harness limits are in
|
|
|
31
31
|
- **Honest caveat:** per *turn*, Chrome Bridge costs slightly more than Claude in Chrome (41,985 vs 36,613 cache-read tokens; $0.0373 vs $0.0337). The win is in the number of turns.
|
|
32
32
|
|
|
33
33
|
### 2. Feature Comparison
|
|
34
|
-
Chrome Bridge ships
|
|
34
|
+
Chrome Bridge ships 60 specialized tools (31 core by default) compared to ~20 in Claude in Chrome. It is also the only automation that drives the real host Chrome on ChromeOS/Crostini.
|
|
35
35
|
|
|
36
36
|
| | Chrome Bridge | Claude in Chrome | Chrome DevTools MCP | Playwright MCP |
|
|
37
37
|
|---|---|---|---|---|
|
|
38
38
|
| **ChromeOS / Crostini** | **Yes** (real host) | No | Container only | Container only |
|
|
39
|
-
| **Tools** | **
|
|
39
|
+
| **Tools** | **60** (31 core) | ~20 | ~50 | 23 core (71 total) |
|
|
40
40
|
| **Requires Paid Plan** | **No** | Yes (Pro+) | No | No |
|
|
41
41
|
| **Network Mocking** | **Yes** (stub/headers) | No | No | Yes |
|
|
42
42
|
| **Visual Regression** | **Yes** (`screenshot_diff`) | No | No | No |
|
|
@@ -63,7 +63,7 @@ Claude Code calls `navigate` (returning clickable element refs), `accessibility_
|
|
|
63
63
|
|
|
64
64
|
## Token-Efficient Design
|
|
65
65
|
|
|
66
|
-
- **
|
|
66
|
+
- **Opt-in Surface**: 31 core tools cost ≈5.7k tokens of `tools/list`, all 60 cost ≈10.8k; specialized groups (audits, visual, network, storage, dom, files) are opt-in via `--caps`. That core figure was ≈3.8k in 1.8.0 — MCP annotations on every tool and fuller descriptions added ≈1.8k, deliberately: the win here is fewer round trips, not a smaller prefix (Playwright MCP's 23 core tools measured ≈4.6k in July 2026, so on this metric alone it is now the leaner one). Measure it yourself with `npm run measure`.
|
|
67
67
|
- **Act-from-Result**: Tools attach a capped preview of interactive elements with short refs. Actions only report a `page_changed` delta when the URL or title actually changes.
|
|
68
68
|
- **Optimized Media**: Screenshots are downscaled to ≤1568px. Full-page captures are sliced into readable segments.
|
|
69
69
|
- **Zero-Token Escape Hatches**: The [CLI](#cli) skips MCP schemas entirely, and recorded flows [replay](#launch-mode-headless--ci) without any model in the loop.
|
|
@@ -84,7 +84,7 @@ Claude Code <--stdio--> MCP Server <--WebSocket :8765--> Chrome Extension
|
|
|
84
84
|
|
|
85
85
|
The **MCP Server** (Node.js) handles the protocol and tool logic. The **Chrome Extension** (MV3) executes commands via Chrome APIs. User scripts (`execute_js`) run via `chrome.userScripts.execute()`, requiring the "Allow user scripts" toggle in extension settings.
|
|
86
86
|
|
|
87
|
-
## Tools (
|
|
87
|
+
## Tools (60 total)
|
|
88
88
|
|
|
89
89
|
### Core & Navigation (7)
|
|
90
90
|
`get_status`, `get_tabs`, `create_tab`, `navigate`, `tab_action`, `get_frames`, `screenshot`.
|
|
@@ -95,8 +95,8 @@ The **MCP Server** (Node.js) handles the protocol and tool logic. The **Chrome E
|
|
|
95
95
|
### DOM & Inspection (11)
|
|
96
96
|
`read_page`, `extract`, `get_page_info`, `query_dom`, `modify_dom`, `find_text`, `get_interactives`, `inject_css`, `highlight_elements`, `watch_dom`, `measure_spacing`.
|
|
97
97
|
|
|
98
|
-
### Debugging & Network (
|
|
99
|
-
`execute_js`, `read_console`, `monitor_network`, `monitor_websocket`, `network_rules` (block/redirect/stub/headers), `get_performance`, `web_vitals`, `list_event_listeners`.
|
|
98
|
+
### Debugging & Network (9)
|
|
99
|
+
`execute_js`, `read_console`, `monitor_network`, `monitor_websocket`, `network_rules` (block/redirect/stub/headers), `http_request` (sent with the user's session cookies), `get_performance`, `web_vitals`, `list_event_listeners`.
|
|
100
100
|
|
|
101
101
|
### Visual & Responsive (7)
|
|
102
102
|
`element_screenshot`, `full_page_screenshot`, `screenshot_diff`, `viewport_resize`, `set_zoom`, `emulate_media`, `set_geolocation`.
|
|
@@ -132,7 +132,7 @@ Configure the server via environment variables (or the matching CLI flags):
|
|
|
132
132
|
- `CHROME_BRIDGE_PORT`: Default `8765`.
|
|
133
133
|
- `CHROME_BRIDGE_HOST` / `--host`: Bind address, default `127.0.0.1`. Set to `0.0.0.0` **only** where the browser lives outside the container (ChromeOS/Crostini port-forward) — and pair it with a token.
|
|
134
134
|
- `CHROME_BRIDGE_TOKEN`: Shared secret required on both `ext_init` and `relay_init`. Strongly recommended whenever the bind is not loopback.
|
|
135
|
-
- `CHROME_BRIDGE_CAPS` / `--caps`: Tool groups to load (`core`, `audits`, `visual`, `network`, `storage`, `dom`, `files`, or `all`). `install.sh` registers with `all`; the bare server defaults to `core` (
|
|
135
|
+
- `CHROME_BRIDGE_CAPS` / `--caps`: Tool groups to load (`core`, `audits`, `visual`, `network`, `storage`, `dom`, `files`, or `all`). `install.sh` registers with `all`; the bare server defaults to `core` (31 tools). `get_status` reports `caps_active` and `caps_available`.
|
|
136
136
|
|
|
137
137
|
Run `node tools/measure-schema.mjs` to see the exact schema cost of the active set.
|
|
138
138
|
|
package/extension/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Chrome Bridge for Claude Code",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "Bridge your browser to Claude Code:
|
|
4
|
+
"version": "1.9.0",
|
|
5
|
+
"description": "Bridge your browser to Claude Code: 60 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",
|
|
8
8
|
"permissions": [
|
|
@@ -423,6 +423,8 @@ async function executeCommand(msg) {
|
|
|
423
423
|
return await cmdManageDownloads(params);
|
|
424
424
|
case 'save_page':
|
|
425
425
|
return await cmdSavePage(params);
|
|
426
|
+
case 'http_request':
|
|
427
|
+
return await cmdHttpRequest(params);
|
|
426
428
|
case 'set_zoom':
|
|
427
429
|
return await cmdSetZoom(params);
|
|
428
430
|
case 'http_auth':
|
|
@@ -2177,12 +2179,12 @@ async function cmdWatchDom({ selector = 'body', attributes = true, childList = t
|
|
|
2177
2179
|
|
|
2178
2180
|
// --- emulate_media ---
|
|
2179
2181
|
|
|
2180
|
-
async function cmdEmulateMedia({ colorScheme, reducedMotion, printMode = false, reset = false, tab_id }) {
|
|
2182
|
+
async function cmdEmulateMedia({ colorScheme, reducedMotion, printMode = false, user_agent = null, reset = false, tab_id }) {
|
|
2181
2183
|
const tabId = await resolveTabId(tab_id);
|
|
2182
2184
|
|
|
2183
2185
|
const results = await chrome.scripting.executeScript({
|
|
2184
2186
|
target: { tabId },
|
|
2185
|
-
func: (cs, rm, print, doReset) => {
|
|
2187
|
+
func: (cs, rm, print, ua, doReset) => {
|
|
2186
2188
|
const STYLE_ID = '__chromeBridge_media';
|
|
2187
2189
|
const META_ID = '__chromeBridge_colorScheme';
|
|
2188
2190
|
|
|
@@ -2196,6 +2198,15 @@ async function cmdEmulateMedia({ colorScheme, reducedMotion, printMode = false,
|
|
|
2196
2198
|
window.matchMedia = window.__chromeBridge_origMatchMedia;
|
|
2197
2199
|
delete window.__chromeBridge_origMatchMedia;
|
|
2198
2200
|
}
|
|
2201
|
+
// navigator.userAgent è stato ridefinito con un getter: per tornare
|
|
2202
|
+
// all'originale va rimossa la proprietà own, così riemerge quella del
|
|
2203
|
+
// prototipo.
|
|
2204
|
+
if (window.__chromeBridge_uaPatched) {
|
|
2205
|
+
for (const prop of ['userAgent', 'appVersion', 'platform']) {
|
|
2206
|
+
try { delete navigator[prop]; } catch { /* proprietà non configurabile: si prosegue */ }
|
|
2207
|
+
}
|
|
2208
|
+
delete window.__chromeBridge_uaPatched;
|
|
2209
|
+
}
|
|
2199
2210
|
return { reset: true };
|
|
2200
2211
|
}
|
|
2201
2212
|
|
|
@@ -2261,9 +2272,26 @@ async function cmdEmulateMedia({ colorScheme, reducedMotion, printMode = false,
|
|
|
2261
2272
|
meta.remove();
|
|
2262
2273
|
}
|
|
2263
2274
|
|
|
2275
|
+
// navigator.userAgent non è scrivibile: va ridefinita come getter sull'
|
|
2276
|
+
// istanza, che precede quella del prototipo. Vale per il JS della pagina,
|
|
2277
|
+
// non per l'header della richiesta (quello è network_rules).
|
|
2278
|
+
if (ua) {
|
|
2279
|
+
for (const [prop, value] of [
|
|
2280
|
+
['userAgent', ua],
|
|
2281
|
+
['appVersion', ua.replace(/^Mozilla\//, '')],
|
|
2282
|
+
['platform', /iPhone|iPad|Android/i.test(ua) ? 'iPhone' : navigator.platform],
|
|
2283
|
+
]) {
|
|
2284
|
+
try {
|
|
2285
|
+
Object.defineProperty(navigator, prop, { get: () => value, configurable: true });
|
|
2286
|
+
} catch { /* alcune pagine congelano navigator: si prosegue */ }
|
|
2287
|
+
}
|
|
2288
|
+
window.__chromeBridge_uaPatched = true;
|
|
2289
|
+
overrides.userAgent = ua;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2264
2292
|
return { emulated: overrides };
|
|
2265
2293
|
},
|
|
2266
|
-
args: [colorScheme || null, reducedMotion || null, printMode, reset],
|
|
2294
|
+
args: [colorScheme || null, reducedMotion || null, printMode, user_agent, reset],
|
|
2267
2295
|
world: 'MAIN',
|
|
2268
2296
|
});
|
|
2269
2297
|
return results?.[0]?.result ?? {};
|
|
@@ -3440,6 +3468,46 @@ async function cmdManageDownloads({ action, timeout = 30000, limit = 10 }) {
|
|
|
3440
3468
|
|
|
3441
3469
|
// --- save_page (MHTML via pageCapture) ---
|
|
3442
3470
|
|
|
3471
|
+
// --- http_request ---
|
|
3472
|
+
|
|
3473
|
+
/**
|
|
3474
|
+
* La fetch parte dal service worker, non dal server Node: con host permission
|
|
3475
|
+
* `<all_urls>` e `credentials: 'include'` Chrome allega i cookie di sessione
|
|
3476
|
+
* dell'utente. È l'unica differenza che conta — la stessa URL richiesta da Node
|
|
3477
|
+
* restituisce la pagina di login.
|
|
3478
|
+
*/
|
|
3479
|
+
async function cmdHttpRequest({ url, method = 'GET', headers, body, binary = false }) {
|
|
3480
|
+
if (!/^https?:\/\//i.test(String(url || ''))) {
|
|
3481
|
+
throw new Error(`http_request needs an absolute http(s) URL, got "${url}"`);
|
|
3482
|
+
}
|
|
3483
|
+
const res = await fetch(url, {
|
|
3484
|
+
method,
|
|
3485
|
+
headers: headers && Object.keys(headers).length ? headers : undefined,
|
|
3486
|
+
body: body ?? undefined,
|
|
3487
|
+
credentials: 'include',
|
|
3488
|
+
redirect: 'follow',
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
const outHeaders = {};
|
|
3492
|
+
for (const [k, v] of res.headers.entries()) outHeaders[k] = v;
|
|
3493
|
+
const common = {
|
|
3494
|
+
status: res.status,
|
|
3495
|
+
ok: res.ok,
|
|
3496
|
+
// res.url riflette i redirect seguiti: senza, un 302 verso il login
|
|
3497
|
+
// sembrerebbe una risposta della URL chiesta.
|
|
3498
|
+
url: res.url,
|
|
3499
|
+
content_type: res.headers.get('content-type') || '',
|
|
3500
|
+
headers: outHeaders,
|
|
3501
|
+
};
|
|
3502
|
+
|
|
3503
|
+
if (binary) {
|
|
3504
|
+
const blob = await res.blob();
|
|
3505
|
+
return { ...common, body_b64: await blobToBase64(blob), size: blob.size };
|
|
3506
|
+
}
|
|
3507
|
+
const text = await res.text();
|
|
3508
|
+
return { ...common, body: text, size: text.length };
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3443
3511
|
async function cmdSavePage({ tab_id }) {
|
|
3444
3512
|
const tabId = await resolveTabId(tab_id);
|
|
3445
3513
|
const blob = await new Promise((resolve, reject) => {
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-bridge-mcp",
|
|
3
3
|
"mcpName": "io.github.frsorrentino/chrome-bridge",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.9.0",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "MCP server with
|
|
6
|
+
"description": "MCP server with 60 tools for browser automation via Chrome extension. Works on ChromeOS/Crostini.",
|
|
7
7
|
"main": "server/index.js",
|
|
8
8
|
"bin": {
|
|
9
9
|
"chrome-bridge-mcp": "server/index.js",
|
package/server/protocol.js
CHANGED
|
@@ -58,6 +58,7 @@ export const MessageType = Object.freeze({
|
|
|
58
58
|
SET_GEOLOCATION: 'set_geolocation',
|
|
59
59
|
MANAGE_DOWNLOADS: 'manage_downloads',
|
|
60
60
|
SAVE_PAGE: 'save_page',
|
|
61
|
+
HTTP_REQUEST: 'http_request',
|
|
61
62
|
SET_ZOOM: 'set_zoom',
|
|
62
63
|
HTTP_AUTH: 'http_auth',
|
|
63
64
|
GET_RESPONSE_HEADERS: 'get_response_headers',
|
|
@@ -80,7 +81,7 @@ export const MessageType = Object.freeze({
|
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
// Versione
|
|
83
|
-
export const VERSION = '1.
|
|
84
|
+
export const VERSION = '1.9.0';
|
|
84
85
|
|
|
85
86
|
// Configurazione
|
|
86
87
|
export const DEFAULT_PORT = 8765;
|
|
@@ -129,6 +130,7 @@ export function getTimeout(type) {
|
|
|
129
130
|
|| type === MessageType.UPLOAD_FILE
|
|
130
131
|
|| type === MessageType.MANAGE_DOWNLOADS
|
|
131
132
|
|| type === MessageType.SAVE_PAGE
|
|
133
|
+
|| type === MessageType.HTTP_REQUEST
|
|
132
134
|
|| type === MessageType.WAIT_FOR_FUNCTION
|
|
133
135
|
|| type === MessageType.SCROLL_UNTIL) return 60000;
|
|
134
136
|
return COMMAND_TIMEOUT_MS;
|
package/server/tools.js
CHANGED
|
@@ -190,6 +190,106 @@ for (const [group, names] of Object.entries(TOOL_CAPS)) {
|
|
|
190
190
|
for (const n of names) TOOL_TO_CAP.set(n, group);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Annotations MCP, una voce per tool.
|
|
195
|
+
*
|
|
196
|
+
* Sono l'unico modo che l'agente ha di sapere cosa fa un tool al mondo PRIMA di
|
|
197
|
+
* chiamarlo: la descrizione in prosa la legge, ma non la può confrontare. Senza
|
|
198
|
+
* `readOnlyHint`, `click` e `read_page` si equivalgono al momento della scelta.
|
|
199
|
+
*
|
|
200
|
+
* Stanno qui e non sui singoli `server.tool(...)` perché il wrapper in
|
|
201
|
+
* registerTools le applica tutte da un punto solo, e
|
|
202
|
+
* `test/unit/tool-annotations.test.js` fallisce se un tool nuovo non ha la sua
|
|
203
|
+
* voce — impossibile aggiungere un tool e dimenticarle.
|
|
204
|
+
*
|
|
205
|
+
* openWorldHint = il tool può raggiungere o farsi raggiungere da server remoti
|
|
206
|
+
* (navigazione, fetch, mock di rete), non solo leggere lo stato già caricato.
|
|
207
|
+
*/
|
|
208
|
+
const ro = (open = false) => ({
|
|
209
|
+
readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: open,
|
|
210
|
+
});
|
|
211
|
+
const rw = ({ destructive = false, idempotent = false, open = false } = {}) => ({
|
|
212
|
+
readOnlyHint: false, destructiveHint: destructive, idempotentHint: idempotent, openWorldHint: open,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
export const TOOL_ANNOTATIONS = {
|
|
216
|
+
// --- osservazione pura ---
|
|
217
|
+
accessibility_audit: ro(),
|
|
218
|
+
assert: ro(),
|
|
219
|
+
check_links: ro(true),
|
|
220
|
+
element_screenshot: ro(),
|
|
221
|
+
extract: ro(),
|
|
222
|
+
extract_table: ro(),
|
|
223
|
+
find_text: ro(),
|
|
224
|
+
full_page_screenshot: ro(),
|
|
225
|
+
get_frames: ro(),
|
|
226
|
+
get_interactives: ro(),
|
|
227
|
+
get_page_info: ro(),
|
|
228
|
+
get_performance: ro(),
|
|
229
|
+
get_status: ro(),
|
|
230
|
+
get_storage: ro(),
|
|
231
|
+
get_tabs: ro(),
|
|
232
|
+
list_event_listeners: ro(),
|
|
233
|
+
manage_downloads: ro(),
|
|
234
|
+
measure_spacing: ro(),
|
|
235
|
+
monitor_network: ro(true),
|
|
236
|
+
monitor_websocket: ro(true),
|
|
237
|
+
query_dom: ro(),
|
|
238
|
+
read_page: ro(),
|
|
239
|
+
screenshot: ro(),
|
|
240
|
+
security_headers: ro(true),
|
|
241
|
+
seo_audit: ro(),
|
|
242
|
+
unused_css: ro(),
|
|
243
|
+
wait_for: ro(),
|
|
244
|
+
watch_dom: ro(),
|
|
245
|
+
web_vitals: ro(),
|
|
246
|
+
|
|
247
|
+
// --- interazione con la pagina ---
|
|
248
|
+
click: rw({ open: true }), // un click può navigare
|
|
249
|
+
drag_and_drop: rw(),
|
|
250
|
+
fill_form: rw({ idempotent: true }),
|
|
251
|
+
hover: rw({ idempotent: true }),
|
|
252
|
+
press_key: rw(),
|
|
253
|
+
scroll: rw(),
|
|
254
|
+
type_text: rw({ idempotent: true }),
|
|
255
|
+
upload_file: rw(),
|
|
256
|
+
|
|
257
|
+
// --- modifica di pagina, tab o resa ---
|
|
258
|
+
dismiss_overlays: rw({ idempotent: true }),
|
|
259
|
+
emulate_media: rw({ idempotent: true }),
|
|
260
|
+
handle_dialogs: rw({ idempotent: true }),
|
|
261
|
+
highlight_elements: rw({ idempotent: true }),
|
|
262
|
+
inject_css: rw({ idempotent: true }),
|
|
263
|
+
modify_dom: rw({ idempotent: true }),
|
|
264
|
+
screenshot_diff: rw({ idempotent: true }),
|
|
265
|
+
set_zoom: rw({ idempotent: true }),
|
|
266
|
+
viewport_resize: rw({ idempotent: true }),
|
|
267
|
+
create_tab: rw({ open: true }),
|
|
268
|
+
navigate: rw({ idempotent: true, open: true }),
|
|
269
|
+
tab_action: rw({ destructive: true, open: true }), // close chiude una tab dell'utente
|
|
270
|
+
|
|
271
|
+
// --- rete, identità, posizione ---
|
|
272
|
+
http_auth: rw({ idempotent: true, open: true }),
|
|
273
|
+
network_rules: rw({ idempotent: true, open: true }),
|
|
274
|
+
set_geolocation: rw({ idempotent: true }),
|
|
275
|
+
|
|
276
|
+
// --- codice arbitrario ---
|
|
277
|
+
execute_js: rw({ destructive: true, open: true }),
|
|
278
|
+
|
|
279
|
+
// http_request non è read-only: un POST cambia lo stato del server remoto, e
|
|
280
|
+
// con save_to scrive un file. destructive perché sovrascrive save_to senza
|
|
281
|
+
// chiedere.
|
|
282
|
+
http_request: rw({ destructive: true, open: true }),
|
|
283
|
+
|
|
284
|
+
// --- stato che può essere distrutto o sovrascritto ---
|
|
285
|
+
clipboard: rw({ idempotent: true }),
|
|
286
|
+
read_console: rw({ destructive: true }), // clear:true cancella il buffer
|
|
287
|
+
save_page: rw({ destructive: true, idempotent: true }), // sovrascrive output_path
|
|
288
|
+
session_fixture: rw({ destructive: true, idempotent: true }), // restore sovrascrive cookie e storage
|
|
289
|
+
session_record: rw(),
|
|
290
|
+
set_storage: rw({ destructive: true, idempotent: true }),
|
|
291
|
+
};
|
|
292
|
+
|
|
193
293
|
/**
|
|
194
294
|
* Registra tutti i tool MCP.
|
|
195
295
|
*
|
|
@@ -203,15 +303,26 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
203
303
|
? ['core', ...Object.keys(TOOL_CAPS)]
|
|
204
304
|
: ['core', ...String(caps).split(',').map((s) => s.trim()).filter((s) => s && s !== 'core')];
|
|
205
305
|
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
306
|
+
// Ogni registrazione passa da qui: il filtro capability scarta i tool opt-in
|
|
307
|
+
// fuori dai gruppi attivi, e le annotations vengono applicate da TOOL_ANNOTATIONS.
|
|
308
|
+
// Un solo wrapper per entrambe le cose, altrimenti con caps != 'all' le
|
|
309
|
+
// annotations sparivano insieme al filtro.
|
|
310
|
+
{
|
|
209
311
|
const target = server;
|
|
312
|
+
const enabled = caps !== 'all'
|
|
313
|
+
? new Set(String(caps).split(',').map((s) => s.trim()).filter(Boolean))
|
|
314
|
+
: null;
|
|
210
315
|
server = {
|
|
211
316
|
tool(name, desc, schema, handler) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
317
|
+
if (enabled) {
|
|
318
|
+
const group = TOOL_TO_CAP.get(name);
|
|
319
|
+
if (group && !enabled.has(group) && !enabled.has('all')) return;
|
|
320
|
+
}
|
|
321
|
+
const annotations = TOOL_ANNOTATIONS[name];
|
|
322
|
+
// Un tool senza voce resta registrato (meglio di un crash all'avvio):
|
|
323
|
+
// è il test tool-annotations a segnalarlo.
|
|
324
|
+
if (annotations) target.tool(name, desc, schema, annotations, handler);
|
|
325
|
+
else target.tool(name, desc, schema, handler);
|
|
215
326
|
},
|
|
216
327
|
};
|
|
217
328
|
}
|
|
@@ -342,7 +453,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
342
453
|
// --- get_tabs ---
|
|
343
454
|
server.tool(
|
|
344
455
|
'get_tabs',
|
|
345
|
-
'List
|
|
456
|
+
'List every open tab with id, url, title and active flag. Read-only. Use it to find a tab_id when the '
|
|
457
|
+
+ 'implicit target (last navigated tab, else the active one) is not the tab you mean.',
|
|
346
458
|
{},
|
|
347
459
|
async () => {
|
|
348
460
|
const data = await send(MessageType.GET_TABS);
|
|
@@ -429,7 +541,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
429
541
|
// --- click ---
|
|
430
542
|
server.tool(
|
|
431
543
|
'click',
|
|
432
|
-
'Click an element by CSS selector or by ref from get_interactives.'
|
|
544
|
+
'Click an element, by CSS selector or by a ref (n1, n2…) from get_interactives or navigate. Fires a real '
|
|
545
|
+
+ 'pointer sequence, so it can submit, open a dialog or navigate away — use wait_after to let that settle. '
|
|
546
|
+
+ 'Not idempotent, and a click that triggers a native confirm() blocks the bridge: install handle_dialogs first.',
|
|
433
547
|
{
|
|
434
548
|
selector: z.string().optional(),
|
|
435
549
|
ref: z.string().optional().describe('From get_interactives, e.g. "n3"'),
|
|
@@ -485,7 +599,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
485
599
|
// --- read_page ---
|
|
486
600
|
server.tool(
|
|
487
601
|
'read_page',
|
|
488
|
-
'Read the
|
|
602
|
+
'Read the page as text (default), raw HTML, or accessibility tree. Read-only. Expensive on large pages: '
|
|
603
|
+
+ 'HTML on a big table costs tens of thousands of tokens for data you then filter anyway — prefer '
|
|
604
|
+
+ 'extract_table or extract for tabular and repeated content, and get_interactives to find click targets.',
|
|
489
605
|
{
|
|
490
606
|
mode: z.enum(['text', 'html', 'accessibility']).default('text'),
|
|
491
607
|
tab_id: z.number().optional(),
|
|
@@ -525,7 +641,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
525
641
|
// --- get_storage ---
|
|
526
642
|
server.tool(
|
|
527
643
|
'get_storage',
|
|
528
|
-
'
|
|
644
|
+
'Read localStorage, sessionStorage or cookies of the current origin (type=all for every one). Read-only. '
|
|
645
|
+
+ 'Cookies come with domain, path, expiry and the httpOnly/secure flags, so this also answers whether a '
|
|
646
|
+
+ 'session cookie is present — write them back with set_storage, or snapshot a logged-in state with session_fixture.',
|
|
529
647
|
{
|
|
530
648
|
type: z.enum(['all', 'localStorage', 'sessionStorage', 'cookies']).default('all'),
|
|
531
649
|
tab_id: z.number().optional(),
|
|
@@ -544,7 +662,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
544
662
|
// --- get_performance ---
|
|
545
663
|
server.tool(
|
|
546
664
|
'get_performance',
|
|
547
|
-
'
|
|
665
|
+
'Navigation timing, paint metrics, JS heap size and per-resource load times, as measured since the current '
|
|
666
|
+
+ 'document loaded. Read-only, no reload triggered — numbers are only meaningful once loading has settled, '
|
|
667
|
+
+ 'so on a page still fetching call wait_for(network_idle) first. For CLS/LCP/INP use web_vitals.',
|
|
548
668
|
{
|
|
549
669
|
tab_id: z.number().optional(),
|
|
550
670
|
},
|
|
@@ -584,7 +704,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
584
704
|
// --- modify_dom ---
|
|
585
705
|
server.tool(
|
|
586
706
|
'modify_dom',
|
|
587
|
-
'
|
|
707
|
+
'Change an element in the live DOM: setAttribute, removeAttribute, addClass, removeClass, setStyle, '
|
|
708
|
+
+ 'setTextContent. Nothing is persisted — the next reload restores the page as the server sends it. '
|
|
709
|
+
+ 'For styling many elements at once inject_css is one call instead of N.',
|
|
588
710
|
{
|
|
589
711
|
selector: z.string(),
|
|
590
712
|
action: z.enum(['setAttribute', 'removeAttribute', 'addClass', 'removeClass', 'setStyle', 'setTextContent']),
|
|
@@ -608,7 +730,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
608
730
|
// --- inject_css ---
|
|
609
731
|
server.tool(
|
|
610
732
|
'inject_css',
|
|
611
|
-
'Inject CSS
|
|
733
|
+
'Inject a CSS rule into the page. Stays until the next navigation or reload, and re-injecting the same id '
|
|
734
|
+
+ 'replaces it rather than stacking. Affects only what is rendered — the stylesheet of the site is untouched.',
|
|
612
735
|
{
|
|
613
736
|
css: z.string(),
|
|
614
737
|
tab_id: z.number().optional(),
|
|
@@ -700,7 +823,8 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
700
823
|
// --- create_tab ---
|
|
701
824
|
server.tool(
|
|
702
825
|
'create_tab',
|
|
703
|
-
'
|
|
826
|
+
'Open a new tab, optionally at a URL, and make it the implicit target of later commands in this session. '
|
|
827
|
+
+ 'Each call creates another tab: reuse a tab_id or navigate() to move an existing one instead of piling up tabs.',
|
|
704
828
|
{
|
|
705
829
|
url: z.string().optional().describe('URL to open (default: new tab page)'),
|
|
706
830
|
active: z.boolean().optional().default(true),
|
|
@@ -785,7 +909,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
785
909
|
// --- set_storage ---
|
|
786
910
|
server.tool(
|
|
787
911
|
'set_storage',
|
|
788
|
-
'Write, delete
|
|
912
|
+
'Write, delete or clear a localStorage/sessionStorage key, or a cookie with its path, domain and expiry. '
|
|
913
|
+
+ 'action=clear wipes every entry of that type for the origin and cannot be undone — on a site the user is '
|
|
914
|
+
+ 'logged into, clearing cookies logs them out. Read the current values first with get_storage.',
|
|
789
915
|
{
|
|
790
916
|
type: z.enum(['localStorage', 'sessionStorage', 'cookie']),
|
|
791
917
|
action: z.enum(['set', 'delete', 'clear']),
|
|
@@ -880,7 +1006,10 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
880
1006
|
// --- full_page_screenshot ---
|
|
881
1007
|
server.tool(
|
|
882
1008
|
'full_page_screenshot',
|
|
883
|
-
'Full-page capture by scrolling: stitched segments of ~2 viewports (≤1568px, top→bottom), or one image per
|
|
1009
|
+
'Full-page capture by scrolling: stitched segments of ~2 viewports (≤1568px, top→bottom), or one image per '
|
|
1010
|
+
+ 'viewport with stitch=false. Read-only, but expensive: each segment costs ~2.7k image tokens, so only the first '
|
|
1011
|
+
+ 'max_segments are returned and the rest need segment_offset. On a long page, reading the text you actually need '
|
|
1012
|
+
+ 'is cheaper by an order of magnitude — prefer read_page, extract or find_text unless the layout itself is the question.',
|
|
884
1013
|
{
|
|
885
1014
|
max_scrolls: z.number().optional().default(20),
|
|
886
1015
|
delay: z.number().optional().default(500).describe('ms between captures (min 500, Chrome quota)'),
|
|
@@ -1003,7 +1132,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1003
1132
|
// --- measure_spacing ---
|
|
1004
1133
|
server.tool(
|
|
1005
1134
|
'measure_spacing',
|
|
1006
|
-
'Measure
|
|
1135
|
+
'Measure the gap, overlap and distance in CSS pixels between two elements, with their margins and paddings. '
|
|
1136
|
+
+ 'Read-only. Values come from the current layout, so zoom and viewport size change them: set_zoom(1) and a '
|
|
1137
|
+
+ 'fixed viewport_resize make results comparable across runs.',
|
|
1007
1138
|
{
|
|
1008
1139
|
selector1: z.string(),
|
|
1009
1140
|
selector2: z.string(),
|
|
@@ -1049,16 +1180,20 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1049
1180
|
// --- emulate_media ---
|
|
1050
1181
|
server.tool(
|
|
1051
1182
|
'emulate_media',
|
|
1052
|
-
'
|
|
1183
|
+
'Make the page believe it runs in a different environment, until reset or reload: prefers-color-scheme, '
|
|
1184
|
+
+ 'prefers-reduced-motion, print mode (matchMedia override + CSS) and navigator.userAgent/platform. '
|
|
1185
|
+
+ 'user_agent only changes what page JS reads — the request header is a separate thing, set it with '
|
|
1186
|
+
+ 'network_rules(action=modify_header, header="User-Agent"). Pair with viewport_resize to emulate a device.',
|
|
1053
1187
|
{
|
|
1054
1188
|
colorScheme: z.enum(['dark', 'light', 'no-preference']).optional(),
|
|
1055
1189
|
reducedMotion: z.enum(['reduce', 'no-preference']).optional(),
|
|
1056
1190
|
printMode: z.boolean().optional().default(false),
|
|
1191
|
+
user_agent: z.string().optional().describe('Overrides navigator.userAgent and appVersion in the page (not the HTTP header)'),
|
|
1057
1192
|
reset: z.boolean().optional().default(false).describe('Remove all emulations'),
|
|
1058
1193
|
tab_id: z.number().optional(),
|
|
1059
1194
|
},
|
|
1060
|
-
async ({ colorScheme, reducedMotion, printMode, reset, tab_id }) => {
|
|
1061
|
-
const data = await send(MessageType.EMULATE_MEDIA, { colorScheme, reducedMotion, printMode, reset, tab_id });
|
|
1195
|
+
async ({ colorScheme, reducedMotion, printMode, user_agent, reset, tab_id }) => {
|
|
1196
|
+
const data = await send(MessageType.EMULATE_MEDIA, { colorScheme, reducedMotion, printMode, user_agent, reset, tab_id });
|
|
1062
1197
|
return {
|
|
1063
1198
|
content: [{
|
|
1064
1199
|
type: 'text',
|
|
@@ -1092,7 +1227,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1092
1227
|
// --- press_key ---
|
|
1093
1228
|
server.tool(
|
|
1094
1229
|
'press_key',
|
|
1095
|
-
'
|
|
1230
|
+
'Send a key to the focused element (or to selector, focusing it first) as a real keydown/keypress/keyup '
|
|
1231
|
+
+ 'sequence, so framework handlers fire. For typing a value use type_text; this is for Enter, Tab, Escape, '
|
|
1232
|
+
+ 'arrows and shortcuts. Not idempotent: two calls send the key twice.',
|
|
1096
1233
|
{
|
|
1097
1234
|
key: z.string().describe('e.g. "Enter", "Escape", "Tab", "ArrowDown"'),
|
|
1098
1235
|
selector: z.string().optional().describe('Target (default: activeElement)'),
|
|
@@ -1130,7 +1267,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1130
1267
|
// --- tab_action ---
|
|
1131
1268
|
server.tool(
|
|
1132
1269
|
'tab_action',
|
|
1133
|
-
'Tab lifecycle
|
|
1270
|
+
'Tab lifecycle: close, activate (focus), reload (optional cache bypass), back, forward. close discards the '
|
|
1271
|
+
+ 'tab and anything unsaved in it, and cannot be undone — it may be a tab the user is working in. '
|
|
1272
|
+
+ 'reload and navigation drop injected CSS, emulations and page hooks.',
|
|
1134
1273
|
{
|
|
1135
1274
|
action: z.enum(['close', 'activate', 'reload', 'back', 'forward']),
|
|
1136
1275
|
bypass_cache: z.boolean().optional().default(false).describe('reload only'),
|
|
@@ -1302,7 +1441,9 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1302
1441
|
// --- web_vitals ---
|
|
1303
1442
|
server.tool(
|
|
1304
1443
|
'web_vitals',
|
|
1305
|
-
'Core Web Vitals since
|
|
1444
|
+
'Core Web Vitals accumulated since the current document loaded: CLS, LCP, FCP, TTFB, long tasks and an INP '
|
|
1445
|
+
+ 'approximation. Read-only. Needs the page instrumentation active from before load, so it reports whether it '
|
|
1446
|
+
+ 'was hooked instead of silently returning zeros — navigate() installs it. For timing and resources use get_performance.',
|
|
1306
1447
|
{
|
|
1307
1448
|
tab_id: z.number().optional(),
|
|
1308
1449
|
},
|
|
@@ -1468,6 +1609,64 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1468
1609
|
}
|
|
1469
1610
|
);
|
|
1470
1611
|
|
|
1612
|
+
// --- http_request ---
|
|
1613
|
+
server.tool(
|
|
1614
|
+
'http_request',
|
|
1615
|
+
'HTTP request sent from the browser, so it carries the session cookies of the logged-in user: '
|
|
1616
|
+
+ 'fetches URLs a plain server-side request would get a login page for (invoices, authenticated JSON, CSV exports). '
|
|
1617
|
+
+ 'Text bodies are returned inline (capped by max_length); with save_to the bytes are written to that path instead, '
|
|
1618
|
+
+ 'which is how you read a PDF — Chrome renders PDFs in a viewer no content script can reach, so read_page returns nothing on a PDF tab.',
|
|
1619
|
+
{
|
|
1620
|
+
url: z.string().describe('Absolute URL. Cookies are sent for its origin'),
|
|
1621
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']).optional().default('GET'),
|
|
1622
|
+
headers: z.record(z.string(), z.string()).optional().describe('Extra request headers'),
|
|
1623
|
+
body: z.string().optional().describe('Request body (POST/PUT/PATCH)'),
|
|
1624
|
+
save_to: z.string().optional().describe('Absolute path: write the response bytes here instead of returning them (PDF, images, archives)'),
|
|
1625
|
+
max_length: z.number().optional().default(DEFAULT_MAX_OUTPUT).describe('Max chars of body returned inline'),
|
|
1626
|
+
},
|
|
1627
|
+
async ({ url, method, headers, body, save_to, max_length }) => {
|
|
1628
|
+
const data = await send(MessageType.HTTP_REQUEST, { url, method, headers, body, binary: Boolean(save_to) });
|
|
1629
|
+
|
|
1630
|
+
if (save_to) {
|
|
1631
|
+
// Il base64 non deve mai raggiungere il modello: un PDF di 300 kB sono
|
|
1632
|
+
// ~100k token di rumore per un contenuto che va letto da file.
|
|
1633
|
+
await writeFile(save_to, Buffer.from(data.body_b64 ?? '', 'base64'));
|
|
1634
|
+
return {
|
|
1635
|
+
content: [{
|
|
1636
|
+
type: 'text',
|
|
1637
|
+
text: jsonText({
|
|
1638
|
+
saved: save_to,
|
|
1639
|
+
status: data.status,
|
|
1640
|
+
ok: data.ok,
|
|
1641
|
+
content_type: data.content_type,
|
|
1642
|
+
size: data.size,
|
|
1643
|
+
url: data.url,
|
|
1644
|
+
}),
|
|
1645
|
+
}],
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
return {
|
|
1650
|
+
content: [{
|
|
1651
|
+
type: 'text',
|
|
1652
|
+
text: truncateText(
|
|
1653
|
+
jsonText({
|
|
1654
|
+
status: data.status,
|
|
1655
|
+
ok: data.ok,
|
|
1656
|
+
content_type: data.content_type,
|
|
1657
|
+
size: data.size,
|
|
1658
|
+
url: data.url,
|
|
1659
|
+
headers: data.headers,
|
|
1660
|
+
body: data.body,
|
|
1661
|
+
}, max_length ?? DEFAULT_MAX_OUTPUT, 'max_length'),
|
|
1662
|
+
max_length ?? DEFAULT_MAX_OUTPUT,
|
|
1663
|
+
'max_length',
|
|
1664
|
+
),
|
|
1665
|
+
}],
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
);
|
|
1669
|
+
|
|
1471
1670
|
// --- set_zoom ---
|
|
1472
1671
|
server.tool(
|
|
1473
1672
|
'set_zoom',
|