chrome-bridge-mcp 1.30.6 → 1.31.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 +5 -5
- package/extension/manifest.json +2 -2
- package/extension/service-worker.js +45 -1
- package/extension/webmcp-hook.js +306 -0
- package/npm-shrinkwrap.json +1419 -0
- package/package.json +4 -3
- package/server/protocol.js +2 -1
- package/server/tools.js +101 -8
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> By [frsorrentino](https://github.com/frsorrentino) · npm `chrome-bridge-mcp` · not affiliated with other projects named "chrome-bridge".
|
|
4
4
|
|
|
5
|
-
    [](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb)
|
|
5
|
+
    [](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb) [](https://mcprush.com/to/chrome-bridge-mcp?ref=badge)
|
|
6
6
|
|
|
7
7
|
**Claude Code borrows a tab from the Chrome you are already using — signed in,
|
|
8
8
|
with your extensions and your cookies — works in it, and hands it back. What
|
|
@@ -164,12 +164,12 @@ user-script toggle isn't available.
|
|
|
164
164
|
|
|
165
165
|
## Tools
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
default; the other 23 sit in seven optional caps that the agent switches on
|
|
167
|
+
69 in total. `core` (46 tools: every tool used in 101 real sessions, plus the
|
|
168
|
+
two WebMCP tools) loads by default; the other 23 sit in seven optional caps that the agent switches on
|
|
169
169
|
mid-session with `get_status({enable: ["visual"]})`, with no restart
|
|
170
170
|
(`--caps` still sets them at startup).
|
|
171
171
|
|
|
172
|
-

|
|
173
173
|
|
|
174
174
|
| Group | N | What's in it |
|
|
175
175
|
|---|---|---|
|
|
@@ -316,7 +316,7 @@ count.
|
|
|
316
316
|
|
|
317
317
|
## Documentation
|
|
318
318
|
|
|
319
|
-
- [docs/TOOLS.md](docs/TOOLS.md) — all
|
|
319
|
+
- [docs/TOOLS.md](docs/TOOLS.md) — all 69 tools, by group
|
|
320
320
|
- [docs/CAPABILITIES.md](docs/CAPABILITIES.md) — what the bridge gets past and what it does not, one dated state per wall
|
|
321
321
|
- [docs/EFFICIENCY.md](docs/EFFICIENCY.md) — the benchmark and the design behind it
|
|
322
322
|
- [docs/PERFORMANCE.md](docs/PERFORMANCE.md) — latency per tool on the real path, `npm run bench:latency`
|
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.31.0",
|
|
5
|
+
"description": "Bridge your browser to Claude Code: 69 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": [
|
|
@@ -50,7 +50,7 @@ async function _applyInstrumentation() {
|
|
|
50
50
|
const isReg = existing.some((s) => s.id === INSTRUMENTATION_ID);
|
|
51
51
|
// network-hook.js dalla 1.25.0: la registrazione persiste agli
|
|
52
52
|
// aggiornamenti, quindi un elenco vecchio va riscritto, non solo creato.
|
|
53
|
-
const JS = ['console-capture.js', 'network-hook.js', 'page-instrumentation.js'];
|
|
53
|
+
const JS = ['console-capture.js', 'network-hook.js', 'page-instrumentation.js', 'webmcp-hook.js'];
|
|
54
54
|
const stale = isReg && JSON.stringify(existing.find((s) => s.id === INSTRUMENTATION_ID)?.js ?? []) !== JSON.stringify(JS);
|
|
55
55
|
if (instrument && stale) {
|
|
56
56
|
await chrome.scripting.updateContentScripts([{ id: INSTRUMENTATION_ID, js: JS }]);
|
|
@@ -553,6 +553,8 @@ async function executeCommand(msg) {
|
|
|
553
553
|
return await cmdGetResponseHeaders(params);
|
|
554
554
|
case 'get_interactives':
|
|
555
555
|
return await cmdGetInteractives(params);
|
|
556
|
+
case 'webmcp':
|
|
557
|
+
return await cmdWebmcp(params);
|
|
556
558
|
case 'wait_for_function':
|
|
557
559
|
return await cmdWaitForFunction(params);
|
|
558
560
|
case 'scroll_until':
|
|
@@ -5539,6 +5541,48 @@ async function cmdWaitForFunction({ expression, timeout = 10000, polling_ms = 10
|
|
|
5539
5541
|
}
|
|
5540
5542
|
}
|
|
5541
5543
|
|
|
5544
|
+
// --- webmcp ---
|
|
5545
|
+
// Gli strumenti WebMCP vivono in window.__cbWebmcp (webmcp-hook.js, MAIN
|
|
5546
|
+
// world). Con la strumentazione spenta il gancio non c'è: si inietta qui, e
|
|
5547
|
+
// vede comunque tutto ciò che getTools elenca.
|
|
5548
|
+
|
|
5549
|
+
async function ensureWebmcpHook(target) {
|
|
5550
|
+
const [{ result } = {}] = await chrome.scripting.executeScript({ target, world: 'MAIN', func: () => Boolean(window.__cbWebmcp) });
|
|
5551
|
+
if (!result) await chrome.scripting.executeScript({ target, files: ['webmcp-hook.js'], world: 'MAIN' });
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
async function cmdWebmcp({ action = 'list', name, input = {}, timeout = 30000, confirm = false, limit = 20, tab_id, frame_id }) {
|
|
5555
|
+
const tabId = await resolveTabId(tab_id);
|
|
5556
|
+
const target = scriptTarget(tabId, frame_id);
|
|
5557
|
+
await ensureWebmcpHook(target);
|
|
5558
|
+
const run = async (func, args = []) => (await chrome.scripting.executeScript({ target, world: 'MAIN', func, args }))[0]?.result;
|
|
5559
|
+
if (action === 'summary') return await run(() => window.__cbWebmcp.summary());
|
|
5560
|
+
if (action === 'list') {
|
|
5561
|
+
const data = await run(() => window.__cbWebmcp.list());
|
|
5562
|
+
return limit > 0 && data ? { ...data, recent_calls: await run((l) => window.__cbWebmcp.calls(l), [limit]) } : data;
|
|
5563
|
+
}
|
|
5564
|
+
if (action !== 'call') throw new Error(`Unknown webmcp action: ${action}`);
|
|
5565
|
+
if (!name) throw new Error('Missing required parameter: name');
|
|
5566
|
+
|
|
5567
|
+
// Una chiamata può portare a un'altra pagina (un form inviato, un redirect):
|
|
5568
|
+
// la risposta dice dove. È il caso aperto dell'explainer (issue #135).
|
|
5569
|
+
const before = (await chrome.tabs.get(tabId)).url;
|
|
5570
|
+
let res;
|
|
5571
|
+
try {
|
|
5572
|
+
res = await run((n, i, o) => window.__cbWebmcp.call(n, i, o), [name, input, { timeout, confirmed: confirm === true }]);
|
|
5573
|
+
} catch (err) {
|
|
5574
|
+
res = { status: 'navigated', note: `the page went away during the call (${err.message})` };
|
|
5575
|
+
}
|
|
5576
|
+
if (res == null) res = { status: 'navigated', note: 'the page went away before the tool answered' };
|
|
5577
|
+
if (['ok', 'error', 'timeout', 'navigated'].includes(res.status)) {
|
|
5578
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
5579
|
+
let tab = await chrome.tabs.get(tabId);
|
|
5580
|
+
if (tab.status === 'loading') { await waitForComplete(tabId, 4000); tab = await chrome.tabs.get(tabId); }
|
|
5581
|
+
if (tab.url !== before) res.navigated_to = tab.url;
|
|
5582
|
+
}
|
|
5583
|
+
return res;
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5542
5586
|
// --- scroll_until ---
|
|
5543
5587
|
|
|
5544
5588
|
async function cmdScrollUntil({ until = 'no_new_content', selector, container, max_scrolls = 20, step_px, settle_ms = 400, tab_id }) {
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Iniettato a document_start in MAIN world: legge e chiama gli strumenti
|
|
3
|
+
* WebMCP che la pagina dichiara, e ne tiene il registro delle chiamate.
|
|
4
|
+
*
|
|
5
|
+
* WebMCP (W3C Web Machine Learning CG): la pagina registra strumenti con
|
|
6
|
+
* document.modelContext.registerTool (imperativi) o li dichiara con
|
|
7
|
+
* <form toolname> (dichiarativi). Senza il flag
|
|
8
|
+
* chrome://flags/#enable-webmcp-testing o l'origin trial document.modelContext
|
|
9
|
+
* non esiste: i form dichiarativi restano visibili ma inerti.
|
|
10
|
+
*
|
|
11
|
+
* Il gancio non crea mai document.modelContext: un sito che fa feature
|
|
12
|
+
* detection deve vedere il browser com'è. Avvolge registerTool dove esiste
|
|
13
|
+
* (nativo a document_start, o un polyfill alla prima lettura), così anche le
|
|
14
|
+
* chiamate dell'agente del browser finiscono nel registro.
|
|
15
|
+
*/
|
|
16
|
+
(() => {
|
|
17
|
+
if (window.__cbWebmcp) return;
|
|
18
|
+
|
|
19
|
+
const MAX_CALLS = 100;
|
|
20
|
+
const CLIP = 2000;
|
|
21
|
+
const calls = [];
|
|
22
|
+
const regs = new Map(); // name → { annotations, execute } dalle registerTool viste
|
|
23
|
+
const patched = new WeakMap(); // modelContext → executeTool originale
|
|
24
|
+
const pending = new Map(); // name → voci di registro di chiamate già aperte e in corso
|
|
25
|
+
let toolchanges = 0;
|
|
26
|
+
|
|
27
|
+
const mcOf = () => document.modelContext ?? navigator.modelContext ?? null;
|
|
28
|
+
|
|
29
|
+
const clip = (v) => {
|
|
30
|
+
if (v === undefined) return null;
|
|
31
|
+
let s;
|
|
32
|
+
try { s = JSON.stringify(v); } catch { s = String(v); }
|
|
33
|
+
if (s === undefined) return null;
|
|
34
|
+
if (s.length <= CLIP) { try { return JSON.parse(s); } catch { return s; } }
|
|
35
|
+
return `${s.slice(0, CLIP)}…[+${s.length - CLIP} chars]`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const record = (entry) => {
|
|
39
|
+
if (calls.length >= MAX_CALLS) calls.shift();
|
|
40
|
+
calls.push(entry);
|
|
41
|
+
return entry;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Una chiamata già aperta (da call o da executeTool) passa anche dal
|
|
45
|
+
// wrapper di execute o dal listener di submit: una sola voce, col chiamante
|
|
46
|
+
// giusto.
|
|
47
|
+
const open = (entry) => {
|
|
48
|
+
if (!pending.has(entry.tool)) pending.set(entry.tool, []);
|
|
49
|
+
pending.get(entry.tool).push(entry);
|
|
50
|
+
};
|
|
51
|
+
const close = (entry) => {
|
|
52
|
+
const q = pending.get(entry.tool);
|
|
53
|
+
const i = q ? q.indexOf(entry) : -1;
|
|
54
|
+
if (i >= 0) q.splice(i, 1);
|
|
55
|
+
};
|
|
56
|
+
const takePending = (name) => pending.get(name)?.shift() ?? null;
|
|
57
|
+
|
|
58
|
+
const settle = (entry, t0, out, err) => Object.assign(entry, err === undefined
|
|
59
|
+
? { status: 'ok', output: clip(out), ms: Math.round(performance.now() - t0) }
|
|
60
|
+
: { status: err?.timeout ? 'timeout' : 'error', error: String(err?.message ?? err), ms: Math.round(performance.now() - t0) });
|
|
61
|
+
|
|
62
|
+
const patch = (mc) => {
|
|
63
|
+
if (!mc || patched.has(mc) || typeof mc.registerTool !== 'function') return;
|
|
64
|
+
const origExecute = typeof mc.executeTool === 'function' ? mc.executeTool : null;
|
|
65
|
+
patched.set(mc, origExecute);
|
|
66
|
+
try { mc.addEventListener?.('toolchange', () => { toolchanges += 1; }); } catch {}
|
|
67
|
+
// executeTool chiamato dalla pagina o da un altro agente: si registra anche
|
|
68
|
+
// per gli strumenti registrati prima del gancio (un polyfill).
|
|
69
|
+
if (origExecute) {
|
|
70
|
+
mc.executeTool = async function (tool, input, ...rest) {
|
|
71
|
+
const name = String(tool?.name);
|
|
72
|
+
const isForm = declaredForms().some((f) => f.getAttribute('toolname') === name);
|
|
73
|
+
const e = record({ at: Date.now(), tool: name, kind: isForm ? 'declarative' : 'imperative', caller: 'other', input: clip(input), status: 'running' });
|
|
74
|
+
open(e);
|
|
75
|
+
const t0 = performance.now();
|
|
76
|
+
try {
|
|
77
|
+
const out = await origExecute.call(this, tool, input, ...rest);
|
|
78
|
+
settle(e, t0, out);
|
|
79
|
+
return out;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
settle(e, t0, undefined, err);
|
|
82
|
+
throw err;
|
|
83
|
+
} finally { close(e); }
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const orig = mc.registerTool;
|
|
87
|
+
mc.registerTool = function (tool, opts) {
|
|
88
|
+
try {
|
|
89
|
+
if (tool && typeof tool.execute === 'function' && tool.name) {
|
|
90
|
+
const exec = tool.execute;
|
|
91
|
+
const name = String(tool.name);
|
|
92
|
+
const wrapped = async function (input, ...rest) {
|
|
93
|
+
if (takePending(name)) return exec.call(this, input, ...rest);
|
|
94
|
+
const e = record({ at: Date.now(), tool: name, kind: 'imperative', caller: 'other', input: clip(input), status: 'running' });
|
|
95
|
+
const t0 = performance.now();
|
|
96
|
+
try {
|
|
97
|
+
const out = await exec.call(this, input, ...rest);
|
|
98
|
+
settle(e, t0, out);
|
|
99
|
+
return out;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
settle(e, t0, undefined, err);
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
regs.set(name, { annotations: tool.annotations ?? null, execute: exec });
|
|
106
|
+
opts?.signal?.addEventListener?.('abort', () => regs.delete(name));
|
|
107
|
+
return orig.call(this, { ...tool, execute: wrapped }, opts);
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
return orig.call(this, tool, opts);
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
patch(mcOf());
|
|
114
|
+
|
|
115
|
+
// --- strumenti dichiarativi: <form toolname> ---
|
|
116
|
+
const fieldSchema = (el) => {
|
|
117
|
+
const type = (el.getAttribute('type') || '').toLowerCase();
|
|
118
|
+
const s = {};
|
|
119
|
+
if (el.tagName === 'SELECT') {
|
|
120
|
+
s.type = 'string';
|
|
121
|
+
s.enum = [...el.options].map((o) => o.value);
|
|
122
|
+
} else if (type === 'number' || type === 'range') s.type = 'number';
|
|
123
|
+
else if (type === 'checkbox') s.type = 'boolean';
|
|
124
|
+
else s.type = 'string';
|
|
125
|
+
const d = el.getAttribute('toolparamdescription');
|
|
126
|
+
if (d) s.description = d;
|
|
127
|
+
return s;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const formSchema = (form) => {
|
|
131
|
+
const properties = {};
|
|
132
|
+
const required = [];
|
|
133
|
+
for (const el of form.elements) {
|
|
134
|
+
const name = el.name;
|
|
135
|
+
if (!name || el.disabled) continue;
|
|
136
|
+
const type = (el.getAttribute('type') || '').toLowerCase();
|
|
137
|
+
if (['submit', 'button', 'reset', 'image', 'hidden'].includes(type) || el.tagName === 'BUTTON' || el.tagName === 'FIELDSET') continue;
|
|
138
|
+
if (type === 'radio') {
|
|
139
|
+
const p = (properties[name] ??= { type: 'string', enum: [] });
|
|
140
|
+
p.enum.push(el.value);
|
|
141
|
+
} else properties[name] = fieldSchema(el);
|
|
142
|
+
if (el.required && !required.includes(name)) required.push(name);
|
|
143
|
+
}
|
|
144
|
+
return { type: 'object', properties, ...(required.length && { required }) };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const declaredForms = () => [...document.querySelectorAll('form[toolname]')];
|
|
148
|
+
|
|
149
|
+
// Invio di un form dichiarativo da parte di un agente: il risultato arriva
|
|
150
|
+
// con respondWith, che si avvolge sull'istanza prima che il gestore della
|
|
151
|
+
// pagina lo chiami (questo listener è in cattura su window).
|
|
152
|
+
window.addEventListener('submit', (e) => {
|
|
153
|
+
const form = e.target;
|
|
154
|
+
if (!e.agentInvoked || !form?.getAttribute?.('toolname')) return;
|
|
155
|
+
const name = form.getAttribute('toolname');
|
|
156
|
+
const input = {};
|
|
157
|
+
try { for (const [k, v] of new FormData(form)) input[k] = typeof v === 'string' ? v : `[file ${v.name}]`; } catch {}
|
|
158
|
+
const entry = takePending(name) ?? record({ at: Date.now(), tool: name, kind: 'declarative', caller: 'other', input: clip(input), status: 'submitted' });
|
|
159
|
+
const t0 = performance.now();
|
|
160
|
+
if (typeof e.respondWith === 'function') {
|
|
161
|
+
const orig = e.respondWith;
|
|
162
|
+
e.respondWith = function (p) {
|
|
163
|
+
Promise.resolve(p).then((out) => settle(entry, t0, out), (err) => settle(entry, t0, undefined, err));
|
|
164
|
+
return orig.call(this, p);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}, true);
|
|
168
|
+
window.addEventListener('toolcancel', (e) => {
|
|
169
|
+
const name = e.target?.getAttribute?.('toolname');
|
|
170
|
+
const last = [...calls].reverse().find((c) => c.tool === name && c.kind === 'declarative');
|
|
171
|
+
if (last && last.status === 'submitted') last.status = 'cancelled';
|
|
172
|
+
}, true);
|
|
173
|
+
|
|
174
|
+
// --- azioni sensibili ---
|
|
175
|
+
// Acquisti, pagamenti, invii, cancellazioni: partono solo con conferma.
|
|
176
|
+
// Un form dichiarativo è un invio di modulo, tranne il GET (ricerca, filtri),
|
|
177
|
+
// che per HTTP non cambia lo stato del server.
|
|
178
|
+
const SENSITIVE = /check.?out|purchase|buy|pay|order|book|reserv|subscri|send|submit|delete|remove|cancel|transfer|donat|sign.?up|register|acquist|pagament|pagare|ordin|prenot|invi|elimin|cancell|bonific|iscri/i;
|
|
179
|
+
const sensitivity = (t) => {
|
|
180
|
+
if (t.read_only === true) return null;
|
|
181
|
+
if (t.kind === 'declarative') {
|
|
182
|
+
return t.method === 'get' ? null : `declarative form (${t.method.toUpperCase()}): submitting it sends data to the site`;
|
|
183
|
+
}
|
|
184
|
+
const m = `${t.name} ${t.description ?? ''}`.match(SENSITIVE);
|
|
185
|
+
return m ? `"${m[0]}" in name or description: it may buy, send or delete` : null;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const schemaOf = (raw) => {
|
|
189
|
+
if (typeof raw !== 'string') return raw ?? null;
|
|
190
|
+
try { return JSON.parse(raw); } catch { return raw; }
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
async function list() {
|
|
194
|
+
const mc = mcOf();
|
|
195
|
+
patch(mc);
|
|
196
|
+
const forms = declaredForms();
|
|
197
|
+
const formByName = new Map(forms.map((f) => [f.getAttribute('toolname'), f]));
|
|
198
|
+
const out = [];
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
const add = (t) => {
|
|
201
|
+
if (seen.has(t.name)) return;
|
|
202
|
+
seen.add(t.name);
|
|
203
|
+
const n = calls.filter((c) => c.tool === t.name).length;
|
|
204
|
+
const tool = { ...t, ...(n && { calls: n }) };
|
|
205
|
+
const why = sensitivity(tool);
|
|
206
|
+
if (why) tool.sensitive = why;
|
|
207
|
+
delete tool.method;
|
|
208
|
+
out.push(tool);
|
|
209
|
+
};
|
|
210
|
+
const formTool = (f, extra = {}) => ({
|
|
211
|
+
name: f.getAttribute('toolname'),
|
|
212
|
+
description: f.getAttribute('tooldescription') ?? '',
|
|
213
|
+
input_schema: formSchema(f),
|
|
214
|
+
kind: 'declarative',
|
|
215
|
+
method: (f.getAttribute('method') || 'get').toLowerCase(),
|
|
216
|
+
autosubmit: f.hasAttribute('toolautosubmit'),
|
|
217
|
+
...extra,
|
|
218
|
+
});
|
|
219
|
+
if (mc && typeof mc.getTools === 'function') {
|
|
220
|
+
let tools = [];
|
|
221
|
+
try { tools = (await mc.getTools()) ?? []; } catch {}
|
|
222
|
+
for (const t of tools) {
|
|
223
|
+
const name = String(t.name);
|
|
224
|
+
const f = formByName.get(name);
|
|
225
|
+
if (f) {
|
|
226
|
+
// Lo schema del browser vince su quello letto dai campi, se dice qualcosa.
|
|
227
|
+
const given = schemaOf(t.inputSchema);
|
|
228
|
+
const useful = given && typeof given === 'object' && Object.keys(given.properties ?? {}).length;
|
|
229
|
+
add(formTool(f, { origin: t.origin ?? location.origin, ...(useful && { input_schema: given }) }));
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const ann = regs.get(name)?.annotations ?? t.annotations ?? null;
|
|
233
|
+
add({
|
|
234
|
+
name,
|
|
235
|
+
description: t.description ?? '',
|
|
236
|
+
input_schema: schemaOf(t.inputSchema),
|
|
237
|
+
kind: 'imperative',
|
|
238
|
+
origin: t.origin ?? location.origin,
|
|
239
|
+
...(ann?.readOnlyHint === true && { read_only: true }),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
} else if (mc) {
|
|
243
|
+
// Senza getTools: almeno quelli registrati dopo il gancio.
|
|
244
|
+
for (const [name, r] of regs) add({ name, kind: 'imperative', origin: location.origin, ...(r.annotations?.readOnlyHint === true && { read_only: true }) });
|
|
245
|
+
}
|
|
246
|
+
for (const f of forms) add(formTool(f));
|
|
247
|
+
return { active: Boolean(mc), tools: out, ...(toolchanges && { toolchange_events: toolchanges }) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function summary() {
|
|
251
|
+
const { active, tools } = await list();
|
|
252
|
+
return { active, tools: tools.map((t) => ({ name: t.name, kind: t.kind, ...(t.sensitive && { sensitive: true }) })) };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function call(name, input = {}, { timeout = 30000, confirmed = false } = {}) {
|
|
256
|
+
const mc = mcOf();
|
|
257
|
+
const { tools } = await list();
|
|
258
|
+
const meta = tools.find((t) => t.name === name);
|
|
259
|
+
if (!meta) {
|
|
260
|
+
return { status: 'not_found', error: `No WebMCP tool "${name}" on this page`, available: tools.map((t) => t.name) };
|
|
261
|
+
}
|
|
262
|
+
if (!mc) {
|
|
263
|
+
return { status: 'inactive', error: 'WebMCP is off in this Chrome (document.modelContext missing): the tool is only declared' };
|
|
264
|
+
}
|
|
265
|
+
if (meta.sensitive && !confirmed) {
|
|
266
|
+
return { status: 'needs_confirmation', tool: name, reason: meta.sensitive, input };
|
|
267
|
+
}
|
|
268
|
+
const entry = record({ at: Date.now(), tool: name, kind: meta.kind, caller: 'chrome-bridge', input: clip(input), status: 'running' });
|
|
269
|
+
const t0 = performance.now();
|
|
270
|
+
const ac = new AbortController();
|
|
271
|
+
let timer;
|
|
272
|
+
const timedOut = new Promise((_, rej) => { timer = setTimeout(() => { ac.abort(); rej(Object.assign(new Error(`timed out after ${timeout} ms`), { timeout: true })); }, timeout); });
|
|
273
|
+
open(entry);
|
|
274
|
+
try {
|
|
275
|
+
let run;
|
|
276
|
+
const exec = patched.get(mc);
|
|
277
|
+
if (exec && typeof mc.getTools === 'function') {
|
|
278
|
+
const tool = ((await mc.getTools()) ?? []).find((t) => String(t.name) === name);
|
|
279
|
+
if (!tool) throw new Error(`"${name}" is no longer registered`);
|
|
280
|
+
run = exec.call(mc, tool, input, { signal: ac.signal });
|
|
281
|
+
} else if (regs.has(name)) {
|
|
282
|
+
run = regs.get(name).execute(input);
|
|
283
|
+
} else {
|
|
284
|
+
throw new Error('this WebMCP version has no executeTool and the tool was registered before chrome-bridge could see it');
|
|
285
|
+
}
|
|
286
|
+
const out = await Promise.race([run, timedOut]);
|
|
287
|
+
settle(entry, t0, out);
|
|
288
|
+
return { status: 'ok', result: entry.output, ms: entry.ms };
|
|
289
|
+
} catch (err) {
|
|
290
|
+
settle(entry, t0, undefined, err);
|
|
291
|
+
return { status: entry.status, error: entry.error, ms: entry.ms };
|
|
292
|
+
} finally {
|
|
293
|
+
clearTimeout(timer);
|
|
294
|
+
// Uno strumento che non è passato dal wrapper (registrato prima del
|
|
295
|
+
// gancio) lascerebbe la voce in coda e ruberebbe la prossima chiamata.
|
|
296
|
+
close(entry);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
window.__cbWebmcp = {
|
|
301
|
+
list,
|
|
302
|
+
summary,
|
|
303
|
+
call,
|
|
304
|
+
calls: (limit = 20) => calls.slice(-Math.max(1, limit)),
|
|
305
|
+
};
|
|
306
|
+
})();
|