@browserbridge/bbx 1.9.0 → 1.11.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 (33) hide show
  1. package/README.md +1 -1
  2. package/package.json +8 -10
  3. package/packages/agent-client/src/cli-args.js +35 -6
  4. package/packages/agent-client/src/cli-batch.js +10 -5
  5. package/packages/agent-client/src/cli-helpers.js +8 -0
  6. package/packages/agent-client/src/cli-proxy-remote.js +7 -4
  7. package/packages/agent-client/src/cli-setup-commands.js +3 -1
  8. package/packages/agent-client/src/cli.js +35 -15
  9. package/packages/agent-client/src/command-registry.js +7 -3
  10. package/packages/agent-client/src/mcp-config.js +8 -8
  11. package/packages/agent-client/src/npm-self-update.js +9 -2
  12. package/packages/agent-client/src/runtime.js +14 -5
  13. package/packages/agent-client/src/types.ts +2 -0
  14. package/packages/mcp-server/src/bin.js +1 -1
  15. package/packages/mcp-server/src/guidance.js +43 -6
  16. package/packages/mcp-server/src/handlers-capture.js +1 -1
  17. package/packages/mcp-server/src/handlers-page.js +162 -9
  18. package/packages/mcp-server/src/handlers-utils.js +145 -169
  19. package/packages/mcp-server/src/handlers.js +17 -5
  20. package/packages/mcp-server/src/server.js +255 -91
  21. package/packages/mcp-server/src/toolset.js +73 -0
  22. package/packages/native-host/src/daemon.js +24 -107
  23. package/packages/native-host/src/dom-baseline-owners.js +210 -0
  24. package/packages/native-host/src/native-host.js +14 -28
  25. package/packages/protocol/src/budget-presets.js +182 -0
  26. package/packages/protocol/src/har.js +34 -0
  27. package/packages/protocol/src/index.js +2 -0
  28. package/packages/protocol/src/json-lines.js +3 -2
  29. package/packages/protocol/src/protocol.js +10 -0
  30. package/packages/protocol/src/registry.js +83 -0
  31. package/packages/protocol/src/types.ts +3 -0
  32. package/skills/browser-bridge/SKILL.md +4 -0
  33. package/skills/browser-bridge/references/protocol.md +11 -5
@@ -2,20 +2,22 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
 
5
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- // zod is required at runtime by @modelcontextprotocol/sdk for tool parameter schema
5
+ import { McpServer } from '@modelcontextprotocol/server';
6
+ import { serveStdio } from '@modelcontextprotocol/server/stdio';
7
+ // zod is required at runtime by @modelcontextprotocol/server for tool parameter schema
8
8
  // declarations (z.object, z.string, etc.). It is not used for request/response
9
9
  // validation - that is handled by the protocol package.
10
10
  import * as z from 'zod/v4';
11
11
 
