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 +8 -6
- package/extension/lib/css-cascade.js +405 -0
- package/extension/manifest.json +2 -2
- package/extension/service-worker.js +38 -0
- package/package.json +2 -2
- package/server/protocol.js +2 -1
- package/server/tools.js +22 -0
- package/skills/chrome-bridge/SKILL.md +4 -1
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
|
    [](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
|
-
|
|
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** | **
|
|
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
|
-
|
|
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
|
-

|
|
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 |
|
|
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
|
|
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
|
+
})();
|
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.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.
|
|
4
|
+
"version": "1.21.0",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "MCP server
|
|
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",
|
package/server/protocol.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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'})`
|