@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
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.9.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
  }
@@ -1,9 +1,12 @@
1
1
  // @ts-check
2
2
 
3
- import { METHODS } from '../../protocol/src/index.js';
3
+ import { applyMethodBudgetPreset, isBudgetPresetName, METHODS } from '../../protocol/src/index.js';
4
4
  import { methodNeedsTab, parseIntArg, parseJsonObject } from './cli-helpers.js';
5
5
 
6
6
  /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
7
+ /** @typedef {import('../../protocol/src/defaults.js').BudgetPresetName} BudgetPresetName */
8
+
9
+ const CALL_USAGE = 'Usage: call [--tab <tabId>] [--preset quick|normal|deep] <method> [paramsJson]';
7
10
 
8
11
  /**
9
12
  * Read all of stdin as UTF-8 text. Resolves once stdin closes.
@@ -119,18 +122,44 @@ export function extractHarFlags(args) {
119
122
  return { limit, urlPattern, delivery: rawDelivery, rest };
120
123
  }
121
124
 
125
+ /**
126
+ * Extract the --preset flag from anywhere in the argument list. Preset names
127
+ * are validated against the shared budget presets; explicit per-call params
128
+ * always override preset defaults.
129
+ *
130
+ * @param {string[]} args
131
+ * @returns {{ preset: BudgetPresetName | null, rest: string[] }}
132
+ */
133
+ export function extractPresetFlag(args) {
134
+ const rest = [...args];
135
+ const index = rest.indexOf('--preset');
136
+ if (index === -1) {
137
+ return { preset: null, rest };
138
+ }
139
+ const value = rest[index + 1];
140
+ if (!value || value.startsWith('--')) {
141
+ throw new Error('--preset requires a value: quick, normal, or deep.');
142
+ }
143
+ if (!isBudgetPresetName(value)) {
144
+ throw new Error(`Unknown preset "${value}". Valid presets: quick, normal, deep.`);
145
+ }
146
+ rest.splice(index, 2);
147
+ return { preset: value, rest };
148
+ }
149
+
122
150
  /**
123
151
  * @param {string[]} args
124
152
  * @returns {Promise<{ tabId: number | null, method: BridgeMethod, params: Record<string, unknown> }>}
125
153
  */
