chrome-bridge-mcp 1.31.3 → 1.32.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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/protocol.js +1 -1
- package/server/tools.js +51 -7
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-bridge-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "chrome-bridge-mcp",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.32.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@jridgewell/trace-mapping": "^0.3.31",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-bridge-mcp",
|
|
3
3
|
"mcpName": "io.github.frsorrentino/chrome-bridge",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.32.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Chrome Bridge by frsorrentino: MCP server that lets Claude Code borrow a tab from your own logged-in Chrome and hand it back — logins, 2FA and CAPTCHAs stay with you (handoff). 69 tools; 2.8-4.3× cheaper and up to 9.3× fewer turns than Claude in Chrome, measured; ChromeOS included.",
|
|
7
7
|
"main": "server/index.js",
|
package/server/protocol.js
CHANGED
package/server/tools.js
CHANGED
|
@@ -671,6 +671,9 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
671
671
|
reg.bySel.set(selKey(frame, e.selector), ref);
|
|
672
672
|
reg.byRef.set(ref, { selector: e.selector, frame });
|
|
673
673
|
}
|
|
674
|
+
// Quel che il modello ha visto per ultimo con questo ref: il controllo
|
|
675
|
+
// STALE_REF lo confronta con la pagina prima di agire.
|
|
676
|
+
if (e.tag) Object.assign(reg.byRef.get(ref), { tag: String(e.tag).toLowerCase(), text: e.text ?? '' });
|
|
674
677
|
e.ref = ref;
|
|
675
678
|
}
|
|
676
679
|
while (reg.byRef.size > REFS_MAX) {
|
|
@@ -745,6 +748,47 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
745
748
|
throw new Error('Either selector or ref is required');
|
|
746
749
|
}
|
|
747
750
|
|
|
751
|
+
// STALE_REF: fra get_interactives e l'azione la pagina può cambiare, e lo
|
|
752
|
+
// stesso selettore trovare un altro elemento (una riga tolta da una lista:
|
|
753
|
+
// "li:nth-of-type(2)" è ora la successiva). Prima di agire per ref si
|
|
754
|
+
// rilegge l'elemento: assente, tag diverso o etichetta che non c'è più vuol
|
|
755
|
+
// dire scaduto, e l'azione non parte. L'etichetta si controlla solo dove
|
|
756
|
+
// viene dall'elemento stesso (testo o attributi): un campo con <label for>
|
|
757
|
+
// o aria-labelledby la prende da fuori e lì conta il tag. Se la rilettura
|
|
758
|
+
// fallisce (chrome://, estensione vecchia) si agisce come prima.
|
|
759
|
+
const FIELD_TAGS = new Set(['input', 'select', 'textarea']);
|
|
760
|
+
const norm = (v) => String(v ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
761
|
+
async function assertFresh(ref, target, tab_id) {
|
|
762
|
+
const entry = interactivesRefs.get(refsKey(tab_id))?.byRef.get(ref);
|
|
763
|
+
if (!entry?.tag) return;
|
|
764
|
+
let el;
|
|
765
|
+
try {
|
|
766
|
+
const r = await send(MessageType.QUERY_DOM, { selector: target.selector, limit: 1, tab_id, frame_id: target.frame_id });
|
|
767
|
+
if (!r || !Array.isArray(r.elements)) return;
|
|
768
|
+
el = r.elements[0];
|
|
769
|
+
} catch {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const seen = `${entry.tag}${entry.text ? ` «${entry.text}»` : ''}`;
|
|
773
|
+
const stale = (now) => new Error(`STALE_REF: ref ${ref} was ${seen}, but the page changed and ${now}. Nothing was done: run get_interactives again and use the new ref`);
|
|
774
|
+
if (!el) throw stale('its selector no longer matches anything');
|
|
775
|
+
const tag = norm(el.tagName);
|
|
776
|
+
if (tag !== entry.tag) throw stale(`its selector now finds a <${tag}>`);
|
|
777
|
+
const attrs = el.attributes ?? {};
|
|
778
|
+
const label = norm(entry.text).slice(0, 40);
|
|
779
|
+
if (label.length < 2 || FIELD_TAGS.has(tag) || attrs['aria-labelledby'] != null) return;
|
|
780
|
+
if (!norm(`${el.textContent ?? ''} ${Object.values(attrs).join(' ')}`).includes(label)) {
|
|
781
|
+
throw stale(`its selector now finds ${el.textContent ? `«${String(el.textContent).trim().slice(0, 60)}»` : 'another element'}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// resolveTarget più il controllo STALE_REF quando il bersaglio è un ref.
|
|
786
|
+
async function freshTarget(selector, ref, tab_id, frame_id) {
|
|
787
|
+
const t = resolveTarget(selector, ref, tab_id, frame_id);
|
|
788
|
+
if (!selector && ref) await assertFresh(ref, t, tab_id);
|
|
789
|
+
return t;
|
|
790
|
+
}
|
|
791
|
+
|
|
748
792
|
// Impronta della pagina per il delta post-azione (page_changed): DOM in
|
|
749
793
|
// pochi interi più url/title. Un'estensione vecchia o una pagina non
|
|
750
794
|
// iniettabile (chrome://) tornano a url/title da get_tabs: nessun errore,
|
|
@@ -1164,7 +1208,7 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1164
1208
|
frame_id: frameId,
|
|
1165
1209
|
},
|
|
1166
1210
|
async ({ selector, ref, force, button, count, trusted, wait_after, tab_id, frame_id }) => {
|
|
1167
|
-
const { selector: target, frame_id: frame } =
|
|
1211
|
+
const { selector: target, frame_id: frame } = await freshTarget(selector, ref, tab_id, frame_id);
|
|
1168
1212
|
const before = await tabSnapshot(tab_id);
|
|
1169
1213
|
const data = await send(MessageType.CLICK, { selector: target, force, button: button ?? 'left', count: count ?? 1, ...(trusted && { trusted: true }), frame_id: frame, tab_id });
|
|
1170
1214
|
// Niente attesa se il click non è andato a buon fine (es. elemento occluso)
|
|
@@ -1200,7 +1244,7 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1200
1244
|
frame_id: frameId,
|
|
1201
1245
|
},
|
|
1202
1246
|
async ({ selector, ref, text, mode, wait_after, tab_id, frame_id }) => {
|
|
1203
|
-
const { selector: target, frame_id: frame } =
|
|
1247
|
+
const { selector: target, frame_id: frame } = await freshTarget(selector, ref, tab_id, frame_id);
|
|
1204
1248
|
const data = await send(MessageType.TYPE_TEXT, { selector: target, text, mode, tab_id, frame_id: frame });
|
|
1205
1249
|
const waited = await applyWaitAfter(send, wait_after, tab_id);
|
|
1206
1250
|
const out = waited ? { ...data, wait_after: waited } : data;
|
|
@@ -1634,13 +1678,13 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
1634
1678
|
const refFrames = new Set(fields.filter((f) => f.ref).map((f) => resolveTarget(undefined, f.ref, tab_id, frame_id).frame_id ?? 0));
|
|
1635
1679
|
if (refFrames.size > 1) throw new Error(`fill_form: the refs come from frames ${[...refFrames].join(', ')}; one call fills one frame`);
|
|
1636
1680
|
if (frame_id == null && refFrames.size === 1) { const [f0] = refFrames; if (f0) frame_id = f0; }
|
|
1637
|
-
const resolved = fields.map((f) => {
|
|
1638
|
-
const { selector } =
|
|
1681
|
+
const resolved = await Promise.all(fields.map(async (f) => {
|
|
1682
|
+
const { selector } = await freshTarget(f.ref ? undefined : f.selector, f.ref, tab_id, frame_id);
|
|
1639
1683
|
const value = f.value ?? (typeof f.checked === 'boolean' ? String(f.checked) : undefined);
|
|
1640
1684
|
if (value === undefined) throw new Error(`fill_form: field ${f.ref ?? f.selector} needs value or checked`);
|
|
1641
1685
|
return { selector, value };
|
|
1642
|
-
});
|
|
1643
|
-
const submitSel = submit_ref ?
|
|
1686
|
+
}));
|
|
1687
|
+
const submitSel = submit_ref ? (await freshTarget(undefined, submit_ref, tab_id, frame_id)).selector : submit_selector;
|
|
1644
1688
|
const [before, textBefore] = await Promise.all([tabSnapshot(tab_id), submitSel ? pageText(tab_id, frame_id) : null]);
|
|
1645
1689
|
const data = await send(MessageType.FILL_FORM, { fields: resolved, submit_selector: submitSel, tab_id, frame_id });
|
|
1646
1690
|
const waited = await applyWaitAfter(send, wait_after, tab_id, before?.url);
|
|
@@ -2246,7 +2290,7 @@ export function registerTools(server, wsManager, caps = 'all', options = {}) {
|
|
|
2246
2290
|
frame_id: frameId,
|
|
2247
2291
|
},
|
|
2248
2292
|
async ({ selector, ref, trusted, tab_id, frame_id }) => {
|
|
2249
|
-
const t =
|
|
2293
|
+
const t = await freshTarget(selector, ref, tab_id, frame_id);
|
|
2250
2294
|
const data = await send(MessageType.HOVER, { selector: t.selector, ...(trusted && { trusted: true }), tab_id, frame_id: t.frame_id });
|
|
2251
2295
|
return {
|
|
2252
2296
|
content: [{
|