@browserbridge/bbx 1.7.2 → 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 +10 -9
- package/packages/agent-client/src/cli.js +8 -2
- package/packages/agent-client/src/mcp-config.js +55 -55
- package/packages/agent-client/src/runtime.js +20 -1
- package/packages/mcp-server/src/guidance.js +4 -4
- package/packages/mcp-server/src/handlers-capture.js +221 -30
- package/packages/mcp-server/src/handlers-navigation.js +10 -0
- package/packages/mcp-server/src/handlers-page.js +154 -67
- package/packages/mcp-server/src/handlers-utils.js +27 -68
- package/packages/mcp-server/src/handlers.js +2 -0
- package/packages/mcp-server/src/server.js +38 -13
- package/packages/native-host/src/daemon-process.js +16 -8
- package/packages/native-host/src/daemon.js +201 -79
- package/packages/protocol/src/capabilities.js +1 -1
- package/packages/protocol/src/defaults.js +17 -0
- package/packages/protocol/src/protocol.js +60 -5
- package/packages/protocol/src/registry.js +15 -15
- package/packages/protocol/src/summary.js +61 -30
- package/packages/protocol/src/types.ts +11 -0
- package/skills/browser-bridge/SKILL.md +15 -11
- package/skills/browser-bridge/references/interaction.md +11 -8
- package/skills/browser-bridge/references/protocol.md +9 -5
- package/skills/browser-bridge/references/token-efficiency.md +5 -4
- package/skills/browser-bridge/references/ui-workflows.md +11 -9
|
@@ -76,7 +76,10 @@ export function isRetrySafeBridgeMethod(method, params) {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* @typedef {{
|
|
79
|
-
* content:
|
|
79
|
+
* content: [
|
|
80
|
+
* { type: 'text', text: string },
|
|
81
|
+
* ...Array<{ type: 'image', data: string, mimeType: string }>
|
|
82
|
+
* ],
|
|
80
83
|
* structuredContent: Record<string, unknown>,
|
|
81
84
|
* isError?: boolean
|
|
82
85
|
* }} ToolResult
|
|
@@ -86,11 +89,20 @@ export function isRetrySafeBridgeMethod(method, params) {
|
|
|
86
89
|
* @param {string} summary
|
|
87
90
|
* @param {Record<string, unknown>} [structuredContent={}]
|
|
88
91
|
* @param {boolean} [isError=false]
|
|
92
|
+
* @param {Array<{ type: 'image', data: string, mimeType: string }>} [additionalContent=[]]
|
|
89
93
|
* @returns {ToolResult}
|
|
90
94
|
*/
|
|
91
|
-
export function createToolResult(
|
|
95
|
+
export function createToolResult(
|
|
96
|
+
summary,
|
|
97
|
+
structuredContent = {},
|
|
98
|
+
isError = false,
|
|
99
|
+
additionalContent = []
|
|
100
|
+
) {
|
|
92
101
|
const toolResult = {
|
|
93
|
-
content:
|
|
102
|
+
content: /** @type {ToolResult['content']} */ ([
|
|
103
|
+
{ type: 'text', text: summary },
|
|
104
|
+
...additionalContent,
|
|
105
|
+
]),
|
|
94
106
|
structuredContent,
|
|
95
107
|
...(isError ? { isError: true } : {}),
|
|
96
108
|
};
|
|
@@ -122,77 +134,22 @@ export function summarizeToolResponse(response, method) {
|
|
|
122
134
|
*/
|
|
123
135
|
export function summarizeToolError(error) {
|
|
124
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);
|
|
125
141
|
return createToolResult(
|
|
126
|
-
|
|
142
|
+
`${code}: ${message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
|
|
127
143
|
{
|
|
128
144
|
ok: false,
|
|
129
145
|
evidence: null,
|
|
146
|
+
error: { code, message },
|
|
147
|
+
recovery,
|
|
130
148
|
},
|
|
131
149
|
true
|
|
132
150
|
);
|
|
133
151
|
}
|
|
134
152
|
|
|
135
|
-
/**
|
|
136
|
-
* @param {ToolResult} result
|
|
137
|
-
* @returns {boolean}
|
|
138
|
-
*/
|
|
139
|
-
function shouldTryRemoteFallback(result) {
|
|
140
|
-
if (!result.isError && result.structuredContent.ok !== false) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
const text = result.content[0]?.text ?? '';
|
|
144
|
-
return /\b(ACCESS_DENIED|DAEMON_OFFLINE|EXTENSION_DISCONNECTED|NATIVE_HOST_UNAVAILABLE|CONNECTION_LOST)\b|\b(ENOENT|ECONNREFUSED|ETIMEDOUT)\b|ERROR: connect\b/u.test(
|
|
145
|
-
text
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* @param {ToolResult} result
|
|
151
|
-
* @param {string} destinationId
|
|
152
|
-
* @returns {ToolResult}
|
|
153
|
-
*/
|
|
154
|
-
function annotateAutoDestination(result, destinationId) {
|
|
155
|
-
return {
|
|
156
|
-
...result,
|
|
157
|
-
structuredContent: {
|
|
158
|
-
...result.structuredContent,
|
|
159
|
-
destinationId,
|
|
160
|
-
autoSelectedDestinationId: destinationId,
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @param {(client: import('../../agent-client/src/client.js').BridgeClient) => Promise<ToolResult>} callback
|
|
167
|
-
* @param {ToolResult} localResult
|
|
168
|
-
* @returns {Promise<ToolResult>}
|
|
169
|
-
*/
|
|
170
|
-
async function tryRemoteFallback(callback, localResult) {
|
|
171
|
-
const remotes = (await listBridgeDestinations()).filter((destination) => !destination.local);
|
|
172
|
-
if (remotes.length === 0) {
|
|
173
|
-
return localResult;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
let lastFallbackResult = localResult;
|
|
177
|
-
for (const remote of remotes) {
|
|
178
|
-
const client = await createBridgeClientForDestination(remote.id);
|
|
179
|
-
try {
|
|
180
|
-
await client.connect();
|
|
181
|
-
const result = annotateAutoDestination(await callback(client), remote.id);
|
|
182
|
-
lastFallbackResult = result;
|
|
183
|
-
if (!shouldTryRemoteFallback(result)) {
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
186
|
-
} catch (error) {
|
|
187
|
-
lastFallbackResult = annotateAutoDestination(summarizeToolError(error), remote.id);
|
|
188
|
-
} finally {
|
|
189
|
-
await client.close();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return lastFallbackResult;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
153
|
/**
|
|
197
154
|
* @param {(client: import('../../agent-client/src/client.js').BridgeClient) => Promise<ToolResult>} callback
|
|
198
155
|
* @param {{ destinationId?: string | null }} [options]
|
|
@@ -201,8 +158,7 @@ async function tryRemoteFallback(callback, localResult) {
|
|
|
201
158
|
export async function withToolClient(callback, options = {}) {
|
|
202
159
|
try {
|
|
203
160
|
if (!options.destinationId) {
|
|
204
|
-
|
|
205
|
-
return shouldTryRemoteFallback(result) ? await tryRemoteFallback(callback, result) : result;
|
|
161
|
+
return await withBridgeClient(callback);
|
|
206
162
|
}
|
|
207
163
|
const client = await createBridgeClientForDestination(options.destinationId);
|
|
208
164
|
await client.connect();
|
|
@@ -367,7 +323,10 @@ export function applyHtmlBudgetPreset(args) {
|
|
|
367
323
|
*/
|
|
368
324
|
export async function requestBridgeWithRetry(client, method, params, options) {
|
|
369
325
|
const response = await requestBridge(client, method, params, options);
|
|
370
|
-
const recovery =
|
|
326
|
+
const recovery =
|
|
327
|
+
!response.ok && response.error
|
|
328
|
+
? (response.error.recovery ?? getErrorRecovery(response.error.code))
|
|
329
|
+
: null;
|
|
371
330
|
if (!response.ok && recovery?.retry && isRetrySafeBridgeMethod(method, params)) {
|
|
372
331
|
const delay = recovery.retryAfterMs ?? 1000;
|
|
373
332
|
process.stderr.write(
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
handleStylesLayoutTool,
|
|
28
28
|
handleTabsTool,
|
|
29
29
|
handleInvestigateTool,
|
|
30
|
+
MAX_BATCH_CALLS,
|
|
30
31
|
} from './handlers.js';
|
|
31
32
|
import {
|
|
32
33
|
BUDGET_PRESETS,
|
|
@@ -40,7 +41,7 @@ import {
|
|
|
40
41
|
getMethodsByMaxComplexity,
|
|
41
42
|
} from '../../protocol/src/index.js';
|
|
42
43
|
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
43
|
-
import { MCP_SERVER_INSTRUCTIONS } from './guidance.js';
|
|
44
|
+
import { MCP_SERVER_INSTRUCTIONS, registerBridgeMcpGuidance } from './guidance.js';
|
|
44
45
|
|
|
45
46
|
export const BUDGET_PRESET_DESCRIPTION = `Budget preset: "quick", "normal", or "deep" (defaults: query ${BUDGET_PRESETS.normal.maxNodes} nodes / depth ${BUDGET_PRESETS.normal.maxDepth} / text ${BUDGET_PRESETS.normal.textBudget}). Numeric fields override the preset when both are provided.`;
|
|
46
47
|
export const TAB_ID_DESCRIPTION =
|
|
@@ -105,6 +106,8 @@ export function createBridgeMcpServer() {
|
|
|
105
106
|
}
|
|
106
107
|
);
|
|
107
108
|
|
|
109
|
+
registerBridgeMcpGuidance(server);
|
|
110
|
+
|
|
108
111
|
server.registerTool(
|
|
109
112
|
'browser_status',
|
|
110
113
|
{
|
|
@@ -172,14 +175,19 @@ export function createBridgeMcpServer() {
|
|
|
172
175
|
{
|
|
173
176
|
title: 'Browser Tabs',
|
|
174
177
|
description:
|
|
175
|
-
'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.',
|
|
176
179
|
inputSchema: {
|
|
177
180
|
action: z
|
|
178
|
-
.enum(['list', 'create', 'close'])
|
|
179
|
-
.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"'),
|
|
180
183
|
url: z.string().optional().describe('URL for create action'),
|
|
181
184
|
active: z.boolean().optional().describe('Focus the new tab (default: true)'),
|
|
182
|
-
tabId: z
|
|
185
|
+
tabId: z
|
|
186
|
+
.number()
|
|
187
|
+
.int()
|
|
188
|
+
.positive()
|
|
189
|
+
.optional()
|
|
190
|
+
.describe('Tab ID (required for close/activate)'),
|
|
183
191
|
destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
|
|
184
192
|
},
|
|
185
193
|
},
|
|
@@ -191,7 +199,7 @@ export function createBridgeMcpServer() {
|
|
|
191
199
|
{
|
|
192
200
|
title: 'Browser DOM',
|
|
193
201
|
description:
|
|
194
|
-
'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.',
|
|
195
203
|
inputSchema: {
|
|
196
204
|
action: z
|
|
197
205
|
.enum([
|
|
@@ -267,7 +275,9 @@ export function createBridgeMcpServer() {
|
|
|
267
275
|
state: z
|
|
268
276
|
.enum(['attached', 'detached', 'visible', 'hidden'])
|
|
269
277
|
.optional()
|
|
270
|
-
.describe(
|
|
278
|
+
.describe(
|
|
279
|
+
'Expected element state (for wait action); hidden also matches complete absence'
|
|
280
|
+
),
|
|
271
281
|
timeoutMs: z
|
|
272
282
|
.number()
|
|
273
283
|
.int()
|
|
@@ -331,7 +341,7 @@ export function createBridgeMcpServer() {
|
|
|
331
341
|
{
|
|
332
342
|
title: 'Browser Page State',
|
|
333
343
|
description:
|
|
334
|
-
'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.',
|
|
335
345
|
inputSchema: {
|
|
336
346
|
action: z
|
|
337
347
|
.enum([
|
|
@@ -367,9 +377,11 @@ export function createBridgeMcpServer() {
|
|
|
367
377
|
.optional()
|
|
368
378
|
.describe('Return actual value vs JSON (default: true)'),
|
|
369
379
|
level: z
|
|
370
|
-
.
|
|
380
|
+
.enum(['all', 'debug', 'log', 'info', 'warn', 'error', 'exception', 'rejection'])
|
|
371
381
|
.optional()
|
|
372
|
-
.describe(
|
|
382
|
+
.describe(
|
|
383
|
+
'Minimum console severity (default: all); exception/rejection select that exact stream'
|
|
384
|
+
),
|
|
373
385
|
clear: z.boolean().optional().describe('Clear buffer after reading (default: false)'),
|
|
374
386
|
limit: z
|
|
375
387
|
.number()
|
|
@@ -402,7 +414,7 @@ export function createBridgeMcpServer() {
|
|
|
402
414
|
{
|
|
403
415
|
title: 'Browser Navigation',
|
|
404
416
|
description:
|
|
405
|
-
'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.',
|
|
406
418
|
inputSchema: {
|
|
407
419
|
action: z
|
|
408
420
|
.enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
|
|
@@ -453,6 +465,7 @@ export function createBridgeMcpServer() {
|
|
|
453
465
|
'click',
|
|
454
466
|
'focus',
|
|
455
467
|
'type',
|
|
468
|
+
'fill',
|
|
456
469
|
'press_key',
|
|
457
470
|
'cdp_press_key',
|
|
458
471
|
'set_checked',
|
|
@@ -485,6 +498,11 @@ export function createBridgeMcpServer() {
|
|
|
485
498
|
.optional()
|
|
486
499
|
.describe('Click count (1=single, 2=double)'),
|
|
487
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)'),
|
|
488
506
|
clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
|
|
489
507
|
submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
|
|
490
508
|
key: z
|
|
@@ -591,7 +609,7 @@ export function createBridgeMcpServer() {
|
|
|
591
609
|
{
|
|
592
610
|
title: 'Browser Capture',
|
|
593
611
|
description:
|
|
594
|
-
'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.',
|
|
595
613
|
inputSchema: {
|
|
596
614
|
action: z
|
|
597
615
|
.enum([
|
|
@@ -642,7 +660,13 @@ export function createBridgeMcpServer() {
|
|
|
642
660
|
{
|
|
643
661
|
title: 'Browser Bridge Batch',
|
|
644
662
|
description:
|
|
645
|
-
'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
|
+
},
|
|
646
670
|
inputSchema: {
|
|
647
671
|
calls: z
|
|
648
672
|
.array(
|
|
@@ -661,6 +685,7 @@ export function createBridgeMcpServer() {
|
|
|
661
685
|
})
|
|
662
686
|
)
|
|
663
687
|
.min(1)
|
|
688
|
+
.max(MAX_BATCH_CALLS)
|
|
664
689
|
.describe('Calls to execute in parallel'),
|
|
665
690
|
},
|
|
666
691
|
},
|
|
@@ -249,11 +249,20 @@ export async function stopBridgeDaemon(options = {}) {
|
|
|
249
249
|
resolvedTransport.type === 'socket' ? resolvedTransport.socketPath : '';
|
|
250
250
|
|
|
251
251
|
const pidFromFile = await readPidFn(pidPath);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
252
|
+
const reachable = await safePingDaemon(resolvedTransport, pingDaemonFn);
|
|
253
|
+
const endpointOwnerPid =
|
|
254
|
+
pidFromFile !== null || reachable ? await findPidByTransportFn(resolvedTransport) : null;
|
|
255
|
+
/** @type {number | null} */
|
|
256
|
+
let previousPid = null;
|
|
257
|
+
let previouslyRunning = false;
|
|
258
|
+
|
|
259
|
+
if (endpointOwnerPid !== null) {
|
|
260
|
+
previousPid = endpointOwnerPid;
|
|
261
|
+
previouslyRunning = true;
|
|
262
|
+
} else if (reachable) {
|
|
263
|
+
// TCP endpoints do not expose an owner pid. In that case daemon
|
|
264
|
+
// reachability is the evidence supporting the recorded pid.
|
|
265
|
+
previousPid = pidFromFile;
|
|
257
266
|
previouslyRunning = true;
|
|
258
267
|
}
|
|
259
268
|
|
|
@@ -275,9 +284,8 @@ export async function stopBridgeDaemon(options = {}) {
|
|
|
275
284
|
sleepFn,
|
|
276
285
|
});
|
|
277
286
|
if (!stopped) {
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
// different pid owns the socket. Target the socket owner before giving up.
|
|
287
|
+
// Endpoint ownership may become discoverable after a recorded daemon pid
|
|
288
|
+
// fails to stop. Only target the verified owner before giving up.
|
|
281
289
|
const socketOwnerPid = await findPidByTransportFn(resolvedTransport);
|
|
282
290
|
if (socketOwnerPid !== null && socketOwnerPid !== previousPid) {
|
|
283
291
|
try {
|