@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.
@@ -76,7 +76,10 @@ export function isRetrySafeBridgeMethod(method, params) {
76
76
 
77
77
  /**
78
78
  * @typedef {{
79
- * content: Array<{ type: 'text', text: string }>,
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(summary, structuredContent = {}, isError = false) {
95
+ export function createToolResult(
96
+ summary,
97
+ structuredContent = {},
98
+ isError = false,
99
+ additionalContent = []
100
+ ) {
92
101
  const toolResult = {
93
- content: [{ type: /** @type {'text'} */ ('text'), text: summary }],
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
- `ERROR: ${message}`,
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
- const result = await withBridgeClient(callback);
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 = !response.ok && response.error ? getErrorRecovery(response.error.code) : null;
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(
@@ -54,6 +54,8 @@ export {
54
54
  } from './handlers-capture.js';
55
55
 
56
56
  export {
57
+ MAX_BATCH_CALLS,
58
+ MAX_BATCH_CONCURRENCY,
57
59
  PAGE_ACTIONS,
58
60
  handlePageTool,
59
61
  handleBatchTool,
@@ -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 close browser tabs. Prefer "list" to work in existing tabs; only use "create" when the user explicitly requests a new page.',
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 "close"'),
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.number().int().positive().optional().describe('Tab ID (required for close)'),
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 — use query/find first.',
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('Expected element state (for wait action)'),
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 — prefer lighter reads first.',
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
- .string()
380
+ .enum(['all', 'debug', 'log', 'info', 'warn', 'error', 'exception', 'rejection'])
371
381
  .optional()
372
- .describe('Minimum console level: log, warn, error (default: all)'),
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 — use only for exact viewport overrides.',
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 — use only when structured reads (browser_dom, browser_styles_layout) are insufficient. Prefer element, then tight region; full_page only for document-level context.',
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 multiple bridge calls in parallel to reduce round-trips. Combine independent reads (e.g., styles + text + console) in one call. Preserves call order in the response.',
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
- let previousPid = pidFromFile;
253
- let previouslyRunning = previousPid !== null;
254
-
255
- if (previousPid === null && (await safePingDaemon(resolvedTransport, pingDaemonFn))) {
256
- previousPid = await findPidByTransportFn(resolvedTransport);
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
- // The pid file can be stale (e.g. a root-owned leftover from a sudo
279
- // install that the daemon could not overwrite) while a daemon with a
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 {