@browserbridge/bbx 1.3.0 → 1.4.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 +3 -1
- package/package.json +2 -2
- package/packages/agent-client/src/cli.js +32 -15
- package/packages/agent-client/src/client.js +74 -20
- package/packages/agent-client/src/command-registry.js +2 -3
- package/packages/agent-client/src/mcp-config.js +19 -2
- package/packages/agent-client/src/types.ts +8 -0
- package/packages/mcp-server/src/guidance.js +241 -0
- package/packages/mcp-server/src/handlers-capture.js +74 -11
- package/packages/mcp-server/src/handlers-dom.js +48 -0
- package/packages/mcp-server/src/handlers-navigation.js +22 -2
- package/packages/mcp-server/src/handlers-page.js +4 -9
- package/packages/mcp-server/src/handlers-utils.js +47 -1
- package/packages/mcp-server/src/server.js +111 -29
- package/packages/native-host/src/auth-token.js +92 -0
- package/packages/native-host/src/daemon.js +171 -27
- package/packages/native-host/src/native-host.js +18 -2
- package/packages/protocol/src/defaults.js +3 -0
- package/packages/protocol/src/json-lines.js +29 -1
- package/packages/protocol/src/protocol.js +3 -0
- package/packages/protocol/src/types.ts +2 -0
- package/skills/browser-bridge/SKILL.md +21 -5
- package/skills/browser-bridge/agents/openai.yaml +1 -1
- package/skills/browser-bridge/references/interaction.md +6 -6
- package/skills/browser-bridge/references/protocol.md +57 -54
- package/skills/browser-bridge/references/ui-workflows.md +1 -1
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
dispatchToolAction,
|
|
5
5
|
getToolTokenBudget,
|
|
6
6
|
REQUEST_SOURCE,
|
|
7
|
-
|
|
7
|
+
requestBridgeWithRetry,
|
|
8
8
|
resolveToolRef,
|
|
9
9
|
resolveRef,
|
|
10
10
|
summarizeToolError,
|
|
@@ -56,11 +56,40 @@ function isCdpNodeCapture(args) {
|
|
|
56
56
|
return args.action === 'cdp_box_model' || args.action === 'cdp_computed_styles';
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @param {unknown} value
|
|
61
|
+
* @returns {value is number}
|
|
62
|
+
*/
|
|
63
|
+
function isFiniteNumber(value) {
|
|
64
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {unknown} rect */
|
|
68
|
+
function isValidCaptureRegion(rect) {
|
|
69
|
+
if (!rect || typeof rect !== 'object' || Array.isArray(rect)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const candidate = /** @type {Record<string, unknown>} */ (rect);
|
|
73
|
+
return (
|
|
74
|
+
isFiniteNumber(candidate.x) &&
|
|
75
|
+
isFiniteNumber(candidate.y) &&
|
|
76
|
+
isFiniteNumber(candidate.width) &&
|
|
77
|
+
candidate.width > 0 &&
|
|
78
|
+
isFiniteNumber(candidate.height) &&
|
|
79
|
+
candidate.height > 0
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
/**
|
|
60
84
|
* @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, nodeId?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
61
85
|
* @returns {Promise<ToolResult>}
|
|
62
86
|
*/
|
|
63
87
|
export async function handleCaptureTool(args) {
|
|
88
|
+
if (args.action === 'region' && !isValidCaptureRegion(args.rect)) {
|
|
89
|
+
return summarizeToolError(
|
|
90
|
+
'rect with finite x, y, width, and height is required for region capture.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
64
93
|
if (
|
|
65
94
|
isCdpNodeCapture(args) &&
|
|
66
95
|
(typeof args.nodeId !== 'number' || !Number.isFinite(args.nodeId))
|
|
@@ -90,6 +119,21 @@ export const INPUT_ACTION_METHODS = {
|
|
|
90
119
|
* @returns {Promise<ToolResult>}
|
|
91
120
|
*/
|
|
92
121
|
export async function handleInputTool(args) {
|
|
122
|
+
if (args.action === 'type' && !hasText(args.text)) {
|
|
123
|
+
return summarizeToolError('text is required for input.type.');
|
|
124
|
+
}
|
|
125
|
+
if ((args.action === 'press_key' || args.action === 'cdp_press_key') && !hasText(args.key)) {
|
|
126
|
+
return summarizeToolError('key is required for key input actions.');
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
args.action === 'select_option' &&
|
|
130
|
+
!hasNonEmptyArray(args.values) &&
|
|
131
|
+
!hasNonEmptyArray(args.labels) &&
|
|
132
|
+
!hasNonEmptyArray(args.indexes)
|
|
133
|
+
) {
|
|
134
|
+
return summarizeToolError('values, labels, or indexes are required for input.select_option.');
|
|
135
|
+
}
|
|
136
|
+
|
|
93
137
|
return withToolClient(async (client) => {
|
|
94
138
|
const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
|
|
95
139
|
const elementTarget = async () => ({
|
|
@@ -98,13 +142,14 @@ export async function handleInputTool(args) {
|
|
|
98
142
|
|
|
99
143
|
switch (args.action) {
|
|
100
144
|
case 'click': {
|
|
101
|
-
const response = await
|
|
145
|
+
const response = await requestBridgeWithRetry(
|
|
102
146
|
client,
|
|
103
147
|
'input.click',
|
|
104
148
|
{
|
|
105
149
|
target: await elementTarget(),
|
|
106
150
|
button: args.button,
|
|
107
151
|
clickCount: args.clickCount,
|
|
152
|
+
modifiers: args.modifiers,
|
|
108
153
|
},
|
|
109
154
|
{
|
|
110
155
|
tabId: requestedTabId,
|
|
@@ -115,7 +160,7 @@ export async function handleInputTool(args) {
|
|
|
115
160
|
return summarizeToolResponse(response, 'input.click');
|
|
116
161
|
}
|
|
117
162
|
case 'focus': {
|
|
118
|
-
const response = await
|
|
163
|
+
const response = await requestBridgeWithRetry(
|
|
119
164
|
client,
|
|
120
165
|
'input.focus',
|
|
121
166
|
{
|
|
@@ -130,7 +175,7 @@ export async function handleInputTool(args) {
|
|
|
130
175
|
return summarizeToolResponse(response, 'input.focus');
|
|
131
176
|
}
|
|
132
177
|
case 'type': {
|
|
133
|
-
const response = await
|
|
178
|
+
const response = await requestBridgeWithRetry(
|
|
134
179
|
client,
|
|
135
180
|
'input.type',
|
|
136
181
|
{
|
|
@@ -138,6 +183,7 @@ export async function handleInputTool(args) {
|
|
|
138
183
|
text: args.text,
|
|
139
184
|
clear: args.clear,
|
|
140
185
|
submit: args.submit,
|
|
186
|
+
modifiers: args.modifiers,
|
|
141
187
|
},
|
|
142
188
|
{
|
|
143
189
|
tabId: requestedTabId,
|
|
@@ -149,7 +195,7 @@ export async function handleInputTool(args) {
|
|
|
149
195
|
}
|
|
150
196
|
case 'press_key': {
|
|
151
197
|
const target = args.elementRef || args.selector ? await elementTarget() : undefined;
|
|
152
|
-
const response = await
|
|
198
|
+
const response = await requestBridgeWithRetry(
|
|
153
199
|
client,
|
|
154
200
|
'input.press_key',
|
|
155
201
|
{
|
|
@@ -166,7 +212,7 @@ export async function handleInputTool(args) {
|
|
|
166
212
|
return summarizeToolResponse(response, 'input.press_key');
|
|
167
213
|
}
|
|
168
214
|
case 'cdp_press_key': {
|
|
169
|
-
const response = await
|
|
215
|
+
const response = await requestBridgeWithRetry(
|
|
170
216
|
client,
|
|
171
217
|
'cdp.dispatch_key_event',
|
|
172
218
|
{
|
|
@@ -183,7 +229,7 @@ export async function handleInputTool(args) {
|
|
|
183
229
|
return summarizeToolResponse(response, 'cdp.dispatch_key_event');
|
|
184
230
|
}
|
|
185
231
|
case 'set_checked': {
|
|
186
|
-
const response = await
|
|
232
|
+
const response = await requestBridgeWithRetry(
|
|
187
233
|
client,
|
|
188
234
|
'input.set_checked',
|
|
189
235
|
{
|
|
@@ -199,7 +245,7 @@ export async function handleInputTool(args) {
|
|
|
199
245
|
return summarizeToolResponse(response, 'input.set_checked');
|
|
200
246
|
}
|
|
201
247
|
case 'select_option': {
|
|
202
|
-
const response = await
|
|
248
|
+
const response = await requestBridgeWithRetry(
|
|
203
249
|
client,
|
|
204
250
|
'input.select_option',
|
|
205
251
|
{
|
|
@@ -217,12 +263,13 @@ export async function handleInputTool(args) {
|
|
|
217
263
|
return summarizeToolResponse(response, 'input.select_option');
|
|
218
264
|
}
|
|
219
265
|
case 'hover': {
|
|
220
|
-
const response = await
|
|
266
|
+
const response = await requestBridgeWithRetry(
|
|
221
267
|
client,
|
|
222
268
|
'input.hover',
|
|
223
269
|
{
|
|
224
270
|
target: await elementTarget(),
|
|
225
271
|
duration: args.duration,
|
|
272
|
+
modifiers: args.modifiers,
|
|
226
273
|
},
|
|
227
274
|
{
|
|
228
275
|
tabId: requestedTabId,
|
|
@@ -252,7 +299,7 @@ export async function handleInputTool(args) {
|
|
|
252
299
|
'sourceElementRef/sourceSelector and destinationElementRef/destinationSelector are required for drag.'
|
|
253
300
|
);
|
|
254
301
|
}
|
|
255
|
-
const response = await
|
|
302
|
+
const response = await requestBridgeWithRetry(
|
|
256
303
|
client,
|
|
257
304
|
'input.drag',
|
|
258
305
|
{
|
|
@@ -270,7 +317,7 @@ export async function handleInputTool(args) {
|
|
|
270
317
|
return summarizeToolResponse(response, 'input.drag');
|
|
271
318
|
}
|
|
272
319
|
case 'scroll_into_view': {
|
|
273
|
-
const response = await
|
|
320
|
+
const response = await requestBridgeWithRetry(
|
|
274
321
|
client,
|
|
275
322
|
'input.scroll_into_view',
|
|
276
323
|
{
|
|
@@ -289,3 +336,19 @@ export async function handleInputTool(args) {
|
|
|
289
336
|
}
|
|
290
337
|
});
|
|
291
338
|
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @param {unknown} value
|
|
342
|
+
* @returns {boolean}
|
|
343
|
+
*/
|
|
344
|
+
function hasText(value) {
|
|
345
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {unknown} value
|
|
350
|
+
* @returns {boolean}
|
|
351
|
+
*/
|
|
352
|
+
function hasNonEmptyArray(value) {
|
|
353
|
+
return Array.isArray(value) && value.length > 0;
|
|
354
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
applyTreeBudgetPreset,
|
|
7
7
|
dispatchToolAction,
|
|
8
8
|
inferBudgetFromSelector,
|
|
9
|
+
summarizeToolError,
|
|
9
10
|
} from './handlers-utils.js';
|
|
10
11
|
|
|
11
12
|
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
@@ -93,6 +94,15 @@ export const DOM_ACTIONS = {
|
|
|
93
94
|
* @returns {Promise<ToolResult>}
|
|
94
95
|
*/
|
|
95
96
|
export async function handleDomTool(args) {
|
|
97
|
+
if (args.action === 'wait' && !hasText(args.selector) && !hasText(args.text)) {
|
|
98
|
+
return summarizeToolError('selector or text is required for dom.wait_for.');
|
|
99
|
+
}
|
|
100
|
+
if (args.action === 'find_text' && !hasText(args.text)) {
|
|
101
|
+
return summarizeToolError('text is required for dom.find_by_text.');
|
|
102
|
+
}
|
|
103
|
+
if (args.action === 'find_role' && !hasText(args.role)) {
|
|
104
|
+
return summarizeToolError('role is required for dom.find_by_role.');
|
|
105
|
+
}
|
|
96
106
|
if (args.action === 'query' || args.action === 'accessibility_tree') {
|
|
97
107
|
const inferred = inferBudgetFromSelector(args);
|
|
98
108
|
const withBudget = inferred ? { ...args, budgetPreset: args.budgetPreset ?? inferred } : args;
|
|
@@ -136,6 +146,15 @@ export const STYLES_LAYOUT_ACTIONS = {
|
|
|
136
146
|
* @returns {Promise<ToolResult>}
|
|
137
147
|
*/
|
|
138
148
|
export async function handleStylesLayoutTool(args) {
|
|
149
|
+
if (
|
|
150
|
+
args.action === 'hit_test' &&
|
|
151
|
+
(typeof args.x !== 'number' ||
|
|
152
|
+
!Number.isFinite(args.x) ||
|
|
153
|
+
typeof args.y !== 'number' ||
|
|
154
|
+
!Number.isFinite(args.y))
|
|
155
|
+
) {
|
|
156
|
+
return summarizeToolError('x and y are required for layout.hit_test.');
|
|
157
|
+
}
|
|
139
158
|
return dispatchToolAction(STYLES_LAYOUT_ACTIONS, args, 'styles/layout');
|
|
140
159
|
}
|
|
141
160
|
|
|
@@ -199,5 +218,34 @@ export const PATCH_ACTIONS = {
|
|
|
199
218
|
* @returns {Promise<ToolResult>}
|
|
200
219
|
*/
|
|
201
220
|
export async function handlePatchTool(args) {
|
|
221
|
+
if (args.action === 'apply_styles' && !hasStringRecord(args.declarations)) {
|
|
222
|
+
return summarizeToolError('declarations are required for patch.apply_styles.');
|
|
223
|
+
}
|
|
224
|
+
if (args.action === 'apply_dom' && !hasText(args.operation)) {
|
|
225
|
+
return summarizeToolError('operation is required for patch.apply_dom.');
|
|
226
|
+
}
|
|
227
|
+
if (args.action === 'rollback' && !hasText(args.patchId)) {
|
|
228
|
+
return summarizeToolError('patchId is required for patch.rollback.');
|
|
229
|
+
}
|
|
202
230
|
return dispatchToolAction(PATCH_ACTIONS, args, 'patch');
|
|
203
231
|
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {unknown} value
|
|
235
|
+
* @returns {boolean}
|
|
236
|
+
*/
|
|
237
|
+
function hasText(value) {
|
|
238
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {unknown} value
|
|
243
|
+
* @returns {value is Record<string, string>}
|
|
244
|
+
*/
|
|
245
|
+
function hasStringRecord(value) {
|
|
246
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
const entries = Object.entries(/** @type {Record<string, unknown>} */ (value));
|
|
250
|
+
return entries.length > 0 && entries.every(([key, val]) => key.trim() && typeof val === 'string');
|
|
251
|
+
}
|
|
@@ -61,17 +61,37 @@ export const NAVIGATION_ACTIONS = {
|
|
|
61
61
|
},
|
|
62
62
|
resize: {
|
|
63
63
|
method: 'viewport.resize',
|
|
64
|
-
params: (a) => ({
|
|
64
|
+
params: (a) => ({
|
|
65
|
+
width: a.width,
|
|
66
|
+
height: a.height,
|
|
67
|
+
deviceScaleFactor: a.deviceScaleFactor,
|
|
68
|
+
reset: a.reset,
|
|
69
|
+
}),
|
|
65
70
|
},
|
|
66
71
|
};
|
|
67
72
|
|
|
68
73
|
/**
|
|
69
|
-
* @param {{ action: string, url?: string, waitForLoad?: boolean, timeoutMs?: number, top?: number, left?: number, behavior?: string, relative?: boolean, width?: number, height?: number, reset?: boolean, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
74
|
+
* @param {{ action: string, url?: string, waitForLoad?: boolean, timeoutMs?: number, top?: number, left?: number, behavior?: string, relative?: boolean, width?: number, height?: number, deviceScaleFactor?: number, reset?: boolean, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
70
75
|
* @returns {Promise<ToolResult>}
|
|
71
76
|
*/
|
|
72
77
|
export async function handleNavigationTool(args) {
|
|
73
78
|
const entry = NAVIGATION_ACTIONS[args.action];
|
|
74
79
|
if (!entry) return summarizeToolError(`Unsupported navigation action "${args.action}".`);
|
|
80
|
+
if (args.action === 'navigate' && (typeof args.url !== 'string' || !args.url.trim())) {
|
|
81
|
+
return summarizeToolError('url is required for navigation.navigate.');
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
args.action === 'resize' &&
|
|
85
|
+
args.reset !== true &&
|
|
86
|
+
(typeof args.width !== 'number' ||
|
|
87
|
+
!Number.isFinite(args.width) ||
|
|
88
|
+
typeof args.height !== 'number' ||
|
|
89
|
+
!Number.isFinite(args.height))
|
|
90
|
+
) {
|
|
91
|
+
return summarizeToolError(
|
|
92
|
+
'width and height are required for viewport.resize unless reset=true.'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
75
95
|
return callBridgeTool(entry.method, entry.params(args), {
|
|
76
96
|
tabId: typeof args.tabId === 'number' ? args.tabId : null,
|
|
77
97
|
tokenBudget: getToolTokenBudget(args),
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
callBridgeTool,
|
|
14
14
|
createToolResult,
|
|
15
15
|
getToolTokenBudget,
|
|
16
|
-
requestBridge,
|
|
17
16
|
requestBridgeWithRetry,
|
|
18
17
|
summarizeBatchErrorItem,
|
|
19
18
|
summarizeBatchResponseItem,
|
|
@@ -147,14 +146,10 @@ export async function handleBatchTool(args) {
|
|
|
147
146
|
|
|
148
147
|
const startTime = Date.now();
|
|
149
148
|
try {
|
|
150
|
-
const response = await client.
|
|
151
|
-
method,
|
|
152
|
-
params: call.params || {},
|
|
149
|
+
const response = await requestBridgeWithRetry(client, method, call.params || {}, {
|
|
153
150
|
tabId,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
...(tokenBudget != null ? { token_budget: tokenBudget } : {}),
|
|
157
|
-
},
|
|
151
|
+
source: REQUEST_SOURCE,
|
|
152
|
+
tokenBudget,
|
|
158
153
|
});
|
|
159
154
|
return summarizeBatchResponseItem({
|
|
160
155
|
method,
|
|
@@ -199,7 +194,7 @@ export async function handleRawCallTool(args) {
|
|
|
199
194
|
}
|
|
200
195
|
|
|
201
196
|
return withToolClient(async (client) => {
|
|
202
|
-
const response = await
|
|
197
|
+
const response = await requestBridgeWithRetry(
|
|
203
198
|
client,
|
|
204
199
|
/** @type {BridgeMethod} */ (args.method),
|
|
205
200
|
args.params || {},
|
|
@@ -24,6 +24,52 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-clie
|
|
|
24
24
|
|
|
25
25
|
export const REQUEST_SOURCE = 'mcp';
|
|
26
26
|
|
|
27
|
+
/** @type {ReadonlySet<BridgeMethod>} */
|
|
28
|
+
const RETRY_SAFE_METHODS = new Set([
|
|
29
|
+
'skill.get_runtime_context',
|
|
30
|
+
'setup.get_status',
|
|
31
|
+
'log.tail',
|
|
32
|
+
'health.ping',
|
|
33
|
+
'daemon.metrics',
|
|
34
|
+
'tabs.list',
|
|
35
|
+
'page.get_state',
|
|
36
|
+
'page.get_storage',
|
|
37
|
+
'page.get_text',
|
|
38
|
+
'dom.query',
|
|
39
|
+
'dom.describe',
|
|
40
|
+
'dom.get_text',
|
|
41
|
+
'dom.get_attributes',
|
|
42
|
+
'dom.wait_for',
|
|
43
|
+
'dom.find_by_text',
|
|
44
|
+
'dom.find_by_role',
|
|
45
|
+
'dom.get_html',
|
|
46
|
+
'dom.get_accessibility_tree',
|
|
47
|
+
'layout.get_box_model',
|
|
48
|
+
'layout.hit_test',
|
|
49
|
+
'styles.get_computed',
|
|
50
|
+
'styles.get_matched_rules',
|
|
51
|
+
'screenshot.capture_region',
|
|
52
|
+
'screenshot.capture_element',
|
|
53
|
+
'screenshot.capture_full_page',
|
|
54
|
+
'performance.get_metrics',
|
|
55
|
+
'cdp.get_document',
|
|
56
|
+
'cdp.get_dom_snapshot',
|
|
57
|
+
'cdp.get_box_model',
|
|
58
|
+
'cdp.get_computed_styles_for_node',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {BridgeMethod} method
|
|
63
|
+
* @param {Record<string, unknown>} params
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
export function isRetrySafeBridgeMethod(method, params) {
|
|
67
|
+
if (method === 'page.get_console' || method === 'page.get_network') {
|
|
68
|
+
return params.clear !== true;
|
|
69
|
+
}
|
|
70
|
+
return RETRY_SAFE_METHODS.has(method);
|
|
71
|
+
}
|
|
72
|
+
|
|
27
73
|
/**
|
|
28
74
|
* @typedef {{
|
|
29
75
|
* content: Array<{ type: 'text', text: string }>,
|
|
@@ -246,7 +292,7 @@ export function applyHtmlBudgetPreset(args) {
|
|
|
246
292
|
export async function requestBridgeWithRetry(client, method, params, options) {
|
|
247
293
|
const response = await requestBridge(client, method, params, options);
|
|
248
294
|
const recovery = !response.ok && response.error ? getErrorRecovery(response.error.code) : null;
|
|
249
|
-
if (!response.ok && recovery?.retry) {
|
|
295
|
+
if (!response.ok && recovery?.retry && isRetrySafeBridgeMethod(method, params)) {
|
|
250
296
|
const delay = recovery.retryAfterMs ?? 1000;
|
|
251
297
|
process.stderr.write(
|
|
252
298
|
`[bbx-mcp] Retrying ${method} after ${delay}ms (${response.error.code})\n`
|