chrome-bridge-mcp 1.20.1 → 1.21.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 CHANGED
@@ -1,12 +1,14 @@
1
1
  # Chrome Bridge
2
2
 
3
+ > By [frsorrentino](https://github.com/frsorrentino) · npm `chrome-bridge-mcp` · not affiliated with other projects named "chrome-bridge".
4
+
3
5
  ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Node 18+](https://img.shields.io/badge/node-%E2%89%A518-brightgreen) ![Chrome 135+](https://img.shields.io/badge/chrome-%E2%89%A5135-blue) ![Tests](https://img.shields.io/badge/tests-257%20unit%20%2B%2032%20e2e-brightgreen) [![Chrome Web Store](https://img.shields.io/badge/web%20store-published-blue)](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb)
4
6
 
5
7
  **An MCP server that gives Claude Code your real, logged-in Chrome — measured
6
8
  2.75× fewer turns and 2.28× lower cost than the official "Claude in Chrome"
7
9
  extension on a form-filling task, with ~3× the toolset and no paid plan.**
8
10
 
9
- 59 web-development tools (navigation, DOM inspection, visual regression, audits,
11
+ 60 web-development tools (navigation, DOM inspection, visual regression, audits,
10
12
  network mocking) over a local WebSocket bridge, plus a headless instance for CI.
11
13
  Self-hosted, local-only.
12
14
 
@@ -53,7 +55,7 @@ register the same server twice.
53
55
  | | Chrome Bridge | Claude in Chrome | Chrome DevTools MCP | Playwright MCP |
54
56
  |---|---|---|---|---|
55
57
  | **ChromeOS / Crostini** | **Yes** (real host) | No | Container only | Container only |
56
- | **Tools** | **59** (38 core) | 22 | 29 default (56 with flags) | 24 core (71 total) |
58
+ | **Tools** | **60** (39 core) | 22 | 29 default (56 with flags) | 24 core (71 total) |
57
59
  | **Requires paid plan** | **No** | Yes (Pro+) | No | No |
58
60
  | **Network mocking** | **Yes** (stub/headers) | No | No | Yes |
59
61
  | **Visual regression** | **Yes** (`screenshot_diff`) | No | No | No |
@@ -115,16 +117,16 @@ user-script toggle isn't available.
115
117
 
116
118
  ## Tools
117
119
 
118
- 59 in total, in seven groups. Only `core` (38 tools) loads by default; the rest
120
+ 60 in total, in seven groups. Only `core` (39 tools) loads by default; the rest
119
121
  are opt-in via `--caps`.
120
122
 
121
- ![59 tools in seven groups, from clicking a button to auditing a whole page](assets/readme/card4-toolbox.png)
123
+ ![60 tools in seven groups, from clicking a button to auditing a whole page](assets/readme/card4-toolbox.png)
122
124
 
123
125
  | Group | N | What's in it |
124
126
  |---|---|---|
125
127
  | Core & Navigation | 13 | tabs, windows, `navigate`, `screenshot`, `tile_windows` |
126
128
  | Interaction | 11 | `click`, `fill_form`, `upload_file`, dialogs, clipboard |
127
- | DOM & Inspection | 10 | `read_page`, `extract`, `query_dom`, `watch_dom` |
129
+ | DOM & Inspection | 11 | `read_page`, `extract`, `query_dom`, `get_css_styles`, `watch_dom` |
128
130
  | Debugging & Network | 8 | `execute_js`, console, network log, mocking, `track_events` |
129
131
  | Visual & Responsive | 5 | `screenshot_diff`, viewport and zoom, media emulation |
130
132
  | Audits | 2 | `audit` (a11y, keyboard, SEO, security, links, vitals, css, resources, cache in one call), `cookie_audit` |
@@ -244,7 +246,7 @@ count.
244
246
 
245
247
  ## Documentation
246
248
 
247
- - [docs/TOOLS.md](docs/TOOLS.md) — all 59 tools, by group
249
+ - [docs/TOOLS.md](docs/TOOLS.md) — all 60 tools, by group
248
250
  - [docs/CAPABILITIES.md](docs/CAPABILITIES.md) — what the bridge gets past and what it does not, one dated state per wall
249
251
  - [docs/EFFICIENCY.md](docs/EFFICIENCY.md) — the benchmark and the design behind it
250
252
  - [docs/PERFORMANCE.md](docs/PERFORMANCE.md) — latency per tool on the real path, `npm run bench:latency`
@@ -0,0 +1,405 @@
1
+ /**
2
+ * cssStylesOf: la cascata CSS di un elemento come la mostra il pannello Styles
3
+ * di DevTools — per ogni proprietà la dichiarazione che vince (selettore,
4
+ * foglio, posizione della regola, !important, @layer, @media) e quelle che ha
5
+ * battuto. `query_dom` dice quanto vale `margin-top`; questo dice chi lo ha
6
+ * deciso e da dove.
7
+ *
8
+ * Senza chrome.debugger non c'è CSS.getMatchedStylesForNode: si ricostruisce
9
+ * dal CSSOM. Ordine (CSS Cascade 5, origine autore): !important, stile inline,
10
+ * @layer (normali: senza layer > ultimo layer dichiarato > primo; important:
11
+ * inverso), specificità, ordine nel sorgente. I longhand sono l'unità della
12
+ * cascata: `margin: 0` compare come margin-top/right/bottom/left.
13
+ *
14
+ * Fuori portata, e detto nell'output o nella descrizione del tool: gli stili
15
+ * dello user agent (non esposti), @container, @scope e @starting-style (non
16
+ * valutabili senza il motore: contati in `skipped`), le regole :host del
17
+ * shadow root di un host, transizioni e animazioni. I fogli di altra origine
18
+ * senza CORS non espongono cssRules: elencati in `opaque_stylesheets`.
19
+ *
20
+ * Non è un modulo ES (vedi element-label.js): get_css_styles inietta prima
21
+ * questo file e poi usa globalThis.__cbCssStyles. In Node i test passano un
22
+ * CSSOM finto come terzo argomento.
23
+ */
24
+ (() => {
25
+ // --- selettori: liste, parentesi, specificità ---
26
+
27
+ const isIdentChar = (ch) => /[\w-]/.test(ch) || ch.charCodeAt(0) > 127;
28
+ const skipIdent = (s, i) => {
29
+ while (i < s.length) {
30
+ if (s[i] === '\\') { i += 2; continue; }
31
+ if (!isIdentChar(s[i])) break;
32
+ i += 1;
33
+ }
34
+ return i;
35
+ };
36
+ const skipString = (s, i) => {
37
+ const q = s[i];
38
+ i += 1;
39
+ while (i < s.length && s[i] !== q) i += s[i] === '\\' ? 2 : 1;
40
+ return i + 1;
41
+ };
42
+ const matchParen = (s, i) => {
43
+ let depth = 0;
44
+ for (; i < s.length; i += 1) {
45
+ const ch = s[i];
46
+ if (ch === '"' || ch === "'") { i = skipString(s, i) - 1; continue; }
47
+ if (ch === '(') depth += 1;
48
+ else if (ch === ')') { depth -= 1; if (depth === 0) return i; }
49
+ }
50
+ return s.length;
51
+ };
52
+ /** Una lista di selettori spezzata sulle virgole di primo livello. */
53
+ const splitList = (s) => {
54
+ const out = [];
55
+ let depth = 0;
56
+ let start = 0;
57
+ for (let i = 0; i < s.length; i += 1) {
58
+ const ch = s[i];
59
+ if (ch === '"' || ch === "'") { i = skipString(s, i) - 1; continue; }
60
+ if (ch === '(' || ch === '[') depth += 1;
61
+ else if (ch === ')' || ch === ']') depth -= 1;
62
+ else if (ch === ',' && depth === 0) { out.push(s.slice(start, i).trim()); start = i + 1; }
63
+ }
64
+ out.push(s.slice(start).trim());
65
+ return out.filter(Boolean);
66
+ };
67
+
68
+ const LEGACY_PSEUDO_ELEMENTS = new Set(['before', 'after', 'first-line', 'first-letter']);
69
+ const ARG_SPECIFICITY = new Set(['is', 'not', 'has', 'matches', 'any', '-webkit-any', '-moz-any']);
70
+ const cmpSpec = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
71
+ const add = (t, u) => { t[0] += u[0]; t[1] += u[1]; t[2] += u[2]; };
72
+ const maxSpec = (list) => list.map(specificity).reduce((m, x) => (cmpSpec(x, m) > 0 ? x : m), [0, 0, 0]);
73
+
74
+ /** Specificità (id, classi/attributi/pseudo-classi, tipi/pseudo-elementi) di un selettore complesso. */
75
+ function specificity(sel) {
76
+ const t = [0, 0, 0];
77
+ const s = String(sel);
78
+ let i = 0;
79
+ while (i < s.length) {
80
+ const ch = s[i];
81
+ if (ch === '"' || ch === "'") { i = skipString(s, i); continue; }
82
+ if (ch === '#') { t[0] += 1; i = skipIdent(s, i + 1); continue; }
83
+ if (ch === '.') { t[1] += 1; i = skipIdent(s, i + 1); continue; }
84
+ if (ch === '[') {
85
+ t[1] += 1;
86
+ const close = s.indexOf(']', i);
87
+ i = close === -1 ? s.length : close + 1;
88
+ continue;
89
+ }
90
+ if (ch === ':') {
91
+ const pseudoElement = s[i + 1] === ':';
92
+ const start = i + (pseudoElement ? 2 : 1);
93
+ const end = skipIdent(s, start);
94
+ const name = s.slice(start, end).toLowerCase();
95
+ i = end;
96
+ let inner = null;
97
+ if (s[i] === '(') { const close = matchParen(s, i); inner = s.slice(i + 1, close); i = close + 1; }
98
+ if (pseudoElement || LEGACY_PSEUDO_ELEMENTS.has(name)) { t[2] += 1; continue; }
99
+ if (name === 'where') continue;
100
+ if (ARG_SPECIFICITY.has(name)) { if (inner != null) add(t, maxSpec(splitList(inner))); continue; }
101
+ t[1] += 1;
102
+ if (inner != null) {
103
+ if (name === 'host' || name === 'host-context') add(t, maxSpec(splitList(inner)));
104
+ else if (name === 'nth-child' || name === 'nth-last-child') {
105
+ const of = /\bof\s+(.+)$/i.exec(inner);
106
+ if (of) add(t, maxSpec(splitList(of[1])));
107
+ }
108
+ }
109
+ continue;
110
+ }
111
+ if (ch === '*') {
112
+ i += 1;
113
+ if (s[i] === '|') { i += 1; if (s[i] === '*') i += 1; else { t[2] += 1; i = skipIdent(s, i); } }
114
+ continue;
115
+ }
116
+ if (isIdentChar(ch) || ch === '\\') {
117
+ i = skipIdent(s, i);
118
+ if (s[i] === '|' && s[i + 1] !== '=') { // ns|tag: conta il tipo, non il namespace
119
+ i += 1;
120
+ if (s[i] === '*') { i += 1; continue; }
121
+ i = skipIdent(s, i);
122
+ }
123
+ t[2] += 1;
124
+ continue;
125
+ }
126
+ i += 1; // combinatori, spazi, & già risolto
127
+ }
128
+ return t;
129
+ }
130
+
131
+ // --- cascata ---
132
+
133
+ const INHERITED = new Set([
134
+ 'border-collapse', 'border-spacing', 'caption-side', 'color', 'color-scheme', 'cursor', 'direction',
135
+ 'empty-cells', 'font', 'font-family', 'font-feature-settings', 'font-kerning', 'font-optical-sizing',
136
+ 'font-palette', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-synthesis',
137
+ 'font-variant', 'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures',
138
+ 'font-variant-numeric', 'font-variant-position', 'font-variation-settings', 'font-weight',
139
+ 'hanging-punctuation', 'hyphens', 'image-rendering', 'letter-spacing', 'line-break', 'line-height',
140
+ 'list-style', 'list-style-image', 'list-style-position', 'list-style-type', 'orphans', 'overflow-wrap',
141
+ 'paint-order', 'pointer-events', 'quotes', 'ruby-align', 'ruby-position', 'tab-size', 'text-align',
142
+ 'text-align-last', 'text-combine-upright', 'text-decoration-skip-ink', 'text-emphasis',
143
+ 'text-emphasis-color', 'text-emphasis-position', 'text-emphasis-style', 'text-indent', 'text-justify',
144
+ 'text-orientation', 'text-rendering', 'text-shadow', 'text-size-adjust', 'text-transform',
145
+ 'text-underline-offset', 'text-underline-position', 'text-wrap', 'text-wrap-mode', 'text-wrap-style',
146
+ 'visibility', 'white-space', 'white-space-collapse', 'widows', 'word-break', 'word-spacing', 'word-wrap',
147
+ 'writing-mode', 'accent-color', 'caret-color', 'scrollbar-color', 'print-color-adjust',
148
+ 'forced-color-adjust', 'math-depth', 'math-style', '-webkit-text-fill-color', '-webkit-text-stroke',
149
+ '-webkit-text-stroke-color', '-webkit-text-stroke-width',
150
+ ]);
151
+ const inherits = (p) => p.startsWith('--') || INHERITED.has(p);
152
+ const MAX_OVERRIDDEN = 5;
153
+ const MAX_ANCESTORS = 40;
154
+
155
+ const kindOf = (rule) => rule?.constructor?.name ?? '';
156
+ const sheetLabel = (href, ownerNode, n) => {
157
+ if (href) return href.split('#')[0].split('?')[0].split('/').pop() || href;
158
+ if (ownerNode?.id) return `<style>#${ownerNode.id}`;
159
+ return ownerNode ? `<style> #${n}` : `adopted #${n}`;
160
+ };
161
+ const describe = (el) => {
162
+ const raw = typeof el.className === 'string' ? el.className : (el.getAttribute?.('class') ?? '');
163
+ const classes = raw.trim().split(/\s+/).filter(Boolean).slice(0, 2);
164
+ return (el.tagName || '').toLowerCase() + (el.id ? `#${el.id}` : '') + classes.map((c) => `.${c}`).join('');
165
+ };
166
+ const safeMatches = (el, sel) => { try { return el.matches(sel); } catch { return false; } };
167
+ const resolveNested = (sel, parents) => {
168
+ if (!parents) return sel;
169
+ const parent = `:is(${parents.join(', ')})`;
170
+ return sel.includes('&') ? sel.split('&').join(parent) : `${parent} ${sel}`;
171
+ };
172
+
173
+ /** Le regole di stile di una radice (document o shadow root), in ordine di cascata, con layer e condizioni. */
174
+ function collect(root, env) {
175
+ const sheets = [];
176
+ const opaque = [];
177
+ const skipped = [];
178
+ const rules = [];
179
+ const layerOrder = new Map();
180
+ const layerCounters = new Map();
181
+ let anonymous = 0;
182
+ let unnamed = 0;
183
+ let order = 0;
184
+ const registerLayer = (path) => {
185
+ for (let i = 1; i <= path.length; i += 1) {
186
+ const key = path.slice(0, i).join('.');
187
+ if (layerOrder.has(key)) continue;
188
+ const parent = path.slice(0, i - 1).join('.');
189
+ const n = layerCounters.get(parent) ?? 0;
190
+ layerCounters.set(parent, n + 1);
191
+ layerOrder.set(key, n);
192
+ }
193
+ };
194
+ const layerPath = (base, name) => {
195
+ const path = (base ?? []).concat(name === '' ? [`(anonymous ${(anonymous += 1)})`] : name.split('.'));
196
+ registerLayer(path);
197
+ return path;
198
+ };
199
+ const rankOf = (path) => path && path.map((_, i) => layerOrder.get(path.slice(0, i + 1).join('.')));
200
+ const countRules = (list) => {
201
+ let n = 0;
202
+ for (const r of list ?? []) { n += 1; if (r.cssRules) n += countRules(r.cssRules); }
203
+ return n;
204
+ };
205
+
206
+ const walk = (list, ctx) => {
207
+ for (const rule of list) {
208
+ const kind = kindOf(rule);
209
+ if (kind === 'CSSKeyframesRule' || kind === 'CSSPageRule') continue;
210
+ if (rule.selectorText != null && rule.style) {
211
+ const index = ctx.sheet.rules;
212
+ ctx.sheet.rules += 1;
213
+ const selectors = splitList(rule.selectorText).map((s) => resolveNested(s, ctx.parentSelectors));
214
+ rules.push({ selectors, sheet: ctx.sheet.label, index, layer: ctx.layer, conditions: ctx.conditions, style: rule.style, order: (order += 1) });
215
+ if (rule.cssRules?.length) walk(rule.cssRules, { ...ctx, parentSelectors: selectors, parentIndex: index });
216
+ continue;
217
+ }
218
+ if (kind === 'CSSNestedDeclarations' && rule.style && ctx.parentSelectors) {
219
+ rules.push({ selectors: ctx.parentSelectors, sheet: ctx.sheet.label, index: ctx.parentIndex, layer: ctx.layer, conditions: ctx.conditions, style: rule.style, order: (order += 1) });
220
+ continue;
221
+ }
222
+ if (kind === 'CSSImportRule') {
223
+ const mediaText = rule.media?.mediaText ?? '';
224
+ if (mediaText && !env.matchMedia(mediaText)) continue;
225
+ if (rule.supportsText && !env.supports(rule.supportsText)) continue;
226
+ const layer = rule.layerName == null ? ctx.layer : layerPath(ctx.layer, rule.layerName);
227
+ addSheet(rule.styleSheet, rule.href, { ...ctx, layer });
228
+ continue;
229
+ }
230
+ if (kind === 'CSSLayerStatementRule') {
231
+ for (const name of rule.nameList ?? []) layerPath(ctx.layer, name);
232
+ continue;
233
+ }
234
+ if (kind === 'CSSLayerBlockRule') {
235
+ walk(rule.cssRules, { ...ctx, layer: layerPath(ctx.layer, rule.name ?? '') });
236
+ continue;
237
+ }
238
+ if (kind === 'CSSMediaRule') {
239
+ const text = rule.conditionText ?? rule.media?.mediaText ?? '';
240
+ if (!env.matchMedia(text)) continue;
241
+ walk(rule.cssRules, { ...ctx, conditions: ctx.conditions.concat(`@media ${text}`) });
242
+ continue;
243
+ }
244
+ if (kind === 'CSSSupportsRule') {
245
+ if (!env.supports(rule.conditionText)) continue;
246
+ walk(rule.cssRules, { ...ctx, conditions: ctx.conditions.concat(`@supports ${rule.conditionText}`) });
247
+ continue;
248
+ }
249
+ if (rule.cssRules && (kind === 'CSSContainerRule' || kind === 'CSSScopeRule' || kind === 'CSSStartingStyleRule')) {
250
+ const label = kind === 'CSSContainerRule' ? `@container ${rule.conditionText}`
251
+ : kind === 'CSSScopeRule' ? `@scope (${rule.start ?? ''})` : '@starting-style';
252
+ skipped.push({ condition: label, rules: countRules(rule.cssRules) });
253
+ }
254
+ // @font-face, @property, @namespace, @counter-style: niente da cascata
255
+ }
256
+ };
257
+ const addSheet = (sheet, href, ctx) => {
258
+ let list = null;
259
+ try { list = sheet?.cssRules ?? null; } catch { list = null; }
260
+ if (!list) { opaque.push(href ?? sheet?.href ?? '(unknown)'); return; }
261
+ const ownHref = sheet.href ?? href ?? null;
262
+ const entry = { label: sheetLabel(ownHref, sheet.ownerNode, ownHref ? 0 : (unnamed += 1)), href: ownHref, rules: 0 };
263
+ sheets.push(entry);
264
+ walk(list, { ...ctx, sheet: entry });
265
+ };
266
+ for (const sheet of env.styleSheetsOf(root)) addSheet(sheet, null, { sheet: null, layer: null, conditions: [], parentSelectors: null, parentIndex: null });
267
+ return { rules, sheets, opaque, skipped, rankOf };
268
+ }
269
+
270
+ // Confronto di cascata fra due dichiarazioni della stessa proprietà: > 0 se vince a.
271
+ const cmpLayer = (ra, rb, important) => {
272
+ const ua = ra == null;
273
+ const ub = rb == null;
274
+ if (ua && ub) return 0;
275
+ if (ua !== ub) return (ua === !important) ? 1 : -1; // senza layer: vince fra le normali, perde fra le important
276
+ const n = Math.min(ra.length, rb.length);
277
+ for (let i = 0; i < n; i += 1) {
278
+ if (ra[i] !== rb[i]) return ((ra[i] > rb[i]) === !important) ? 1 : -1; // normali: l'ultimo layer vince
279
+ }
280
+ if (ra.length === rb.length) return 0;
281
+ return ((ra.length < rb.length) === !important) ? 1 : -1; // il layer padre batte i suoi sottolayer fra le normali
282
+ };
283
+ const cmpDecl = (a, b) => {
284
+ if (a.important !== b.important) return a.important ? 1 : -1;
285
+ if (a.inline !== b.inline) return a.inline ? 1 : -1;
286
+ return cmpLayer(a.layerRank, b.layerRank, a.important) || cmpSpec(a.spec, b.spec) || a.order - b.order;
287
+ };
288
+
289
+ const declarationsOf = (style) => {
290
+ const out = [];
291
+ for (let i = 0; i < style.length; i += 1) {
292
+ const prop = style[i];
293
+ out.push({ prop, value: style.getPropertyValue(prop), important: style.getPropertyPriority(prop) === 'important' });
294
+ }
295
+ return out;
296
+ };
297
+
298
+ /** Per ogni proprietà dichiarata su `el`, le dichiarazioni in ordine di cascata (vincitrice prima). */
299
+ function candidatesFor(el, { rules, rankOf }) {
300
+ const byProp = new Map();
301
+ let matched = 0;
302
+ const push = (d) => { if (!byProp.has(d.prop)) byProp.set(d.prop, []); byProp.get(d.prop).push(d); };
303
+ for (const rule of rules) {
304
+ const joined = rule.selectors.length === 1 ? rule.selectors[0] : rule.selectors.join(', ');
305
+ if (!safeMatches(el, joined)) continue;
306
+ matched += 1;
307
+ const matching = rule.selectors.length === 1 ? rule.selectors : rule.selectors.filter((s) => safeMatches(el, s));
308
+ let selector = matching[0] ?? rule.selectors[0];
309
+ let spec = specificity(selector);
310
+ for (const s of matching.slice(1)) { const sp = specificity(s); if (cmpSpec(sp, spec) > 0) { spec = sp; selector = s; } }
311
+ const layerRank = rankOf(rule.layer);
312
+ for (const d of declarationsOf(rule.style)) {
313
+ push({ ...d, inline: false, selector, sheet: rule.sheet, index: rule.index, layer: rule.layer, conditions: rule.conditions, layerRank, spec, order: rule.order });
314
+ }
315
+ }
316
+ if (el.style?.length) {
317
+ for (const d of declarationsOf(el.style)) push({ ...d, inline: true, layerRank: null, spec: [0, 0, 0], order: Infinity });
318
+ }
319
+ for (const list of byProp.values()) list.sort((a, b) => cmpDecl(b, a));
320
+ return { byProp, matched };
321
+ }
322
+
323
+ const fmtSource = (d) => {
324
+ if (d.inline) return { inline: true };
325
+ const out = { selector: d.selector, sheet: d.sheet, rule: d.index };
326
+ if (d.layer) out.layer = d.layer.join('.');
327
+ if (d.conditions?.length) out.conditions = d.conditions;
328
+ return out;
329
+ };
330
+ const fmtEntry = (list, computed, inheritedFrom) => {
331
+ const [winner, ...rest] = list;
332
+ const entry = { value: winner.value };
333
+ if (winner.important) entry.important = true;
334
+ if (computed != null) entry.computed = computed;
335
+ if (inheritedFrom) entry.inherited_from = inheritedFrom;
336
+ entry.source = fmtSource(winner);
337
+ entry.overridden = rest.slice(0, MAX_OVERRIDDEN).map((o) => ({ value: o.value, ...(o.important ? { important: true } : {}), ...fmtSource(o) }));
338
+ if (rest.length > MAX_OVERRIDDEN) entry.overridden_more = rest.length - MAX_OVERRIDDEN;
339
+ return entry;
340
+ };
341
+ const parentOf = (node) => node.parentElement ?? (node.parentNode?.host ?? null);
342
+
343
+ const defaultEnv = () => ({
344
+ styleSheetsOf: (root) => [...(root.styleSheets ?? []), ...(root.adoptedStyleSheets ?? [])],
345
+ getComputedStyle: (el) => getComputedStyle(el),
346
+ matchMedia: (q) => matchMedia(q).matches,
347
+ supports: (c) => CSS.supports(c),
348
+ });
349
+
350
+ /**
351
+ * @param {Element} el
352
+ * @param {{properties?: string[]|null, include_inherited?: boolean, selector?: string}} [opts]
353
+ * @param {object} [env] - CSSOM finto nei test
354
+ */
355
+ function cssStylesOf(el, opts = {}, env = defaultEnv()) {
356
+ const wanted = Array.isArray(opts.properties) && opts.properties.length ? opts.properties.map((p) => String(p).trim().toLowerCase()) : null;
357
+ const want = (p) => !wanted || wanted.some((q) => p === q || p.startsWith(`${q}-`));
358
+ const collections = new Map();
359
+ const collectionOf = (node) => {
360
+ const root = node.getRootNode ? node.getRootNode() : (node.ownerDocument ?? document);
361
+ if (!collections.has(root)) collections.set(root, collect(root, env));
362
+ return collections.get(root);
363
+ };
364
+ const own = collectionOf(el);
365
+ const computed = env.getComputedStyle(el);
366
+ const computedOf = (p) => computed.getPropertyValue(p);
367
+ const usedSheets = new Set();
368
+ const noteSheets = (list) => { for (const d of list) if (!d.inline) usedSheets.add(d.sheet); };
369
+
370
+ const properties = {};
371
+ const { byProp, matched } = candidatesFor(el, own);
372
+ for (const [prop, list] of byProp) {
373
+ if (!want(prop)) continue;
374
+ properties[prop] = fmtEntry(list, computedOf(prop), null);
375
+ noteSheets(list);
376
+ }
377
+ if (opts.include_inherited) {
378
+ let node = parentOf(el);
379
+ for (let depth = 0; node && node.nodeType === 1 && depth < MAX_ANCESTORS; depth += 1, node = parentOf(node)) {
380
+ const ancestor = candidatesFor(node, collectionOf(node));
381
+ for (const [prop, list] of ancestor.byProp) {
382
+ if (properties[prop] || !inherits(prop) || !want(prop)) continue;
383
+ properties[prop] = fmtEntry(list, computedOf(prop), describe(node));
384
+ noteSheets(list);
385
+ }
386
+ }
387
+ }
388
+
389
+ const sorted = {};
390
+ for (const k of Object.keys(properties).sort()) sorted[k] = properties[k];
391
+ const out = { selector: opts.selector ?? null, element: describe(el), matched_rules: matched };
392
+ const sheets = [];
393
+ for (const c of collections.values()) for (const s of c.sheets) if (usedSheets.has(s.label)) sheets.push(s.href ? { sheet: s.label, href: s.href, rules: s.rules } : { sheet: s.label, rules: s.rules });
394
+ out.stylesheets = sheets;
395
+ const opaque = [...collections.values()].flatMap((c) => c.opaque);
396
+ if (opaque.length) out.opaque_stylesheets = opaque;
397
+ const skipped = [...collections.values()].flatMap((c) => c.skipped);
398
+ if (skipped.length) out.skipped = skipped;
399
+ out.properties = sorted;
400
+ return out;
401
+ }
402
+
403
+ globalThis.__cbCssStyles = cssStylesOf;
404
+ globalThis.__cbCssSpecificity = specificity;
405
+ })();
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Chrome Bridge for Claude Code",
4
- "version": "1.20.1",
5
- "description": "Bridge your browser to Claude Code: 59 token-efficient web-dev automation tools over a local WebSocket. ChromeOS included.",
4
+ "version": "1.21.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": [
@@ -357,6 +357,8 @@ async function executeCommand(msg) {
357
357
  return await cmdGetPerformance(params);
358
358
  case 'query_dom':
359
359
  return await cmdQueryDom(params);
360
+ case 'get_css_styles':
361
+ return await cmdGetCssStyles(params);
360
362
  case 'modify_dom':
361
363
  return await cmdModifyDom(params);
362
364
  case 'inject_css':
@@ -1564,6 +1566,42 @@ async function cmdQueryDom({ selector, properties, limit = 50, tab_id, frame_id
1564
1566
  return { count: elements.length, elements };
1565
1567
  }
1566
1568
 
1569
+ // --- DevTools: get_css_styles ---
1570
+
1571
+ // La cascata sta in lib/css-cascade.js, iniettato prima della funzione come
1572
+ // element-label.js: chrome.scripting serializza solo la funzione. Mondo
1573
+ // ISOLATED: il CSSOM è lo stesso e la pagina non vede né manomette la libreria.
1574
+ async function cmdGetCssStyles({ selector, properties, include_inherited = false, tab_id, frame_id }) {
1575
+ if (!selector) throw new Error('Missing required parameter: selector');
1576
+ const tabId = await resolveTabId(tab_id);
1577
+ const target = scriptTarget(tabId, frame_id);
1578
+ await chrome.scripting.executeScript({ target, files: ['lib/css-cascade.js'], world: 'ISOLATED' });
1579
+ const results = await chrome.scripting.executeScript({
1580
+ target,
1581
+ func: (sel, props, inherited) => {
1582
+ function deepQuery(sel) {
1583
+ if (!sel.includes('>>>')) return document.querySelector(sel);
1584
+ const parts = sel.split('>>>').map((s) => s.trim());
1585
+ let ctx = document;
1586
+ for (let i = 0; i < parts.length; i++) {
1587
+ const found = ctx.querySelector(parts[i]);
1588
+ if (!found) return null;
1589
+ if (i === parts.length - 1) return found;
1590
+ if (!found.shadowRoot) return null;
1591
+ ctx = found.shadowRoot;
1592
+ }
1593
+ return null;
1594
+ }
1595
+ const el = deepQuery(sel);
1596
+ if (!el) throw new Error(`Element not found: ${sel}`);
1597
+ return globalThis.__cbCssStyles(el, { selector: sel, properties: props, include_inherited: inherited });
1598
+ },
1599
+ args: [selector, properties ?? null, Boolean(include_inherited)],
1600
+ world: 'ISOLATED',
1601
+ });
1602
+ return results?.[0]?.result ?? {};
1603
+ }
1604
+
1567
1605
  // --- DevTools: modify_dom ---
1568
1606
 
1569
1607
  async function cmdModifyDom({ selector, action, name, value, className, tab_id, frame_id }) {
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.20.1",
4
+ "version": "1.21.0",
5
5
  "type": "module",
6
- "description": "MCP server with 59 tools for browser automation via Chrome extension. Works on ChromeOS/Crostini.",
6
+ "description": "Chrome Bridge by frsorrentino: MCP server that gives Claude Code your real, logged-in Chrome — 60 tools, 2.75× fewer turns than Claude in Chrome, ChromeOS included.",
7
7
  "main": "server/index.js",
8
8
  "bin": {
9
9
  "chrome-bridge-mcp": "server/index.js",
@@ -18,6 +18,7 @@ export const MessageType = Object.freeze({
18
18
  GET_STORAGE: 'get_storage',
19
19
  GET_PERFORMANCE: 'get_performance',
20
20
  QUERY_DOM: 'query_dom',
21
+ GET_CSS_STYLES: 'get_css_styles',
21
22
  MODIFY_DOM: 'modify_dom',
22
23
  INJECT_CSS: 'inject_css',
23
24
  READ_CONSOLE: 'read_console',
@@ -92,7 +93,7 @@ export const MessageType = Object.freeze({
92
93
  });
93
94
 
94
95
  // Versione
95
- export const VERSION = '1.20.1';
96
+ export const VERSION = '1.21.0';
96
97
 
97
98
  // Configurazione
98
99
  export const DEFAULT_PORT = 8765;
package/server/tools.js CHANGED
@@ -307,6 +307,7 @@ export const TOOL_ANNOTATIONS = {
307
307
  measure_spacing: ro(),
308
308
  monitor_network: ro(true),
309
309
  query_dom: ro(),
310
+ get_css_styles: ro(),
310
311
  read_page: ro(),
311
312
  screenshot: ro(),
312
313
  wait_for: ro(),
@@ -982,6 +983,27 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
982
983
  }
983
984
  );
984
985
 
986
+ // --- get_css_styles ---
987
+ server.tool(
988
+ 'get_css_styles',
989
+ 'Why an element looks the way it does: for each CSS property the winning declaration — selector, stylesheet, '
990
+ + 'rule position, !important, @layer, @media — and the ones it overrides, like the Styles panel of DevTools; '
991
+ + 'query_dom gives the computed value, this gives who set it. Read-only, from the CSSOM: stylesheets of '
992
+ + 'another origin without CORS are opaque and listed by href; user-agent styles, @container and @scope rules '
993
+ + 'are not evaluated. properties narrows the output.',
994
+ {
995
+ selector: z.string().describe('CSS selector; ">>>" pierces shadow DOM. Only the first match is inspected'),
996
+ properties: z.array(z.string()).optional().describe('Only these, e.g. ["margin-top","--brand"]; margin covers its longhands. Omitted = every property a matching rule sets'),
997
+ include_inherited: z.boolean().optional().default(false).describe('Also inherited properties (color, font-*, custom properties) with the ancestor that sets them'),
998
+ tab_id: tabId,
999
+ frame_id: frameId,
1000
+ },
1001
+ async ({ selector, properties, include_inherited, tab_id, frame_id }) => {
1002
+ const data = await send(MessageType.GET_CSS_STYLES, { selector, properties, include_inherited, tab_id, frame_id });
1003
+ return { content: [{ type: 'text', text: jsonText(data, DEFAULT_MAX_OUTPUT, 'properties') }] };
1004
+ }
1005
+ );
1006
+
985
1007
  // --- modify_dom ---
986
1008
  server.tool(
987
1009
  'modify_dom',
@@ -140,7 +140,10 @@ top group and re-running both.
140
140
  Triggers: "does it use the design's fonts/colours?", "does it match the
141
141
  Figma?", «somiglia al design?».
142
142
  `query_dom({selector, properties:['font-family','font-size','color','background-color','margin','padding']})`
143
- on the key elements; compare with the token file in the repo. Spacing between
143
+ on the key elements; compare with the token file in the repo. Why a value is
144
+ what it is («chi mi azzera il margine?»): `get_css_styles({selector, properties:['margin-top']})`
145
+ → the winning rule with selector, stylesheet and rule position, plus the ones it
146
+ overrides; `include_inherited:true` for `color` and `font-*`. Spacing between
144
147
  two elements: `measure_spacing({selector1, selector2})`. Visual comparison:
145
148
  `viewport_resize({width, height})` to the mockup's size, then
146
149
  `screenshot_diff({action:'baseline', name:'home', from_file:'./mockups/home.png'})`