12
12
  import {
13
13
  handleAccessTool,
14
+ handleArtifactTool,
14
15
  handleCaptureTool,
15
16
  handleBatchTool,
16
17
  handleDomTool,
17
18
  handleHealthTool,
18
19
  handleInputTool,
20
+ handleInterceptTool,
19
21
  handleLogTool,
20
22
  handleNavigationTool,
21
23
  handlePageTool,
@@ -28,9 +30,12 @@ import {
28
30
  handleStylesLayoutTool,
29
31
  handleTabsTool,
30
32
  handleInvestigateTool,
33
+ createToolResult,
31
34
  MAX_BATCH_CALLS,
35
+ runWithMcpRequestEra,
32
36
  } from './handlers.js';
33
37
  import {
38
+ ARTIFACT_CHUNK_BYTES,
34
39
  BUDGET_PRESETS,
35
40
  DEFAULT_CONSOLE_LIMIT,
36
41
  DEFAULT_EVAL_TIMEOUT_MS,
@@ -47,7 +52,8 @@ import {
47
52
  getMethodsByMaxComplexity,
48
53
  } from '../../protocol/src/index.js';
49
54
  import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
50
- import { MCP_SERVER_INSTRUCTIONS } from './guidance.js';
55
+ import { getMcpServerInstructions } from './guidance.js';
56
+ import { isToolEnabledForEra, LOADABLE_TOOLSET_TOOLS } from './toolset.js';
51
57
 
52
58
  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.`;
53
59
  export const TAB_ID_DESCRIPTION =
@@ -81,74 +87,138 @@ const INVESTIGATE_SUBAGENT_BRIDGE_METHODS = Object.freeze(
81
87
  )
82
88
  );
83
89
 
84
- const INVESTIGATE_DELEGATION_HINT = Object.freeze({
85
- recommended: true,
86
- costTier: 'low',
87
- preferredAgentProfile: {
88
- modelClass: 'small',
89
- reasoningEffort: 'low',
90
- },
91
- preferredTools: ['browser_dom', 'browser_page', 'browser_styles_layout', 'browser_batch'],
92
- escalationTools: ['browser_capture'],
93
- preferredBridgeMethods: INVESTIGATE_SUBAGENT_BRIDGE_METHODS,
94
- escalationTriggers: [
95
- 'Structured DOM or page reads are insufficient.',
96
- 'Visual confirmation is required.',
97
- 'Debugger-backed evidence is required.',
98
- ],
99
- });
90
+ const INVESTIGATE_PREFERRED_TOOLS = Object.freeze(['browser_call', 'browser_batch']);
100
91
 
101
92
  /**
93
+ * Build the subagent delegation hint for browser_investigate using tools that
94
+ * are available before any on-demand expansion.
95
+ */
96
+ function createInvestigateDelegationHint() {
97
+ return Object.freeze({
98
+ recommended: true,
99
+ costTier: 'low',
100
+ preferredAgentProfile: {
101
+ modelClass: 'small',
102
+ reasoningEffort: 'low',
103
+ },
104
+ preferredTools: INVESTIGATE_PREFERRED_TOOLS,
105
+ escalationTools: Object.freeze(['browser_call']),
106
+ preferredBridgeMethods: INVESTIGATE_SUBAGENT_BRIDGE_METHODS,
107
+ escalationTriggers: [
108
+ 'Structured DOM or page reads are insufficient.',
109
+ 'Visual confirmation is required.',
110
+ 'Debugger-backed evidence is required.',
111
+ ],
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Build the browser_investigate description for the progressive tool surface.
117
+ *
118
+ * @returns {string}
119
+ */
120
+ function getInvestigateToolDescription() {
121
+ return (
122
+ 'Investigate a page to answer a question or verify a condition. ' +
123
+ 'Pass a natural-language objective and an optional scope (quick/normal/deep). ' +
124
+ 'DELEGATION HINT: Prefer delegating this to a smaller, low-cost subagent ' +
125
+ 'that starts with structured reads via browser_call and browser_batch. ' +
126
+ 'Escalate through browser_call only ' +
127
+ 'when structured reads are insufficient. ' +
128
+ 'If subagent delegation is not available, a deterministic heuristic fallback ' +
129
+ 'runs a scripted inspection sequence and returns a best-effort summary.'
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Create the MCP server for one negotiated protocol era.
135
+ *
136
+ * @param {{ era?: 'legacy' | 'modern' }} [options]
102
137
  * @returns {McpServer}
103
138
  */
104
- export function createBridgeMcpServer() {
139
+ export function createBridgeMcpServer(options = {}) {
140
+ const { era = 'legacy' } = options;
105
141
  const server = new McpServer(
106
142
  {
107
143
  name: 'browser-bridge',
108
144
  version: MCP_SERVER_VERSION,
109
145
  },
110
146
  {
111
- instructions: MCP_SERVER_INSTRUCTIONS,
147
+ instructions: getMcpServerInstructions(era),
112
148
  }
113
149
  );
114
150
 
115
- server.registerTool(
151
+ /** @type {Map<string, ReturnType<typeof server.registerTool>>} */
152
+ const registrations = new Map();
153
+
154
+ /**
155
+ * Register a tool, then hide it when it is not part of the era's compact surface.
156
+ *
157
+ * Registering first keeps every call site checked against the SDK's own
158
+ * generic signature instead of a hand-written passthrough type. Disabling
159
+ * happens before `connect()`, so an excluded tool never reaches `tools/list`.
160
+ *
161
+ * @template {z.ZodObject} InputSchema
162
+ * @param {string} name
163
+ * @param {{
164
+ * title?: string,
165
+ * description?: string,
166
+ * inputSchema: InputSchema,
167
+ * annotations?: import('@modelcontextprotocol/server').ToolAnnotations,
168
+ * _meta?: Record<string, unknown>,
169
+ * }} config
170
+ * @param {import('@modelcontextprotocol/server').AnyToolHandler<InputSchema>} handler
171
+ * @returns {ReturnType<typeof server.registerTool>}
172
+ */
173
+ const registerTool = (name, config, handler) => {
174
+ const wrappedHandler = /** @type {typeof handler} */ (
175
+ (args, context) => runWithMcpRequestEra(era, () => handler(args, context))
176
+ );
177
+ const registration = server.registerTool(name, config, wrappedHandler);
178
+ registrations.set(name, registration);
179
+ if (!isToolEnabledForEra(name, era)) {
180
+ registration.disable();
181
+ }
182
+ return registration;
183
+ };
184
+
185
+ registerTool(
116
186
  'browser_status',
117
187
  {
118
188
  title: 'Browser Bridge Status',
119
189
  description:
120
190
  '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.',
121
- inputSchema: {
191
+ inputSchema: z.object({
122
192
  destinationId: z
123
193
  .string()
124
194
  .optional()
125
195
  .describe('Inspect one destination instead of aggregating all configured destinations.'),
126
- },
196
+ }),
127
197
  },
128
198
  handleStatusTool
129
199
  );
130
200
 
131
- server.registerTool(
201
+ registerTool(
132
202
  'browser_setup',
133
203
  {
134
204
  title: 'Browser Bridge Setup Status',
135
205
  description: 'Check MCP and CLI skill installation status for agent integration.',
136
- inputSchema: {
206
+ inputSchema: z.object({
137
207
  global: z
138
208
  .boolean()
139
209
  .optional()
140
210
  .describe('Check global (true) or local (false) config (default: true)'),
141
- },
211
+ }),
142
212
  },
143
213
  handleSetupTool
144
214
  );
145
215
 
146
- server.registerTool(
216
+ registerTool(
147
217
  'browser_logs',
148
218
  {
149
219
  title: 'Browser Bridge Logs',
150
220
  description: 'Tail recent bridge request logs for debugging connection or routing issues.',
151
- inputSchema: {
221
+ inputSchema: z.object({
152
222
  limit: z
153
223
  .number()
154
224
  .int()
@@ -160,34 +230,34 @@ export function createBridgeMcpServer() {
160
230
  .optional()
161
231
  .describe(BUDGET_PRESET_DESCRIPTION),
162
232
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
163
- },
233
+ }),
164
234
  },
165
235
  handleLogTool
166
236
  );
167
237
 
168
- server.registerTool(
238
+ registerTool(
169
239
  'browser_health',
170
240
  {
171
241
  title: 'Browser Bridge Health',
172
242
  description: 'Ping the bridge to verify daemon and extension connectivity.',
173
- inputSchema: {
243
+ inputSchema: z.object({
174
244
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
175
245
  intent: z
176
246
  .enum(['inspect', 'interact', 'capture', 'navigate', 'debugger', 'general'])
177
247
  .optional()
178
248
  .describe('Reported operation family shown in the access prompt (defaults to general).'),
179
- },
249
+ }),
180
250
  },
181
251
  handleHealthTool
182
252
  );
183
253
 
184
- server.registerTool(
254
+ registerTool(
185
255
  'browser_tabs',
186
256
  {
187
257
  title: 'Browser Tabs',
188
258
  description:
189
259
  '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.',
190
- inputSchema: {
260
+ inputSchema: z.object({
191
261
  action: z
192
262
  .enum(['list', 'create', 'close', 'activate'])
193
263
  .describe('"list" (preferred), "create" (only when needed), "close", or "activate"'),
@@ -203,18 +273,18 @@ export function createBridgeMcpServer() {
203
273
  .string()
204
274
  .optional()
205
275
  .describe('Target one destination; list without it aggregates configured destinations.'),
206
- },
276
+ }),
207
277
  },
208
278
  handleTabsTool
209
279
  );
210
280
 
211
- server.registerTool(
281
+ registerTool(
212
282
  'browser_dom',
213
283
  {
214
284
  title: 'Browser DOM',
215
285
  description:
216
286
  '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 and depth-limited - use query/find first.',
217
- inputSchema: {
287
+ inputSchema: z.object({
218
288
  action: z
219
289
  .enum([
220
290
  'query',
@@ -332,18 +402,18 @@ export function createBridgeMcpServer() {
332
402
  .positive()
333
403
  .optional()
334
404
  .describe(`Max HTML chars to return (default: ${DEFAULT_MAX_HTML_LENGTH})`),
335
- },
405
+ }),
336
406
  },
337
407
  handleDomTool
338
408
  );
339
409
 
340
- server.registerTool(
410
+ registerTool(
341
411
  'browser_styles_layout',
342
412
  {
343
413
  title: 'Browser Styles And Layout',
344
414
  description:
345
415
  'Read computed styles, matched CSS rules, box model, or hit-test a viewport point. Reuse elementRef from prior queries. For DOM structure, use browser_dom.',
346
- inputSchema: {
416
+ inputSchema: z.object({
347
417
  action: z
348
418
  .enum(['computed', 'matched_rules', 'box_model', 'hit_test'])
349
419
  .describe('Style/layout operation to perform'),
@@ -371,18 +441,18 @@ export function createBridgeMcpServer() {
371
441
  .nonnegative()
372
442
  .optional()
373
443
  .describe('Y coordinate for hit_test (viewport relative)'),
374
- },
444
+ }),
375
445
  },
376
446
  handleStylesLayoutTool
377
447
  );
378
448
 
379
- server.registerTool(
449
+ registerTool(
380
450
  'browser_sensitive_read',
381
451
  {
382
452
  title: 'Sensitive Browser Read',
383
453
  description:
384
454
  'High-risk exact-value read for one local or session storage key. This operation is logged as Sensitive access, is never batched or retried, and returns the whole value or fails atomically.',
385
- inputSchema: {
455
+ inputSchema: z.object({
386
456
  source: z.enum(['local_storage', 'session_storage']).describe('Exact storage source'),
387
457
  key: z
388
458
  .string()
@@ -390,18 +460,18 @@ export function createBridgeMcpServer() {
390
460
  .describe('Exact storage key, including an empty key if intended'),
391
461
  tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
392
462
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
393
- },
463
+ }),
394
464
  },
395
465
  handleSensitiveReadTool
396
466
  );
397
467
 
398
- server.registerTool(
468
+ registerTool(
399
469
  'browser_page',
400
470
  {
401
471
  title: 'Browser Page State',
402
472
  description:
403
473
  'Read page-level data, export captured network evidence as HAR 1.2, wait for load/URL conditions, or explicitly inspect/handle JavaScript dialogs. The performance action returns a raw CDP Performance.getMetrics point sample: Chrome defines names and units, which vary, while BBX sets no navigation observation window and does not measure LCP, CLS, or INP. For element-level reads, use browser_dom. evaluate, performance, handle_dialog, and source=cdp network capture are debugger-backed.',
404
- inputSchema: {
474
+ inputSchema: z.object({
405
475
  action: z
406
476
  .enum([
407
477
  'state',
@@ -533,18 +603,18 @@ export function createBridgeMcpServer() {
533
603
  .enum(['exact', 'contains', 'regex'])
534
604
  .optional()
535
605
  .describe('URL match mode for wait_for_load (default: exact)'),
536
- },
606
+ }),
537
607
  },
538
608
  handlePageTool
539
609
  );
540
610
 
541
- server.registerTool(
611
+ registerTool(
542
612
  'browser_navigation',
543
613
  {
544
614
  title: 'Browser Navigation',
545
615
  description:
546
616
  'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed - use only for exact viewport overrides.',
547
- inputSchema: {
617
+ inputSchema: z.object({
548
618
  action: z
549
619
  .enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
550
620
  .describe('Navigation operation to perform'),
@@ -577,18 +647,18 @@ export function createBridgeMcpServer() {
577
647
  .optional()
578
648
  .describe('Viewport device scale factor (for resize)'),
579
649
  reset: z.boolean().optional().describe('Reset viewport to original size (for resize)'),
580
- },
650
+ }),
581
651
  },
582
652
  handleNavigationTool
583
653
  );
584
654
 
585
- server.registerTool(
655
+ registerTool(
586
656
  'browser_input',
587
657
  {
588
658
  title: 'Browser Input',
589
659
  description:
590
660
  'Dispatch browser input. Targeted click, focus, type, fill, press_key, set_checked, select_option, hover, and drag actions perform actionability checks and report resolution/execution metadata. cdp_press_key and scroll_into_view use separate contracts. Reuse elementRef values and verify application state after mutations.',
591
- inputSchema: {
661
+ inputSchema: z.object({
592
662
  action: z
593
663
  .enum([
594
664
  'click',
@@ -689,18 +759,18 @@ export function createBridgeMcpServer() {
689
759
  .describe('Drag destination selector (alternative to destinationElementRef)'),
690
760
  offsetX: z.number().optional().describe('Drag drop offset X (default: 0)'),
691
761
  offsetY: z.number().optional().describe('Drag drop offset Y (default: 0)'),
692
- },
762
+ }),
693
763
  },
694
764
  handleInputTool
695
765
  );
696
766
 
697
- server.registerTool(
767
+ registerTool(
698
768
  'browser_patch',
699
769
  {
700
770
  title: 'Browser Patch',
701
771
  description:
702
772
  'Apply or rollback document-local style and DOM patches for live prototyping before editing source. commit_baseline keeps current changes but discards rollback history. Set verify=true to get computed results inline.',
703
- inputSchema: {
773
+ inputSchema: z.object({
704
774
  action: z
705
775
  .enum(['apply_styles', 'apply_dom', 'list', 'rollback', 'commit_baseline'])
706
776
  .describe('Patch operation to perform'),
@@ -740,18 +810,18 @@ export function createBridgeMcpServer() {
740
810
  .describe(
741
811
  'Return computed result inline after applying, eliminating a verification round-trip'
742
812
  ),
743
- },
813
+ }),
744
814
  },
745
815
  handlePatchTool
746
816
  );
747
817
 
748
- server.registerTool(
818
+ registerTool(
749
819
  'browser_capture',
750
820
  {
751
821
  title: 'Browser Capture',
752
822
  description:
753
823
  '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.',
754
- inputSchema: {
824
+ inputSchema: z.object({
755
825
  action: z
756
826
  .enum([
757
827
  'element',
@@ -817,12 +887,91 @@ export function createBridgeMcpServer() {
817
887
  .max(4)
818
888
  .optional()
819
889
  .describe('Screenshot capture scale (0.1 to 4)'),
820
- },
890
+ }),
821
891
  },
822
892
  handleCaptureTool
823
893
  );
824
894
 
825
- server.registerTool(
895
+ registerTool(
896
+ 'browser_artifact',
897
+ {
898
+ title: 'Browser Artifact',
899
+ description:
900
+ 'Read (chunked base64) or delete a daemon-owned artifact produced by artifact delivery (screenshots, HAR exports). Artifacts are scoped to the current local MCP process and expire quickly. Page reads with offset until nextOffset is null, then verify the reassembled bytes against sha256.',
901
+ inputSchema: z.object({
902
+ action: z.enum(['read', 'delete']).describe('Artifact operation to perform'),
903
+ artifactId: z
904
+ .string()
905
+ .min(1)
906
+ .describe('Artifact identifier returned by a capture or HAR export'),
907
+ offset: z
908
+ .number()
909
+ .int()
910
+ .nonnegative()
911
+ .optional()
912
+ .describe('Byte offset to read from (default: 0)'),
913
+ limit: z
914
+ .number()
915
+ .int()
916
+ .positive()
917
+ .max(ARTIFACT_CHUNK_BYTES)
918
+ .optional()
919
+ .describe(`Maximum bytes to read in this chunk (default/max: ${ARTIFACT_CHUNK_BYTES})`),
920
+ destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
921
+ budgetPreset: z
922
+ .enum(['quick', 'normal', 'deep'])
923
+ .optional()
924
+ .describe(BUDGET_PRESET_DESCRIPTION),
925
+ }),
926
+ },
927
+ handleArtifactTool
928
+ );
929
+
930
+ registerTool(
931
+ 'browser_intercept',
932
+ {
933
+ title: 'Browser Network Intercept',
934
+ description:
935
+ 'Manage CDP Fetch request interception rules: add (block, fulfill, or continue matching requests), list, remove, or clear. Debugger-backed; rules persist until removed or cleared.',
936
+ inputSchema: z.object({
937
+ action: z
938
+ .enum(['add', 'list', 'remove', 'clear'])
939
+ .describe('Intercept operation to perform'),
940
+ tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
941
+ destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
942
+ budgetPreset: z
943
+ .enum(['quick', 'normal', 'deep'])
944
+ .optional()
945
+ .describe(BUDGET_PRESET_DESCRIPTION),
946
+ urlPattern: z
947
+ .string()
948
+ .min(1)
949
+ .max(2_048)
950
+ .optional()
951
+ .describe('URL substring to match (required for add)'),
952
+ ruleAction: z
953
+ .enum(['block', 'fulfill', 'continue'])
954
+ .optional()
955
+ .describe('How to handle matched requests (default: continue)'),
956
+ statusCode: z
957
+ .number()
958
+ .int()
959
+ .min(100)
960
+ .max(599)
961
+ .optional()
962
+ .describe('HTTP status for fulfill responses'),
963
+ body: z.string().optional().describe('Response body for fulfill'),
964
+ headers: z
965
+ .record(z.string(), z.string())
966
+ .optional()
967
+ .describe('Response headers for fulfill'),
968
+ ruleId: z.string().min(1).optional().describe('Rule ID (required for remove)'),
969
+ }),
970
+ },
971
+ handleInterceptTool
972
+ );
973
+
974
+ registerTool(
826
975
  'browser_batch',
827
976
  {
828
977
  title: 'Browser Bridge Batch',
@@ -834,7 +983,7 @@ export function createBridgeMcpServer() {
834
983
  idempotentHint: true,
835
984
  openWorldHint: true,
836
985
  },
837
- inputSchema: {
986
+ inputSchema: z.object({
838
987
  calls: z
839
988
  .array(
840
989
  z.object({
@@ -854,18 +1003,18 @@ export function createBridgeMcpServer() {
854
1003
  .min(1)
855
1004
  .max(MAX_BATCH_CALLS)
856
1005
  .describe('Calls to execute in parallel'),
857
- },
1006
+ }),
858
1007
  },
859
1008
  handleBatchTool
860
1009
  );
861
1010
 
862
- server.registerTool(
1011
+ registerTool(
863
1012
  'browser_call',
864
1013
  {
865
1014
  title: 'Raw Browser Bridge Call',
866
1015
  description:
867
1016
  'Primary Browser Bridge tool for permission-ask hosts: call any bridge method directly by name so the user can approve one BBX MCP tool instead of each specialized tool separately.',
868
- inputSchema: {
1017
+ inputSchema: z.object({
869
1018
  method: z.string().describe('Bridge method name (e.g., "dom.query", "input.click")'),
870
1019
  params: z
871
1020
  .record(z.string(), z.unknown())
@@ -877,50 +1026,42 @@ export function createBridgeMcpServer() {
877
1026
  .enum(['quick', 'normal', 'deep'])
878
1027
  .optional()
879
1028
  .describe(BUDGET_PRESET_DESCRIPTION),
880
- },
1029
+ }),
881
1030
  },
882
1031
  handleRawCallTool
883
1032
  );
884
1033
 
885
- server.registerTool(
1034
+ registerTool(
886
1035
  'browser_skill',
887
1036
  {
888
1037
  title: 'Browser Bridge Runtime Context',
889
1038
  description:
890
1039
  'Return runtime context: budget presets, method groups, and active limits. Call to discover defaults before inspecting a page.',
891
- inputSchema: {
1040
+ inputSchema: z.object({
892
1041
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
893
- },
1042
+ }),
894
1043
  },
895
1044
  handleSkillTool
896
1045
  );
897
1046
 
898
- server.registerTool(
1047
+ registerTool(
899
1048
  'browser_access',
900
1049
  {
901
1050
  title: 'Request Browser Bridge Access',
902
1051
  description:
903
1052
  'Request window access for Browser Bridge. Surfaces an Enable prompt in the extension popup or side panel. Use once per window; if access is already pending, ask the user to enable that window instead of requesting again.',
904
- inputSchema: {
1053
+ inputSchema: z.object({
905
1054
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
906
- },
1055
+ }),
907
1056
  },
908
1057
  handleAccessTool
909
1058
  );
910
1059
 
911
- server.registerTool(
1060
+ registerTool(
912
1061
  'browser_investigate',
913
1062
  {
914
1063
  title: 'Browser Investigate',
915
- description:
916
- 'Investigate a page to answer a question or verify a condition. ' +
917
- 'Pass a natural-language objective and an optional scope (quick/normal/deep). ' +
918
- 'DELEGATION HINT: Prefer delegating this to a smaller, low-cost subagent ' +
919
- 'that starts with structured reads via browser_dom, browser_page, ' +
920
- 'browser_styles_layout, and browser_batch. Escalate to browser_capture only ' +
921
- 'when structured reads are insufficient. ' +
922
- 'If subagent delegation is not available, a deterministic heuristic fallback ' +
923
- 'runs a scripted inspection sequence and returns a best-effort summary.',
1064
+ description: getInvestigateToolDescription(),
924
1065
  annotations: {
925
1066
  readOnlyHint: true,
926
1067
  destructiveHint: false,
@@ -928,9 +1069,9 @@ export function createBridgeMcpServer() {
928
1069
  openWorldHint: true,
929
1070
  },
930
1071
  _meta: {
931
- delegationHint: INVESTIGATE_DELEGATION_HINT,
1072
+ delegationHint: createInvestigateDelegationHint(),
932
1073
  },
933
- inputSchema: {
1074
+ inputSchema: z.object({
934
1075
  objective: z
935
1076
  .string()
936
1077
  .describe('What to find, verify, or extract from the current page (natural language).'),
@@ -948,20 +1089,43 @@ export function createBridgeMcpServer() {
948
1089
  .string()
949
1090
  .optional()
950
1091
  .describe('Optional CSS selector to scope the investigation to a subtree.'),
951
- },
1092
+ }),
952
1093
  },
953
1094
  handleInvestigateTool
954
1095
  );
955
1096
 
1097
+ registerTool(
1098
+ 'browser_toolset',
1099
+ {
1100
+ title: 'Load Browser Bridge Tool',
1101
+ description:
1102
+ 'Load one specialized typed Browser Bridge tool by exact name. The tool appears in tools/list automatically and remains enabled for this legacy MCP connection.',
1103
+ inputSchema: z.object({
1104
+ tool: z
1105
+ .enum(LOADABLE_TOOLSET_TOOLS)
1106
+ .describe('Exact specialized Browser Bridge tool to load.'),
1107
+ }),
1108
+ },
1109
+ ({ tool }) => {
1110
+ const registration = registrations.get(tool);
1111
+ const newlyEnabled = Boolean(registration && !registration.enabled);
1112
+ if (newlyEnabled) registration?.enable();
1113
+ return createToolResult(newlyEnabled ? `Loaded ${tool}.` : `${tool} is already loaded.`, {
1114
+ tool,
1115
+ newlyEnabled,
1116
+ });
1117
+ }
1118
+ );
1119
+
956
1120
  return server;
957
1121
  }
958
1122
 
959
1123
  /**
960
- * @returns {Promise<void>}
1124
+ * @param {{ serve?: typeof serveStdio }} [options]
1125
+ * @returns {void}
961
1126
  */
962
- export async function startBridgeMcpServer() {
1127
+ export function startBridgeMcpServer(options = {}) {
1128
+ const { serve = serveStdio } = options;
963
1129
  applyWindowsTcpTransportDefaults();
964
- const server = createBridgeMcpServer();
965
- const transport = new StdioServerTransport();
966
- await server.connect(transport);
1130
+ serve(({ era }) => createBridgeMcpServer({ era }));
967
1131
  }