@browserbridge/bbx 1.7.4 → 1.7.6

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 (34) hide show
  1. package/package.json +6 -6
  2. package/packages/agent-client/src/atomic-write.js +99 -0
  3. package/packages/agent-client/src/cli-args.js +152 -0
  4. package/packages/agent-client/src/cli-batch.js +140 -0
  5. package/packages/agent-client/src/cli-helpers.js +20 -3
  6. package/packages/agent-client/src/cli-output.js +57 -0
  7. package/packages/agent-client/src/cli-proxy-remote.js +350 -0
  8. package/packages/agent-client/src/cli-setup-commands.js +404 -0
  9. package/packages/agent-client/src/cli.js +63 -906
  10. package/packages/agent-client/src/command-registry.js +12 -6
  11. package/packages/agent-client/src/install.js +39 -4
  12. package/packages/agent-client/src/mcp-config.js +28 -14
  13. package/packages/agent-client/src/remotes.js +93 -15
  14. package/packages/agent-client/src/runtime.js +6 -4
  15. package/packages/mcp-server/src/bin.js +10 -1
  16. package/packages/mcp-server/src/guidance.js +1 -1
  17. package/packages/mcp-server/src/handlers-capture.js +8 -5
  18. package/packages/mcp-server/src/handlers-dom.js +2 -2
  19. package/packages/mcp-server/src/handlers-navigation.js +20 -9
  20. package/packages/mcp-server/src/handlers-page.js +111 -111
  21. package/packages/mcp-server/src/handlers-utils.js +268 -8
  22. package/packages/mcp-server/src/handlers.js +31 -10
  23. package/packages/mcp-server/src/lifecycle.js +413 -0
  24. package/packages/mcp-server/src/server.js +27 -19
  25. package/packages/native-host/src/auth-token.js +1 -1
  26. package/packages/native-host/src/config.js +9 -4
  27. package/packages/native-host/src/daemon-process.js +8 -1
  28. package/packages/protocol/src/capabilities.js +33 -110
  29. package/packages/protocol/src/capability-values.js +45 -0
  30. package/packages/protocol/src/defaults.js +19 -2
  31. package/packages/protocol/src/protocol.js +106 -5
  32. package/packages/protocol/src/registry.js +299 -86
  33. package/packages/protocol/src/types.ts +19 -1
  34. package/skills/browser-bridge/SKILL.md +1 -1
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import os from 'node:os';
4
- import { DEFAULT_CONSOLE_LIMIT, createRuntimeContext } from '../../protocol/src/index.js';
4
+ import { DEFAULT_LOG_TAIL_LIMIT, createRuntimeContext } from '../../protocol/src/index.js';
5
5
  import { collectSetupStatus } from '../../agent-client/src/setup-status.js';
6
6
  import {
7
7
  callBridgeTool,
@@ -82,15 +82,17 @@ export async function handleStatusTool(args = {}) {
82
82
  );
83
83
  const result = await callRemoteHealth(requestedDestinationId);
84
84
  return createToolResult(
85
- result.reachable
86
- ? `Browser Bridge destination "${requestedDestinationId}" is reachable.`
87
- : `Browser Bridge destination "${requestedDestinationId}" is not reachable.`,
85
+ result.routeReady
86
+ ? `Browser Bridge destination "${requestedDestinationId}" is ready.`
87
+ : result.reachable
88
+ ? `Browser Bridge destination "${requestedDestinationId}" is reachable, but its browser route is not ready.`
89
+ : `Browser Bridge destination "${requestedDestinationId}" is not reachable.`,
88
90
  {
89
- ok: result.reachable === true,
91
+ ok: result.routeReady === true,
90
92
  destination: destination ?? { id: requestedDestinationId, local: false },
91
93
  ...result,
92
94
  },
93
- result.reachable !== true
95
+ result.routeReady !== true
94
96
  );
95
97
  }
96
98
 
