@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 +1 -1
- package/package.json +8 -10
- package/packages/agent-client/src/cli-proxy-remote.js +7 -4
- package/packages/agent-client/src/command-registry.js +1 -1
- package/packages/agent-client/src/npm-self-update.js +9 -2
- package/packages/agent-client/src/runtime.js +12 -3
- package/packages/agent-client/src/types.ts +2 -0
- package/packages/mcp-server/src/bin.js +1 -1
- package/packages/mcp-server/src/guidance.js +12 -6
- package/packages/mcp-server/src/handlers-utils.js +21 -4
- package/packages/mcp-server/src/handlers.js +10 -4
- package/packages/mcp-server/src/server.js +77 -62
- package/packages/mcp-server/src/toolset.js +25 -0
- package/packages/native-host/src/daemon.js +4 -1
- package/packages/protocol/src/protocol.js +10 -0
- package/packages/protocol/src/types.ts +2 -0
- package/skills/browser-bridge/SKILL.md +1 -1
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
|
|
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.
|
|
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/
|
|
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
|
-
"@
|
|
83
|
-
"@types/
|
|
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.
|
|
89
|
-
"oxlint": "^1.
|
|
90
|
-
"tsx": "^4.23.
|
|
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: ${
|
|
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
|
-
|
|
113
|
+
await restartBridgeDaemon();
|
|
111
114
|
process.stdout.write(
|
|
112
|
-
`Browser Bridge proxy disabled. Daemon ${
|
|
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
|
|
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))) %
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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,
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
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/
|
|
6
|
-
import {
|
|
7
|
-
// zod is required at runtime by @modelcontextprotocol/
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
1124
|
+
* @param {{ serve?: typeof serveStdio }} [options]
|
|
1125
|
+
* @returns {void}
|
|
1110
1126
|
*/
|
|
1111
|
-
export
|
|
1127
|
+
export function startBridgeMcpServer(options = {}) {
|
|
1128
|
+
const { serve = serveStdio } = options;
|
|
1112
1129
|
applyWindowsTcpTransportDefaults();
|
|
1113
|
-
|
|
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
|
|
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
|
|