@browserbridge/bbx 1.9.0 → 1.10.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/package.json +1 -1
- package/packages/agent-client/src/cli-args.js +35 -6
- package/packages/agent-client/src/cli-batch.js +10 -5
- package/packages/agent-client/src/cli-helpers.js +8 -0
- package/packages/agent-client/src/cli-setup-commands.js +3 -1
- package/packages/agent-client/src/cli.js +35 -15
- package/packages/agent-client/src/command-registry.js +7 -3
- package/packages/agent-client/src/mcp-config.js +8 -8
- package/packages/agent-client/src/runtime.js +2 -2
- package/packages/mcp-server/src/guidance.js +37 -6
- package/packages/mcp-server/src/handlers-capture.js +1 -1
- package/packages/mcp-server/src/handlers-page.js +162 -9
- package/packages/mcp-server/src/handlers-utils.js +126 -167
- package/packages/mcp-server/src/handlers.js +7 -1
- package/packages/mcp-server/src/server.js +195 -46
- package/packages/mcp-server/src/toolset.js +48 -0
- package/packages/native-host/src/daemon.js +20 -106
- package/packages/native-host/src/dom-baseline-owners.js +210 -0
- package/packages/native-host/src/native-host.js +14 -28
- package/packages/protocol/src/budget-presets.js +182 -0
- package/packages/protocol/src/har.js +34 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/json-lines.js +3 -2
- package/packages/protocol/src/registry.js +83 -0
- package/packages/protocol/src/types.ts +1 -0
- package/skills/browser-bridge/SKILL.md +4 -0
- package/skills/browser-bridge/references/protocol.md +11 -5
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { METHODS } from '../../protocol/src/index.js';
|
|
3
|
+
import { applyMethodBudgetPreset, isBudgetPresetName, METHODS } from '../../protocol/src/index.js';
|
|
4
4
|
import { methodNeedsTab, parseIntArg, parseJsonObject } from './cli-helpers.js';
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
7
|
+
/** @typedef {import('../../protocol/src/defaults.js').BudgetPresetName} BudgetPresetName */
|
|
8
|
+
|
|
9
|
+
const CALL_USAGE = 'Usage: call [--tab <tabId>] [--preset quick|normal|deep] <method> [paramsJson]';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Read all of stdin as UTF-8 text. Resolves once stdin closes.
|
|
@@ -119,18 +122,44 @@ export function extractHarFlags(args) {
|
|
|
119
122
|
return { limit, urlPattern, delivery: rawDelivery, rest };
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Extract the --preset flag from anywhere in the argument list. Preset names
|
|
127
|
+
* are validated against the shared budget presets; explicit per-call params
|
|
128
|
+
* always override preset defaults.
|
|
129
|
+
*
|
|
130
|
+
* @param {string[]} args
|
|
131
|
+
* @returns {{ preset: BudgetPresetName | null, rest: string[] }}
|
|
132
|
+
*/
|
|
133
|
+
export function extractPresetFlag(args) {
|
|
134
|
+
const rest = [...args];
|
|
135
|
+
const index = rest.indexOf('--preset');
|
|
136
|
+
if (index === -1) {
|
|
137
|
+
return { preset: null, rest };
|
|
138
|
+
}
|
|
139
|
+
const value = rest[index + 1];
|
|
140
|
+
if (!value || value.startsWith('--')) {
|
|
141
|
+
throw new Error('--preset requires a value: quick, normal, or deep.');
|
|
142
|
+
}
|
|
143
|
+
if (!isBudgetPresetName(value)) {
|
|
144
|
+
throw new Error(`Unknown preset "${value}". Valid presets: quick, normal, deep.`);
|
|
145
|
+
}
|
|
146
|
+
rest.splice(index, 2);
|
|
147
|
+
return { preset: value, rest };
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
/**
|
|
123
151
|
* @param {string[]} args
|
|
124
152
|
* @returns {Promise<{ tabId: number | null, method: BridgeMethod, params: Record<string, unknown> }>}
|
|
125
153
|
*/
|
|
126
154
|
export async function parseCallCommand(args) {
|
|
127
|
-
const
|
|
155
|
+
const presetParsed = extractPresetFlag(args);
|
|
156
|
+
const parsed = extractTabFlag(presetParsed.rest);
|
|
128
157
|
const [first, second, ...extra] = parsed.rest;
|
|
129
158
|
if (!first) {
|
|
130
|
-
throw new Error(
|
|
159
|
+
throw new Error(CALL_USAGE);
|
|
131
160
|
}
|
|
132
161
|
if (extra.length > 0) {
|
|
133
|
-
throw new Error(
|
|
162
|
+
throw new Error(CALL_USAGE);
|
|
134
163
|
}
|
|
135
164
|
|
|
136
165
|
if (first.includes('.')) {
|
|
@@ -146,11 +175,11 @@ export async function parseCallCommand(args) {
|
|
|
146
175
|
return {
|
|
147
176
|
method,
|
|
148
177
|
tabId: methodNeedsTab(method) ? parsed.tabId : null,
|
|
149
|
-
params: parseJsonObject(rawParams),
|
|
178
|
+
params: applyMethodBudgetPreset(method, parseJsonObject(rawParams), presetParsed.preset),
|
|
150
179
|
};
|
|
151
180
|
}
|
|
152
181
|
|
|
153
|
-
throw new Error(
|
|
182
|
+
throw new Error(CALL_USAGE);
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
applyMethodBudgetPreset,
|
|
4
5
|
getProtocolVersion,
|
|
5
6
|
isBatchSafeBridgeCall,
|
|
6
7
|
MAX_BATCH_CALLS,
|
|
@@ -14,6 +15,7 @@ import { requestBridge } from './runtime.js';
|
|
|
14
15
|
|
|
15
16
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
16
17
|
/** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
|
|
18
|
+
/** @typedef {import('../../protocol/src/defaults.js').BudgetPresetName} BudgetPresetName */
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* @typedef {{
|
|
@@ -72,9 +74,10 @@ function invalidBatchItem(method, message) {
|
|
|
72
74
|
* @param {import('./client.js').BridgeClient} client
|
|
73
75
|
* @param {string | undefined} input - Raw JSON array argument
|
|
74
76
|
* @param {BridgeRequestSource} source - Request source tag (e.g. 'cli')
|
|
77
|
+
* @param {{ preset?: BudgetPresetName | null }} [options] - Optional budget preset applied to every call
|
|
75
78
|
* @returns {Promise<BatchResultItem[]>}
|
|
76
79
|
*/
|
|
77
|
-
export async function runBatchCalls(client, input, source) {
|
|
80
|
+
export async function runBatchCalls(client, input, source, options = {}) {
|
|
78
81
|
if (!input) {
|
|
79
82
|
throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
|
|
80
83
|
}
|
|
@@ -90,7 +93,7 @@ export async function runBatchCalls(client, input, source) {
|
|
|
90
93
|
if (calls.length === 0 || calls.length > MAX_BATCH_CALLS) {
|
|
91
94
|
throw new Error(`Batch input must contain between 1 and ${MAX_BATCH_CALLS} calls.`);
|
|
92
95
|
}
|
|
93
|
-
const prepared = calls.map((call) => prepareBatchCall(call));
|
|
96
|
+
const prepared = calls.map((call) => prepareBatchCall(call, options.preset ?? null));
|
|
94
97
|
if (prepared.some((item) => 'error' in item)) {
|
|
95
98
|
return prepared.map((item) =>
|
|
96
99
|
'error' in item
|
|
@@ -130,9 +133,10 @@ export async function runBatchCalls(client, input, source) {
|
|
|
130
133
|
|
|
131
134
|
/**
|
|
132
135
|
* @param {unknown} call
|
|
136
|
+
* @param {BudgetPresetName | null} preset
|
|
133
137
|
* @returns {{ value: PreparedBatchCall } | { error: InvalidBatchItem }}
|
|
134
138
|
*/
|
|
135
|
-
function prepareBatchCall(call) {
|
|
139
|
+
function prepareBatchCall(call, preset) {
|
|
136
140
|
if (!call || typeof call !== 'object' || Array.isArray(call)) {
|
|
137
141
|
return { error: invalidBatchItem('', 'Each batch call needs a method.') };
|
|
138
142
|
}
|
|
@@ -162,7 +166,8 @@ function prepareBatchCall(call) {
|
|
|
162
166
|
}
|
|
163
167
|
const params =
|
|
164
168
|
batchCall.params === undefined ? {} : /** @type {Record<string, unknown>} */ (batchCall.params);
|
|
165
|
-
|
|
169
|
+
const mergedParams = applyMethodBudgetPreset(method, params, preset);
|
|
170
|
+
if (!isBatchSafeBridgeCall(method, mergedParams)) {
|
|
166
171
|
return {
|
|
167
172
|
error: invalidBatchItem(
|
|
168
173
|
method,
|
|
@@ -177,7 +182,7 @@ function prepareBatchCall(call) {
|
|
|
177
182
|
batchCall.tabId > 0
|
|
178
183
|
? batchCall.tabId
|
|
179
184
|
: null;
|
|
180
|
-
return { value: { method, params, tabId } };
|
|
185
|
+
return { value: { method, params: mergedParams, tabId } };
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
/**
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
import readline from 'node:readline';
|
|
4
4
|
import { bridgeMethodNeedsTab } from '../../protocol/src/index.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} value
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isElementRef(value) {
|
|
11
|
+
return value.startsWith('el_');
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* Strip ANSI escape sequences from a string to prevent terminal injection
|
|
8
16
|
* from malicious page content (e.g. DOM text, console output, eval results).
|
|
@@ -150,7 +150,8 @@ export async function runInstallMcpCommand(args) {
|
|
|
150
150
|
/** @type {string[]} */
|
|
151
151
|
const positionals = [];
|
|
152
152
|
|
|
153
|
-
for (
|
|
153
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
154
|
+
const arg = args[index];
|
|
154
155
|
if (arg === '--local') {
|
|
155
156
|
isGlobal = false;
|
|
156
157
|
continue;
|
|
@@ -276,6 +277,7 @@ export async function runInstallMcpCommand(args) {
|
|
|
276
277
|
projectPath: process.cwd(),
|
|
277
278
|
stdout: process.stdout,
|
|
278
279
|
});
|
|
280
|
+
|
|
279
281
|
process.exit(0);
|
|
280
282
|
}
|
|
281
283
|
|
|
@@ -10,7 +10,13 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
|
|
11
11
|
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
12
12
|
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
createRuntimeContext,
|
|
15
|
+
describeBridgeMethods,
|
|
16
|
+
isBridgeMethod,
|
|
17
|
+
isHarDocument,
|
|
18
|
+
METHODS,
|
|
19
|
+
} from '../../protocol/src/index.js';
|
|
14
20
|
import {
|
|
15
21
|
restartRegisteredMcpProcesses,
|
|
16
22
|
tryStartMcpProcessControl,
|
|
@@ -19,6 +25,7 @@ import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
|
|
|
19
25
|
import {
|
|
20
26
|
extractRemoteFlag,
|
|
21
27
|
extractHarFlags,
|
|
28
|
+
extractPresetFlag,
|
|
22
29
|
extractScreenshotFlags,
|
|
23
30
|
extractTabFlag,
|
|
24
31
|
parseCallCommand,
|
|
@@ -26,7 +33,7 @@ import {
|
|
|
26
33
|
readStdin,
|
|
27
34
|
} from './cli-args.js';
|
|
28
35
|
import { runBatchCalls } from './cli-batch.js';
|
|
29
|
-
import { parseIntArg } from './cli-helpers.js';
|
|
36
|
+
import { isElementRef, parseIntArg } from './cli-helpers.js';
|
|
30
37
|
import { printCallResponse, printJson, printSummary, printUsage } from './cli-output.js';
|
|
31
38
|
import { handleProxyCommand, handleRemoteCommand } from './cli-proxy-remote.js';
|
|
32
39
|
import {
|
|
@@ -64,6 +71,7 @@ const LOCAL_ONLY_COMMANDS = new Set([
|
|
|
64
71
|
'--version',
|
|
65
72
|
'-v',
|
|
66
73
|
'skill',
|
|
74
|
+
'protocol',
|
|
67
75
|
'install',
|
|
68
76
|
'uninstall',
|
|
69
77
|
'install-skill',
|
|
@@ -111,6 +119,22 @@ if (command === 'skill') {
|
|
|
111
119
|
process.exit(0);
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
if (command === 'protocol') {
|
|
123
|
+
const [subcommand, target, ...extra] = rest;
|
|
124
|
+
if (subcommand !== 'describe' || extra.length > 0) {
|
|
125
|
+
process.stderr.write('Usage: bbx protocol describe [method|group]\n');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
await runLocalCommand(async () => {
|
|
129
|
+
printJson(
|
|
130
|
+
target
|
|
131
|
+
? describeBridgeMethods(isBridgeMethod(target) ? { method: target } : { group: target })
|
|
132
|
+
: describeBridgeMethods()
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
process.exit();
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
if (command === 'install') {
|
|
115
139
|
const { execFileSync } = await import('node:child_process');
|
|
116
140
|
const installScript = path.resolve(
|
|
@@ -142,7 +166,7 @@ if (command === 'install-mcp') {
|
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
if (command === 'mcp') {
|
|
145
|
-
const [subcommand, clientName] = rest;
|
|
169
|
+
const [subcommand, clientName, ...extra] = rest;
|
|
146
170
|
if (subcommand === 'serve') {
|
|
147
171
|
const control = await tryStartMcpProcessControl();
|
|
148
172
|
try {
|
|
@@ -154,7 +178,7 @@ if (command === 'mcp') {
|
|
|
154
178
|
await new Promise(() => {});
|
|
155
179
|
}
|
|
156
180
|
if (subcommand === 'config') {
|
|
157
|
-
if (!clientName || !isMcpClientName(clientName)) {
|
|
181
|
+
if (!clientName || !isMcpClientName(clientName) || extra.length > 0) {
|
|
158
182
|
process.stderr.write(`Usage: bbx mcp config <${MCP_CLIENT_NAMES.join('|')}>\n`);
|
|
159
183
|
process.exit(1);
|
|
160
184
|
}
|
|
@@ -260,8 +284,10 @@ async function main() {
|
|
|
260
284
|
}
|
|
261
285
|
|
|
262
286
|
if (command === 'access-request') {
|
|
287
|
+
if (rest.length > 1) throw new Error('Usage: bbx access-request [intent]');
|
|
288
|
+
const params = SHORTCUT_COMMANDS['access-request'].build(rest);
|
|
263
289
|
await printSummary(
|
|
264
|
-
await requestBridge(client, 'access.request',
|
|
290
|
+
await requestBridge(client, 'access.request', params, { source: REQUEST_SOURCE })
|
|
265
291
|
);
|
|
266
292
|
return;
|
|
267
293
|
}
|
|
@@ -377,7 +403,8 @@ async function main() {
|
|
|
377
403
|
}
|
|
378
404
|
|
|
379
405
|
if (command === 'batch') {
|
|
380
|
-
const
|
|
406
|
+
const { preset, rest: batchArgs } = extractPresetFlag(rest);
|
|
407
|
+
const results = await runBatchCalls(client, batchArgs[0], REQUEST_SOURCE, { preset });
|
|
381
408
|
if (results.some((result) => !result.ok)) {
|
|
382
409
|
process.exitCode = 1;
|
|
383
410
|
}
|
|
@@ -412,7 +439,7 @@ async function main() {
|
|
|
412
439
|
const [key, refOrSelector] = rest;
|
|
413
440
|
if (!key) throw new Error('Usage: press-key <key> [ref|selector]');
|
|
414
441
|
const target = refOrSelector
|
|
415
|
-
? refOrSelector
|
|
442
|
+
? isElementRef(refOrSelector)
|
|
416
443
|
? { elementRef: refOrSelector }
|
|
417
444
|
: { selector: refOrSelector }
|
|
418
445
|
: undefined;
|
|
@@ -755,14 +782,7 @@ function assertValidHarDocument(document) {
|
|
|
755
782
|
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
756
783
|
throw new Error('HAR response does not contain an inline HAR object.');
|
|
757
784
|
}
|
|
758
|
-
|
|
759
|
-
if (
|
|
760
|
-
!log ||
|
|
761
|
-
typeof log !== 'object' ||
|
|
762
|
-
Array.isArray(log) ||
|
|
763
|
-
Reflect.get(log, 'version') !== '1.2' ||
|
|
764
|
-
!Array.isArray(Reflect.get(log, 'entries'))
|
|
765
|
-
) {
|
|
785
|
+
if (!isHarDocument(document)) {
|
|
766
786
|
throw new Error('HAR response does not contain a valid HAR 1.2 document.');
|
|
767
787
|
}
|
|
768
788
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { BRIDGE_METHOD_REGISTRY, BRIDGE_METHODS } from '../../protocol/src/index.js';
|
|
4
4
|
import {
|
|
5
|
+
isElementRef,
|
|
5
6
|
parseCommaList,
|
|
6
7
|
parseIntArg,
|
|
7
8
|
parseNumberArg,
|
|
@@ -39,7 +40,7 @@ function createShortcutCommand(method, usage, build, options = {}) {
|
|
|
39
40
|
*/
|
|
40
41
|
function createAtomicTarget(value) {
|
|
41
42
|
if (!value) throw new Error('A ref or selector is required.');
|
|
42
|
-
return value
|
|
43
|
+
return isElementRef(value) ? { elementRef: value } : { selector: value };
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/** @type {Record<string, ShortcutCommand>} */
|
|
@@ -266,6 +267,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
266
267
|
'bbx uninstall Remove native host manifests, Browser Bridge runtime files, and managed MCP/skill installs',
|
|
267
268
|
'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
|
|
268
269
|
'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
|
|
270
|
+
' MCP starts with common tools; browser_toolset loads one specialized typed tool by name when needed.',
|
|
269
271
|
'bbx status Check bridge connection',
|
|
270
272
|
'bbx doctor Consolidated local runtime, setup, protocol, and readiness diagnostics',
|
|
271
273
|
'bbx restart Restart the local daemon and running MCP servers',
|
|
@@ -285,15 +287,17 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
285
287
|
'bbx tab-close <tabId> Close a tab',
|
|
286
288
|
'bbx tab-activate <tabId> Bring a tab to the foreground',
|
|
287
289
|
'bbx skill Runtime budget presets and method groups',
|
|
290
|
+
'bbx protocol describe [method|group] Describe method params or list registry groups locally',
|
|
288
291
|
'bbx mcp serve Start Browser Bridge as an MCP stdio server',
|
|
289
292
|
],
|
|
290
293
|
},
|
|
291
294
|
{
|
|
292
295
|
title: 'Generic RPC',
|
|
293
296
|
lines: [
|
|
294
|
-
'bbx call [--tab <tabId>] <method> [paramsJson|-]
|
|
297
|
+
'bbx call [--tab <tabId>] [--preset quick|normal|deep] <method> [paramsJson|-] Call any bridge method (- reads JSON from stdin)',
|
|
295
298
|
'bbx <method> [--tab <tabId>] [paramsJson|-] Direct alias for exact bridge methods such as page.get_state',
|
|
296
|
-
"bbx batch '[{method,params,tabId?},...]'
|
|
299
|
+
"bbx batch [--preset quick|normal|deep] '[{method,params,tabId?},...]' Up to 20 parallel read-only calls",
|
|
300
|
+
'--preset fills budget gaps (maxNodes, maxDepth, textBudget, limit) from the shared quick|normal|deep presets; explicit params always win.',
|
|
297
301
|
'Every bridge command accepts --remote <name> (or BBX_REMOTE env) to target a remote destination from `bbx remote list`.',
|
|
298
302
|
'Advanced bridge params stay available through `bbx call`, even when shortcuts expose only the common case.',
|
|
299
303
|
'For open-ended investigation, start with `bbx batch` on `page.get_state`, `dom.query`, and `page.get_text` before any screenshot or CDP call.',
|
|
@@ -91,23 +91,23 @@ export function getMcpConfigShape(clientName) {
|
|
|
91
91
|
* env: Record<string, string>
|
|
92
92
|
* } | {
|
|
93
93
|
* type: 'local',
|
|
94
|
-
* command: string[]
|
|
94
|
+
* command: string[],
|
|
95
|
+
* environment?: Record<string, string>
|
|
95
96
|
* }}
|
|
96
97
|
*/
|
|
97
98
|
function createBaseServerConfig(clientName) {
|
|
98
|
-
const serverConfig = {
|
|
99
|
-
command: MCP_COMMAND,
|
|
100
|
-
args: [...MCP_ARGS],
|
|
101
|
-
env: {},
|
|
102
|
-
};
|
|
103
|
-
|
|
104
99
|
if (clientName === 'opencode') {
|
|
105
100
|
return {
|
|
106
101
|
type: 'local',
|
|
107
102
|
command: [MCP_COMMAND, ...MCP_ARGS],
|
|
108
103
|
};
|
|
109
104
|
}
|
|
110
|
-
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
command: MCP_COMMAND,
|
|
108
|
+
args: [...MCP_ARGS],
|
|
109
|
+
env: {},
|
|
110
|
+
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** @type {Record<McpClientName, { key: string, includeType: boolean, keepEmptyBlock?: boolean }>} */
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
sanitizeIncidentalPath,
|
|
34
34
|
sanitizeIncidentalText,
|
|
35
35
|
} from '../../protocol/src/index.js';
|
|
36
|
-
import { methodNeedsTab } from './cli-helpers.js';
|
|
36
|
+
import { isElementRef, methodNeedsTab } from './cli-helpers.js';
|
|
37
37
|
import { BridgeClient } from './client.js';
|
|
38
38
|
import { applyConfiguredAutoUpdate } from './config.js';
|
|
39
39
|
import { readRemoteConfig } from './remotes.js';
|
|
@@ -140,7 +140,7 @@ export async function requestBridge(client, method, params = {}, options = {}) {
|
|
|
140
140
|
* @returns {Promise<string>}
|
|
141
141
|
*/
|
|
142
142
|
export async function resolveRef(client, refOrSelector, tabId = null, source) {
|
|
143
|
-
if (refOrSelector
|
|
143
|
+
if (isElementRef(refOrSelector)) {
|
|
144
144
|
return refOrSelector;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Guidance lines shared by the progressive tool surface.
|
|
5
|
+
*
|
|
6
|
+
* @type {readonly string[]}
|
|
7
|
+
*/
|
|
8
|
+
const SHARED_INSTRUCTIONS = Object.freeze([
|
|
6
9
|
'Prefer Browser Bridge MCP tools over shelling out to bbx. Use bbx only for explicit CLI setup, doctor, logs, or raw debugging requests.',
|
|
7
10
|
'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.',
|
|
8
11
|
'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, page.extract_content, styles.get_computed, or layout.get_box_model. Prefer semantic extraction for articles and documentation. Use browser_batch only for parallel reads; run mutations sequentially and keep limits tight before widening.',
|
|
9
|
-
'Reuse elementRef values returned by DOM
|
|
12
|
+
'Reuse elementRef values returned by DOM reads. Use attribute allowlists for focused DOM reads.',
|
|
10
13
|
'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.',
|
|
11
14
|
'Use patch.apply_styles or patch.apply_dom for temporary experiments, and rollback patches before finishing unless the user asks to keep them.',
|
|
12
|
-
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Workflow guidance appended after the tool-selection rules.
|
|
19
|
+
*
|
|
20
|
+
* @type {readonly string[]}
|
|
21
|
+
*/
|
|
22
|
+
const WORKFLOW_INSTRUCTIONS = Object.freeze([
|
|
13
23
|
'',
|
|
14
24
|
'Page investigation: read page state, scoped DOM, and relevant page text first, batching independent reads. Find targets by text or role when selectors are unknown. Add styles, console, or network reads only when they directly answer the objective, and use screenshots or evaluation only when structured evidence is insufficient.',
|
|
15
25
|
'Layout debugging: locate the target, read only relevant computed properties and its box model, and inspect matched rules only when the cascade is unclear. Prototype the smallest fix with a reversible style patch, verify the result, check for new console errors, then rollback before editing source unless the user asks to keep the patch.',
|
|
16
26
|
'Flow verification: read initial page state, locate controls semantically, reuse elementRef values, interact through input tools, and wait for navigation or UI state changes. Verify the final DOM or page text plus console and network state when relevant. Do not create a new tab unless requested or required by the flow.',
|
|
17
|
-
]
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SERVER_HEADER =
|
|
30
|
+
"Browser Bridge MCP inspects and interacts with the user's real Chrome tab through a compact tool set.";
|
|
31
|
+
|
|
32
|
+
const TOOL_RULES = Object.freeze([
|
|
33
|
+
'The common tools are available immediately. When a specialized typed tool is useful, call browser_toolset with its exact tool name; browser_call always reaches every bridge method without loading another tool.',
|
|
34
|
+
'Use browser_call method protocol.describe with method or group params to load unfamiliar signatures cheaply.',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build instructions for progressive tool discovery.
|
|
39
|
+
*
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function getMcpServerInstructions() {
|
|
43
|
+
return [SERVER_HEADER, ...TOOL_RULES, ...SHARED_INSTRUCTIONS, ...WORKFLOW_INSTRUCTIONS].join(
|
|
44
|
+
'\n'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const MCP_SERVER_INSTRUCTIONS = getMcpServerInstructions();
|
|
@@ -153,7 +153,7 @@ export function createScreenshotResult(response, method) {
|
|
|
153
153
|
return summarizeToolError(new Error(`${method} returned invalid artifact metadata.`));
|
|
154
154
|
}
|
|
155
155
|
return createToolResult(
|
|
156
|
-
`Captured screenshot artifact (${artifact.byteLength} bytes, expires ${artifact.expiresAt ?? 'soon'}).`,
|
|
156
|
+
`Captured screenshot artifact (${artifact.byteLength} bytes, expires ${artifact.expiresAt ?? 'soon'}). Read it in chunks with the browser_artifact tool (action "read", paging offset until nextOffset is null), then delete it with browser_artifact action "delete" when done.`,
|
|
157
157
|
{
|
|
158
158
|
ok: true,
|
|
159
159
|
method,
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
DEFAULT_CONSOLE_LIMIT,
|
|
5
5
|
DEFAULT_HAR_LIMIT,
|
|
6
6
|
DEFAULT_NETWORK_LIMIT,
|
|
7
|
+
describeBridgeMethods,
|
|
7
8
|
ERROR_CODES,
|
|
8
9
|
getErrorRecovery,
|
|
9
10
|
isBatchSafeBridgeCall,
|
|
11
|
+
isHarDocument,
|
|
10
12
|
MAX_BATCH_CALLS,
|
|
11
13
|
MAX_BATCH_CONCURRENCY,
|
|
12
14
|
METHOD_SET,
|
|
@@ -166,6 +168,157 @@ export async function handlePageTool(args) {
|
|
|
166
168
|
});
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Read one chunk of a daemon-owned artifact or delete it. Reads are paged by
|
|
173
|
+
* the caller: pass the returned nextOffset until it comes back null, then
|
|
174
|
+
* verify the reassembled bytes against sha256.
|
|
175
|
+
*
|
|
176
|
+
* @param {{ action: string, artifactId?: string, offset?: number, limit?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
177
|
+
* @returns {Promise<ToolResult>}
|
|
178
|
+
*/
|
|
179
|
+
export async function handleArtifactTool(args) {
|
|
180
|
+
if (args.action !== 'read' && args.action !== 'delete') {
|
|
181
|
+
return summarizeToolError(`Unsupported artifact action "${args.action}".`);
|
|
182
|
+
}
|
|
183
|
+
if (typeof args.artifactId !== 'string' || !args.artifactId.trim()) {
|
|
184
|
+
return summarizeToolError('artifactId is required.');
|
|
185
|
+
}
|
|
186
|
+
if (args.offset !== undefined && (!Number.isInteger(args.offset) || args.offset < 0)) {
|
|
187
|
+
return summarizeToolError('offset must be a non-negative integer.');
|
|
188
|
+
}
|
|
189
|
+
if (args.limit !== undefined && (!Number.isInteger(args.limit) || args.limit < 1)) {
|
|
190
|
+
return summarizeToolError('limit must be a positive integer.');
|
|
191
|
+
}
|
|
192
|
+
const method = /** @type {BridgeMethod} */ (
|
|
193
|
+
args.action === 'read' ? 'artifact.read' : 'artifact.delete'
|
|
194
|
+
);
|
|
195
|
+
const params =
|
|
196
|
+
args.action === 'read'
|
|
197
|
+
? {
|
|
198
|
+
artifactId: args.artifactId,
|
|
199
|
+
offset: args.offset ?? 0,
|
|
200
|
+
...(typeof args.limit === 'number' ? { maxBytes: args.limit } : {}),
|
|
201
|
+
}
|
|
202
|
+
: { artifactId: args.artifactId };
|
|
203
|
+
return withToolClient(
|
|
204
|
+
async (client) => {
|
|
205
|
+
const response = await requestBridgeWithRetry(client, method, params, {
|
|
206
|
+
tabId: null,
|
|
207
|
+
source: REQUEST_SOURCE,
|
|
208
|
+
tokenBudget: getToolTokenBudget(args),
|
|
209
|
+
});
|
|
210
|
+
return method === 'artifact.read'
|
|
211
|
+
? createArtifactReadResult(response)
|
|
212
|
+
: summarizeToolResponse(response, method, params);
|
|
213
|
+
},
|
|
214
|
+
{ destinationId: args.destinationId ?? null }
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Manage CDP Fetch request interception rules for a tab.
|
|
220
|
+
*
|
|
221
|
+
* @param {{ action: string, urlPattern?: string, ruleAction?: 'block' | 'fulfill' | 'continue', statusCode?: number, body?: string, headers?: Record<string, string>, ruleId?: string, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
222
|
+
* @returns {Promise<ToolResult>}
|
|
223
|
+
*/
|
|
224
|
+
export async function handleInterceptTool(args) {
|
|
225
|
+
const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
|
|
226
|
+
return withToolClient(
|
|
227
|
+
async (client) => {
|
|
228
|
+
/** @type {BridgeMethod} */
|
|
229
|
+
let method;
|
|
230
|
+
/** @type {Record<string, unknown>} */
|
|
231
|
+
let params;
|
|
232
|
+
switch (args.action) {
|
|
233
|
+
case 'add': {
|
|
234
|
+
if (typeof args.urlPattern !== 'string' || !args.urlPattern.trim()) {
|
|
235
|
+
return summarizeToolError('urlPattern is required for intercept add.');
|
|
236
|
+
}
|
|
237
|
+
method = 'network.intercept.add';
|
|
238
|
+
params = {
|
|
239
|
+
urlPattern: args.urlPattern,
|
|
240
|
+
...(args.ruleAction ? { action: args.ruleAction } : {}),
|
|
241
|
+
...(typeof args.statusCode === 'number' ? { statusCode: args.statusCode } : {}),
|
|
242
|
+
...(typeof args.body === 'string' ? { body: args.body } : {}),
|
|
243
|
+
...(args.headers ? { headers: args.headers } : {}),
|
|
244
|
+
};
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case 'remove': {
|
|
248
|
+
if (typeof args.ruleId !== 'string' || !args.ruleId.trim()) {
|
|
249
|
+
return summarizeToolError('ruleId is required for intercept remove.');
|
|
250
|
+
}
|
|
251
|
+
method = 'network.intercept.remove';
|
|
252
|
+
params = { ruleId: args.ruleId };
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case 'list':
|
|
256
|
+
method = 'network.intercept.list';
|
|
257
|
+
params = {};
|
|
258
|
+
break;
|
|
259
|
+
case 'clear':
|
|
260
|
+
method = 'network.intercept.clear';
|
|
261
|
+
params = {};
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
return summarizeToolError(`Unsupported intercept action "${args.action}".`);
|
|
265
|
+
}
|
|
266
|
+
const response = await requestBridgeWithRetry(client, method, params, {
|
|
267
|
+
tabId: requestedTabId,
|
|
268
|
+
source: REQUEST_SOURCE,
|
|
269
|
+
tokenBudget: getToolTokenBudget(args),
|
|
270
|
+
});
|
|
271
|
+
return summarizeToolResponse(response, method, params);
|
|
272
|
+
},
|
|
273
|
+
{ destinationId: args.destinationId ?? null }
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
279
|
+
* @returns {ToolResult}
|
|
280
|
+
*/
|
|
281
|
+
function createArtifactReadResult(response) {
|
|
282
|
+
if (!response.ok) return summarizeToolResponse(response, 'artifact.read', {});
|
|
283
|
+
const result =
|
|
284
|
+
response.result && typeof response.result === 'object' && !Array.isArray(response.result)
|
|
285
|
+
? /** @type {Record<string, unknown>} */ (response.result)
|
|
286
|
+
: {};
|
|
287
|
+
if (
|
|
288
|
+
typeof result.artifactId !== 'string' ||
|
|
289
|
+
typeof result.data !== 'string' ||
|
|
290
|
+
typeof result.offset !== 'number' ||
|
|
291
|
+
typeof result.byteLength !== 'number' ||
|
|
292
|
+
typeof result.totalBytes !== 'number' ||
|
|
293
|
+
typeof result.sha256 !== 'string' ||
|
|
294
|
+
(result.nextOffset !== null && typeof result.nextOffset !== 'number')
|
|
295
|
+
) {
|
|
296
|
+
return summarizeToolError('artifact.read returned invalid chunk metadata.');
|
|
297
|
+
}
|
|
298
|
+
const nextOffset = /** @type {number | null} */ (result.nextOffset);
|
|
299
|
+
return createToolResult(
|
|
300
|
+
`Read ${result.byteLength} bytes from artifact ${result.artifactId} at offset ${result.offset} (${result.totalBytes} bytes total). ${
|
|
301
|
+
nextOffset === null
|
|
302
|
+
? 'Artifact fully read; verify the reassembled bytes against sha256.'
|
|
303
|
+
: `Continue reading with offset ${nextOffset}.`
|
|
304
|
+
}`,
|
|
305
|
+
{
|
|
306
|
+
ok: true,
|
|
307
|
+
method: 'artifact.read',
|
|
308
|
+
artifactId: result.artifactId,
|
|
309
|
+
data: result.data,
|
|
310
|
+
offset: result.offset,
|
|
311
|
+
byteLength: result.byteLength,
|
|
312
|
+
nextOffset,
|
|
313
|
+
totalBytes: result.totalBytes,
|
|
314
|
+
sha256: result.sha256,
|
|
315
|
+
chunkIndex: result.chunkIndex,
|
|
316
|
+
chunkCount: result.chunkCount,
|
|
317
|
+
expiresAt: result.expiresAt,
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
169
322
|
/**
|
|
170
323
|
* Deliberately read one exact storage value without retries, budgeting, or
|
|
171
324
|
* value-derived delivery metrics.
|
|
@@ -355,6 +508,14 @@ export async function handleRawCallTool(args) {
|
|
|
355
508
|
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
|
|
356
509
|
return summarizeToolError(`Unknown bridge method "${args.method}".`);
|
|
357
510
|
}
|
|
511
|
+
if (args.method === 'protocol.describe') {
|
|
512
|
+
try {
|
|
513
|
+
const result = describeBridgeMethods(args.params);
|
|
514
|
+
return createToolResult('Bridge protocol description loaded.', result);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
return summarizeToolError(error);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
358
519
|
return withToolClient(
|
|
359
520
|
async (client) => {
|
|
360
521
|
const method = /** @type {BridgeMethod} */ (args.method);
|
|
@@ -422,15 +583,7 @@ export function createHarExportResult(response) {
|
|
|
422
583
|
}
|
|
423
584
|
|
|
424
585
|
if (result.delivery === 'inline') {
|
|
425
|
-
|
|
426
|
-
result.har && typeof result.har === 'object' && !Array.isArray(result.har)
|
|
427
|
-
? /** @type {Record<string, unknown>} */ (result.har)
|
|
428
|
-
: {};
|
|
429
|
-
const log =
|
|
430
|
-
har.log && typeof har.log === 'object' && !Array.isArray(har.log)
|
|
431
|
-
? /** @type {Record<string, unknown>} */ (har.log)
|
|
432
|
-
: {};
|
|
433
|
-
if (log.version !== '1.2' || !Array.isArray(log.entries)) {
|
|
586
|
+
if (!isHarDocument(result.har)) {
|
|
434
587
|
return summarizeToolError('network.export_har returned an invalid inline HAR document.');
|
|
435
588
|
}
|
|
436
589
|
} else if (result.delivery === 'artifact') {
|