@browserbridge/bbx 1.2.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.
Files changed (35) hide show
  1. package/README.md +8 -5
  2. package/package.json +2 -2
  3. package/packages/agent-client/src/cli.js +56 -31
  4. package/packages/agent-client/src/client.js +81 -65
  5. package/packages/agent-client/src/command-registry.js +4 -15
  6. package/packages/agent-client/src/detect.js +3 -3
  7. package/packages/agent-client/src/install.js +3 -7
  8. package/packages/agent-client/src/mcp-config.js +20 -5
  9. package/packages/agent-client/src/runtime.js +7 -41
  10. package/packages/agent-client/src/setup-status.js +3 -13
  11. package/packages/agent-client/src/types.ts +139 -0
  12. package/packages/mcp-server/src/guidance.js +241 -0
  13. package/packages/mcp-server/src/handlers-capture.js +91 -16
  14. package/packages/mcp-server/src/handlers-dom.js +59 -4
  15. package/packages/mcp-server/src/handlers-navigation.js +22 -2
  16. package/packages/mcp-server/src/handlers-page.js +6 -11
  17. package/packages/mcp-server/src/handlers-utils.js +69 -1
  18. package/packages/mcp-server/src/server.js +111 -28
  19. package/packages/native-host/bin/postinstall.js +42 -21
  20. package/packages/native-host/src/auth-token.js +92 -0
  21. package/packages/native-host/src/daemon-process.js +1 -2
  22. package/packages/native-host/src/daemon.js +199 -30
  23. package/packages/native-host/src/framing.js +13 -0
  24. package/packages/native-host/src/native-host.js +25 -7
  25. package/packages/protocol/src/defaults.js +3 -0
  26. package/packages/protocol/src/json-lines.js +29 -1
  27. package/packages/protocol/src/protocol.js +43 -0
  28. package/packages/protocol/src/registry.js +3 -9
  29. package/packages/protocol/src/types.ts +574 -0
  30. package/skills/browser-bridge/SKILL.md +21 -5
  31. package/skills/browser-bridge/agents/openai.yaml +1 -1
  32. package/skills/browser-bridge/references/interaction.md +6 -6
  33. package/skills/browser-bridge/references/protocol.md +57 -54
  34. package/skills/browser-bridge/references/ui-workflows.md +1 -1
  35. package/packages/protocol/src/types.js +0 -626
@@ -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
 
@@ -148,6 +167,7 @@ export const PATCH_ACTIONS = {
148
167
  target: { elementRef: r },
149
168
  declarations: a.declarations,
150
169
  important: a.important,
170
+ patchId: a.patchId,
151
171
  verify: a.verify,
152
172
  }),
153
173
  },
@@ -160,16 +180,22 @@ export const PATCH_ACTIONS = {
160
180
  const opMap = {
161
181
  setAttribute: 'set_attribute',
162
182
  removeAttribute: 'remove_attribute',
163
- addClass: 'toggle_class',
164
- removeClass: 'toggle_class',
183
+ addClass: 'add_class',
184
+ removeClass: 'remove_class',
165
185
  setTextContent: 'set_text',
166
186
  setProperty: 'set_attribute',
167
187
  };
188
+ const normalizedOperation = opMap[operation] || operation;
189
+ const value =
190
+ normalizedOperation === 'add_class' || normalizedOperation === 'remove_class'
191
+ ? (a.value ?? a.name)
192
+ : a.value;
168
193
  return {
169
194
  target: { elementRef: r },
170
- operation: opMap[operation] || operation,
171
- value: a.value,
195
+ operation: normalizedOperation,
196
+ value,
172
197
  name: a.name,
198
+ patchId: a.patchId,
173
199
  verify: a.verify,
174
200
  };
175
201
  },
@@ -192,5 +218,34 @@ export const PATCH_ACTIONS = {
192
218
  * @returns {Promise<ToolResult>}
193
219
  */
194
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
+ }
195
230
  return dispatchToolAction(PATCH_ACTIONS, args, 'patch');
