@browserbridge/bbx 1.7.3 → 1.7.5

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.
Files changed (29) hide show
  1. package/package.json +7 -7
  2. package/packages/agent-client/src/atomic-write.js +99 -0
  3. package/packages/agent-client/src/cli-helpers.js +20 -3
  4. package/packages/agent-client/src/cli.js +273 -76
  5. package/packages/agent-client/src/command-registry.js +12 -6
  6. package/packages/agent-client/src/install.js +39 -4
  7. package/packages/agent-client/src/mcp-config.js +28 -14
  8. package/packages/agent-client/src/remotes.js +93 -15
  9. package/packages/agent-client/src/runtime.js +6 -3
  10. package/packages/mcp-server/src/bin.js +10 -1
  11. package/packages/mcp-server/src/guidance.js +2 -2
  12. package/packages/mcp-server/src/handlers-capture.js +62 -32
  13. package/packages/mcp-server/src/handlers-dom.js +2 -2
  14. package/packages/mcp-server/src/handlers-navigation.js +30 -9
  15. package/packages/mcp-server/src/handlers-page.js +170 -143
  16. package/packages/mcp-server/src/handlers-utils.js +277 -8
  17. package/packages/mcp-server/src/handlers.js +31 -10
  18. package/packages/mcp-server/src/lifecycle.js +413 -0
  19. package/packages/mcp-server/src/server.js +59 -30
  20. package/packages/native-host/src/daemon.js +3 -1
  21. package/packages/protocol/src/capabilities.js +1 -1
  22. package/packages/protocol/src/defaults.js +2 -0
  23. package/packages/protocol/src/protocol.js +122 -9
  24. package/packages/protocol/src/registry.js +14 -14
  25. package/packages/protocol/src/summary.js +61 -30
  26. package/packages/protocol/src/types.ts +30 -1
  27. package/skills/browser-bridge/SKILL.md +4 -4
  28. package/skills/browser-bridge/references/interaction.md +1 -1
  29. package/skills/browser-bridge/references/protocol.md +3 -3
