@browserbridge/bbx 1.7.3 → 1.7.4
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/package.json +7 -7
- package/packages/agent-client/src/cli.js +2 -2
- package/packages/agent-client/src/runtime.js +1 -1
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +55 -28
- package/packages/mcp-server/src/handlers-navigation.js +10 -0
- package/packages/mcp-server/src/handlers-page.js +89 -62
- package/packages/mcp-server/src/handlers-utils.js +11 -2
- package/packages/mcp-server/src/server.js +33 -12
- package/packages/native-host/src/daemon.js +3 -1
- package/packages/protocol/src/capabilities.js +1 -1
- package/packages/protocol/src/protocol.js +16 -4
- package/packages/protocol/src/registry.js +14 -14
- package/packages/protocol/src/summary.js +61 -30
- package/packages/protocol/src/types.ts +11 -0
- package/skills/browser-bridge/SKILL.md +3 -3
- package/skills/browser-bridge/references/interaction.md +1 -1
- package/skills/browser-bridge/references/protocol.md +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbridge/bbx",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -73,13 +73,13 @@
|
|
|
73
73
|
"zod": "^4.4.3"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@types/chrome": "^0.
|
|
77
|
-
"@types/node": "^
|
|
76
|
+
"@types/chrome": "^0.2.2",
|
|
77
|
+
"@types/node": "^26.1.1",
|
|
78
78
|
"c8": "^11.0.0",
|
|
79
|
-
"linkedom": "^0.18.
|
|
80
|
-
"oxfmt": "^0.
|
|
81
|
-
"oxlint": "^1.
|
|
82
|
-
"typescript": "^
|
|
79
|
+
"linkedom": "^0.18.13",
|
|
80
|
+
"oxfmt": "^0.58.0",
|
|
81
|
+
"oxlint": "^1.73.0",
|
|
82
|
+
"typescript": "^7.0.2"
|
|
83
83
|
},
|
|
84
84
|
"engines": {
|
|
85
85
|
"node": ">=18"
|
|
@@ -966,9 +966,9 @@ async function handleProxyCommand(args) {
|
|
|
966
966
|
const exampleHost = getProxyExampleHost(bindHost);
|
|
967
967
|
const tokenNote =
|
|
968
968
|
tokenSource === 'existing'
|
|
969
|
-
? ' (unchanged
|
|
969
|
+
? ' (unchanged - already-configured clients keep working; pass --rotate-token to generate a new secret)'
|
|
970
970
|
: tokenSource === 'generated' && existing
|
|
971
|
-
? ' (rotated
|
|
971
|
+
? ' (rotated - update every configured client with `bbx remote add`)'
|
|
972
972
|
: '';
|
|
973
973
|
const lines = [
|
|
974
974
|
`Browser Bridge proxy enabled on ${bindHost}:${port}.`,
|
|
@@ -350,7 +350,7 @@ export async function checkNativeHostManifestHealth(browserManifests) {
|
|
|
350
350
|
*/
|
|
351
351
|
export async function checkUnwritableBridgePaths() {
|
|
352
352
|
// The root-owned-files scenario comes from sudo installs, which do not exist
|
|
353
|
-
// on Windows
|
|
353
|
+
// on Windows - and fs.access(W_OK) there only reflects the read-only file
|
|
354
354
|
// attribute, so it would produce false positives.
|
|
355
355
|
if (os.platform() === 'win32') {
|
|
356
356
|
return [];
|
|
@@ -10,7 +10,7 @@ export const MCP_SERVER_INSTRUCTIONS = [
|
|
|
10
10
|
'In permission-ask hosts, use browser_call as the default Browser Bridge MCP tool so the user can approve one BBX tool instead of separate browser_status, browser_page, browser_dom, browser_input, and other tools.',
|
|
11
11
|
'Prefer Browser Bridge MCP tools over shelling out to bbx. Use bbx only for explicit CLI setup, doctor, logs, or raw debugging requests.',
|
|
12
12
|
'Start with browser_call method health.ping. If window access is disabled, call browser_call method access.request once, ask the user to click Enable in the Browser Bridge popup or side panel, then retry once.',
|
|
13
|
-
'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, styles.get_computed, or layout.get_box_model. Use browser_batch for parallel reads and keep limits tight before widening.',
|
|
13
|
+
'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, styles.get_computed, or layout.get_box_model. Use browser_batch only for parallel reads; run mutations sequentially and keep limits tight before widening.',
|
|
14
14
|
'Reuse elementRef values returned by DOM tools. Use attribute allowlists for focused DOM reads.',
|
|
15
15
|
'Escalate to screenshot.capture_element, screenshot.capture_region, dom.get_accessibility_tree, page.evaluate, viewport.resize, or CDP only when structured reads cannot answer the question.',
|
|
16
16
|
'Use patch.apply_styles or patch.apply_dom for temporary experiments, and rollback patches before finishing unless the user asks to keep them.',
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
REQUEST_SOURCE,
|
|
7
7
|
requestBridgeWithRetry,
|
|
8
8
|
resolveToolRef,
|
|
9
|
-
resolveRef,
|
|
10
9
|
summarizeToolError,
|
|
11
10
|
summarizeToolResponse,
|
|
12
11
|
withToolClient,
|
|
@@ -269,6 +268,7 @@ export const INPUT_ACTION_METHODS = {
|
|
|
269
268
|
click: 'input.click',
|
|
270
269
|
focus: 'input.focus',
|
|
271
270
|
type: 'input.type',
|
|
271
|
+
fill: 'input.fill',
|
|
272
272
|
press_key: 'input.press_key',
|
|
273
273
|
cdp_press_key: 'cdp.dispatch_key_event',
|
|
274
274
|
set_checked: 'input.set_checked',
|
|
@@ -279,13 +279,30 @@ export const INPUT_ACTION_METHODS = {
|
|
|
279
279
|
};
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
|
-
*
|
|
282
|
+
* Keep selector-based actions atomic instead of resolving a ref in a separate
|
|
283
|
+
* bridge request that can race with a page rerender.
|
|
284
|
+
*
|
|
285
|
+
* @param {unknown} elementRef
|
|
286
|
+
* @param {unknown} selector
|
|
287
|
+
* @returns {import('../../protocol/src/types.js').InputTarget | null}
|
|
288
|
+
*/
|
|
289
|
+
function createInputTarget(elementRef, selector) {
|
|
290
|
+
if (typeof elementRef === 'string' && elementRef) return { elementRef };
|
|
291
|
+
if (typeof selector === 'string' && selector) return { selector };
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, value?: string, mode?: 'auto' | 'setter' | 'keystrokes', clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
283
297
|
* @returns {Promise<ToolResult>}
|
|
284
298
|
*/
|
|
285
299
|
export async function handleInputTool(args) {
|
|
286
300
|
if (args.action === 'type' && !hasText(args.text)) {
|
|
287
301
|
return summarizeToolError('text is required for input.type.');
|
|
288
302
|
}
|
|
303
|
+
if (args.action === 'fill' && typeof args.value !== 'string') {
|
|
304
|
+
return summarizeToolError('value is required for input.fill.');
|
|
305
|
+
}
|
|
289
306
|
if ((args.action === 'press_key' || args.action === 'cdp_press_key') && !hasText(args.key)) {
|
|
290
307
|
return summarizeToolError('key is required for key input actions.');
|
|
291
308
|
}
|
|
@@ -301,9 +318,11 @@ export async function handleInputTool(args) {
|
|
|
301
318
|
return withToolClient(
|
|
302
319
|
async (client) => {
|
|
303
320
|
const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
|
|
304
|
-
const elementTarget =
|
|
305
|
-
|
|
306
|
-
|
|
321
|
+
const elementTarget = () => {
|
|
322
|
+
const target = createInputTarget(args.elementRef, args.selector);
|
|
323
|
+
if (!target) throw new Error('Provide either elementRef or selector.');
|
|
324
|
+
return target;
|
|
325
|
+
};
|
|
307
326
|
|
|
308
327
|
switch (args.action) {
|
|
309
328
|
case 'click': {
|
|
@@ -311,7 +330,7 @@ export async function handleInputTool(args) {
|
|
|
311
330
|
client,
|
|
312
331
|
'input.click',
|
|
313
332
|
{
|
|
314
|
-
target:
|
|
333
|
+
target: elementTarget(),
|
|
315
334
|
button: args.button,
|
|
316
335
|
clickCount: args.clickCount,
|
|
317
336
|
modifiers: args.modifiers,
|
|
@@ -329,7 +348,7 @@ export async function handleInputTool(args) {
|
|
|
329
348
|
client,
|
|
330
349
|
'input.focus',
|
|
331
350
|
{
|
|
332
|
-
target:
|
|
351
|
+
target: elementTarget(),
|
|
333
352
|
},
|
|
334
353
|
{
|
|
335
354
|
tabId: requestedTabId,
|
|
@@ -344,7 +363,7 @@ export async function handleInputTool(args) {
|
|
|
344
363
|
client,
|
|
345
364
|
'input.type',
|
|
346
365
|
{
|
|
347
|
-
target:
|
|
366
|
+
target: elementTarget(),
|
|
348
367
|
text: args.text,
|
|
349
368
|
clear: args.clear,
|
|
350
369
|
submit: args.submit,
|
|
@@ -358,8 +377,25 @@ export async function handleInputTool(args) {
|
|
|
358
377
|
);
|
|
359
378
|
return summarizeToolResponse(response, 'input.type');
|
|
360
379
|
}
|
|
380
|
+
case 'fill': {
|
|
381
|
+
const response = await requestBridgeWithRetry(
|
|
382
|
+
client,
|
|
383
|
+
'input.fill',
|
|
384
|
+
{
|
|
385
|
+
target: elementTarget(),
|
|
386
|
+
value: args.value,
|
|
387
|
+
mode: args.mode,
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
tabId: requestedTabId,
|
|
391
|
+
source: REQUEST_SOURCE,
|
|
392
|
+
tokenBudget: getToolTokenBudget(args),
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
return summarizeToolResponse(response, 'input.fill');
|
|
396
|
+
}
|
|
361
397
|
case 'press_key': {
|
|
362
|
-
const target = args.elementRef || args.selector ?
|
|
398
|
+
const target = args.elementRef || args.selector ? elementTarget() : undefined;
|
|
363
399
|
const response = await requestBridgeWithRetry(
|
|
364
400
|
client,
|
|
365
401
|
'input.press_key',
|
|
@@ -398,7 +434,7 @@ export async function handleInputTool(args) {
|
|
|
398
434
|
client,
|
|
399
435
|
'input.set_checked',
|
|
400
436
|
{
|
|
401
|
-
target:
|
|
437
|
+
target: elementTarget(),
|
|
402
438
|
checked: args.checked,
|
|
403
439
|
},
|
|
404
440
|
{
|
|
@@ -414,7 +450,7 @@ export async function handleInputTool(args) {
|
|
|
414
450
|
client,
|
|
415
451
|
'input.select_option',
|
|
416
452
|
{
|
|
417
|
-
target:
|
|
453
|
+
target: elementTarget(),
|
|
418
454
|
values: args.values,
|
|
419
455
|
labels: args.labels,
|
|
420
456
|
indexes: args.indexes,
|
|
@@ -432,7 +468,7 @@ export async function handleInputTool(args) {
|
|
|
432
468
|
client,
|
|
433
469
|
'input.hover',
|
|
434
470
|
{
|
|
435
|
-
target:
|
|
471
|
+
target: elementTarget(),
|
|
436
472
|
duration: args.duration,
|
|
437
473
|
modifiers: args.modifiers,
|
|
438
474
|
},
|
|
@@ -445,21 +481,12 @@ export async function handleInputTool(args) {
|
|
|
445
481
|
return summarizeToolResponse(response, 'input.hover');
|
|
446
482
|
}
|
|
447
483
|
case 'drag': {
|
|
448
|
-
const source =
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
};
|
|
455
|
-
const destination = {
|
|
456
|
-
elementRef:
|
|
457
|
-
args.destinationElementRef ||
|
|
458
|
-
(args.destinationSelector
|
|
459
|
-
? await resolveRef(client, args.destinationSelector, requestedTabId, REQUEST_SOURCE)
|
|
460
|
-
: ''),
|
|
461
|
-
};
|
|
462
|
-
if (!source.elementRef || !destination.elementRef) {
|
|
484
|
+
const source = createInputTarget(args.sourceElementRef, args.sourceSelector);
|
|
485
|
+
const destination = createInputTarget(
|
|
486
|
+
args.destinationElementRef,
|
|
487
|
+
args.destinationSelector
|
|
488
|
+
);
|
|
489
|
+
if (!source || !destination) {
|
|
463
490
|
return summarizeToolError(
|
|
464
491
|
'sourceElementRef/sourceSelector and destinationElementRef/destinationSelector are required for drag.'
|
|
465
492
|
);
|
|
@@ -486,7 +513,7 @@ export async function handleInputTool(args) {
|
|
|
486
513
|
client,
|
|
487
514
|
'input.scroll_into_view',
|
|
488
515
|
{
|
|
489
|
-
target:
|
|
516
|
+
target: elementTarget(),
|
|
490
517
|
},
|
|
491
518
|
{
|
|
492
519
|
tabId: requestedTabId,
|
|
@@ -96,6 +96,16 @@ export async function handleTabsTool(args) {
|
|
|
96
96
|
{ destinationId: args.destinationId ?? null }
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
|
+
if (args.action === 'activate') {
|
|
100
|
+
if (typeof args.tabId !== 'number') {
|
|
101
|
+
return summarizeToolError('tabId is required for tabs.activate.');
|
|
102
|
+
}
|
|
103
|
+
return callBridgeTool(
|
|
104
|
+
'tabs.activate',
|
|
105
|
+
{ tabId: args.tabId },
|
|
106
|
+
{ destinationId: args.destinationId ?? null }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
99
109
|
return summarizeToolError(`Unsupported tabs action "${args.action}".`);
|
|
100
110
|
}
|
|
101
111
|
|
|
@@ -30,6 +30,51 @@ import {
|
|
|
30
30
|
export const MAX_BATCH_CALLS = 20;
|
|
31
31
|
export const MAX_BATCH_CONCURRENCY = 5;
|
|
32
32
|
|
|
33
|
+
/** @type {ReadonlySet<BridgeMethod>} */
|
|
34
|
+
const BATCH_SAFE_METHODS = new Set([
|
|
35
|
+
'health.ping',
|
|
36
|
+
'daemon.metrics',
|
|
37
|
+
'tabs.list',
|
|
38
|
+
'skill.get_runtime_context',
|
|
39
|
+
'setup.get_status',
|
|
40
|
+
'log.tail',
|
|
41
|
+
'page.get_state',
|
|
42
|
+
'page.get_console',
|
|
43
|
+
'page.wait_for_load_state',
|
|
44
|
+
'page.get_storage',
|
|
45
|
+
'page.get_text',
|
|
46
|
+
'page.get_network',
|
|
47
|
+
'dom.query',
|
|
48
|
+
'dom.describe',
|
|
49
|
+
'dom.get_text',
|
|
50
|
+
'dom.get_attributes',
|
|
51
|
+
'dom.wait_for',
|
|
52
|
+
'dom.find_by_text',
|
|
53
|
+
'dom.find_by_role',
|
|
54
|
+
'dom.get_html',
|
|
55
|
+
'dom.get_accessibility_tree',
|
|
56
|
+
'layout.get_box_model',
|
|
57
|
+
'layout.hit_test',
|
|
58
|
+
'styles.get_computed',
|
|
59
|
+
'styles.get_matched_rules',
|
|
60
|
+
'network.intercept.list',
|
|
61
|
+
'patch.list',
|
|
62
|
+
'performance.get_metrics',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {BridgeMethod} method
|
|
67
|
+
* @param {Record<string, unknown>} params
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function isBatchSafeBridgeCall(method, params) {
|
|
71
|
+
if (!BATCH_SAFE_METHODS.has(method)) return false;
|
|
72
|
+
if ((method === 'page.get_console' || method === 'page.get_network') && params.clear === true) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
33
78
|
/** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
|
|
34
79
|
export const PAGE_ACTIONS = {
|
|
35
80
|
state: { method: 'page.get_state', params: () => ({}) },
|
|
@@ -118,54 +163,24 @@ export async function handleBatchTool(args) {
|
|
|
118
163
|
}
|
|
119
164
|
|
|
120
165
|
const calls = args.calls;
|
|
166
|
+
for (const [index, call] of calls.entries()) {
|
|
167
|
+
if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
|
|
168
|
+
return summarizeToolError(`calls[${index}] needs a method.`);
|
|
169
|
+
}
|
|
170
|
+
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
|
|
171
|
+
return summarizeToolError(`Unknown bridge method "${call.method}" at calls[${index}].`);
|
|
172
|
+
}
|
|
173
|
+
const method = /** @type {BridgeMethod} */ (call.method);
|
|
174
|
+
if (!isBatchSafeBridgeCall(method, call.params || {})) {
|
|
175
|
+
return summarizeToolError(
|
|
176
|
+
`${method} is not safe for parallel batch execution. Use its specialized tool or browser_call sequentially.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
121
181
|
/** @param {BridgeClient | null} client */
|
|
122
182
|
const executeBatch = async (client) => {
|
|
123
183
|
const results = await mapWithConcurrency(calls, MAX_BATCH_CONCURRENCY, async (call) => {
|
|
124
|
-
if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
|
|
125
|
-
return {
|
|
126
|
-
method: '',
|
|
127
|
-
tabId: null,
|
|
128
|
-
ok: false,
|
|
129
|
-
summary: 'INVALID_REQUEST: Each batch call needs a method.',
|
|
130
|
-
evidence: null,
|
|
131
|
-
error: {
|
|
132
|
-
code: 'INVALID_REQUEST',
|
|
133
|
-
message: 'Each batch call needs a method.',
|
|
134
|
-
},
|
|
135
|
-
response: null,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
|
|
140
|
-
return {
|
|
141
|
-
method: call.method,
|
|
142
|
-
tabId: null,
|
|
143
|
-
ok: false,
|
|
144
|
-
summary: `INVALID_REQUEST: Unknown bridge method "${call.method}".`,
|
|
145
|
-
evidence: null,
|
|
146
|
-
error: {
|
|
147
|
-
code: 'INVALID_REQUEST',
|
|
148
|
-
message: `Unknown bridge method "${call.method}".`,
|
|
149
|
-
},
|
|
150
|
-
response: null,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (call.method === 'setup.install') {
|
|
155
|
-
return {
|
|
156
|
-
method: call.method,
|
|
157
|
-
tabId: null,
|
|
158
|
-
ok: false,
|
|
159
|
-
summary: 'INVALID_REQUEST: Use browser_setup for setup changes.',
|
|
160
|
-
evidence: null,
|
|
161
|
-
error: {
|
|
162
|
-
code: 'INVALID_REQUEST',
|
|
163
|
-
message: 'Use browser_setup for setup changes.',
|
|
164
|
-
},
|
|
165
|
-
response: null,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
184
|
const method = /** @type {BridgeMethod} */ (call.method);
|
|
170
185
|
const tabId = bridgeMethodNeedsTab(method)
|
|
171
186
|
? typeof call.tabId === 'number'
|
|
@@ -180,12 +195,15 @@ export async function handleBatchTool(args) {
|
|
|
180
195
|
? await createBridgeClientForDestination(destinationId)
|
|
181
196
|
: client;
|
|
182
197
|
if (!callClient) {
|
|
183
|
-
return summarizeBatchErrorItem(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
return summarizeBatchErrorItem(
|
|
199
|
+
{
|
|
200
|
+
method,
|
|
201
|
+
tabId,
|
|
202
|
+
error: new Error('A local bridge client is required for calls without destinationId.'),
|
|
203
|
+
durationMs: Date.now() - startTime,
|
|
204
|
+
},
|
|
205
|
+
{ compact: true }
|
|
206
|
+
);
|
|
189
207
|
}
|
|
190
208
|
try {
|
|
191
209
|
if (destinationId) {
|
|
@@ -198,20 +216,26 @@ export async function handleBatchTool(args) {
|
|
|
198
216
|
});
|
|
199
217
|
return {
|
|
200
218
|
destinationId,
|
|
201
|
-
...summarizeBatchResponseItem(
|
|
219
|
+
...summarizeBatchResponseItem(
|
|
220
|
+
{
|
|
221
|
+
method,
|
|
222
|
+
tabId,
|
|
223
|
+
response,
|
|
224
|
+
durationMs: Date.now() - startTime,
|
|
225
|
+
},
|
|
226
|
+
{ compact: true }
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return summarizeBatchErrorItem(
|
|
231
|
+
{
|
|
202
232
|
method,
|
|
203
233
|
tabId,
|
|
204
|
-
|
|
234
|
+
error,
|
|
205
235
|
durationMs: Date.now() - startTime,
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return summarizeBatchErrorItem({
|
|
210
|
-
method,
|
|
211
|
-
tabId,
|
|
212
|
-
error,
|
|
213
|
-
durationMs: Date.now() - startTime,
|
|
214
|
-
});
|
|
236
|
+
},
|
|
237
|
+
{ compact: true }
|
|
238
|
+
);
|
|
215
239
|
} finally {
|
|
216
240
|
if (destinationId) {
|
|
217
241
|
await callClient.close();
|
|
@@ -228,6 +252,9 @@ export async function handleBatchTool(args) {
|
|
|
228
252
|
summary,
|
|
229
253
|
{
|
|
230
254
|
ok: failureCount === 0,
|
|
255
|
+
successCount: results.length - failureCount,
|
|
256
|
+
failureCount,
|
|
257
|
+
compactOutput: true,
|
|
231
258
|
results,
|
|
232
259
|
},
|
|
233
260
|
failureCount > 0
|
|
@@ -134,11 +134,17 @@ export function summarizeToolResponse(response, method) {
|
|
|
134
134
|
*/
|
|
135
135
|
export function summarizeToolError(error) {
|
|
136
136
|
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
const record =
|
|
138
|
+
error && typeof error === 'object' ? /** @type {Record<string, unknown>} */ (error) : {};
|
|
139
|
+
const code = typeof record.code === 'string' ? record.code : 'INTERNAL_ERROR';
|
|
140
|
+
const recovery = getErrorRecovery(code);
|
|
137
141
|
return createToolResult(
|
|
138
|
-
|
|
142
|
+
`${code}: ${message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
|
|
139
143
|
{
|
|
140
144
|
ok: false,
|
|
141
145
|
evidence: null,
|
|
146
|
+
error: { code, message },
|
|
147
|
+
recovery,
|
|
142
148
|
},
|
|
143
149
|
true
|
|
144
150
|
);
|
|
@@ -317,7 +323,10 @@ export function applyHtmlBudgetPreset(args) {
|
|
|
317
323
|
*/
|
|
318
324
|
export async function requestBridgeWithRetry(client, method, params, options) {
|
|
319
325
|
const response = await requestBridge(client, method, params, options);
|
|
320
|
-
const recovery =
|
|
326
|
+
const recovery =
|
|
327
|
+
!response.ok && response.error
|
|
328
|
+
? (response.error.recovery ?? getErrorRecovery(response.error.code))
|
|
329
|
+
: null;
|
|
321
330
|
if (!response.ok && recovery?.retry && isRetrySafeBridgeMethod(method, params)) {
|
|
322
331
|
const delay = recovery.retryAfterMs ?? 1000;
|
|
323
332
|
process.stderr.write(
|
|
@@ -175,14 +175,19 @@ export function createBridgeMcpServer() {
|
|
|
175
175
|
{
|
|
176
176
|
title: 'Browser Tabs',
|
|
177
177
|
description:
|
|
178
|
-
'List, create, or
|
|
178
|
+
'List, create, close, or activate browser tabs. Prefer "list" to work in existing tabs; only use "create" when the user explicitly requests a new page.',
|
|
179
179
|
inputSchema: {
|
|
180
180
|
action: z
|
|
181
|
-
.enum(['list', 'create', 'close'])
|
|
182
|
-
.describe('"list" (preferred), "create" (only when needed), or "
|
|
181
|
+
.enum(['list', 'create', 'close', 'activate'])
|
|
182
|
+
.describe('"list" (preferred), "create" (only when needed), "close", or "activate"'),
|
|
183
183
|
url: z.string().optional().describe('URL for create action'),
|
|
184
184
|
active: z.boolean().optional().describe('Focus the new tab (default: true)'),
|
|
185
|
-
tabId: z
|
|
185
|
+
tabId: z
|
|
186
|
+
.number()
|
|
187
|
+
.int()
|
|
188
|
+
.positive()
|
|
189
|
+
.optional()
|
|
190
|
+
.describe('Tab ID (required for close/activate)'),
|
|
186
191
|
destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
|
|
187
192
|
},
|
|
188
193
|
},
|
|
@@ -194,7 +199,7 @@ export function createBridgeMcpServer() {
|
|
|
194
199
|
{
|
|
195
200
|
title: 'Browser DOM',
|
|
196
201
|
description:
|
|
197
|
-
'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For full-page text, use browser_page action "text". accessibility_tree is debugger-backed
|
|
202
|
+
'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For full-page text, use browser_page action "text". accessibility_tree is debugger-backed - use query/find first.',
|
|
198
203
|
inputSchema: {
|
|
199
204
|
action: z
|
|
200
205
|
.enum([
|
|
@@ -270,7 +275,9 @@ export function createBridgeMcpServer() {
|
|
|
270
275
|
state: z
|
|
271
276
|
.enum(['attached', 'detached', 'visible', 'hidden'])
|
|
272
277
|
.optional()
|
|
273
|
-
.describe(
|
|
278
|
+
.describe(
|
|
279
|
+
'Expected element state (for wait action); hidden also matches complete absence'
|
|
280
|
+
),
|
|
274
281
|
timeoutMs: z
|
|
275
282
|
.number()
|
|
276
283
|
.int()
|
|
@@ -334,7 +341,7 @@ export function createBridgeMcpServer() {
|
|
|
334
341
|
{
|
|
335
342
|
title: 'Browser Page State',
|
|
336
343
|
description:
|
|
337
|
-
'Read page-level data: state (URL/title), evaluate (JS), console, storage, text, network, or performance. For element-level reads, use browser_dom. evaluate and performance are debugger-backed
|
|
344
|
+
'Read page-level data: state (URL/title), evaluate (JS), console, storage, text, network, or performance. For element-level reads, use browser_dom. evaluate and performance are debugger-backed - prefer lighter reads first.',
|
|
338
345
|
inputSchema: {
|
|
339
346
|
action: z
|
|
340
347
|
.enum([
|
|
@@ -370,9 +377,11 @@ export function createBridgeMcpServer() {
|
|
|
370
377
|
.optional()
|
|
371
378
|
.describe('Return actual value vs JSON (default: true)'),
|
|
372
379
|
level: z
|
|
373
|
-
.
|
|
380
|
+
.enum(['all', 'debug', 'log', 'info', 'warn', 'error', 'exception', 'rejection'])
|
|
374
381
|
.optional()
|
|
375
|
-
.describe(
|
|
382
|
+
.describe(
|
|
383
|
+
'Minimum console severity (default: all); exception/rejection select that exact stream'
|
|
384
|
+
),
|
|
376
385
|
clear: z.boolean().optional().describe('Clear buffer after reading (default: false)'),
|
|
377
386
|
limit: z
|
|
378
387
|
.number()
|
|
@@ -405,7 +414,7 @@ export function createBridgeMcpServer() {
|
|
|
405
414
|
{
|
|
406
415
|
title: 'Browser Navigation',
|
|
407
416
|
description:
|
|
408
|
-
'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed
|
|
417
|
+
'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed - use only for exact viewport overrides.',
|
|
409
418
|
inputSchema: {
|
|
410
419
|
action: z
|
|
411
420
|
.enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
|
|
@@ -456,6 +465,7 @@ export function createBridgeMcpServer() {
|
|
|
456
465
|
'click',
|
|
457
466
|
'focus',
|
|
458
467
|
'type',
|
|
468
|
+
'fill',
|
|
459
469
|
'press_key',
|
|
460
470
|
'cdp_press_key',
|
|
461
471
|
'set_checked',
|
|
@@ -488,6 +498,11 @@ export function createBridgeMcpServer() {
|
|
|
488
498
|
.optional()
|
|
489
499
|
.describe('Click count (1=single, 2=double)'),
|
|
490
500
|
text: z.string().max(100000).optional().describe('Text to type (for type action)'),
|
|
501
|
+
value: z.string().max(100000).optional().describe('Value to set (for fill action)'),
|
|
502
|
+
mode: z
|
|
503
|
+
.enum(['auto', 'setter', 'keystrokes'])
|
|
504
|
+
.optional()
|
|
505
|
+
.describe('Fill strategy (default: auto)'),
|
|
491
506
|
clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
|
|
492
507
|
submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
|
|
493
508
|
key: z
|
|
@@ -594,7 +609,7 @@ export function createBridgeMcpServer() {
|
|
|
594
609
|
{
|
|
595
610
|
title: 'Browser Capture',
|
|
596
611
|
description:
|
|
597
|
-
'Capture screenshots or CDP snapshots. Debugger-backed and token-expensive
|
|
612
|
+
'Capture screenshots or CDP snapshots. Debugger-backed and token-expensive - use only when structured reads (browser_dom, browser_styles_layout) are insufficient. Prefer element, then tight region; full_page only for document-level context.',
|
|
598
613
|
inputSchema: {
|
|
599
614
|
action: z
|
|
600
615
|
.enum([
|
|
@@ -645,7 +660,13 @@ export function createBridgeMcpServer() {
|
|
|
645
660
|
{
|
|
646
661
|
title: 'Browser Bridge Batch',
|
|
647
662
|
description:
|
|
648
|
-
'Execute
|
|
663
|
+
'Execute independent read-only bridge calls in parallel to reduce round-trips. Mutations and destructive reads are rejected; run them sequentially with specialized tools or browser_call. Preserves call order in the response.',
|
|
664
|
+
annotations: {
|
|
665
|
+
readOnlyHint: true,
|
|
666
|
+
destructiveHint: false,
|
|
667
|
+
idempotentHint: true,
|
|
668
|
+
openWorldHint: true,
|
|
669
|
+
},
|
|
649
670
|
inputSchema: {
|
|
650
671
|
calls: z
|
|
651
672
|
.array(
|
|
@@ -602,7 +602,9 @@ export class BridgeDaemon {
|
|
|
602
602
|
|
|
603
603
|
if (message?.type === 'agent.request') {
|
|
604
604
|
const isExtensionSetupRequest =
|
|
605
|
-
socket.__role === 'extension' &&
|
|
605
|
+
socket.__role === 'extension' &&
|
|
606
|
+
(message.request?.method === 'setup.get_status' ||
|
|
607
|
+
message.request?.method === 'setup.install');
|
|
606
608
|
if (socket.__role !== 'agent' && !isExtensionSetupRequest) {
|
|
607
609
|
return this.rejectMessageForRole(socket, message);
|
|
608
610
|
}
|
|
@@ -64,7 +64,7 @@ export const METHOD_CAPABILITIES = Object.freeze({
|
|
|
64
64
|
'page.get_storage': CAPABILITIES.PAGE_READ,
|
|
65
65
|
'page.get_text': CAPABILITIES.PAGE_READ,
|
|
66
66
|
'page.get_network': CAPABILITIES.NETWORK_READ,
|
|
67
|
-
// Interception mutates traffic (block/forge responses)
|
|
67
|
+
// Interception mutates traffic (block/forge responses) - distinct from
|
|
68
68
|
// passive network reads so hosts can gate it independently.
|
|
69
69
|
'network.intercept.add': CAPABILITIES.NETWORK_INTERCEPT,
|
|
70
70
|
'network.intercept.remove': CAPABILITIES.NETWORK_INTERCEPT,
|
|
@@ -361,6 +361,7 @@ function normalizeRequestParams(method, params) {
|
|
|
361
361
|
case 'input.click':
|
|
362
362
|
case 'input.focus':
|
|
363
363
|
case 'input.type':
|
|
364
|
+
case 'input.fill':
|
|
364
365
|
case 'input.press_key':
|
|
365
366
|
return normalizeInputAction(params);
|
|
366
367
|
case 'cdp.dispatch_key_event':
|
|
@@ -412,7 +413,7 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
|
|
|
412
413
|
case 'page.evaluate':
|
|
413
414
|
return normalizeEvaluateParams(params).timeoutMs;
|
|
414
415
|
case 'dom.wait_for':
|
|
415
|
-
return
|
|
416
|
+
return clampInt(params.timeoutMs, 100, 30_000, 5_000);
|
|
416
417
|
default:
|
|
417
418
|
return null;
|
|
418
419
|
}
|
|
@@ -423,11 +424,13 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
|
|
|
423
424
|
* @returns {NormalizedDomQuery}
|
|
424
425
|
*/
|
|
425
426
|
export function normalizeDomQuery(params = {}) {
|
|
427
|
+
const nestedBudget =
|
|
428
|
+
params.budget && typeof params.budget === 'object' ? params.budget : undefined;
|
|
426
429
|
return {
|
|
427
430
|
selector:
|
|
428
431
|
typeof params.selector === 'string' && params.selector.trim() ? params.selector : 'body',
|
|
429
432
|
withinRef: typeof params.withinRef === 'string' ? params.withinRef : null,
|
|
430
|
-
budget: applyBudget(params),
|
|
433
|
+
budget: applyBudget({ ...nestedBudget, ...params }),
|
|
431
434
|
};
|
|
432
435
|
}
|
|
433
436
|
|
|
@@ -706,9 +709,18 @@ export function normalizeConsoleParams(params = {}) {
|
|
|
706
709
|
*/
|
|
707
710
|
export function normalizeWaitForParams(params = {}) {
|
|
708
711
|
const validStates = ['attached', 'detached', 'visible', 'hidden'];
|
|
712
|
+
const selector =
|
|
713
|
+
typeof params.selector === 'string' && params.selector.trim() ? params.selector : '';
|
|
714
|
+
const text = typeof params.text === 'string' && params.text.trim() ? params.text : null;
|
|
715
|
+
if (!selector && text === null) {
|
|
716
|
+
throw new BridgeError(
|
|
717
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
718
|
+
'selector or text is required for dom.wait_for.'
|
|
719
|
+
);
|
|
720
|
+
}
|
|
709
721
|
return {
|
|
710
|
-
selector:
|
|
711
|
-
text
|
|
722
|
+
selector: selector || '*',
|
|
723
|
+
text,
|
|
712
724
|
state: validStates.includes(String(params.state ?? ''))
|
|
713
725
|
? /** @type {'attached' | 'detached' | 'visible' | 'hidden'} */ (String(params.state))
|
|
714
726
|
: 'attached',
|
|
@@ -112,7 +112,7 @@ function createRegistryEntry(method, group, tab, params, complexity = 'low') {
|
|
|
112
112
|
|
|
113
113
|
/** @type {Readonly<Record<import('./types.js').BridgeMethod, BridgeMethodRegistryEntry>>} */
|
|
114
114
|
export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
115
|
-
// system
|
|
115
|
+
// system - trivial
|
|
116
116
|
'access.request': createRegistryEntry('access.request', 'system', false, [], 'trivial'),
|
|
117
117
|
'skill.get_runtime_context': createRegistryEntry(
|
|
118
118
|
'skill.get_runtime_context',
|
|
@@ -132,12 +132,12 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
132
132
|
'log.tail': createRegistryEntry('log.tail', 'system', false, ['limit'], 'trivial'),
|
|
133
133
|
'health.ping': createRegistryEntry('health.ping', 'system', false, [], 'trivial'),
|
|
134
134
|
'daemon.metrics': createRegistryEntry('daemon.metrics', 'system', false, [], 'trivial'),
|
|
135
|
-
// tabs
|
|
135
|
+
// tabs - trivial
|
|
136
136
|
'tabs.list': createRegistryEntry('tabs.list', 'tabs', false, [], 'trivial'),
|
|
137
137
|
'tabs.create': createRegistryEntry('tabs.create', 'tabs', false, ['url', 'active'], 'trivial'),
|
|
138
138
|
'tabs.close': createRegistryEntry('tabs.close', 'tabs', false, ['tabId'], 'trivial'),
|
|
139
139
|
'tabs.activate': createRegistryEntry('tabs.activate', 'tabs', false, ['tabId'], 'trivial'),
|
|
140
|
-
// page
|
|
140
|
+
// page - low (basic reads), moderate (evaluate, debugger-backed)
|
|
141
141
|
'page.get_state': createRegistryEntry('page.get_state', 'page', true, [], 'low'),
|
|
142
142
|
'page.evaluate': {
|
|
143
143
|
...createRegistryEntry(
|
|
@@ -177,7 +177,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
177
177
|
['clear', 'limit', 'urlPattern'],
|
|
178
178
|
'low'
|
|
179
179
|
),
|
|
180
|
-
// network intercept
|
|
180
|
+
// network intercept - moderate (holds debugger session)
|
|
181
181
|
'network.intercept.add': createRegistryEntry(
|
|
182
182
|
'network.intercept.add',
|
|
183
183
|
'page',
|
|
@@ -206,7 +206,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
206
206
|
[],
|
|
207
207
|
'low'
|
|
208
208
|
),
|
|
209
|
-
// navigation
|
|
209
|
+
// navigation - low
|
|
210
210
|
'navigation.navigate': createRegistryEntry(
|
|
211
211
|
'navigation.navigate',
|
|
212
212
|
'navigate',
|
|
@@ -235,7 +235,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
235
235
|
['waitForLoad', 'timeoutMs'],
|
|
236
236
|
'low'
|
|
237
237
|
),
|
|
238
|
-
// dom
|
|
238
|
+
// dom - low (reads), moderate (accessibility tree)
|
|
239
239
|
'dom.query': {
|
|
240
240
|
...createRegistryEntry(
|
|
241
241
|
'dom.query',
|
|
@@ -303,7 +303,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
303
303
|
['maxNodes', 'maxDepth'],
|
|
304
304
|
'moderate'
|
|
305
305
|
),
|
|
306
|
-
// layout
|
|
306
|
+
// layout - low
|
|
307
307
|
'layout.get_box_model': createRegistryEntry(
|
|
308
308
|
'layout.get_box_model',
|
|
309
309
|
'inspect',
|
|
@@ -312,7 +312,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
312
312
|
'low'
|
|
313
313
|
),
|
|
314
314
|
'layout.hit_test': createRegistryEntry('layout.hit_test', 'inspect', true, ['x', 'y'], 'low'),
|
|
315
|
-
// styles
|
|
315
|
+
// styles - low (computed), moderate (matched rules)
|
|
316
316
|
'styles.get_computed': createRegistryEntry(
|
|
317
317
|
'styles.get_computed',
|
|
318
318
|
'inspect',
|
|
@@ -327,7 +327,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
327
327
|
['elementRef'],
|
|
328
328
|
'moderate'
|
|
329
329
|
),
|
|
330
|
-
// viewport
|
|
330
|
+
// viewport - low (scroll), moderate (resize, debugger-backed)
|
|
331
331
|
'viewport.scroll': createRegistryEntry(
|
|
332
332
|
'viewport.scroll',
|
|
333
333
|
'navigate',
|
|
@@ -342,7 +342,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
342
342
|
['width', 'height', 'deviceScaleFactor', 'reset'],
|
|
343
343
|
'moderate'
|
|
344
344
|
),
|
|
345
|
-
// input
|
|
345
|
+
// input - low (simple), moderate (drag)
|
|
346
346
|
'input.click': createRegistryEntry(
|
|
347
347
|
'input.click',
|
|
348
348
|
'interact',
|
|
@@ -407,7 +407,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
407
407
|
['target'],
|
|
408
408
|
'low'
|
|
409
409
|
),
|
|
410
|
-
// capture
|
|
410
|
+
// capture - high (screenshots, CDP)
|
|
411
411
|
'screenshot.capture_region': createRegistryEntry(
|
|
412
412
|
'screenshot.capture_region',
|
|
413
413
|
'capture',
|
|
@@ -429,7 +429,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
429
429
|
[],
|
|
430
430
|
'high'
|
|
431
431
|
),
|
|
432
|
-
// patch
|
|
432
|
+
// patch - moderate (side effects)
|
|
433
433
|
'patch.apply_styles': createRegistryEntry(
|
|
434
434
|
'patch.apply_styles',
|
|
435
435
|
'patch',
|
|
@@ -453,7 +453,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
453
453
|
[],
|
|
454
454
|
'low'
|
|
455
455
|
),
|
|
456
|
-
// cdp
|
|
456
|
+
// cdp - high (raw protocol, large payloads)
|
|
457
457
|
'cdp.get_document': createRegistryEntry('cdp.get_document', 'cdp', true, [], 'high'),
|
|
458
458
|
'cdp.get_dom_snapshot': createRegistryEntry('cdp.get_dom_snapshot', 'cdp', true, [], 'high'),
|
|
459
459
|
'cdp.get_box_model': createRegistryEntry('cdp.get_box_model', 'cdp', true, ['nodeId'], 'high'),
|
|
@@ -471,7 +471,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
471
471
|
['key', 'code', 'modifiers'],
|
|
472
472
|
'moderate'
|
|
473
473
|
),
|
|
474
|
-
// performance
|
|
474
|
+
// performance - moderate (debugger-backed)
|
|
475
475
|
'performance.get_metrics': createRegistryEntry(
|
|
476
476
|
'performance.get_metrics',
|
|
477
477
|
'performance',
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
estimateSerializedPayloadCost,
|
|
5
|
+
getErrorRecovery,
|
|
5
6
|
getCostClass,
|
|
6
7
|
getProtocolVersion,
|
|
7
8
|
serializeJsonPayload,
|
|
@@ -26,7 +27,8 @@ import {
|
|
|
26
27
|
* @typedef {{
|
|
27
28
|
* ok: boolean,
|
|
28
29
|
* summary: string,
|
|
29
|
-
* evidence: unknown
|
|
30
|
+
* evidence: unknown,
|
|
31
|
+
* recovery?: import('./types.js').BridgeRecovery | null
|
|
30
32
|
* }} BridgeSummary
|
|
31
33
|
*/
|
|
32
34
|
|
|
@@ -49,7 +51,7 @@ import {
|
|
|
49
51
|
* approxTokens: number,
|
|
50
52
|
* meta?: Record<string, unknown>,
|
|
51
53
|
* error: unknown,
|
|
52
|
-
* response
|
|
54
|
+
* response?: unknown
|
|
53
55
|
* }} BatchItemSummary
|
|
54
56
|
*/
|
|
55
57
|
|
|
@@ -164,14 +166,15 @@ function getMetaCostClass(meta, field) {
|
|
|
164
166
|
export function summarizeBridgeResponse(response, method) {
|
|
165
167
|
const protocolWarning = getProtocolWarning(response.meta);
|
|
166
168
|
if (!response.ok) {
|
|
167
|
-
const
|
|
169
|
+
const recovery = response.error.recovery ?? getErrorRecovery(response.error.code);
|
|
168
170
|
return {
|
|
169
171
|
ok: false,
|
|
170
172
|
summary: appendProtocolWarning(
|
|
171
|
-
`${response.error.code}: ${response.error.message}${hint ? ` ${hint}` : ''}`,
|
|
173
|
+
`${response.error.code}: ${response.error.message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
|
|
172
174
|
protocolWarning
|
|
173
175
|
),
|
|
174
176
|
evidence: response.error.details ?? null,
|
|
177
|
+
recovery,
|
|
175
178
|
};
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -348,16 +351,38 @@ export function summarizeBridgeResponse(response, method) {
|
|
|
348
351
|
}
|
|
349
352
|
return entry;
|
|
350
353
|
});
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
const isFind = method === 'dom.find_by_text' || method === 'dom.find_by_role';
|
|
355
|
+
if (isFind) {
|
|
356
|
+
const count = typeof result.count === 'number' ? result.count : nodes.length;
|
|
357
|
+
const scanned = typeof result.scanned === 'number' ? result.scanned : null;
|
|
358
|
+
const truncated = result.truncated === true;
|
|
359
|
+
let summary;
|
|
360
|
+
if (count === 0) {
|
|
361
|
+
summary = truncated
|
|
362
|
+
? `No matching elements returned; search stopped${scanned === null ? '' : ` after scanning ${scanned}`}.`
|
|
363
|
+
: 'No matching elements found.';
|
|
364
|
+
} else if (truncated) {
|
|
365
|
+
summary = `Found ${count} matching element(s); additional matches were omitted.`;
|
|
366
|
+
} else {
|
|
367
|
+
summary = `Found ${count} matching element(s)${scanned === null ? '' : ` after scanning ${scanned}`}.`;
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
summary: appendProtocolWarning(summary, protocolWarning),
|
|
372
|
+
evidence: {
|
|
373
|
+
found: count > 0,
|
|
374
|
+
count,
|
|
375
|
+
scanned,
|
|
376
|
+
truncated,
|
|
377
|
+
truncationReason: result.truncationReason ?? null,
|
|
378
|
+
nodes: compact,
|
|
379
|
+
},
|
|
380
|
+
};
|
|
356
381
|
}
|
|
357
382
|
return {
|
|
358
383
|
ok: true,
|
|
359
384
|
summary: appendProtocolWarning(
|
|
360
|
-
|
|
385
|
+
`DOM query returned ${nodes.length} element(s)${nodes.length > 15 ? '; showing first 15' : ''}.`,
|
|
361
386
|
protocolWarning
|
|
362
387
|
),
|
|
363
388
|
evidence: compact,
|
|
@@ -666,9 +691,10 @@ export function summarizeBridgeResponse(response, method) {
|
|
|
666
691
|
|
|
667
692
|
/**
|
|
668
693
|
* @param {{ method: SummaryMethod, tabId: number | null, response: BridgeResponse, durationMs: number }} input
|
|
694
|
+
* @param {{ compact?: boolean }} [options]
|
|
669
695
|
* @returns {BatchItemSummary}
|
|
670
696
|
*/
|
|
671
|
-
export function summarizeBatchResponseItem({ method, tabId, response, durationMs }) {
|
|
697
|
+
export function summarizeBatchResponseItem({ method, tabId, response, durationMs }, options = {}) {
|
|
672
698
|
const summary = annotateBridgeSummary(summarizeBridgeResponse(response, method), response);
|
|
673
699
|
return {
|
|
674
700
|
method,
|
|
@@ -676,17 +702,18 @@ export function summarizeBatchResponseItem({ method, tabId, response, durationMs
|
|
|
676
702
|
...summary,
|
|
677
703
|
durationMs,
|
|
678
704
|
approxTokens: summary.transportTokens,
|
|
679
|
-
meta: response.meta,
|
|
705
|
+
meta: options.compact ? compactBatchMeta(response.meta) : response.meta,
|
|
680
706
|
error: response.ok ? null : response.error,
|
|
681
|
-
response: response.ok ? response.result : null,
|
|
707
|
+
...(options.compact ? {} : { response: response.ok ? response.result : null }),
|
|
682
708
|
};
|
|
683
709
|
}
|
|
684
710
|
|
|
685
711
|
/**
|
|
686
712
|
* @param {{ method: SummaryMethod, tabId: number | null, error: unknown, durationMs: number }} input
|
|
713
|
+
* @param {{ compact?: boolean }} [options]
|
|
687
714
|
* @returns {BatchItemSummary}
|
|
688
715
|
*/
|
|
689
|
-
export function summarizeBatchErrorItem({ method, tabId, error, durationMs }) {
|
|
716
|
+
export function summarizeBatchErrorItem({ method, tabId, error, durationMs }, options = {}) {
|
|
690
717
|
const message = error instanceof Error ? error.message : String(error);
|
|
691
718
|
const response = /** @type {BridgeResponse} */ ({
|
|
692
719
|
id: 'batch_error',
|
|
@@ -706,12 +733,30 @@ export function summarizeBatchErrorItem({ method, tabId, error, durationMs }) {
|
|
|
706
733
|
...summary,
|
|
707
734
|
durationMs,
|
|
708
735
|
approxTokens: 0,
|
|
709
|
-
meta: response.meta,
|
|
736
|
+
meta: options.compact ? compactBatchMeta(response.meta) : response.meta,
|
|
710
737
|
error: response.error,
|
|
711
|
-
response: null,
|
|
738
|
+
...(options.compact ? {} : { response: null }),
|
|
712
739
|
};
|
|
713
740
|
}
|
|
714
741
|
|
|
742
|
+
/**
|
|
743
|
+
* @param {Record<string, unknown>} meta
|
|
744
|
+
* @returns {Record<string, unknown>}
|
|
745
|
+
*/
|
|
746
|
+
function compactBatchMeta(meta) {
|
|
747
|
+
/** @type {Record<string, unknown>} */
|
|
748
|
+
const compact = {};
|
|
749
|
+
for (const key of [
|
|
750
|
+
'protocol_version',
|
|
751
|
+
'protocol_warning',
|
|
752
|
+
'budget_truncated',
|
|
753
|
+
'continuation_hint',
|
|
754
|
+
]) {
|
|
755
|
+
if (meta[key] !== undefined && meta[key] !== null) compact[key] = meta[key];
|
|
756
|
+
}
|
|
757
|
+
return compact;
|
|
758
|
+
}
|
|
759
|
+
|
|
715
760
|
/**
|
|
716
761
|
* @param {unknown} value
|
|
717
762
|
* @returns {Record<string, unknown>}
|
|
@@ -758,20 +803,6 @@ function truncateUrl(url) {
|
|
|
758
803
|
}
|
|
759
804
|
}
|
|
760
805
|
|
|
761
|
-
/**
|
|
762
|
-
* @param {string} code
|
|
763
|
-
* @returns {string}
|
|
764
|
-
*/
|
|
765
|
-
function summarizeErrorHint(code) {
|
|
766
|
-
if (code === 'ELEMENT_STALE') {
|
|
767
|
-
return 'Re-query the current page after navigation or DOM updates.';
|
|
768
|
-
}
|
|
769
|
-
if (code === 'RESULT_TRUNCATED') {
|
|
770
|
-
return 'Narrow the query or raise the relevant budget if more detail is required.';
|
|
771
|
-
}
|
|
772
|
-
return '';
|
|
773
|
-
}
|
|
774
|
-
|
|
775
806
|
/**
|
|
776
807
|
* Check if an array looks like a list of patch entries.
|
|
777
808
|
* Returns true for empty arrays when method context suggests patch.list.
|
|
@@ -140,6 +140,7 @@ export interface BridgeRecovery {
|
|
|
140
140
|
hint: string;
|
|
141
141
|
retry?: boolean;
|
|
142
142
|
retryAfterMs?: number;
|
|
143
|
+
alternativeMethod?: string;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
export interface BridgeFailure {
|
|
@@ -192,6 +193,7 @@ export interface TruncateResult {
|
|
|
192
193
|
export interface DomQueryParams extends BudgetOptions {
|
|
193
194
|
selector?: string;
|
|
194
195
|
withinRef?: string | null;
|
|
196
|
+
budget?: BudgetOptions;
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
export interface NormalizedDomQuery extends BridgeParams {
|
|
@@ -200,6 +202,15 @@ export interface NormalizedDomQuery extends BridgeParams {
|
|
|
200
202
|
budget: Budget;
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
export interface DomFindResult {
|
|
206
|
+
found: boolean;
|
|
207
|
+
nodes: Record<string, unknown>[];
|
|
208
|
+
count: number;
|
|
209
|
+
scanned: number;
|
|
210
|
+
truncated: boolean;
|
|
211
|
+
truncationReason: 'maxResults' | 'scanLimit' | null;
|
|
212
|
+
}
|
|
213
|
+
|
|
203
214
|
export interface StyleQueryParams {
|
|
204
215
|
elementRef?: string;
|
|
205
216
|
target?: InputTarget;
|
|
@@ -5,7 +5,7 @@ description: 'Token-efficient Chrome tab inspection, interaction, and patching v
|
|
|
5
5
|
|
|
6
6
|
# Browser Bridge
|
|
7
7
|
|
|
8
|
-
Token-efficient Chrome tab inspection, interaction, and CSS/DOM patching through a local native-messaging bridge. Reads structured DOM, styles, layout, console, storage, network, and applies reversible patches
|
|
8
|
+
Token-efficient Chrome tab inspection, interaction, and CSS/DOM patching through a local native-messaging bridge. Reads structured DOM, styles, layout, console, storage, network, and applies reversible patches - all from the real tab the user already has open.
|
|
9
9
|
|
|
10
10
|
This CLI skill is for agents that can run shell commands and where direct `bbx` control fits better than MCP tools: manual debugging, terminal reproduction, install/doctor flows, raw protocol access, or environments without MCP.
|
|
11
11
|
|
|
@@ -235,7 +235,7 @@ bbx page-text 2000 # extract page content
|
|
|
235
235
|
| Inspect | `dom.query`, `dom.describe`, `dom.get_html`, `styles.get_computed`, `layout.get_box_model` |
|
|
236
236
|
| Find | `dom.find_by_text`, `dom.find_by_role`, `dom.wait_for`, `dom.get_accessibility_tree` |
|
|
237
237
|
| Page State | `page.get_console`, `page.get_storage`, `page.get_text`, `page.wait_for_load_state`, `page.evaluate` (debugger-backed) |
|
|
238
|
-
| Network | `page.get_network`, `network.intercept.add/remove/list/clear` (debugger-backed; rules are in-memory and drop if the debugger detaches
|
|
238
|
+
| Network | `page.get_network`, `network.intercept.add/remove/list/clear` (debugger-backed; rules are in-memory and drop if the debugger detaches - verify with `list`) |
|
|
239
239
|
| Interact | `input.click`, `input.type`, `input.fill`, `input.focus`, `input.press_key`, `cdp.dispatch_key_event`, `input.hover`, `input.drag`, `input.scroll_into_view` |
|
|
240
240
|
| Tabs | `tabs.list` (preferred), `tabs.create` (avoid unless necessary), `tabs.close`, `tabs.activate` |
|
|
241
241
|
| Patch | `patch.apply_styles`, `patch.apply_dom`, `patch.rollback` |
|
|
@@ -291,7 +291,7 @@ dom.find_by_role('button', 'Login') → input.click
|
|
|
291
291
|
|
|
292
292
|
`bbx a11y-tree` and `dom.get_accessibility_tree` are sensitive to `maxDepth` and `maxNodes`. Shallow runs can undercount interactive nodes on real pages, so widen those limits before treating a low interactive count as a bug.
|
|
293
293
|
|
|
294
|
-
> **MCP mode:** If Browser Bridge is connected via MCP (tools named `browser_dom`, `browser_capture`, etc.), use the MCP tools directly
|
|
294
|
+
> **MCP mode:** If Browser Bridge is connected via MCP (tools named `browser_dom`, `browser_capture`, etc.), use the MCP tools directly - do not shell out to `bbx`. The MCP tools map 1:1 to CLI capabilities. In prompts, `BB MCP` and `Browser Bridge MCP` both work. Do not treat `bbx-mcp` as a skill alias.
|
|
295
295
|
>
|
|
296
296
|
> For open-ended MCP inspection tasks, prefer `browser_investigate` first. It is read-only, designed for cheaper delegated investigation, and falls back to a deterministic sequence when the client cannot delegate. Escalate to `browser_capture` only when the structured investigation is not enough.
|
|
297
297
|
|
|
@@ -195,7 +195,7 @@ bbx call input.select_option '{"target":{"elementRef":"el_456"},"values":["us"]}
|
|
|
195
195
|
|
|
196
196
|
Select by value, label, or index. Multiple values for multi-select.
|
|
197
197
|
|
|
198
|
-
**Text fields
|
|
198
|
+
**Text fields - `fill` vs `type`:**
|
|
199
199
|
|
|
200
200
|
```bash
|
|
201
201
|
bbx fill el_123 hello@example.com # set value instantly (preferred for forms)
|
|
@@ -121,7 +121,7 @@ bbx access-request
|
|
|
121
121
|
bbx call access.request
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
If a tab-bound call returns `ACCESS_DENIED`, it also surfaces the Enable prompt automatically
|
|
124
|
+
If a tab-bound call returns `ACCESS_DENIED`, it also surfaces the Enable prompt automatically - so explicit `access.request` is optional but useful for proactive setup.
|
|
125
125
|
|
|
126
126
|
If access is already pending for a window, do not call `access.request` again. Ask the user to click `Enable` for the requested window and wait for confirmation before continuing.
|
|
127
127
|
|
|
@@ -139,7 +139,7 @@ bbx call page.evaluate '{"expression":"await fetch(\"/api/health\").then(r=>r.js
|
|
|
139
139
|
|
|
140
140
|
### page.get_console
|
|
141
141
|
|
|
142
|
-
Read buffered console output. The console interceptor is auto-installed on first call. Captures `log`, `warn`, `error`, `info`, `debug` plus uncaught exceptions and unhandled rejections.
|
|
142
|
+
Read buffered console output. The console interceptor is auto-installed on first call. Captures `log`, `warn`, `error`, `info`, `debug` plus uncaught exceptions and unhandled rejections. `level` is a minimum severity, so `warn` includes errors, exceptions, and rejections; `exception` and `rejection` select only that exact stream.
|
|
143
143
|
|
|
144
144
|
To capture a reproduction reliably, call it once with `clear: true` before triggering the event. This installs the interceptor and removes older entries. Reproduce the issue, then read again without `clear`.
|
|
145
145
|
|
|
@@ -182,7 +182,7 @@ If `_registryPruned` is true, refresh previously cached refs before reusing them
|
|
|
182
182
|
|
|
183
183
|
### dom.wait_for
|
|
184
184
|
|
|
185
|
-
Wait for a DOM condition using MutationObserver + 250 ms polling fallback. Returns `{found, elementRef, duration}`.
|
|
185
|
+
Wait for a DOM condition using MutationObserver + 250 ms polling fallback. A text-only wait searches all elements. `hidden` succeeds when no matching visible element remains, including when it is absent. Returns `{found, elementRef, duration}`.
|
|
186
186
|
|
|
187
187
|
- `state`: `attached` (default), `detached`, `visible`, `hidden`
|
|
188
188
|
- `text`: optional text content filter
|