@browserbridge/bbx 1.10.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.
package/README.md CHANGED
@@ -116,7 +116,7 @@ Browser Bridge is optimized for the opposite starting point: **inspect the state
116
116
 
117
117
  On Ubuntu, Chromium is commonly installed as a strict snap, and Flatpak Chromium is similarly sandboxed. If native messaging stays disconnected there, use a non-sandboxed Chromium-based browser such as Google Chrome, Brave, or Edge.
118
118
 
119
- MCP mode is self-contained: the server exposes tools and startup instructions, so a separate CLI skill is not required for MCP guidance.
119
+ MCP mode is self-contained: the dual-era stdio server supports legacy MCP clients and modern stateless MCP revision `2026-07-28`, and exposes tools and matching startup instructions without requiring a separate CLI skill.
120
120
 
121
121
  ## How it works
122
122
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -73,26 +73,24 @@
73
73
  "install-manifest": "node packages/native-host/bin/install-manifest.js"
74
74
  },
75
75
  "dependencies": {
76
- "@modelcontextprotocol/sdk": "^1.29.0",
76
+ "@modelcontextprotocol/server": "^2.0.0",
77
77
  "@mozilla/readability": "^0.6.0",
78
78
  "linkedom": "^0.18.13",
79
79
  "zod": "^4.4.3"
80
80
  },
81
81
  "devDependencies": {
82
- "@types/chrome": "^0.2.2",
83
- "@types/node": "^26.1.1",
82
+ "@modelcontextprotocol/client": "^2.0.0",
83
+ "@types/chrome": "^0.2.5",
84
+ "@types/node": "^26.2.0",
84
85
  "ajv": "^8.20.0",
85
86
  "ajv-formats": "^3.0.1",
86
87
  "c8": "^12.0.0",
87
88
  "har-schema": "^2.0.0",
88
- "oxfmt": "^0.60.0",
89
- "oxlint": "^1.75.0",
90
- "tsx": "^4.23.1",
89
+ "oxfmt": "^0.63.0",
90
+ "oxlint": "^1.78.0",
91
+ "tsx": "^4.23.12",
91
92
  "typescript": "^7.0.2"
92
93
  },
93
- "overrides": {
94
- "@hono/node-server": "2.0.11"
95
- },
96
94
  "engines": {
97
95
  "node": ">=20"
98
96
  }
@@ -11,7 +11,7 @@ import {
11
11
  readProxyConfig,
12
12
  } from '../../native-host/src/config.js';
13
13
  import { pingExistingDaemon } from '../../native-host/src/daemon.js';
14
- import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
14
+ import { restartBridgeDaemon, stopBridgeDaemon } from '../../native-host/src/daemon-process.js';
15
15
  import { atomicWriteFile } from './atomic-write.js';
16
16
  import { parseIntArg } from './cli-helpers.js';