@@ -0,0 +1,413 @@
1
+ // @ts-check
2
+
3
+ import { randomUUID } from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import net from 'node:net';
6
+ import path from 'node:path';
7
+
8
+ import { getBridgeDir } from '../../native-host/src/config.js';
9
+
10
+ const MCP_CONTROL_PROTOCOL_VERSION = 1;
11
+ const MCP_CONTROL_TIMEOUT_MS = 1_000;
12
+ const MCP_REGISTRY_DIRNAME = 'mcp-processes';
13
+
14
+ /**
15
+ * @typedef {{
16
+ * protocolVersion: number,
17
+ * instanceId: string,
18
+ * pid: number,
19
+ * port: number,
20
+ * token: string,
21
+ * }} McpProcessRegistration
22
+ */
23
+
24
+ /**
25
+ * @typedef {{
26
+ * registryDir?: string,
27
+ * onRestart?: () => void,
28
+ * }} StartMcpProcessControlOptions
29
+ */
30
+
31
+ /**
32
+ * @typedef {{
33
+ * registryDir?: string,
34
+ * timeoutMs?: number,
35
+ * }} RestartMcpProcessesOptions
36
+ */
37
+
38
+ /**
39
+ * @param {NodeJS.ProcessEnv} [env=process.env]
40
+ * @returns {string}
41
+ */
42
+ export function getMcpProcessRegistryDir(env = process.env) {
43
+ return path.join(getBridgeDir(env), MCP_REGISTRY_DIRNAME);
44
+ }
45
+
46
+ /**
47
+ * Register a private localhost control endpoint for this MCP process. The
48
+ * endpoint lets `bbx restart` retire the loaded MCP code without relying on a
49
+ * stale PID that may have been reused by an unrelated process.
50
+ *
51
+ * @param {StartMcpProcessControlOptions} [options={}]
52
+ * @returns {Promise<{ registrationPath: string, dispose: () => Promise<void> }>}
53
+ */
54
+ export async function startMcpProcessControl(options = {}) {
55
+ const { registryDir = getMcpProcessRegistryDir(), onRestart = () => process.exit(0) } = options;
56
+ const instanceId = randomUUID();
57
+ const token = randomUUID();
58
+ const registrationPath = path.join(registryDir, `${instanceId}.json`);
59
+ let restartRequested = false;
60
+ let disposed = false;
61
+
62
+ const server = net.createServer((socket) => {
63
+ socket.setEncoding('utf8');
64
+ socket.setTimeout(MCP_CONTROL_TIMEOUT_MS, () => socket.destroy());
65
+ socket.on('error', () => socket.destroy());
66
+ let input = '';
67
+ let handled = false;
68
+
69
+ socket.on('data', (chunk) => {
70
+ if (handled) {
71
+ return;
72
+ }
73
+ input += chunk;
74
+ if (input.length > 4_096) {
75
+ socket.destroy();
76
+ return;
77
+ }
78
+
79
+ const newlineIndex = input.indexOf('\n');
80
+ if (newlineIndex === -1) {
81
+ return;
82
+ }
83
+ handled = true;
84
+
85
+ const request = parseControlRequest(input.slice(0, newlineIndex));
86
+ if (!request || request.token !== token || request.action !== 'restart') {
87
+ socket.end(`${JSON.stringify({ ok: false })}\n`);
88
+ return;
89
+ }
90
+ if (restartRequested) {
91
+ socket.end(`${JSON.stringify({ ok: true })}\n`);
92
+ return;
93
+ }
94
+
95
+ restartRequested = true;
96
+ removeRegistrationSync(registrationPath);
97
+ server.close();
98
+ socket.end(`${JSON.stringify({ ok: true })}\n`, onRestart);
99
+ });
100
+ });
101
+
102
+ try {
103
+ await listenOnLoopback(server);
104
+ const address = server.address();
105
+ if (!address || typeof address === 'string') {
106
+ throw new Error('MCP process control endpoint did not expose a TCP port.');
107
+ }
108
+
109
+ const registration = {
110
+ protocolVersion: MCP_CONTROL_PROTOCOL_VERSION,
111
+ instanceId,
112
+ pid: process.pid,
113
+ port: address.port,
114
+ token,
115
+ };
116
+ await writeRegistration(registrationPath, registration);
117
+ } catch (error) {
118
+ server.close();
119
+ removeRegistrationSync(registrationPath);
120
+ throw error;
121
+ }
122
+
123
+ const cleanupOnExit = () => removeRegistrationSync(registrationPath);
124
+ const cleanupOnStdinEnd = () => {
125
+ void dispose();
126
+ };
127
+ process.once('exit', cleanupOnExit);
128
+ process.stdin.once('end', cleanupOnStdinEnd);
129
+
130
+ async function dispose() {
131
+ if (disposed) {
132
+ return;
133
+ }
134
+ disposed = true;
135
+ process.removeListener('exit', cleanupOnExit);
136
+ process.stdin.removeListener('end', cleanupOnStdinEnd);
137
+ removeRegistrationSync(registrationPath);
138
+ await closeServer(server);
139
+ }
140
+
141
+ return { registrationPath, dispose };
142
+ }
143
+
144
+ /**
145
+ * Lifecycle coordination must never prevent the stdio MCP server from serving.
146
+ * Sandboxed agents and read-only installations can legitimately reject the
147
+ * loopback listener or registry write.
148
+ *
149
+ * @param {StartMcpProcessControlOptions} [options={}]
150
+ * @returns {Promise<{ registrationPath: string, dispose: () => Promise<void> } | null>}
151
+ */
152
+ export async function tryStartMcpProcessControl(options = {}) {
153
+ try {
154
+ return await startMcpProcessControl(options);
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Ask every registered MCP process to exit so its owning agent can launch the
162
+ * current installed version. Registrations are removed only when the process is
163
+ * gone; transient control failures remain visible for a later retry.
164
+ *
165
+ * @param {RestartMcpProcessesOptions} [options={}]
166
+ * @returns {Promise<{ registered: number, restartRequested: number, restartFailed: number, staleRegistrationsRemoved: number }>}
167
+ */
168
+ export async function restartRegisteredMcpProcesses(options = {}) {
169
+ const { registryDir = getMcpProcessRegistryDir(), timeoutMs = MCP_CONTROL_TIMEOUT_MS } = options;
170
+ let entries;
171
+ try {
172
+ entries = (await fs.promises.readdir(registryDir, { withFileTypes: true })).filter(
173
+ (entry) => entry.isFile() && entry.name.endsWith('.json')
174
+ );
175
+ } catch (error) {
176
+ if (isMissingFileError(error)) {
177
+ return {
178
+ registered: 0,
179
+ restartRequested: 0,
180
+ restartFailed: 0,
181
+ staleRegistrationsRemoved: 0,
182
+ };
183
+ }
184
+ throw error;
185
+ }
186
+
187
+ let restartRequested = 0;
188
+ let restartFailed = 0;
189
+ let staleRegistrationsRemoved = 0;
190
+ await Promise.all(
191
+ entries.map(async (entry) => {
192
+ const registrationPath = path.join(registryDir, entry.name);
193
+ const registration = await readRegistration(registrationPath);
194
+ if (!registration) {
195
+ await fs.promises.rm(registrationPath, { force: true });
196
+ staleRegistrationsRemoved += 1;
197
+ return;
198
+ }
199
+
200
+ const accepted = await requestProcessRestart(registration, timeoutMs).catch(() => false);
201
+ if (!accepted) {
202
+ if (isProcessRunning(registration.pid)) {
203
+ restartFailed += 1;
204
+ return;
205
+ }
206
+ await fs.promises.rm(registrationPath, { force: true });
207
+ staleRegistrationsRemoved += 1;
208
+ return;
209
+ }
210
+
211
+ restartRequested += 1;
212
+ await fs.promises.rm(registrationPath, { force: true });
213
+ })
214
+ );
215
+
216
+ return {
217
+ registered: entries.length,
218
+ restartRequested,
219
+ restartFailed,
220
+ staleRegistrationsRemoved,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * @param {net.Server} server
226
+ * @returns {Promise<void>}
227
+ */
228
+ function listenOnLoopback(server) {
229
+ return new Promise((resolve, reject) => {
230
+ /** @param {Error} error */
231
+ const onError = (error) => {
232
+ server.removeListener('listening', onListening);
233
+ reject(error);
234
+ };
235
+ const onListening = () => {
236
+ server.removeListener('error', onError);
237
+ resolve();
238
+ };
239
+ server.once('error', onError);
240
+ server.once('listening', onListening);
241
+ server.listen(0, '127.0.0.1');
242
+ });
243
+ }
244
+
245
+ /**
246
+ * @param {net.Server} server
247
+ * @returns {Promise<void>}
248
+ */
249
+ function closeServer(server) {
250
+ if (!server.listening) {
251
+ return Promise.resolve();
252
+ }
253
+ return new Promise((resolve, reject) => {
254
+ server.close((error) => {
255
+ if (error) {
256
+ reject(error);
257
+ return;
258
+ }
259
+ resolve();
260
+ });
261
+ });
262
+ }
263
+
264
+ /**
265
+ * @param {string} registrationPath
266
+ * @param {McpProcessRegistration} registration
267
+ * @returns {Promise<void>}
268
+ */
269
+ async function writeRegistration(registrationPath, registration) {
270
+ await fs.promises.mkdir(path.dirname(registrationPath), { recursive: true });
271
+ const temporaryPath = `${registrationPath}.${process.pid}.tmp`;
272
+ await fs.promises.writeFile(temporaryPath, `${JSON.stringify(registration)}\n`, {
273
+ encoding: 'utf8',
274
+ mode: 0o600,
275
+ });
276
+ await fs.promises.rename(temporaryPath, registrationPath);
277
+ }
278
+
279
+ /**
280
+ * @param {string} registrationPath
281
+ * @returns {Promise<McpProcessRegistration | null>}
282
+ */
283
+ async function readRegistration(registrationPath) {
284
+ try {
285
+ const parsed = JSON.parse(await fs.promises.readFile(registrationPath, 'utf8'));
286
+ if (
287
+ !parsed ||
288
+ typeof parsed !== 'object' ||
289
+ parsed.protocolVersion !== MCP_CONTROL_PROTOCOL_VERSION ||
290
+ typeof parsed.instanceId !== 'string' ||
291
+ !Number.isInteger(parsed.pid) ||
292
+ parsed.pid <= 0 ||
293
+ !Number.isInteger(parsed.port) ||
294
+ parsed.port <= 0 ||
295
+ parsed.port > 65_535 ||
296
+ typeof parsed.token !== 'string' ||
297
+ parsed.token.length === 0
298
+ ) {
299
+ return null;
300
+ }
301
+ return parsed;
302
+ } catch {
303
+ return null;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * @param {McpProcessRegistration} registration
309
+ * @param {number} timeoutMs
310
+ * @returns {Promise<boolean>}
311
+ */
312
+ function requestProcessRestart(registration, timeoutMs) {
313
+ return new Promise((resolve, reject) => {
314
+ const socket = net.createConnection({ host: '127.0.0.1', port: registration.port });
315
+ let response = '';
316
+ let settled = false;
317
+
318
+ /**
319
+ * @param {Error | null} error
320
+ * @param {boolean} [accepted]
321
+ */
322
+ const finish = (error, accepted = false) => {
323
+ if (settled) {
324
+ return;
325
+ }
326
+ settled = true;
327
+ socket.destroy();
328
+ if (error) {
329
+ reject(error);
330
+ return;
331
+ }
332
+ resolve(accepted);
333
+ };
334
+
335
+ socket.setEncoding('utf8');
336
+ socket.setTimeout(timeoutMs, () => finish(new Error('MCP process control request timed out.')));
337
+ socket.once('error', (error) => finish(error));
338
+ socket.once('connect', () => {
339
+ socket.write(`${JSON.stringify({ action: 'restart', token: registration.token })}\n`);
340
+ });
341
+ socket.on('data', (chunk) => {
342
+ response += chunk;
343
+ const newlineIndex = response.indexOf('\n');
344
+ if (newlineIndex === -1) {
345
+ return;
346
+ }
347
+ try {
348
+ const result = JSON.parse(response.slice(0, newlineIndex));
349
+ finish(null, Boolean(result && typeof result === 'object' && result.ok === true));
350
+ } catch (error) {
351
+ finish(error instanceof Error ? error : new Error(String(error)));
352
+ }
353
+ });
354
+ socket.once('close', () => finish(new Error('MCP process control connection closed.')));
355
+ });
356
+ }
357
+
358
+ /**
359
+ * @param {string} input
360
+ * @returns {{ action: string, token: string } | null}
361
+ */
362
+ function parseControlRequest(input) {
363
+ try {
364
+ const parsed = JSON.parse(input);
365
+ if (
366
+ !parsed ||
367
+ typeof parsed !== 'object' ||
368
+ typeof parsed.action !== 'string' ||
369
+ typeof parsed.token !== 'string'
370
+ ) {
371
+ return null;
372
+ }
373
+ return parsed;
374
+ } catch {
375
+ return null;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * @param {string} registrationPath
381
+ */
382
+ function removeRegistrationSync(registrationPath) {
383
+ try {
384
+ fs.rmSync(registrationPath, { force: true });
385
+ } catch {
386
+ // Cleanup is best-effort during process shutdown.
387
+ }
388
+ }
389
+
390
+ /**
391
+ * @param {unknown} error
392
+ * @returns {boolean}
393
+ */
394
+ function isMissingFileError(error) {
395
+ return Boolean(
396
+ error && typeof error === 'object' && 'code' in error && String(error.code) === 'ENOENT'
397
+ );
398
+ }
399
+
400
+ /**
401
+ * @param {number} pid
402
+ * @returns {boolean}
403
+ */
404
+ function isProcessRunning(pid) {
405
+ try {
406
+ process.kill(pid, 0);
407
+ return true;
408
+ } catch (error) {
409
+ return Boolean(
410
+ error && typeof error === 'object' && 'code' in error && String(error.code) === 'EPERM'
411
+ );
412
+ }
413
+ }
@@ -32,11 +32,14 @@ import {
32
32
  import {
33
33
  BUDGET_PRESETS,
34
34
  DEFAULT_CONSOLE_LIMIT,
35
+ DEFAULT_EVAL_TIMEOUT_MS,
36
+ DEFAULT_LOG_TAIL_LIMIT,
35
37
  DEFAULT_MAX_DEPTH,
36
38
  DEFAULT_MAX_HTML_LENGTH,
37
39
  DEFAULT_MAX_NODES,
38
40
  DEFAULT_PAGE_TEXT_BUDGET,
39
41
  DEFAULT_TEXT_BUDGET,
42
+ DEFAULT_NAV_TIMEOUT_MS,
40
43
  DEFAULT_WAIT_TIMEOUT_MS,
41
44
  getMethodsByMaxComplexity,
42
45
  } from '../../protocol/src/index.js';
@@ -113,9 +116,12 @@ export function createBridgeMcpServer() {
113
116
  {
114
117
  title: 'Browser Bridge Status',
115
118
  description:
116
- 'Check bridge readiness: daemon connectivity, extension state, and window access. Call first to confirm the bridge is usable. If access is not enabled, ask the user to click Enable in the extension popup or side panel, then retry.',
119
+ 'Check bridge readiness: daemon connectivity, extension state, and window access. Omit destinationId to inspect local and configured remote destinations. If access is not enabled, ask the user to click Enable in the extension popup or side panel, then retry.',
117
120
  inputSchema: {
118
- destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
121
+ destinationId: z
122
+ .string()
123
+ .optional()
124
+ .describe('Inspect one destination instead of aggregating all configured destinations.'),
119
125
  },
120
126
  },
121
127
  handleStatusTool
@@ -147,7 +153,7 @@ export function createBridgeMcpServer() {
147
153
  .int()
148
154
  .positive()
149
155
  .optional()
150
- .describe(`Maximum log entries to return (default: ${DEFAULT_CONSOLE_LIMIT})`),
156
+ .describe(`Maximum log entries to return (default: ${DEFAULT_LOG_TAIL_LIMIT})`),
151
157
  budgetPreset: z
152
158
  .enum(['quick', 'normal', 'deep'])
153
159
  .optional()
@@ -175,15 +181,23 @@ export function createBridgeMcpServer() {
175
181
  {
176
182
  title: 'Browser Tabs',
177
183
  description:
178
- 'List, create, or close browser tabs. Prefer "list" to work in existing tabs; only use "create" when the user explicitly requests a new page.',
184
+ 'List, create, close, or activate browser tabs. List without destinationId aggregates configured destinations; other actions default to local. Only create a page when the user explicitly requests it.',
179
185
  inputSchema: {
180
186
  action: z
181
- .enum(['list', 'create', 'close'])
182
- .describe('"list" (preferred), "create" (only when needed), or "close"'),
187
+ .enum(['list', 'create', 'close', 'activate'])
188
+ .describe('"list" (preferred), "create" (only when needed), "close", or "activate"'),
183
189
  url: z.string().optional().describe('URL for create action'),
184
190
  active: z.boolean().optional().describe('Focus the new tab (default: true)'),
185
- tabId: z.number().int().positive().optional().describe('Tab ID (required for close)'),
186
- destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
191
+ tabId: z
192
+ .number()
193
+ .int()
194
+ .positive()
195
+ .optional()
196
+ .describe('Tab ID (required for close/activate)'),
197
+ destinationId: z
198
+ .string()
199
+ .optional()
200
+ .describe('Target one destination; list without it aggregates configured destinations.'),
187
201
  },
188
202
  },
189
203
  handleTabsTool
@@ -194,7 +208,7 @@ export function createBridgeMcpServer() {
194
208
  {
195
209
  title: 'Browser DOM',
196
210
  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 — use query/find first.',
211
+ '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
212
  inputSchema: {
199
213
  action: z
200
214
  .enum([
@@ -270,7 +284,9 @@ export function createBridgeMcpServer() {
270
284
  state: z
271
285
  .enum(['attached', 'detached', 'visible', 'hidden'])
272
286
  .optional()
273
- .describe('Expected element state (for wait action)'),
287
+ .describe(
288
+ 'Expected element state (for wait action); hidden also matches complete absence'
289
+ ),
274
290
  timeoutMs: z
275
291
  .number()
276
292
  .int()
@@ -334,7 +350,7 @@ export function createBridgeMcpServer() {
334
350
  {
335
351
  title: 'Browser Page State',
336
352
  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 — prefer lighter reads first.',
353
+ '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
354
  inputSchema: {
339
355
  action: z
340
356
  .enum([
@@ -364,15 +380,19 @@ export function createBridgeMcpServer() {
364
380
  .int()
365
381
  .positive()
366
382
  .optional()
367
- .describe(`Timeout for evaluate/wait operations (default: ${DEFAULT_WAIT_TIMEOUT_MS})`),
383
+ .describe(
384
+ `Timeout (default: ${DEFAULT_EVAL_TIMEOUT_MS} for evaluate, ${DEFAULT_NAV_TIMEOUT_MS} for wait_for_load)`
385
+ ),
368
386
  returnByValue: z
369
- .boolean()
387
+ .literal(true)
370
388
  .optional()
371
- .describe('Return actual value vs JSON (default: true)'),
389
+ .describe('Return serializable values (only true is supported; default: true)'),
372
390
  level: z
373
- .string()
391
+ .enum(['all', 'debug', 'log', 'info', 'warn', 'error', 'exception', 'rejection'])
374
392
  .optional()
375
- .describe('Minimum console level: log, warn, error (default: all)'),
393
+ .describe(
394
+ 'Minimum console severity (default: all); exception/rejection select that exact stream'
395
+ ),
376
396
  clear: z.boolean().optional().describe('Clear buffer after reading (default: false)'),
377
397
  limit: z
378
398
  .number()
@@ -405,7 +425,7 @@ export function createBridgeMcpServer() {
405
425
  {
406
426
  title: 'Browser Navigation',
407
427
  description:
408
- 'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed — use only for exact viewport overrides.',
428
+ 'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed - use only for exact viewport overrides.',
409
429
  inputSchema: {
410
430
  action: z
411
431
  .enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
@@ -423,7 +443,7 @@ export function createBridgeMcpServer() {
423
443
  .int()
424
444
  .positive()
425
445
  .optional()
426
- .describe('Timeout for navigation (default: 30000)'),
446
+ .describe(`Timeout for navigation (default: ${DEFAULT_NAV_TIMEOUT_MS})`),
427
447
  top: z.number().optional().describe('Scroll target Y position (pixels)'),
428
448
  left: z.number().optional().describe('Scroll target X position (pixels)'),
429
449
  behavior: z.enum(['auto', 'smooth']).optional().describe('Scroll behavior (default: auto)'),
@@ -456,6 +476,7 @@ export function createBridgeMcpServer() {
456
476
  'click',
457
477
  'focus',
458
478
  'type',
479
+ 'fill',
459
480
  'press_key',
460
481
  'cdp_press_key',
461
482
  'set_checked',
@@ -488,6 +509,11 @@ export function createBridgeMcpServer() {
488
509
  .optional()
489
510
  .describe('Click count (1=single, 2=double)'),
490
511
  text: z.string().max(100000).optional().describe('Text to type (for type action)'),
512
+ value: z.string().max(100000).optional().describe('Value to set (for fill action)'),
513
+ mode: z
514
+ .enum(['auto', 'setter', 'keystrokes'])
515
+ .optional()
516
+ .describe('Fill strategy (default: auto)'),
491
517
  clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
492
518
  submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
493
519
  key: z
@@ -517,7 +543,7 @@ export function createBridgeMcpServer() {
517
543
  .int()
518
544
  .nonnegative()
519
545
  .optional()
520
- .describe('Hover duration in ms (default: 100)'),
546
+ .describe('Hover duration in ms (default: 0)'),
521
547
  sourceElementRef: z.string().optional().describe('Drag source element (for drag action)'),
522
548
  sourceSelector: z
523
549
  .string()
@@ -565,19 +591,12 @@ export function createBridgeMcpServer() {
565
591
  .describe('CSS property: value pairs (for apply_styles)'),
566
592
  important: z.boolean().optional().describe('Add !important flag (default: false)'),
567
593
  operation: z
568
- .enum([
569
- 'setAttribute',
570
- 'removeAttribute',
571
- 'addClass',
572
- 'removeClass',
573
- 'setTextContent',
574
- 'setProperty',
575
- ])
594
+ .enum(['setAttribute', 'removeAttribute', 'addClass', 'removeClass', 'setTextContent'])
576
595
  .optional()
577
596
  .describe('DOM mutation type'),
578
597
  value: z.unknown().optional().describe('Value for the DOM operation'),
579
598
  name: z.string().optional().describe('Attribute/class/property name (for apply_dom)'),
580
- patchId: z.string().optional().describe('Patch ID to rollback (omit for most recent)'),
599
+ patchId: z.string().optional().describe('Patch ID (required for rollback)'),
581
600
  verify: z
582
601
  .boolean()
583
602
  .optional()
@@ -594,7 +613,7 @@ export function createBridgeMcpServer() {
594
613
  {
595
614
  title: 'Browser Capture',
596
615
  description:
597
- '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.',
616
+ '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
617
  inputSchema: {
599
618
  action: z
600
619
  .enum([
@@ -645,7 +664,13 @@ export function createBridgeMcpServer() {
645
664
  {
646
665
  title: 'Browser Bridge Batch',
647
666
  description:
648
- '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.',
667
+ '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.',
668
+ annotations: {
669
+ readOnlyHint: true,
670
+ destructiveHint: false,
671
+ idempotentHint: true,
672
+ openWorldHint: true,
673
+ },
649
674
  inputSchema: {
650
675
  calls: z
651
676
  .array(
@@ -685,6 +710,10 @@ export function createBridgeMcpServer() {
685
710
  .describe('Method parameters as object'),
686
711
  tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
687
712
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
713
+ budgetPreset: z
714
+ .enum(['quick', 'normal', 'deep'])
715
+ .optional()
716
+ .describe(BUDGET_PRESET_DESCRIPTION),
688
717
  },
689
718
  },
690
719
  handleRawCallTool
@@ -602,7 +602,9 @@ export class BridgeDaemon {
602
602
 
603
603
  if (message?.type === 'agent.request') {
604
604
  const isExtensionSetupRequest =
605
- socket.__role === 'extension' && message.request?.method === 'setup.install';
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) — distinct from
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,
@@ -20,6 +20,8 @@ export const DEFAULT_CONSOLE_LIMIT = 50;
20
20
  export const DEFAULT_VIEWPORT_WIDTH = 1280;
21
21
  export const DEFAULT_VIEWPORT_HEIGHT = 720;
22
22
  export const DEFAULT_DEVICE_SCALE_FACTOR = 0;
23
+ /** @type {'continue'} */
24
+ export const DEFAULT_NETWORK_INTERCEPT_ACTION = 'continue';
23
25
 
24
26
  /** Maximum size of a Chrome native messaging message in bytes. */
25
27
  export const MAX_NATIVE_MESSAGE_BYTES = 1_048_576;