@@ -131,17 +133,36 @@ export async function handleStatusTool(args = {}) {
131
133
  */
132
134
  async function callRemoteHealth(destinationId) {
133
135
  const result = await callBridgeTool('health.ping', {}, { destinationId });
136
+ const evidence =
137
+ result.structuredContent.evidence && typeof result.structuredContent.evidence === 'object'
138
+ ? /** @type {Record<string, unknown>} */ (result.structuredContent.evidence)
139
+ : {};
140
+ const access =
141
+ evidence.access && typeof evidence.access === 'object'
142
+ ? /** @type {Record<string, unknown>} */ (evidence.access)
143
+ : {};
144
+ const reachable = result.structuredContent.ok === true && evidence.daemon === 'ok';
134
145
  return {
135
- reachable: result.structuredContent.ok === true,
146
+ reachable,
147
+ daemonReachable: reachable,
148
+ extensionConnected: evidence.extensionConnected === true,
149
+ accessEnabled: access.enabled === true,
150
+ routeReady: reachable && access.routeReady === true,
151
+ routeTabId: typeof access.routeTabId === 'number' ? access.routeTabId : null,
152
+ routeReason: typeof access.reason === 'string' ? access.reason : null,
136
153
  health: result.structuredContent,
137
154
  };
138
155
  }
139
156
 
140
157
  /**
158
+ * @param {{ destinationId?: string }} [args]
141
159
  * @returns {Promise<ToolResult>}
142
160
  */
143
- export async function handleSkillTool() {
161
+ export async function handleSkillTool(args = {}) {
144
162
  try {
163
+ if (typeof args.destinationId === 'string') {
164
+ return callBridgeTool('skill.get_runtime_context', {}, { destinationId: args.destinationId });
165
+ }
145
166
  const ctx = createRuntimeContext();
146
167
  return createToolResult('Runtime context retrieved.', {
147
168
  ok: true,
@@ -176,13 +197,13 @@ export async function handleSetupTool(args) {
176
197
  export async function handleLogTool(args) {
177
198
  const normalizedArgs = applyLimitBudgetPreset(args, {
178
199
  quick: 10,
179
- normal: DEFAULT_CONSOLE_LIMIT,
200
+ normal: DEFAULT_LOG_TAIL_LIMIT,
180
201
  deep: 100,
181
202
  });
182
203
  return callBridgeTool(
183
204
  'log.tail',
184
205
  {
185
- limit: normalizedArgs.limit ?? DEFAULT_CONSOLE_LIMIT,
206
+ limit: normalizedArgs.limit ?? DEFAULT_LOG_TAIL_LIMIT,
186
207
  },
187
208
  {
188
209
  tokenBudget: getToolTokenBudget(normalizedArgs),
@@ -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,7 +181,7 @@ export function createBridgeMcpServer() {
175
181
  {
176
182
  title: 'Browser Tabs',
177
183
  description:
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.',
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
187
  .enum(['list', 'create', 'close', 'activate'])
@@ -188,7 +194,10 @@ export function createBridgeMcpServer() {
188
194
  .positive()
189
195
  .optional()
190
196
  .describe('Tab ID (required for close/activate)'),
191
- destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
197
+ destinationId: z
198
+ .string()
199
+ .optional()
200
+ .describe('Target one destination; list without it aggregates configured destinations.'),
192
201
  },
193
202
  },
194
203
  handleTabsTool
@@ -371,11 +380,13 @@ export function createBridgeMcpServer() {
371
380
  .int()
372
381
  .positive()
373
382
  .optional()
374
- .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
+ ),
375
386
  returnByValue: z
376
- .boolean()
387
+ .literal(true)
377
388
  .optional()
378
- .describe('Return actual value vs JSON (default: true)'),
389
+ .describe('Return serializable values (only true is supported; default: true)'),
379
390
  level: z
380
391
  .enum(['all', 'debug', 'log', 'info', 'warn', 'error', 'exception', 'rejection'])
381
392
  .optional()
@@ -432,7 +443,7 @@ export function createBridgeMcpServer() {
432
443
  .int()
433
444
  .positive()
434
445
  .optional()
435
- .describe('Timeout for navigation (default: 30000)'),
446
+ .describe(`Timeout for navigation (default: ${DEFAULT_NAV_TIMEOUT_MS})`),
436
447
  top: z.number().optional().describe('Scroll target Y position (pixels)'),
437
448
  left: z.number().optional().describe('Scroll target X position (pixels)'),
438
449
  behavior: z.enum(['auto', 'smooth']).optional().describe('Scroll behavior (default: auto)'),
@@ -532,7 +543,7 @@ export function createBridgeMcpServer() {
532
543
  .int()
533
544
  .nonnegative()
534
545
  .optional()
535
- .describe('Hover duration in ms (default: 100)'),
546
+ .describe('Hover duration in ms (default: 0)'),
536
547
  sourceElementRef: z.string().optional().describe('Drag source element (for drag action)'),
537
548
  sourceSelector: z
538
549
  .string()
@@ -580,19 +591,12 @@ export function createBridgeMcpServer() {
580
591
  .describe('CSS property: value pairs (for apply_styles)'),
581
592
  important: z.boolean().optional().describe('Add !important flag (default: false)'),
582
593
  operation: z
583
- .enum([
584
- 'setAttribute',
585
- 'removeAttribute',
586
- 'addClass',
587
- 'removeClass',
588
- 'setTextContent',
589
- 'setProperty',
590
- ])
594
+ .enum(['setAttribute', 'removeAttribute', 'addClass', 'removeClass', 'setTextContent'])
591
595
  .optional()
592
596
  .describe('DOM mutation type'),
593
597
  value: z.unknown().optional().describe('Value for the DOM operation'),
594
598
  name: z.string().optional().describe('Attribute/class/property name (for apply_dom)'),
595
- patchId: z.string().optional().describe('Patch ID to rollback (omit for most recent)'),
599
+ patchId: z.string().optional().describe('Patch ID (required for rollback)'),
596
600
  verify: z
597
601
  .boolean()
598
602
  .optional()
@@ -706,6 +710,10 @@ export function createBridgeMcpServer() {
706
710
  .describe('Method parameters as object'),
707
711
  tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
708
712
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
713
+ budgetPreset: z
714
+ .enum(['quick', 'normal', 'deep'])
715
+ .optional()
716
+ .describe(BUDGET_PRESET_DESCRIPTION),
709
717
  },
710
718
  },
711
719
  handleRawCallTool
@@ -60,7 +60,7 @@ export async function readBridgeAuthTokenOverride(options = {}) {
60
60
  }
61
61
 
62
62
  /**
63
- * @param {{ tokenPath?: string, readFile?: typeof fs.promises.readFile }} [options={}]
63
+ * @param {{ env?: NodeJS.ProcessEnv, tokenPath?: string, readFile?: typeof fs.promises.readFile }} [options={}]
64
64
  * @returns {Promise<string | null>}
65
65
  */
66
66
  export async function readBridgeAuthToken(options = {}) {
@@ -3,6 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { createHash } from 'node:crypto';
6
7
 
7
8
  export const APP_NAME = 'com.browserbridge.browser_bridge';
8
9
  export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
@@ -62,15 +63,19 @@ export function getBridgeDir(env = process.env) {
62
63
  * connect to. On Windows we use a Named Pipe by default because Node's AF_UNIX
63
64
  * support fails with EACCES on listen() under recent Node + Windows 11
64
65
  * combinations, while Named Pipes are the historical and reliable Windows IPC
65
- * mechanism. An explicit bridge-home override keeps the legacy socket path so
66
- * callers can opt into custom test or compatibility setups.
66
+ * mechanism. Custom bridge homes use a stable suffix so isolated daemon and
67
+ * test setups do not contend for the default pipe.
67
68
  *
68
69
  * @param {NodeJS.ProcessEnv} [env=process.env]
69
70
  * @returns {string}
70
71
  */
71
72
  export function getSocketPath(env = process.env) {
72
- if (os.platform() === 'win32' && !env[BRIDGE_HOME_ENV]) {
73
- return `\\\\.\\pipe\\${APP_NAME}`;
73
+ if (os.platform() === 'win32') {
74
+ const bridgeHome = env[BRIDGE_HOME_ENV];
75
+ const suffix = bridgeHome
76
+ ? `-${createHash('sha256').update(bridgeHome.toLowerCase()).digest('hex').slice(0, 16)}`
77
+ : '';
78
+ return `\\\\.\\pipe\\${APP_NAME}${suffix}`;
74
79
  }
75
80
  return path.join(getBridgeDir(env), 'bridge.sock');
76
81
  }
@@ -407,10 +407,17 @@ async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
407
407
  throw new Error('Timed out waiting for Browser Bridge daemon to start.');
408
408
  }
409
409
 
410
+ let pid = await readPidFn(pidPath);
411
+ if (pid === null) {
412
+ // The daemon starts listening immediately before it persists its PID.
413
+ await sleepFn(pollIntervalMs);
414
+ pid = await readPidFn(pidPath);
415
+ }
416
+
410
417
  return {
411
418
  ...stopResult,
412
419
  pidPath,
413
- pid: await readPidFn(pidPath),
420
+ pid,
414
421
  };
415
422
  }
416
423