126
154
  export async function parseCallCommand(args) {
127
- const parsed = extractTabFlag(args);
155
+ const presetParsed = extractPresetFlag(args);
156
+ const parsed = extractTabFlag(presetParsed.rest);
128
157
  const [first, second, ...extra] = parsed.rest;
129
158
  if (!first) {
130
- throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
159
+ throw new Error(CALL_USAGE);
131
160
  }
132
161
  if (extra.length > 0) {
133
- throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
162
+ throw new Error(CALL_USAGE);
134
163
  }
135
164
 
136
165
  if (first.includes('.')) {
@@ -146,11 +175,11 @@ export async function parseCallCommand(args) {
146
175
  return {
147
176
  method,
148
177
  tabId: methodNeedsTab(method) ? parsed.tabId : null,
149
- params: parseJsonObject(rawParams),
178
+ params: applyMethodBudgetPreset(method, parseJsonObject(rawParams), presetParsed.preset),
150
179
  };
151
180
  }
152
181
 
153
- throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
182
+ throw new Error(CALL_USAGE);
154
183
  }
155
184
 
156
185
  /**
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import {
4
+ applyMethodBudgetPreset,
4
5
  getProtocolVersion,
5
6
  isBatchSafeBridgeCall,
6
7
  MAX_BATCH_CALLS,
@@ -14,6 +15,7 @@ import { requestBridge } from './runtime.js';
14
15
 
15
16
  /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
16
17
  /** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
18
+ /** @typedef {import('../../protocol/src/defaults.js').BudgetPresetName} BudgetPresetName */
17
19
 
18
20
  /**
19
21
  * @typedef {{
@@ -72,9 +74,10 @@ function invalidBatchItem(method, message) {
72
74
  * @param {import('./client.js').BridgeClient} client
73
75
  * @param {string | undefined} input - Raw JSON array argument
74
76
  * @param {BridgeRequestSource} source - Request source tag (e.g. 'cli')
77
+ * @param {{ preset?: BudgetPresetName | null }} [options] - Optional budget preset applied to every call
75
78
  * @returns {Promise<BatchResultItem[]>}
76
79
  */
77
- export async function runBatchCalls(client, input, source) {
80
+ export async function runBatchCalls(client, input, source, options = {}) {
78
81
  if (!input) {
79
82
  throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
80
83
  }
@@ -90,7 +93,7 @@ export async function runBatchCalls(client, input, source) {
90
93
  if (calls.length === 0 || calls.length > MAX_BATCH_CALLS) {
91
94
  throw new Error(`Batch input must contain between 1 and ${MAX_BATCH_CALLS} calls.`);
92
95
  }
93
- const prepared = calls.map((call) => prepareBatchCall(call));
96
+ const prepared = calls.map((call) => prepareBatchCall(call, options.preset ?? null));
94
97
  if (prepared.some((item) => 'error' in item)) {
95
98
  return prepared.map((item) =>
96
99
  'error' in item
@@ -130,9 +133,10 @@ export async function runBatchCalls(client, input, source) {
130
133
 
131
134
  /**
132
135
  * @param {unknown} call
136
+ * @param {BudgetPresetName | null} preset
133
137
  * @returns {{ value: PreparedBatchCall } | { error: InvalidBatchItem }}
134
138
  */
135
- function prepareBatchCall(call) {
139
+ function prepareBatchCall(call, preset) {
136
140
  if (!call || typeof call !== 'object' || Array.isArray(call)) {
137
141
  return { error: invalidBatchItem('', 'Each batch call needs a method.') };
138
142
  }
@@ -162,7 +166,8 @@ function prepareBatchCall(call) {
162
166
  }
163
167
  const params =
164
168
  batchCall.params === undefined ? {} : /** @type {Record<string, unknown>} */ (batchCall.params);
165
- if (!isBatchSafeBridgeCall(method, params)) {
169
+ const mergedParams = applyMethodBudgetPreset(method, params, preset);
170
+ if (!isBatchSafeBridgeCall(method, mergedParams)) {
166
171
  return {
167
172
  error: invalidBatchItem(
168
173
  method,
@@ -177,7 +182,7 @@ function prepareBatchCall(call) {
177
182
  batchCall.tabId > 0
178
183
  ? batchCall.tabId
179
184
  : null;
180
- return { value: { method, params, tabId } };
185
+ return { value: { method, params: mergedParams, tabId } };
181
186
  }
182
187
 
183
188
  /**
@@ -3,6 +3,14 @@
3
3
  import readline from 'node:readline';
4
4
  import { bridgeMethodNeedsTab } from '../../protocol/src/index.js';
5
5
 
6
+ /**
7
+ * @param {string} value
8
+ * @returns {boolean}
9
+ */
10
+ export function isElementRef(value) {
11
+ return value.startsWith('el_');
12
+ }
13
+
6
14
  /**
7
15
  * Strip ANSI escape sequences from a string to prevent terminal injection
8
16
  * from malicious page content (e.g. DOM text, console output, eval results).
@@ -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
  }
@@ -150,7 +150,8 @@ export async function runInstallMcpCommand(args) {
150
150
  /** @type {string[]} */
151
151
  const positionals = [];
152
152
 
153
- for (const arg of args) {
153
+ for (let index = 0; index < args.length; index += 1) {
154
+ const arg = args[index];
154
155
  if (arg === '--local') {
155
156
  isGlobal = false;
156
157
  continue;
@@ -276,6 +277,7 @@ export async function runInstallMcpCommand(args) {
276
277
  projectPath: process.cwd(),
277
278
  stdout: process.stdout,
278
279
  });
280
+
279
281
  process.exit(0);
280
282
  }
281
283
 
@@ -10,7 +10,13 @@ import { fileURLToPath } from 'node:url';
10
10
 
11
11
  import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
12
12
  import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
13
- import { createRuntimeContext, METHODS } from '../../protocol/src/index.js';
13
+ import {
14
+ createRuntimeContext,
15
+ describeBridgeMethods,
16
+ isBridgeMethod,
17
+ isHarDocument,
18
+ METHODS,
19
+ } from '../../protocol/src/index.js';
14
20
  import {
15
21
  restartRegisteredMcpProcesses,
16
22
  tryStartMcpProcessControl,
@@ -19,6 +25,7 @@ import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
19
25
  import {
20
26
  extractRemoteFlag,
21
27
  extractHarFlags,
28
+ extractPresetFlag,
22
29
  extractScreenshotFlags,
23
30
  extractTabFlag,
24
31
  parseCallCommand,
@@ -26,7 +33,7 @@ import {
26
33
  readStdin,
27
34
  } from './cli-args.js';
28
35
  import { runBatchCalls } from './cli-batch.js';
29
- import { parseIntArg } from './cli-helpers.js';
36
+ import { isElementRef, parseIntArg } from './cli-helpers.js';
30
37
  import { printCallResponse, printJson, printSummary, printUsage } from './cli-output.js';
31
38
  import { handleProxyCommand, handleRemoteCommand } from './cli-proxy-remote.js';
32
39
  import {
@@ -64,6 +71,7 @@ const LOCAL_ONLY_COMMANDS = new Set([
64
71
  '--version',
65
72
  '-v',
66
73
  'skill',
74
+ 'protocol',
67
75
  'install',
68
76
  'uninstall',
69
77
  'install-skill',
@@ -111,6 +119,22 @@ if (command === 'skill') {
111
119
  process.exit(0);
112
120
  }
113
121
 
122
+ if (command === 'protocol') {
123
+ const [subcommand, target, ...extra] = rest;
124
+ if (subcommand !== 'describe' || extra.length > 0) {
125
+ process.stderr.write('Usage: bbx protocol describe [method|group]\n');
126
+ process.exit(1);
127
+ }
128
+ await runLocalCommand(async () => {
129
+ printJson(
130
+ target
131
+ ? describeBridgeMethods(isBridgeMethod(target) ? { method: target } : { group: target })
132
+ : describeBridgeMethods()
133
+ );
134
+ });
135
+ process.exit();
136
+ }
137
+
114
138
  if (command === 'install') {
115
139
  const { execFileSync } = await import('node:child_process');
116
140
  const installScript = path.resolve(
@@ -142,7 +166,7 @@ if (command === 'install-mcp') {
142
166
  }
143
167
 
144
168
  if (command === 'mcp') {
145
- const [subcommand, clientName] = rest;
169
+ const [subcommand, clientName, ...extra] = rest;
146
170
  if (subcommand === 'serve') {
147
171
  const control = await tryStartMcpProcessControl();
148
172
  try {
@@ -154,7 +178,7 @@ if (command === 'mcp') {
154
178
  await new Promise(() => {});
155
179
  }
156
180
  if (subcommand === 'config') {
157
- if (!clientName || !isMcpClientName(clientName)) {
181
+ if (!clientName || !isMcpClientName(clientName) || extra.length > 0) {
158
182
  process.stderr.write(`Usage: bbx mcp config <${MCP_CLIENT_NAMES.join('|')}>\n`);
159
183
  process.exit(1);
160
184
  }
@@ -260,8 +284,10 @@ async function main() {
260
284
  }
261
285
 
262
286
  if (command === 'access-request') {
287
+ if (rest.length > 1) throw new Error('Usage: bbx access-request [intent]');
288
+ const params = SHORTCUT_COMMANDS['access-request'].build(rest);
263
289
  await printSummary(
264
- await requestBridge(client, 'access.request', {}, { source: REQUEST_SOURCE })
290
+ await requestBridge(client, 'access.request', params, { source: REQUEST_SOURCE })
265
291
  );
266
292
  return;
267
293
  }
@@ -377,7 +403,8 @@ async function main() {
377
403
  }
378
404
 
379
405
  if (command === 'batch') {
380
- const results = await runBatchCalls(client, rest[0], REQUEST_SOURCE);
406
+ const { preset, rest: batchArgs } = extractPresetFlag(rest);
407
+ const results = await runBatchCalls(client, batchArgs[0], REQUEST_SOURCE, { preset });
381
408
  if (results.some((result) => !result.ok)) {
382
409
  process.exitCode = 1;
383
410
  }
@@ -412,7 +439,7 @@ async function main() {
412
439
  const [key, refOrSelector] = rest;
413
440
  if (!key) throw new Error('Usage: press-key <key> [ref|selector]');
414
441
  const target = refOrSelector
415
- ? refOrSelector.startsWith('el_')
442
+ ? isElementRef(refOrSelector)
416
443
  ? { elementRef: refOrSelector }
417
444
  : { selector: refOrSelector }
418
445
  : undefined;
@@ -755,14 +782,7 @@ function assertValidHarDocument(document) {
755
782
  if (!document || typeof document !== 'object' || Array.isArray(document)) {
756
783
  throw new Error('HAR response does not contain an inline HAR object.');
757
784
  }
758
- const log = Reflect.get(document, 'log');
759
- if (
760
- !log ||
761
- typeof log !== 'object' ||
762
- Array.isArray(log) ||
763
- Reflect.get(log, 'version') !== '1.2' ||
764
- !Array.isArray(Reflect.get(log, 'entries'))
765
- ) {
785
+ if (!isHarDocument(document)) {
766
786
  throw new Error('HAR response does not contain a valid HAR 1.2 document.');
767
787
  }
768
788
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { BRIDGE_METHOD_REGISTRY, BRIDGE_METHODS } from '../../protocol/src/index.js';
4
4
  import {
5
+ isElementRef,
5
6
  parseCommaList,
6
7
  parseIntArg,
7
8
  parseNumberArg,
@@ -39,7 +40,7 @@ function createShortcutCommand(method, usage, build, options = {}) {
39
40
  */
40
41
  function createAtomicTarget(value) {
41
42
  if (!value) throw new Error('A ref or selector is required.');
42
- return value.startsWith('el_') ? { elementRef: value } : { selector: value };
43
+ return isElementRef(value) ? { elementRef: value } : { selector: value };
43
44
  }
44
45
 
45
46
  /** @type {Record<string, ShortcutCommand>} */
@@ -266,6 +267,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
266
267
  'bbx uninstall Remove native host manifests, Browser Bridge runtime files, and managed MCP/skill installs',
267
268
  'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
268
269
  'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
270
+ ' MCP uses a compact surface: legacy clients can load typed tools; modern stateless clients use browser_call.',
269
271
  'bbx status Check bridge connection',
270
272
  'bbx doctor Consolidated local runtime, setup, protocol, and readiness diagnostics',
271
273
  'bbx restart Restart the local daemon and running MCP servers',
@@ -285,15 +287,17 @@ export const CLI_HELP_SECTIONS = Object.freeze([
285
287
  'bbx tab-close <tabId> Close a tab',
286
288
  'bbx tab-activate <tabId> Bring a tab to the foreground',
287
289
  'bbx skill Runtime budget presets and method groups',
290
+ 'bbx protocol describe [method|group] Describe method params or list registry groups locally',
288
291
  'bbx mcp serve Start Browser Bridge as an MCP stdio server',
289
292
  ],
290
293
  },
291
294
  {
292
295
  title: 'Generic RPC',
293
296
  lines: [
294
- 'bbx call [--tab <tabId>] <method> [paramsJson|-] Call any bridge method (- reads JSON from stdin)',
297
+ 'bbx call [--tab <tabId>] [--preset quick|normal|deep] <method> [paramsJson|-] Call any bridge method (- reads JSON from stdin)',
295
298
  'bbx <method> [--tab <tabId>] [paramsJson|-] Direct alias for exact bridge methods such as page.get_state',
296
- "bbx batch '[{method,params,tabId?},...]' Up to 20 parallel read-only calls",
299
+ "bbx batch [--preset quick|normal|deep] '[{method,params,tabId?},...]' Up to 20 parallel read-only calls",
300
+ '--preset fills budget gaps (maxNodes, maxDepth, textBudget, limit) from the shared quick|normal|deep presets; explicit params always win.',
297
301
  'Every bridge command accepts --remote <name> (or BBX_REMOTE env) to target a remote destination from `bbx remote list`.',
298
302
  'Advanced bridge params stay available through `bbx call`, even when shortcuts expose only the common case.',
299
303
  'For open-ended investigation, start with `bbx batch` on `page.get_state`, `dom.query`, and `page.get_text` before any screenshot or CDP call.',
@@ -91,23 +91,23 @@ export function getMcpConfigShape(clientName) {
91
91
  * env: Record<string, string>
92
92
  * } | {
93
93
  * type: 'local',
94
- * command: string[]
94
+ * command: string[],
95
+ * environment?: Record<string, string>
95
96
  * }}
96
97
  */
97
98
  function createBaseServerConfig(clientName) {
98
- const serverConfig = {
99
- command: MCP_COMMAND,
100
- args: [...MCP_ARGS],
101
- env: {},
102
- };
103
-
104
99
  if (clientName === 'opencode') {
105
100
  return {
106
101
  type: 'local',
107
102
  command: [MCP_COMMAND, ...MCP_ARGS],
108
103
  };
109
104
  }
110
- return serverConfig;
105
+
106
+ return {
107
+ command: MCP_COMMAND,
108
+ args: [...MCP_ARGS],
109
+ env: {},
110
+ };
111
111
  }
112
112
 
113
113
  /** @type {Record<McpClientName, { key: string, includeType: boolean, keepEmptyBlock?: boolean }>} */
@@ -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
  }
@@ -33,7 +33,7 @@ import {
33
33
  sanitizeIncidentalPath,
34
34
  sanitizeIncidentalText,
35
35
  } from '../../protocol/src/index.js';
36
- import { methodNeedsTab } from './cli-helpers.js';
36
+ import { isElementRef, methodNeedsTab } from './cli-helpers.js';
37
37
  import { BridgeClient } from './client.js';
38
38
  import { applyConfiguredAutoUpdate } from './config.js';
39
39
  import { readRemoteConfig } from './remotes.js';
@@ -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
  }
@@ -140,7 +145,7 @@ export async function requestBridge(client, method, params = {}, options = {}) {
140
145
  * @returns {Promise<string>}
141
146
  */
142
147
  export async function resolveRef(client, refOrSelector, tabId = null, source) {
143
- if (refOrSelector.startsWith('el_')) {
148
+ if (isElementRef(refOrSelector)) {
144
149
  return refOrSelector;
145
150
  }
146
151
 
@@ -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 },