196
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) => ({ width: a.width, height: a.height, reset: a.reset }),
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),
@@ -8,12 +8,11 @@ import {
8
8
  import {
9
9
  annotateBridgeSummary,
10
10
  applyLimitBudgetPreset,
11
- applyTextBudgetPreset,
11
+ applyPageTextBudgetPreset,
12
12
  bridgeMethodNeedsTab,
13
13
  callBridgeTool,
14
14
  createToolResult,
15
15
  getToolTokenBudget,
16
- requestBridge,
17
16
  requestBridgeWithRetry,
18
17
  summarizeBatchErrorItem,
19
18
  summarizeBatchResponseItem,
@@ -72,7 +71,7 @@ export const PAGE_ACTIONS = {
72
71
  export async function handlePageTool(args) {
73
72
  let normalizedArgs = args;
74
73
  if (args.action === 'text') {
75
- normalizedArgs = applyTextBudgetPreset(args);
74
+ normalizedArgs = applyPageTextBudgetPreset(args);
76
75
  } else if (args.action === 'console') {
77
76
  normalizedArgs = applyLimitBudgetPreset(args, {
78
77
  quick: 10,
@@ -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.request({
151
- method,
152
- params: call.params || {},
149
+ const response = await requestBridgeWithRetry(client, method, call.params || {}, {
153
150
  tabId,
154
- meta: {
155
- source: REQUEST_SOURCE,
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 requestBridge(
197
+ const response = await requestBridgeWithRetry(
203
198
  client,
204
199
  /** @type {BridgeMethod} */ (args.method),
205
200
  args.params || {},
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  bridgeMethodNeedsTab,
5
5
  DEFAULT_MAX_HTML_LENGTH,
6
+ DEFAULT_PAGE_TEXT_BUDGET,
6
7
  estimateJsonPayloadCost,
7
8
  getBudgetPreset,
8
9
  getErrorRecovery,
@@ -23,6 +24,52 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-clie
23
24
 
24
25
  export const REQUEST_SOURCE = 'mcp';
25
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
+
26
73
  /**
27
74
  * @typedef {{
28
75
  * content: Array<{ type: 'text', text: string }>,
@@ -176,6 +223,27 @@ export function applyTextBudgetPreset(args) {
176
223
  });
177
224
  }
178
225
 
226
+ /**
227
+ * @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
228
+ * @param {T} args
229
+ * @returns {T}
230
+ */
231
+ export function applyPageTextBudgetPreset(args) {
232
+ const presetName = getBudgetPresetName(args.budgetPreset);
233
+ if (!presetName) {
234
+ return args;
235
+ }
236
+ const textBudgetByPreset = {
237
+ quick: 2000,
238
+ normal: DEFAULT_PAGE_TEXT_BUDGET,
239
+ deep: DEFAULT_PAGE_TEXT_BUDGET * 2,
240
+ };
241
+ return /** @type {T} */ ({
242
+ ...args,
243
+ textBudget: args.textBudget ?? textBudgetByPreset[presetName],
244
+ });
245
+ }
246
+
179
247
  /**
180
248
  * @template {{ budgetPreset?: unknown, limit?: unknown }} T
181
249
  * @param {T} args
@@ -224,7 +292,7 @@ export function applyHtmlBudgetPreset(args) {
224
292
  export async function requestBridgeWithRetry(client, method, params, options) {
225
293
  const response = await requestBridge(client, method, params, options);
226
294
  const recovery = !response.ok && response.error ? getErrorRecovery(response.error.code) : null;
227
- if (!response.ok && recovery?.retry) {
295
+ if (!response.ok && recovery?.retry && isRetrySafeBridgeMethod(method, params)) {
228
296
  const delay = recovery.retryAfterMs ?? 1000;
229
297
  process.stderr.write(
230
298
  `[bbx-mcp] Retrying ${method} after ${delay}ms (${response.error.code})\n`
@@ -1,5 +1,7 @@
1
1
  // @ts-check
2
2
 
3
+ import fs from 'node:fs';
4
+
3
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
7
  // zod is required at runtime by @modelcontextprotocol/sdk for tool parameter schema
@@ -38,11 +40,27 @@ import {
38
40
  getMethodsByMaxComplexity,
39
41
  } from '../../protocol/src/index.js';
40
42
  import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
43
+ import { MCP_SERVER_INSTRUCTIONS, registerBridgeMcpGuidance } from './guidance.js';
41
44
 
42
45
  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.`;
43
46
  export const TAB_ID_DESCRIPTION =
44
47
  'Target a specific tab instead of the active tab in the enabled window.';
45
48
 
49
+ const MCP_SERVER_VERSION = loadPackageVersion();
50
+
51
+ /**
52
+ * @returns {string}
53
+ */
54
+ function loadPackageVersion() {
55
+ try {
56
+ const raw = fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8');
57
+ const parsed = JSON.parse(raw);
58
+ return parsed && typeof parsed.version === 'string' ? parsed.version : '0.0.0';
59
+ } catch {
60
+ return '0.0.0';
61
+ }
62
+ }
63
+
46
64
  /** @type {readonly import('../../protocol/src/types.js').BridgeMethod[]} */
47
65
  const INVESTIGATE_SUBAGENT_BRIDGE_METHODS = Object.freeze(
48
66
  getMethodsByMaxComplexity('low').filter(
@@ -75,10 +93,15 @@ const INVESTIGATE_DELEGATION_HINT = Object.freeze({
75
93
  * @returns {McpServer}
76
94
  */
77
95
  export function createBridgeMcpServer() {
78
- const server = new McpServer({
79
- name: 'browser-bridge',
80
- version: '1.0.0',
81
- });
96
+ const server = new McpServer(
97
+ {
98
+ name: 'browser-bridge',
99
+ version: MCP_SERVER_VERSION,
100
+ },
101
+ {
102
+ instructions: MCP_SERVER_INSTRUCTIONS,
103
+ }
104
+ );
82
105
 
83
106
  server.registerTool(
84
107
  'browser_status',
@@ -114,6 +137,8 @@ export function createBridgeMcpServer() {
114
137
  inputSchema: {
115
138
  limit: z
116
139
  .number()
140
+ .int()
141
+ .positive()
117
142
  .optional()
118
143
  .describe(`Maximum log entries to return (default: ${DEFAULT_CONSOLE_LIMIT})`),
119
144
  budgetPreset: z
@@ -147,7 +172,7 @@ export function createBridgeMcpServer() {
147
172
  .describe('"list" (preferred), "create" (only when needed), or "close"'),
148
173
  url: z.string().optional().describe('URL for create action'),
149
174
  active: z.boolean().optional().describe('Focus the new tab (default: true)'),
150
- tabId: z.number().optional().describe('Tab ID (required for close)'),
175
+ tabId: z.number().int().positive().optional().describe('Tab ID (required for close)'),
151
176
  },
152
177
  },
153
178
  handleTabsTool
@@ -173,7 +198,7 @@ export function createBridgeMcpServer() {
173
198
  'accessibility_tree',
174
199
  ])
175
200
  .describe('DOM operation to perform'),
176
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
201
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
177
202
  budgetPreset: z
178
203
  .enum(['quick', 'normal', 'deep'])
179
204
  .optional()
@@ -189,14 +214,20 @@ export function createBridgeMcpServer() {
189
214
  withinRef: z.string().optional().describe('Scope query to this elementRef subtree'),
190
215
  maxNodes: z
191
216
  .number()
217
+ .int()
218
+ .positive()
192
219
  .optional()
193
220
  .describe(`Maximum nodes to return (default: ${DEFAULT_MAX_NODES})`),
194
221
  maxDepth: z
195
222
  .number()
223
+ .int()
224
+ .positive()
196
225
  .optional()
197
226
  .describe(`Maximum tree depth (default: ${DEFAULT_MAX_DEPTH})`),
198
227
  textBudget: z
199
228
  .number()
229
+ .int()
230
+ .positive()
200
231
  .optional()
201
232
  .describe(`Max chars of text content per node (default: ${DEFAULT_TEXT_BUDGET})`),
202
233
  includeBbox: z
@@ -216,7 +247,12 @@ export function createBridgeMcpServer() {
216
247
  .boolean()
217
248
  .optional()
218
249
  .describe('Require exact text match (default: false, substring match)'),
219
- maxResults: z.number().optional().describe('Maximum search results (default: 10)'),
250
+ maxResults: z
251
+ .number()
252
+ .int()
253
+ .positive()
254
+ .optional()
255
+ .describe('Maximum search results (default: 10)'),
220
256
  role: z.string().optional().describe('ARIA role to search for (for find_role action)'),
221
257
  name: z.string().optional().describe('Accessible name to match with role'),
222
258
  state: z
@@ -225,6 +261,8 @@ export function createBridgeMcpServer() {
225
261
  .describe('Expected element state (for wait action)'),
226
262
  timeoutMs: z
227
263
  .number()
264
+ .int()
265
+ .positive()
228
266
  .optional()
229
267
  .describe(`Timeout for wait operations (default: ${DEFAULT_WAIT_TIMEOUT_MS})`),
230
268
  outer: z
@@ -233,6 +271,8 @@ export function createBridgeMcpServer() {
233
271
  .describe('Return outerHTML instead of innerHTML (default: false)'),
234
272
  maxLength: z
235
273
  .number()
274
+ .int()
275
+ .positive()
236
276
  .optional()
237
277
  .describe(`Max HTML chars to return (default: ${DEFAULT_MAX_HTML_LENGTH})`),
238
278
  },
@@ -250,7 +290,7 @@ export function createBridgeMcpServer() {
250
290
  action: z
251
291
  .enum(['computed', 'matched_rules', 'box_model', 'hit_test'])
252
292
  .describe('Style/layout operation to perform'),
253
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
293
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
254
294
  budgetPreset: z
255
295
  .enum(['quick', 'normal', 'deep'])
256
296
  .optional()
@@ -261,8 +301,16 @@ export function createBridgeMcpServer() {
261
301
  .array(z.string())
262
302
  .optional()
263
303
  .describe('Style properties to fetch (omitting returns all - expensive)'),
264
- x: z.number().optional().describe('X coordinate for hit_test (viewport relative)'),
265
- y: z.number().optional().describe('Y coordinate for hit_test (viewport relative)'),
304
+ x: z
305
+ .number()
306
+ .nonnegative()
307
+ .optional()
308
+ .describe('X coordinate for hit_test (viewport relative)'),
309
+ y: z
310
+ .number()
311
+ .nonnegative()
312
+ .optional()
313
+ .describe('Y coordinate for hit_test (viewport relative)'),
266
314
  },
267
315
  },
268
316
  handleStylesLayoutTool
@@ -287,7 +335,7 @@ export function createBridgeMcpServer() {
287
335
  'performance',
288
336
  ])
289
337
  .describe('Page operation to perform'),
290
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
338
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
291
339
  budgetPreset: z
292
340
  .enum(['quick', 'normal', 'deep'])
293
341
  .optional()
@@ -299,6 +347,8 @@ export function createBridgeMcpServer() {
299
347
  awaitPromise: z.boolean().optional().describe('Await returned promises (default: false)'),
300
348
  timeoutMs: z
301
349
  .number()
350
+ .int()
351
+ .positive()
302
352
  .optional()
303
353
  .describe(`Timeout for evaluate/wait operations (default: ${DEFAULT_WAIT_TIMEOUT_MS})`),
304
354
  returnByValue: z
@@ -312,6 +362,8 @@ export function createBridgeMcpServer() {
312
362
  clear: z.boolean().optional().describe('Clear buffer after reading (default: false)'),
313
363
  limit: z
314
364
  .number()
365
+ .int()
366
+ .positive()
315
367
  .optional()
316
368
  .describe(`Maximum entries to return (default: ${DEFAULT_CONSOLE_LIMIT})`),
317
369
  type: z
@@ -324,6 +376,8 @@ export function createBridgeMcpServer() {
324
376
  .describe('Specific storage keys to fetch (omitting returns all)'),
325
377
  textBudget: z
326
378
  .number()
379
+ .int()
380
+ .positive()
327
381
  .optional()
328
382
  .describe(`Max chars for page text (default: ${DEFAULT_PAGE_TEXT_BUDGET})`),
329
383
  urlPattern: z.string().optional().describe('Filter network entries by URL pattern'),
@@ -342,14 +396,19 @@ export function createBridgeMcpServer() {
342
396
  action: z
343
397
  .enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
344
398
  .describe('Navigation operation to perform'),
345
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
399
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
346
400
  budgetPreset: z
347
401
  .enum(['quick', 'normal', 'deep'])
348
402
  .optional()
349
403
  .describe(BUDGET_PRESET_DESCRIPTION),
350
404
  url: z.string().optional().describe('URL to navigate to (for navigate action)'),
351
405
  waitForLoad: z.boolean().optional().describe('Wait for load event (default: true)'),
352
- timeoutMs: z.number().optional().describe('Timeout for navigation (default: 30000)'),
406
+ timeoutMs: z
407
+ .number()
408
+ .int()
409
+ .positive()
410
+ .optional()
411
+ .describe('Timeout for navigation (default: 30000)'),
353
412
  top: z.number().optional().describe('Scroll target Y position (pixels)'),
354
413
  left: z.number().optional().describe('Scroll target X position (pixels)'),
355
414
  behavior: z.enum(['auto', 'smooth']).optional().describe('Scroll behavior (default: auto)'),
@@ -357,8 +416,13 @@ export function createBridgeMcpServer() {
357
416
  .boolean()
358
417
  .optional()
359
418
  .describe('Scroll relative to current position (default: false)'),
360
- width: z.number().optional().describe('Viewport width in pixels'),
361
- height: z.number().optional().describe('Viewport height in pixels'),
419
+ width: z.number().int().positive().optional().describe('Viewport width in pixels'),
420
+ height: z.number().int().positive().optional().describe('Viewport height in pixels'),
421
+ deviceScaleFactor: z
422
+ .number()
423
+ .nonnegative()
424
+ .optional()
425
+ .describe('Viewport device scale factor (for resize)'),
362
426
  reset: z.boolean().optional().describe('Reset viewport to original size (for resize)'),
363
427
  },
364
428
  },
@@ -386,7 +450,7 @@ export function createBridgeMcpServer() {
386
450
  'scroll_into_view',
387
451
  ])
388
452
  .describe('Input operation to perform'),
389
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
453
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
390
454
  budgetPreset: z
391
455
  .enum(['quick', 'normal', 'deep'])
392
456
  .optional()
@@ -400,7 +464,13 @@ export function createBridgeMcpServer() {
400
464
  .enum(['left', 'middle', 'right'])
401
465
  .optional()
402
466
  .describe('Mouse button for click (default: left)'),
403
- clickCount: z.number().optional().describe('Click count (1=single, 2=double)'),
467
+ clickCount: z
468
+ .number()
469
+ .int()
470
+ .min(1)
471
+ .max(2)
472
+ .optional()
473
+ .describe('Click count (1=single, 2=double)'),
404
474
  text: z.string().max(100000).optional().describe('Text to type (for type action)'),
405
475
  clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
406
476
  submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
@@ -423,10 +493,15 @@ export function createBridgeMcpServer() {
423
493
  .optional()
424
494
  .describe('Option labels to select (alternative to values)'),
425
495
  indexes: z
426
- .array(z.number())
496
+ .array(z.number().int().nonnegative())
427
497
  .optional()
428
498
  .describe('Option indexes to select (alternative to values/labels)'),
429
- duration: z.number().optional().describe('Hover duration in ms (default: 100)'),
499
+ duration: z
500
+ .number()
501
+ .int()
502
+ .nonnegative()
503
+ .optional()
504
+ .describe('Hover duration in ms (default: 100)'),
430
505
  sourceElementRef: z.string().optional().describe('Drag source element (for drag action)'),
431
506
  sourceSelector: z
432
507
  .string()
@@ -457,7 +532,7 @@ export function createBridgeMcpServer() {
457
532
  action: z
458
533
  .enum(['apply_styles', 'apply_dom', 'list', 'rollback', 'commit_baseline'])
459
534
  .describe('Patch operation to perform'),
460
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
535
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
461
536
  budgetPreset: z
462
537
  .enum(['quick', 'normal', 'deep'])
463
538
  .optional()
@@ -517,7 +592,7 @@ export function createBridgeMcpServer() {
517
592
  .describe(
518
593
  'element (preferred), region (tight crop), full_page (document-level only), or cdp_* for low-level data'
519
594
  ),
520
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
595
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
521
596
  budgetPreset: z
522
597
  .enum(['quick', 'normal', 'deep'])
523
598
  .optional()
@@ -527,12 +602,18 @@ export function createBridgeMcpServer() {
527
602
  .optional()
528
603
  .describe('Element reference (for element action, preferred)'),
529
604
  selector: z.string().optional().describe('CSS selector (used if no elementRef)'),
605
+ nodeId: z
606
+ .number()
607
+ .int()
608
+ .positive()
609
+ .optional()
610
+ .describe('CDP node id for cdp_box_model/cdp_computed_styles'),
530
611
  rect: z
531
612
  .object({
532
- x: z.number().describe('Region left edge (viewport pixels)'),
533
- y: z.number().describe('Region top edge (viewport pixels)'),
534
- width: z.number().describe('Region width (pixels)'),
535
- height: z.number().describe('Region height (pixels)'),
613
+ x: z.number().nonnegative().describe('Region left edge (viewport pixels)'),
614
+ y: z.number().nonnegative().describe('Region top edge (viewport pixels)'),
615
+ width: z.number().positive().describe('Region width (pixels)'),
616
+ height: z.number().positive().describe('Region height (pixels)'),
536
617
  })
537
618
  .optional()
538
619
  .describe('Viewport region for region action (keep crop tight)'),
@@ -556,7 +637,7 @@ export function createBridgeMcpServer() {
556
637
  .record(z.string(), z.unknown())
557
638
  .optional()
558
639
  .describe('Method params for this call'),
559
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
640
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
560
641
  budgetPreset: z
561
642
  .enum(['quick', 'normal', 'deep'])
562
643
  .optional()
@@ -582,7 +663,7 @@ export function createBridgeMcpServer() {
582
663
  .record(z.string(), z.unknown())
583
664
  .optional()
584
665
  .describe('Method parameters as object'),
585
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
666
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
586
667
  },
587
668
  },
588
669
  handleRawCallTool
@@ -644,7 +725,7 @@ export function createBridgeMcpServer() {
644
725
  '"normal" (state + DOM + text, default), ' +
645
726
  '"deep" (state + DOM + text + console + network).'
646
727
  ),
647
- tabId: z.number().optional().describe(TAB_ID_DESCRIPTION),
728
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
648
729
  selector: z
649
730
  .string()
650
731
  .optional()
@@ -654,6 +735,8 @@ export function createBridgeMcpServer() {
654
735
  handleInvestigateTool
655
736
  );
656
737
 
738
+ registerBridgeMcpGuidance(server);
739
+
657
740
  return server;
658
741
  }
659
742