17
17
  import {
@@ -44,6 +44,8 @@ export async function handleProxyCommand(args) {
44
44
  randomUUID
45
45
  );
46
46
  assertProxyBindSafety(existing, options, bindHost);
47
+ // Stop against the current config before changing its port or auth token.
48
+ const stopResult = await stopBridgeDaemon();
47
49
  await atomicWriteFile(getBridgeAuthTokenPath(), `${token}\n`, {
48
50
  encoding: 'utf8',
49
51
  mode: 0o600,
@@ -75,7 +77,7 @@ export async function handleProxyCommand(args) {
75
77
  '',
76
78
  `Token: ${token}${tokenNote}`,
77
79
  `Config: ${configPath}`,
78
- `Daemon: ${result.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
80
+ `Daemon: ${stopResult.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
79
81
  ];
80
82
  if (tokenSource !== 'existing') {
81
83
  if (isLoopbackHost(bindHost)) {
@@ -106,10 +108,11 @@ export async function handleProxyCommand(args) {
106
108
 
107
109
  if (subcommand === 'disable') {
108
110
  const configPath = getProxyConfigPath();
111
+ const stopResult = await stopBridgeDaemon();
109
112
  await fs.promises.rm(configPath, { force: true });
110
- const result = await restartBridgeDaemon();
113
+ await restartBridgeDaemon();
111
114
  process.stdout.write(
112
- `Browser Bridge proxy disabled. Daemon ${result.previouslyRunning ? 'restarted' : 'started'}.\n`
115
+ `Browser Bridge proxy disabled. Daemon ${stopResult.previouslyRunning ? 'restarted' : 'started'}.\n`
113
116
  );
114
117
  return;
115
118
  }
@@ -267,7 +267,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
267
267
  'bbx uninstall Remove native host manifests, Browser Bridge runtime files, and managed MCP/skill installs',
268
268
  'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
269
269
  'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
270
- ' MCP starts with common tools; browser_toolset loads one specialized typed tool by name when needed.',
270
+ ' MCP uses a compact surface: legacy clients can load typed tools; modern stateless clients use browser_call.',
271
271
  'bbx status Check bridge connection',
272
272
  'bbx doctor Consolidated local runtime, setup, protocol, and readiness diagnostics',
273
273
  'bbx restart Restart the local daemon and running MCP servers',
@@ -14,6 +14,8 @@ const DEFAULT_LOCK_TIMEOUT_MS = 120_000;
14
14
  const UPDATE_LOCK_PORT_START = 49_152;
15
15
  const UPDATE_LOCK_PORT_COUNT = 16_384;
16
16
  const UPDATE_LOCK_PORT_ATTEMPTS = 32;
17
+ const UPDATE_LOCK_PORT_STEP = UPDATE_LOCK_PORT_COUNT / UPDATE_LOCK_PORT_ATTEMPTS;
18
+ const UNAVAILABLE_LOCK_PORT_CODES = new Set(['EACCES', 'EADDRINUSE']);
17
19
 
18
20
  /** @typedef {import('./types.js').NpmUpdateResult} NpmUpdateResult */
19
21
 
@@ -113,6 +115,7 @@ export async function runNpmCommand(args, options = {}) {
113
115
  ? process.execPath
114
116
  : npmExecPath || (process.platform === 'win32' ? 'npm.cmd' : 'npm');
115
117
  const commandArgs = useNode && npmExecPath ? [npmExecPath, ...args] : args;
118
+ const useWindowsShell = process.platform === 'win32' && /\.(?:cmd|bat)$/iu.test(command);
116
119
 
117
120
  return new Promise((resolve, reject) => {
118
121
  execFile(
@@ -124,6 +127,7 @@ export async function runNpmCommand(args, options = {}) {
124
127
  timeout: options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
125
128
  maxBuffer: 4 * 1024 * 1024,
126
129
  windowsHide: true,
130
+ shell: useWindowsShell,
127
131
  },
128
132
  (error, stdout, stderr) => {
129
133
  if (!error) {
@@ -154,7 +158,8 @@ export function getNpmUpdateLockPort(lockKey, offset = 0) {
154
158
  const digest = createHash('sha256').update(lockKey).digest();
155
159
  return (
156
160
  UPDATE_LOCK_PORT_START +
157
- ((digest.readUInt32BE(0) + Math.max(0, Math.trunc(offset))) % UPDATE_LOCK_PORT_COUNT)
161
+ ((digest.readUInt32BE(0) + Math.max(0, Math.trunc(offset)) * UPDATE_LOCK_PORT_STEP) %
162
+ UPDATE_LOCK_PORT_COUNT)
158
163
  );
159
164
  }
160
165
 
@@ -200,7 +205,9 @@ export async function acquireNpmUpdateLock(lockKey, timeoutMs = DEFAULT_LOCK_TIM
200
205
  const server = net.createServer((socket) => socket.end(`${identity}\n`));
201
206
  const acquired = await new Promise((resolve, reject) => {
202
207
  server.once('error', (error) => {
203
- if (/** @type {NodeJS.ErrnoException} */ (error).code === 'EADDRINUSE') {
208
+ if (
209
+ UNAVAILABLE_LOCK_PORT_CODES.has(/** @type {NodeJS.ErrnoException} */ (error).code ?? '')
210
+ ) {
204
211
  resolve(false);
205
212
  return;
206
213
  }
@@ -106,7 +106,7 @@ export async function ensureClientConnected(client) {
106
106
  * @param {BridgeClient} client
107
107
  * @param {BridgeMethod} method
108
108
  * @param {Record<string, unknown>} [params={}]
109
- * @param {{ tabId?: number | null, source?: BridgeRequestSource, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} [options]
109
+ * @param {{ tabId?: number | null, source?: BridgeRequestSource, mcpEra?: import('./types.js').McpProtocolEra, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} [options]
110
110
  * @returns {Promise<BridgeResponse>}
111
111
  */
112
112
  export async function requestBridge(client, method, params = {}, options = {}) {
@@ -127,7 +127,12 @@ export async function requestBridge(client, method, params = {}, options = {}) {
127
127
  method,
128
128
  params,
129
129
  tabId: methodNeedsTab(method) ? (options.tabId ?? null) : null,
130
- meta: withRequestMeta(options.source, options.tokenBudget, options.automaticRetry),
130
+ meta: withRequestMeta(
131
+ options.source,
132
+ options.mcpEra,
133
+ options.tokenBudget,
134
+ options.automaticRetry
135
+ ),
131
136
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
132
137
  });
133
138
  }
@@ -168,16 +173,20 @@ export async function resolveRef(client, refOrSelector, tabId = null, source) {
168
173
 
169
174
  /**
170
175
  * @param {BridgeRequestSource | undefined} source
176
+ * @param {import('./types.js').McpProtocolEra | undefined} mcpEra
171
177
  * @param {number | null | undefined} tokenBudget
172
178
  * @param {'mcp_second_attempt' | undefined} automaticRetry
173
179
  * @returns {BridgeMeta}
174
180
  */
175
- function withRequestMeta(source, tokenBudget, automaticRetry) {
181
+ function withRequestMeta(source, mcpEra, tokenBudget, automaticRetry) {
176
182
  /** @type {BridgeMeta} */
177
183
  const meta = {};
178
184
  if (source) {
179
185
  meta.source = source;
180
186
  }
187
+ if (source === 'mcp' && mcpEra) {
188
+ meta.mcp_era = mcpEra;
189
+ }
181
190
  if (typeof tokenBudget === 'number' && Number.isFinite(tokenBudget)) {
182
191
  meta.token_budget = tokenBudget;
183
192
  }
@@ -4,6 +4,7 @@ import type {
4
4
  BridgeMethod,
5
5
  BridgeRequestSource,
6
6
  BridgeResponse,
7
+ McpProtocolEra,
7
8
  RecoveryEventKind,
8
9
  RecoveryTelemetrySummary,
9
10
  ScreenshotResult,
@@ -17,6 +18,7 @@ export type {
17
18
  BridgeRequestSource,
18
19
  BridgeResponse,
19
20
  BridgeTransport,
21
+ McpProtocolEra,
20
22
  ScreenshotResult,
21
23
  };
22
24
 
@@ -9,7 +9,7 @@ import { startBridgeMcpServer } from './server.js';
9
9
 
10
10
  /**
11
11
  * @typedef {{
12
- * start?: () => Promise<void>,
12
+ * start?: () => void | Promise<void>,
13
13
  * startControl?: typeof tryStartMcpProcessControl,
14
14
  * argv?: string[],
15
15
  * stdout?: { write: (chunk: string) => unknown },
@@ -29,20 +29,26 @@ const WORKFLOW_INSTRUCTIONS = Object.freeze([
29
29
  const SERVER_HEADER =
30
30
  "Browser Bridge MCP inspects and interacts with the user's real Chrome tab through a compact tool set.";
31
31
 
32
- const TOOL_RULES = Object.freeze([
32
+ const LEGACY_TOOL_RULES = Object.freeze([
33
33
  'The common tools are available immediately. When a specialized typed tool is useful, call browser_toolset with its exact tool name; browser_call always reaches every bridge method without loading another tool.',
34
34
  'Use browser_call method protocol.describe with method or group params to load unfamiliar signatures cheaply.',
35
35
  ]);
36
36
 
37
+ const MODERN_TOOL_RULES = Object.freeze([
38
+ 'The compact tool list is static for stateless MCP. browser_call reaches every bridge method, and browser_skill returns runtime groups and limits.',
39
+ 'Use browser_call method protocol.describe with method or group params to load unfamiliar signatures cheaply.',
40
+ ]);
41
+
37
42
  /**
38
- * Build instructions for progressive tool discovery.
43
+ * Build instructions for the negotiated MCP era.
39
44
  *
45
+ * @param {'legacy' | 'modern'} [era='legacy']
40
46
  * @returns {string}
41
47
  */
42
- export function getMcpServerInstructions() {
43
- return [SERVER_HEADER, ...TOOL_RULES, ...SHARED_INSTRUCTIONS, ...WORKFLOW_INSTRUCTIONS].join(
44
- '\n'
45
- );
48
+ export function getMcpServerInstructions(era = 'legacy') {
49
+ const toolRules = era === 'modern' ? MODERN_TOOL_RULES : LEGACY_TOOL_RULES;
50
+ return [SERVER_HEADER, ...toolRules, ...SHARED_INSTRUCTIONS, ...WORKFLOW_INSTRUCTIONS].join('\n');
46
51
  }
47
52
 
48
53
  export const MCP_SERVER_INSTRUCTIONS = getMcpServerInstructions();
54
+ export const MODERN_MCP_SERVER_INSTRUCTIONS = getMcpServerInstructions('modern');
@@ -1,5 +1,6 @@
1
1
  // @ts-check
2
2
 
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
4
  import { randomUUID } from 'node:crypto';
4
5
 
5
6
  import {
@@ -46,6 +47,18 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-clie
46
47
 
47
48
  export const REQUEST_SOURCE = 'mcp';
48
49
  const MCP_CLIENT_ID = `mcp_${randomUUID()}`;
50
+ /** @type {AsyncLocalStorage<import('../../protocol/src/types.js').McpProtocolEra>} */
51
+ const MCP_REQUEST_ERA = new AsyncLocalStorage();
52
+
53
+ /**
54
+ * @template T
55
+ * @param {import('../../protocol/src/types.js').McpProtocolEra} era
56
+ * @param {() => T} callback
57
+ * @returns {T}
58
+ */
59
+ export function runWithMcpRequestEra(era, callback) {
60
+ return MCP_REQUEST_ERA.run(era, callback);
61
+ }
49
62
 
50
63
  /** @type {ReadonlySet<BridgeMethod>} */
51
64
  const RETRY_SAFE_METHODS = new Set([
@@ -606,14 +619,18 @@ export async function waitForClientReconnect(client, timeoutMs = RECONNECT_WAIT_
606
619
  * @param {import('../../agent-client/src/client.js').BridgeClient} client
607
620
  * @param {BridgeMethod} method
608
621
  * @param {Record<string, unknown>} params
609
- * @param {{ tabId?: number | null, source?: import('../../protocol/src/types.js').BridgeRequestSource, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} options
622
+ * @param {{ tabId?: number | null, source?: import('../../protocol/src/types.js').BridgeRequestSource, mcpEra?: import('../../protocol/src/types.js').McpProtocolEra, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} options
610
623
  * @returns {Promise<BridgeResponse>}
611
624
  */
612
625
  export async function requestBridgeWithRetry(client, method, params, options) {
626
+ const requestOptions = {
627
+ ...options,
628
+ mcpEra: options.mcpEra ?? MCP_REQUEST_ERA.getStore(),
629
+ };
613
630
  /** @type {BridgeResponse} */
614
631
  let response;
615
632
  try {
616
- response = await requestBridge(client, method, params, options);
633
+ response = await requestBridge(client, method, params, requestOptions);
617
634
  } catch (error) {
618
635
  if (!isConnectionLossError(error) || !isRetrySafeBridgeMethod(method, params)) {
619
636
  throw error;
@@ -625,7 +642,7 @@ export async function requestBridgeWithRetry(client, method, params, options) {
625
642
  throw error;
626
643
  }
627
644
  return requestBridge(client, method, params, {
628
- ...options,
645
+ ...requestOptions,
629
646
  automaticRetry: 'mcp_second_attempt',
630
647
  });
631
648
  }
@@ -640,7 +657,7 @@ export async function requestBridgeWithRetry(client, method, params, options) {
640
657
  );
641
658
  await new Promise((r) => setTimeout(r, delay));
642
659
  return requestBridge(client, method, params, {
643
- ...options,
660
+ ...requestOptions,
644
661
  automaticRetry: 'mcp_second_attempt',
645
662
  });
646
663
  }
@@ -11,6 +11,7 @@ import {
11
11
  getToolTokenBudget,
12
12
  getBridgeDestinations,
13
13
  applyLimitBudgetPreset,
14
+ requestBridgeWithRetry,
14
15
  summarizeToolError,
15
16
  summarizeToolResponse,
16
17
  withToolClient,
@@ -34,6 +35,7 @@ export {
34
35
  callBridgeTool,
35
36
  dispatchToolAction,
36
37
  REQUEST_SOURCE,
38
+ runWithMcpRequestEra,
37
39
  } from './handlers-utils.js';
38
40
 
39
41
  export {
@@ -233,10 +235,14 @@ export async function handleHealthTool(args = {}) {
233
235
  const destinationId = typeof args.destinationId === 'string' ? args.destinationId : null;
234
236
  return withToolClient(
235
237
  async (client) => {
236
- const response = await client.request({
237
- method: 'health.ping',
238
- meta: { source: REQUEST_SOURCE },
239
- });
238
+ const response = await requestBridgeWithRetry(
239
+ client,
240
+ 'health.ping',
241
+ {},
242
+ {
243
+ source: REQUEST_SOURCE,
244
+ }
245
+ );
240
246
  return summarizeToolResponse(response, 'health.ping');
241
247
  },
242
248
  { destinationId }
@@ -2,9 +2,9 @@
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';
@@ -32,6 +32,7 @@ import {
32
32
  handleInvestigateTool,
33
33
  createToolResult,
34
34
  MAX_BATCH_CALLS,
35
+ runWithMcpRequestEra,
35
36
  } from './handlers.js';
36
37
  import {
37
38
  ARTIFACT_CHUNK_BYTES,
@@ -52,7 +53,7 @@ import {
52
53
  } from '../../protocol/src/index.js';
53
54
  import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
54
55
  import { getMcpServerInstructions } from './guidance.js';
55
- import { isInitiallyEnabledTool, LOADABLE_TOOLSET_TOOLS } from './toolset.js';
56
+ import { isToolEnabledForEra, LOADABLE_TOOLSET_TOOLS } from './toolset.js';
56
57
 
57
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.`;
58
59
  export const TAB_ID_DESCRIPTION =
@@ -130,18 +131,20 @@ function getInvestigateToolDescription() {
130
131
  }
131
132
 
132
133
  /**
133
- * Create the MCP server with a compact initial surface and on-demand typed tools.
134
+ * Create the MCP server for one negotiated protocol era.
134
135
  *
136
+ * @param {{ era?: 'legacy' | 'modern' }} [options]
135
137
  * @returns {McpServer}
136
138
  */
137
- export function createBridgeMcpServer() {
139
+ export function createBridgeMcpServer(options = {}) {
140
+ const { era = 'legacy' } = options;
138
141
  const server = new McpServer(
139
142
  {
140
143
  name: 'browser-bridge',
141
144
  version: MCP_SERVER_VERSION,
142
145
  },
143
146
  {
144
- instructions: getMcpServerInstructions(),
147
+ instructions: getMcpServerInstructions(era),
145
148
  }
146
149
  );
147
150
 
@@ -149,19 +152,31 @@ export function createBridgeMcpServer() {
149
152
  const registrations = new Map();
150
153
 
151
154
  /**
152
- * Register a tool, then hide it when it is not part of the initial surface.
155
+ * Register a tool, then hide it when it is not part of the era's compact surface.
153
156
  *
154
157
  * Registering first keeps every call site checked against the SDK's own
155
158
  * generic signature instead of a hand-written passthrough type. Disabling
156
- * happens before `connect()`, so an excluded tool never reaches `tools/list`
157
- * but can be enabled later by browser_toolset.
159
+ * happens before `connect()`, so an excluded tool never reaches `tools/list`.
158
160
  *
159
- * @type {typeof server.registerTool}
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>}
160
172
  */
161
173
  const registerTool = (name, config, handler) => {
162
- const registration = server.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);
163
178
  registrations.set(name, registration);
164
- if (!isInitiallyEnabledTool(name)) {
179
+ if (!isToolEnabledForEra(name, era)) {
165
180
  registration.disable();
166
181
  }
167
182
  return registration;
@@ -173,12 +188,12 @@ export function createBridgeMcpServer() {
173
188
  title: 'Browser Bridge Status',
174
189
  description:
175
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.',
176
- inputSchema: {
191
+ inputSchema: z.object({
177
192
  destinationId: z
178
193
  .string()
179
194
  .optional()
180
195
  .describe('Inspect one destination instead of aggregating all configured destinations.'),
181
- },
196
+ }),
182
197
  },
183
198
  handleStatusTool
184
199
  );
@@ -188,12 +203,12 @@ export function createBridgeMcpServer() {
188
203
  {
189
204
  title: 'Browser Bridge Setup Status',
190
205
  description: 'Check MCP and CLI skill installation status for agent integration.',
191
- inputSchema: {
206
+ inputSchema: z.object({
192
207
  global: z
193
208
  .boolean()
194
209
  .optional()
195
210
  .describe('Check global (true) or local (false) config (default: true)'),
196
- },
211
+ }),
197
212
  },
198
213
  handleSetupTool
199
214
  );
@@ -203,7 +218,7 @@ export function createBridgeMcpServer() {
203
218
  {
204
219
  title: 'Browser Bridge Logs',
205
220
  description: 'Tail recent bridge request logs for debugging connection or routing issues.',
206
- inputSchema: {
221
+ inputSchema: z.object({
207
222
  limit: z
208
223
  .number()
209
224
  .int()
@@ -215,7 +230,7 @@ export function createBridgeMcpServer() {
215
230
  .optional()
216
231
  .describe(BUDGET_PRESET_DESCRIPTION),
217
232
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
218
- },
233
+ }),
219
234
  },
220
235
  handleLogTool
221
236
  );
@@ -225,13 +240,13 @@ export function createBridgeMcpServer() {
225
240
  {
226
241
  title: 'Browser Bridge Health',
227
242
  description: 'Ping the bridge to verify daemon and extension connectivity.',
228
- inputSchema: {
243
+ inputSchema: z.object({
229
244
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
230
245
  intent: z
231
246
  .enum(['inspect', 'interact', 'capture', 'navigate', 'debugger', 'general'])
232
247
  .optional()
233
248
  .describe('Reported operation family shown in the access prompt (defaults to general).'),
234
- },
249
+ }),
235
250
  },
236
251
  handleHealthTool
237
252
  );
@@ -242,7 +257,7 @@ export function createBridgeMcpServer() {
242
257
  title: 'Browser Tabs',
243
258
  description:
244
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.',
245
- inputSchema: {
260
+ inputSchema: z.object({
246
261
  action: z
247
262
  .enum(['list', 'create', 'close', 'activate'])
248
263
  .describe('"list" (preferred), "create" (only when needed), "close", or "activate"'),
@@ -258,7 +273,7 @@ export function createBridgeMcpServer() {
258
273
  .string()
259
274
  .optional()
260
275
  .describe('Target one destination; list without it aggregates configured destinations.'),
261
- },
276
+ }),
262
277
  },
263
278
  handleTabsTool
264
279
  );
@@ -269,7 +284,7 @@ export function createBridgeMcpServer() {
269
284
  title: 'Browser DOM',
270
285
  description:
271
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.',
272
- inputSchema: {
287
+ inputSchema: z.object({
273
288
  action: z
274
289
  .enum([
275
290
  'query',
@@ -387,7 +402,7 @@ export function createBridgeMcpServer() {
387
402
  .positive()
388
403
  .optional()
389
404
  .describe(`Max HTML chars to return (default: ${DEFAULT_MAX_HTML_LENGTH})`),
390
- },
405
+ }),
391
406
  },
392
407
  handleDomTool
393
408
  );
@@ -398,7 +413,7 @@ export function createBridgeMcpServer() {
398
413
  title: 'Browser Styles And Layout',
399
414
  description:
400
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.',
401
- inputSchema: {
416
+ inputSchema: z.object({
402
417
  action: z
403
418
  .enum(['computed', 'matched_rules', 'box_model', 'hit_test'])
404
419
  .describe('Style/layout operation to perform'),
@@ -426,7 +441,7 @@ export function createBridgeMcpServer() {
426
441
  .nonnegative()
427
442
  .optional()
428
443
  .describe('Y coordinate for hit_test (viewport relative)'),
429
- },
444
+ }),
430
445
  },
431
446
  handleStylesLayoutTool
432
447
  );
@@ -437,7 +452,7 @@ export function createBridgeMcpServer() {
437
452
  title: 'Sensitive Browser Read',
438
453
  description:
439
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.',
440
- inputSchema: {
455
+ inputSchema: z.object({
441
456
  source: z.enum(['local_storage', 'session_storage']).describe('Exact storage source'),
442
457
  key: z
443
458
  .string()
@@ -445,7 +460,7 @@ export function createBridgeMcpServer() {
445
460
  .describe('Exact storage key, including an empty key if intended'),
446
461
  tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
447
462
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
448
- },
463
+ }),
449
464
  },
450
465
  handleSensitiveReadTool
451
466
  );
@@ -456,7 +471,7 @@ export function createBridgeMcpServer() {
456
471
  title: 'Browser Page State',
457
472
  description:
458
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.',
459
- inputSchema: {
474
+ inputSchema: z.object({
460
475
  action: z
461
476
  .enum([
462
477
  'state',
@@ -588,7 +603,7 @@ export function createBridgeMcpServer() {
588
603
  .enum(['exact', 'contains', 'regex'])
589
604
  .optional()
590
605
  .describe('URL match mode for wait_for_load (default: exact)'),
591
- },
606
+ }),
592
607
  },
593
608
  handlePageTool
594
609
  );
@@ -599,7 +614,7 @@ export function createBridgeMcpServer() {
599
614
  title: 'Browser Navigation',
600
615
  description:
601
616
  'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed - use only for exact viewport overrides.',
602
- inputSchema: {
617
+ inputSchema: z.object({
603
618
  action: z
604
619
  .enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
605
620
  .describe('Navigation operation to perform'),
@@ -632,7 +647,7 @@ export function createBridgeMcpServer() {
632
647
  .optional()
633
648
  .describe('Viewport device scale factor (for resize)'),
634
649
  reset: z.boolean().optional().describe('Reset viewport to original size (for resize)'),
635
- },
650
+ }),
636
651
  },
637
652
  handleNavigationTool
638
653
  );
@@ -643,7 +658,7 @@ export function createBridgeMcpServer() {
643
658
  title: 'Browser Input',
644
659
  description:
645
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.',
646
- inputSchema: {
661
+ inputSchema: z.object({
647
662
  action: z
648
663
  .enum([
649
664
  'click',
@@ -744,7 +759,7 @@ export function createBridgeMcpServer() {
744
759
  .describe('Drag destination selector (alternative to destinationElementRef)'),
745
760
  offsetX: z.number().optional().describe('Drag drop offset X (default: 0)'),
746
761
  offsetY: z.number().optional().describe('Drag drop offset Y (default: 0)'),
747
- },
762
+ }),
748
763
  },
749
764
  handleInputTool
750
765
  );
@@ -755,7 +770,7 @@ export function createBridgeMcpServer() {
755
770
  title: 'Browser Patch',
756
771
  description:
757
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.',
758
- inputSchema: {
773
+ inputSchema: z.object({
759
774
  action: z
760
775
  .enum(['apply_styles', 'apply_dom', 'list', 'rollback', 'commit_baseline'])
761
776
  .describe('Patch operation to perform'),
@@ -795,7 +810,7 @@ export function createBridgeMcpServer() {
795
810
  .describe(
796
811
  'Return computed result inline after applying, eliminating a verification round-trip'
797
812
  ),
798
- },
813
+ }),
799
814
  },
800
815
  handlePatchTool
801
816
  );
@@ -806,7 +821,7 @@ export function createBridgeMcpServer() {
806
821
  title: 'Browser Capture',
807
822
  description:
808
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.',
809
- inputSchema: {
824
+ inputSchema: z.object({
810
825
  action: z
811
826
  .enum([
812
827
  'element',
@@ -872,7 +887,7 @@ export function createBridgeMcpServer() {
872
887
  .max(4)
873
888
  .optional()
874
889
  .describe('Screenshot capture scale (0.1 to 4)'),
875
- },
890
+ }),
876
891
  },
877
892
  handleCaptureTool
878
893
  );
@@ -882,8 +897,8 @@ export function createBridgeMcpServer() {
882
897
  {
883
898
  title: 'Browser Artifact',
884
899
  description:
885
- 'Read (chunked base64) or delete a daemon-owned artifact produced by artifact delivery (screenshots, HAR exports). Artifacts are scoped to this MCP session and expire quickly. Page reads with offset until nextOffset is null, then verify the reassembled bytes against sha256.',
886
- inputSchema: {
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({
887
902
  action: z.enum(['read', 'delete']).describe('Artifact operation to perform'),
888
903
  artifactId: z
889
904
  .string()
@@ -907,7 +922,7 @@ export function createBridgeMcpServer() {
907
922
  .enum(['quick', 'normal', 'deep'])
908
923
  .optional()
909
924
  .describe(BUDGET_PRESET_DESCRIPTION),
910
- },
925
+ }),
911
926
  },
912
927
  handleArtifactTool
913
928
  );
@@ -918,7 +933,7 @@ export function createBridgeMcpServer() {
918
933
  title: 'Browser Network Intercept',
919
934
  description:
920
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.',
921
- inputSchema: {
936
+ inputSchema: z.object({
922
937
  action: z
923
938
  .enum(['add', 'list', 'remove', 'clear'])
924
939
  .describe('Intercept operation to perform'),
@@ -951,7 +966,7 @@ export function createBridgeMcpServer() {
951
966
  .optional()
952
967
  .describe('Response headers for fulfill'),
953
968
  ruleId: z.string().min(1).optional().describe('Rule ID (required for remove)'),
954
- },
969
+ }),
955
970
  },
956
971
  handleInterceptTool
957
972
  );
@@ -968,7 +983,7 @@ export function createBridgeMcpServer() {
968
983
  idempotentHint: true,
969
984
  openWorldHint: true,
970
985
  },
971
- inputSchema: {
986
+ inputSchema: z.object({
972
987
  calls: z
973
988
  .array(
974
989
  z.object({
@@ -988,7 +1003,7 @@ export function createBridgeMcpServer() {
988
1003
  .min(1)
989
1004
  .max(MAX_BATCH_CALLS)
990
1005
  .describe('Calls to execute in parallel'),
991
- },
1006
+ }),
992
1007
  },
993
1008
  handleBatchTool
994
1009
  );
@@ -999,7 +1014,7 @@ export function createBridgeMcpServer() {
999
1014
  title: 'Raw Browser Bridge Call',
1000
1015
  description:
1001
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.',
1002
- inputSchema: {
1017
+ inputSchema: z.object({
1003
1018
  method: z.string().describe('Bridge method name (e.g., "dom.query", "input.click")'),
1004
1019
  params: z
1005
1020
  .record(z.string(), z.unknown())
@@ -1011,7 +1026,7 @@ export function createBridgeMcpServer() {
1011
1026
  .enum(['quick', 'normal', 'deep'])
1012
1027
  .optional()
1013
1028
  .describe(BUDGET_PRESET_DESCRIPTION),
1014
- },
1029
+ }),
1015
1030
  },
1016
1031
  handleRawCallTool
1017
1032
  );
@@ -1022,9 +1037,9 @@ export function createBridgeMcpServer() {
1022
1037
  title: 'Browser Bridge Runtime Context',
1023
1038
  description:
1024
1039
  'Return runtime context: budget presets, method groups, and active limits. Call to discover defaults before inspecting a page.',
1025
- inputSchema: {
1040
+ inputSchema: z.object({
1026
1041
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
1027
- },
1042
+ }),
1028
1043
  },
1029
1044
  handleSkillTool
1030
1045
  );
@@ -1035,9 +1050,9 @@ export function createBridgeMcpServer() {
1035
1050
  title: 'Request Browser Bridge Access',
1036
1051
  description:
1037
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.',
1038
- inputSchema: {
1053
+ inputSchema: z.object({
1039
1054
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
1040
- },
1055
+ }),
1041
1056
  },
1042
1057
  handleAccessTool
1043
1058
  );
@@ -1056,7 +1071,7 @@ export function createBridgeMcpServer() {
1056
1071
  _meta: {
1057
1072
  delegationHint: createInvestigateDelegationHint(),
1058
1073
  },
1059
- inputSchema: {
1074
+ inputSchema: z.object({
1060
1075
  objective: z
1061
1076
  .string()
1062
1077
  .describe('What to find, verify, or extract from the current page (natural language).'),
@@ -1074,7 +1089,7 @@ export function createBridgeMcpServer() {
1074
1089
  .string()
1075
1090
  .optional()
1076
1091
  .describe('Optional CSS selector to scope the investigation to a subtree.'),
1077
- },
1092
+ }),
1078
1093
  },
1079
1094
  handleInvestigateTool
1080
1095
  );
@@ -1084,12 +1099,12 @@ export function createBridgeMcpServer() {
1084
1099
  {
1085
1100
  title: 'Load Browser Bridge Tool',
1086
1101
  description:
1087
- 'Load one specialized typed Browser Bridge tool by exact name. The tool appears in tools/list automatically and remains enabled for this MCP session.',
1088
- inputSchema: {
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({
1089
1104
  tool: z
1090
1105
  .enum(LOADABLE_TOOLSET_TOOLS)
1091
1106
  .describe('Exact specialized Browser Bridge tool to load.'),
1092
- },
1107
+ }),
1093
1108
  },
1094
1109
  ({ tool }) => {
1095
1110
  const registration = registrations.get(tool);
@@ -1106,11 +1121,11 @@ export function createBridgeMcpServer() {
1106
1121
  }
1107
1122
 
1108
1123
  /**
1109
- * @returns {Promise<void>}
1124
+ * @param {{ serve?: typeof serveStdio }} [options]
1125
+ * @returns {void}
1110
1126
  */
1111
- export async function startBridgeMcpServer() {
1127
+ export function startBridgeMcpServer(options = {}) {
1128
+ const { serve = serveStdio } = options;
1112
1129
  applyWindowsTcpTransportDefaults();
1113
- const server = createBridgeMcpServer();
1114
- const transport = new StdioServerTransport();
1115
- await server.connect(transport);
1130
+ serve(({ era }) => createBridgeMcpServer({ era }));
1116
1131
  }
@@ -16,6 +16,21 @@ export const INITIAL_TOOLSET_TOOLS = Object.freeze([
16
16
  'browser_toolset',
17
17
  ]);
18
18
 
19
+ /**
20
+ * Modern MCP tool lists cannot change as a side effect of another request.
21
+ * Keep the compact surface static and use browser_call for full protocol reach.
22
+ *
23
+ * @type {readonly string[]}
24
+ */
25
+ export const MODERN_TOOLSET_TOOLS = Object.freeze([
26
+ 'browser_access',
27
+ 'browser_batch',
28
+ 'browser_call',
29
+ 'browser_health',
30
+ 'browser_skill',
31
+ 'browser_status',
32
+ ]);
33
+
19
34
  /** @typedef {'browser_dom' | 'browser_styles_layout' | 'browser_page' | 'browser_logs' | 'browser_input' | 'browser_navigation' | 'browser_tabs' | 'browser_capture' | 'browser_artifact' | 'browser_patch' | 'browser_intercept' | 'browser_investigate' | 'browser_sensitive_read' | 'browser_setup' | 'browser_skill'} LoadableToolName */
20
35
 
21
36
  /** @type {readonly LoadableToolName[]} */
@@ -38,6 +53,7 @@ export const LOADABLE_TOOLSET_TOOLS = Object.freeze([
38
53
  ]);
39
54
 
40
55
  const INITIAL_TOOLSET = new Set(INITIAL_TOOLSET_TOOLS);
56
+ const MODERN_TOOLSET = new Set(MODERN_TOOLSET_TOOLS);
41
57
 
42
58
  /**
43
59
  * @param {string} toolName
@@ -46,3 +62,12 @@ const INITIAL_TOOLSET = new Set(INITIAL_TOOLSET_TOOLS);
46
62
  export function isInitiallyEnabledTool(toolName) {
47
63
  return INITIAL_TOOLSET.has(toolName);
48
64
  }
65
+
66
+ /**
67
+ * @param {string} toolName
68
+ * @param {'legacy' | 'modern'} era
69
+ * @returns {boolean}
70
+ */
71
+ export function isToolEnabledForEra(toolName, era) {
72
+ return era === 'modern' ? MODERN_TOOLSET.has(toolName) : INITIAL_TOOLSET.has(toolName);
73
+ }
@@ -60,7 +60,7 @@ setProtocolPackageVersion(DAEMON_VERSION);
60
60
  /** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
61
61
  /** @typedef {'agent' | 'extension'} SocketRole */
62
62
  /** @typedef {import('node:net').Socket & { readonly __role?: SocketRole, __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __browserExtensionId?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
63
- /** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, protocolVersion?: string, baselineId?: string | null, automaticMcpRetry?: boolean, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
63
+ /** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, mcpEra?: string, method?: string, protocolVersion?: string, baselineId?: string | null, automaticMcpRetry?: boolean, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
64
64
  /**
65
65
  * @typedef {{
66
66
  * installAgentFiles: typeof import('../../agent-client/src/install.js').installAgentFiles,
@@ -1165,6 +1165,7 @@ export class BridgeDaemon {
1165
1165
  method: request.method,
1166
1166
  protocolVersion: request.meta?.protocol_version,
1167
1167
  source: typeof request.meta?.source === 'string' ? request.meta.source : '',
1168
+ mcpEra: typeof request.meta?.mcp_era === 'string' ? request.meta.mcp_era : '',
1168
1169
  baselineId,
1169
1170
  automaticMcpRetry: isAutomaticMcpRetry(request.meta),
1170
1171
  targets: new Set([target]),
@@ -1603,6 +1604,7 @@ export class BridgeDaemon {
1603
1604
  ok: true,
1604
1605
  id: responseMessage.id,
1605
1606
  source: pending.source || null,
1607
+ ...(pending.mcpEra ? { mcpEra: pending.mcpEra } : {}),
1606
1608
  });
1607
1609
  return;
1608
1610
  }
@@ -1702,6 +1704,7 @@ export class BridgeDaemon {
1702
1704
  ok: false,
1703
1705
  id: requestId,
1704
1706
  source: pending.source || null,
1707
+ ...(pending.mcpEra ? { mcpEra: pending.mcpEra } : {}),
1705
1708
  });
1706
1709
 
1707
1710
  await writeJsonLine(pending.socket, {
@@ -372,11 +372,21 @@ export function validateBridgeRequest(request) {
372
372
  typeof meta.protocol_version === 'string' ? meta.protocol_version : getProtocolVersion(),
373
373
  token_budget: typeof meta.token_budget === 'number' ? meta.token_budget : null,
374
374
  source: meta.source === 'cli' || meta.source === 'mcp' ? meta.source : undefined,
375
+ mcp_era: normalizeMcpEra(meta.source, meta.mcp_era),
375
376
  automatic_retry: normalizeAutomaticRetryMeta(meta.source, meta.automatic_retry),
376
377
  },
377
378
  };
378
379
  }
379
380
 
381
+ /**
382
+ * @param {unknown} source
383
+ * @param {unknown} value
384
+ * @returns {import('./types.js').McpProtocolEra | undefined}
385
+ */
386
+ function normalizeMcpEra(source, value) {
387
+ return source === 'mcp' && (value === 'legacy' || value === 'modern') ? value : undefined;
388
+ }
389
+
380
390
  /**
381
391
  * @param {unknown} source
382
392
  * @param {unknown} value
@@ -1,4 +1,5 @@
1
1
  export type BridgeRequestSource = 'cli' | 'mcp';
2
+ export type McpProtocolEra = 'legacy' | 'modern';
2
3
 
3
4
  export type AccessIntent = 'inspect' | 'interact' | 'capture' | 'navigate' | 'debugger' | 'general';
4
5
 
@@ -153,6 +154,7 @@ export interface BridgeMeta {
153
154
  image_approx_tokens?: number;
154
155
  image_bytes?: number;
155
156
  source?: BridgeRequestSource;
157
+ mcp_era?: McpProtocolEra;
156
158
  response_bytes?: number;
157
159
  approx_tokens?: number;
158
160
  cost_class?: CostClass;
@@ -15,7 +15,7 @@ Skill name: `browser-bridge` (also known as `bbx`). In GitHub Copilot, invoke as
15
15
  When the runtime supports subagents, delegate bridge inspection to a smaller, lower-cost worker and return only concise findings to the parent.
16
16
  For open-ended investigation, start with structured reads (`page.get_state`, `dom.query`, `page.get_text`, `page.extract_content`, `styles.get_computed`, and `bbx batch` for CLI or `browser_batch` for MCP) and escalate to screenshots or debugger-backed methods only when structured evidence is insufficient. `browser_call` accepts one protocol method at a time; `batch` is not a valid `browser_call` method.
17
17
  When an unfamiliar method is needed, run `bbx protocol describe <method|group>` before guessing parameters. In MCP mode, call `browser_call` with method `protocol.describe` and params `{ "method": "dom.query" }` or `{ "group": "inspect" }`.
18
- MCP exposes common tools first. Load one specialized typed tool only when needed by calling `browser_toolset` with its exact name, for example `{ "tool": "browser_dom" }`; `browser_call` remains the universal fallback.
18
+ MCP exposes a compact tool surface. If `browser_toolset` is available on a legacy connection, load one specialized typed tool only when needed, for example `{ "tool": "browser_dom" }`. Modern stateless connections use a fixed surface with `browser_skill` instead. `browser_call` remains the universal path in both eras.
19
19
 
20
20
  ## CLI
21
21