@browserbridge/bbx 1.7.3 → 1.7.5
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/package.json +7 -7
- package/packages/agent-client/src/atomic-write.js +99 -0
- package/packages/agent-client/src/cli-helpers.js +20 -3
- package/packages/agent-client/src/cli.js +273 -76
- package/packages/agent-client/src/command-registry.js +12 -6
- package/packages/agent-client/src/install.js +39 -4
- package/packages/agent-client/src/mcp-config.js +28 -14
- package/packages/agent-client/src/remotes.js +93 -15
- package/packages/agent-client/src/runtime.js +6 -3
- package/packages/mcp-server/src/bin.js +10 -1
- package/packages/mcp-server/src/guidance.js +2 -2
- package/packages/mcp-server/src/handlers-capture.js +62 -32
- package/packages/mcp-server/src/handlers-dom.js +2 -2
- package/packages/mcp-server/src/handlers-navigation.js +30 -9
- package/packages/mcp-server/src/handlers-page.js +170 -143
- package/packages/mcp-server/src/handlers-utils.js +277 -8
- package/packages/mcp-server/src/handlers.js +31 -10
- package/packages/mcp-server/src/lifecycle.js +413 -0
- package/packages/mcp-server/src/server.js +59 -30
- package/packages/native-host/src/daemon.js +3 -1
- package/packages/protocol/src/capabilities.js +1 -1
- package/packages/protocol/src/defaults.js +2 -0
- package/packages/protocol/src/protocol.js +122 -9
- package/packages/protocol/src/registry.js +14 -14
- package/packages/protocol/src/summary.js +61 -30
- package/packages/protocol/src/types.ts +30 -1
- package/skills/browser-bridge/SKILL.md +4 -4
- package/skills/browser-bridge/references/interaction.md +1 -1
- package/skills/browser-bridge/references/protocol.md +3 -3
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { getBridgeDir } from '../../native-host/src/config.js';
|
|
9
|
+
|
|
10
|
+
const MCP_CONTROL_PROTOCOL_VERSION = 1;
|
|
11
|
+
const MCP_CONTROL_TIMEOUT_MS = 1_000;
|
|
12
|
+
const MCP_REGISTRY_DIRNAME = 'mcp-processes';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{
|
|
16
|
+
* protocolVersion: number,
|
|
17
|
+
* instanceId: string,
|
|
18
|
+
* pid: number,
|
|
19
|
+
* port: number,
|
|
20
|
+
* token: string,
|
|
21
|
+
* }} McpProcessRegistration
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{
|
|
26
|
+
* registryDir?: string,
|
|
27
|
+
* onRestart?: () => void,
|
|
28
|
+
* }} StartMcpProcessControlOptions
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {{
|
|
33
|
+
* registryDir?: string,
|
|
34
|
+
* timeoutMs?: number,
|
|
35
|
+
* }} RestartMcpProcessesOptions
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function getMcpProcessRegistryDir(env = process.env) {
|
|
43
|
+
return path.join(getBridgeDir(env), MCP_REGISTRY_DIRNAME);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register a private localhost control endpoint for this MCP process. The
|
|
48
|
+
* endpoint lets `bbx restart` retire the loaded MCP code without relying on a
|
|
49
|
+
* stale PID that may have been reused by an unrelated process.
|
|
50
|
+
*
|
|
51
|
+
* @param {StartMcpProcessControlOptions} [options={}]
|
|
52
|
+
* @returns {Promise<{ registrationPath: string, dispose: () => Promise<void> }>}
|
|
53
|
+
*/
|
|
54
|
+
export async function startMcpProcessControl(options = {}) {
|
|
55
|
+
const { registryDir = getMcpProcessRegistryDir(), onRestart = () => process.exit(0) } = options;
|
|
56
|
+
const instanceId = randomUUID();
|
|
57
|
+
const token = randomUUID();
|
|
58
|
+
const registrationPath = path.join(registryDir, `${instanceId}.json`);
|
|
59
|
+
let restartRequested = false;
|
|
60
|
+
let disposed = false;
|
|
61
|
+
|
|
62
|
+
const server = net.createServer((socket) => {
|
|
63
|
+
socket.setEncoding('utf8');
|
|
64
|
+
socket.setTimeout(MCP_CONTROL_TIMEOUT_MS, () => socket.destroy());
|
|
65
|
+
socket.on('error', () => socket.destroy());
|
|
66
|
+
let input = '';
|
|
67
|
+
let handled = false;
|
|
68
|
+
|
|
69
|
+
socket.on('data', (chunk) => {
|
|
70
|
+
if (handled) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
input += chunk;
|
|
74
|
+
if (input.length > 4_096) {
|
|
75
|
+
socket.destroy();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const newlineIndex = input.indexOf('\n');
|
|
80
|
+
if (newlineIndex === -1) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
handled = true;
|
|
84
|
+
|
|
85
|
+
const request = parseControlRequest(input.slice(0, newlineIndex));
|
|
86
|
+
if (!request || request.token !== token || request.action !== 'restart') {
|
|
87
|
+
socket.end(`${JSON.stringify({ ok: false })}\n`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (restartRequested) {
|
|
91
|
+
socket.end(`${JSON.stringify({ ok: true })}\n`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
restartRequested = true;
|
|
96
|
+
removeRegistrationSync(registrationPath);
|
|
97
|
+
server.close();
|
|
98
|
+
socket.end(`${JSON.stringify({ ok: true })}\n`, onRestart);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await listenOnLoopback(server);
|
|
104
|
+
const address = server.address();
|
|
105
|
+
if (!address || typeof address === 'string') {
|
|
106
|
+
throw new Error('MCP process control endpoint did not expose a TCP port.');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const registration = {
|
|
110
|
+
protocolVersion: MCP_CONTROL_PROTOCOL_VERSION,
|
|
111
|
+
instanceId,
|
|
112
|
+
pid: process.pid,
|
|
113
|
+
port: address.port,
|
|
114
|
+
token,
|
|
115
|
+
};
|
|
116
|
+
await writeRegistration(registrationPath, registration);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
server.close();
|
|
119
|
+
removeRegistrationSync(registrationPath);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const cleanupOnExit = () => removeRegistrationSync(registrationPath);
|
|
124
|
+
const cleanupOnStdinEnd = () => {
|
|
125
|
+
void dispose();
|
|
126
|
+
};
|
|
127
|
+
process.once('exit', cleanupOnExit);
|
|
128
|
+
process.stdin.once('end', cleanupOnStdinEnd);
|
|
129
|
+
|
|
130
|
+
async function dispose() {
|
|
131
|
+
if (disposed) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
disposed = true;
|
|
135
|
+
process.removeListener('exit', cleanupOnExit);
|
|
136
|
+
process.stdin.removeListener('end', cleanupOnStdinEnd);
|
|
137
|
+
removeRegistrationSync(registrationPath);
|
|
138
|
+
await closeServer(server);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { registrationPath, dispose };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Lifecycle coordination must never prevent the stdio MCP server from serving.
|
|
146
|
+
* Sandboxed agents and read-only installations can legitimately reject the
|
|
147
|
+
* loopback listener or registry write.
|
|
148
|
+
*
|
|
149
|
+
* @param {StartMcpProcessControlOptions} [options={}]
|
|
150
|
+
* @returns {Promise<{ registrationPath: string, dispose: () => Promise<void> } | null>}
|
|
151
|
+
*/
|
|
152
|
+
export async function tryStartMcpProcessControl(options = {}) {
|
|
153
|
+
try {
|
|
154
|
+
return await startMcpProcessControl(options);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Ask every registered MCP process to exit so its owning agent can launch the
|
|
162
|
+
* current installed version. Registrations are removed only when the process is
|
|
163
|
+
* gone; transient control failures remain visible for a later retry.
|
|
164
|
+
*
|
|
165
|
+
* @param {RestartMcpProcessesOptions} [options={}]
|
|
166
|
+
* @returns {Promise<{ registered: number, restartRequested: number, restartFailed: number, staleRegistrationsRemoved: number }>}
|
|
167
|
+
*/
|
|
168
|
+
export async function restartRegisteredMcpProcesses(options = {}) {
|
|
169
|
+
const { registryDir = getMcpProcessRegistryDir(), timeoutMs = MCP_CONTROL_TIMEOUT_MS } = options;
|
|
170
|
+
let entries;
|
|
171
|
+
try {
|
|
172
|
+
entries = (await fs.promises.readdir(registryDir, { withFileTypes: true })).filter(
|
|
173
|
+
(entry) => entry.isFile() && entry.name.endsWith('.json')
|
|
174
|
+
);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (isMissingFileError(error)) {
|
|
177
|
+
return {
|
|
178
|
+
registered: 0,
|
|
179
|
+
restartRequested: 0,
|
|
180
|
+
restartFailed: 0,
|
|
181
|
+
staleRegistrationsRemoved: 0,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let restartRequested = 0;
|
|
188
|
+
let restartFailed = 0;
|
|
189
|
+
let staleRegistrationsRemoved = 0;
|
|
190
|
+
await Promise.all(
|
|
191
|
+
entries.map(async (entry) => {
|
|
192
|
+
const registrationPath = path.join(registryDir, entry.name);
|
|
193
|
+
const registration = await readRegistration(registrationPath);
|
|
194
|
+
if (!registration) {
|
|
195
|
+
await fs.promises.rm(registrationPath, { force: true });
|
|
196
|
+
staleRegistrationsRemoved += 1;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const accepted = await requestProcessRestart(registration, timeoutMs).catch(() => false);
|
|
201
|
+
if (!accepted) {
|
|
202
|
+
if (isProcessRunning(registration.pid)) {
|
|
203
|
+
restartFailed += 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
await fs.promises.rm(registrationPath, { force: true });
|
|
207
|
+
staleRegistrationsRemoved += 1;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
restartRequested += 1;
|
|
212
|
+
await fs.promises.rm(registrationPath, { force: true });
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
registered: entries.length,
|
|
218
|
+
restartRequested,
|
|
219
|
+
restartFailed,
|
|
220
|
+
staleRegistrationsRemoved,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {net.Server} server
|
|
226
|
+
* @returns {Promise<void>}
|
|
227
|
+
*/
|
|
228
|
+
function listenOnLoopback(server) {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
/** @param {Error} error */
|
|
231
|
+
const onError = (error) => {
|
|
232
|
+
server.removeListener('listening', onListening);
|
|
233
|
+
reject(error);
|
|
234
|
+
};
|
|
235
|
+
const onListening = () => {
|
|
236
|
+
server.removeListener('error', onError);
|
|
237
|
+
resolve();
|
|
238
|
+
};
|
|
239
|
+
server.once('error', onError);
|
|
240
|
+
server.once('listening', onListening);
|
|
241
|
+
server.listen(0, '127.0.0.1');
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {net.Server} server
|
|
247
|
+
* @returns {Promise<void>}
|
|
248
|
+
*/
|
|
249
|
+
function closeServer(server) {
|
|
250
|
+
if (!server.listening) {
|
|
251
|
+
return Promise.resolve();
|
|
252
|
+
}
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
server.close((error) => {
|
|
255
|
+
if (error) {
|
|
256
|
+
reject(error);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
resolve();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {string} registrationPath
|
|
266
|
+
* @param {McpProcessRegistration} registration
|
|
267
|
+
* @returns {Promise<void>}
|
|
268
|
+
*/
|
|
269
|
+
async function writeRegistration(registrationPath, registration) {
|
|
270
|
+
await fs.promises.mkdir(path.dirname(registrationPath), { recursive: true });
|
|
271
|
+
const temporaryPath = `${registrationPath}.${process.pid}.tmp`;
|
|
272
|
+
await fs.promises.writeFile(temporaryPath, `${JSON.stringify(registration)}\n`, {
|
|
273
|
+
encoding: 'utf8',
|
|
274
|
+
mode: 0o600,
|
|
275
|
+
});
|
|
276
|
+
await fs.promises.rename(temporaryPath, registrationPath);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @param {string} registrationPath
|
|
281
|
+
* @returns {Promise<McpProcessRegistration | null>}
|
|
282
|
+
*/
|
|
283
|
+
async function readRegistration(registrationPath) {
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(await fs.promises.readFile(registrationPath, 'utf8'));
|
|
286
|
+
if (
|
|
287
|
+
!parsed ||
|
|
288
|
+
typeof parsed !== 'object' ||
|
|
289
|
+
parsed.protocolVersion !== MCP_CONTROL_PROTOCOL_VERSION ||
|
|
290
|
+
typeof parsed.instanceId !== 'string' ||
|
|
291
|
+
!Number.isInteger(parsed.pid) ||
|
|
292
|
+
parsed.pid <= 0 ||
|
|
293
|
+
!Number.isInteger(parsed.port) ||
|
|
294
|
+
parsed.port <= 0 ||
|
|
295
|
+
parsed.port > 65_535 ||
|
|
296
|
+
typeof parsed.token !== 'string' ||
|
|
297
|
+
parsed.token.length === 0
|
|
298
|
+
) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
return parsed;
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @param {McpProcessRegistration} registration
|
|
309
|
+
* @param {number} timeoutMs
|
|
310
|
+
* @returns {Promise<boolean>}
|
|
311
|
+
*/
|
|
312
|
+
function requestProcessRestart(registration, timeoutMs) {
|
|
313
|
+
return new Promise((resolve, reject) => {
|
|
314
|
+
const socket = net.createConnection({ host: '127.0.0.1', port: registration.port });
|
|
315
|
+
let response = '';
|
|
316
|
+
let settled = false;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @param {Error | null} error
|
|
320
|
+
* @param {boolean} [accepted]
|
|
321
|
+
*/
|
|
322
|
+
const finish = (error, accepted = false) => {
|
|
323
|
+
if (settled) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
settled = true;
|
|
327
|
+
socket.destroy();
|
|
328
|
+
if (error) {
|
|
329
|
+
reject(error);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
resolve(accepted);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
socket.setEncoding('utf8');
|
|
336
|
+
socket.setTimeout(timeoutMs, () => finish(new Error('MCP process control request timed out.')));
|
|
337
|
+
socket.once('error', (error) => finish(error));
|
|
338
|
+
socket.once('connect', () => {
|
|
339
|
+
socket.write(`${JSON.stringify({ action: 'restart', token: registration.token })}\n`);
|
|
340
|
+
});
|
|
341
|
+
socket.on('data', (chunk) => {
|
|
342
|
+
response += chunk;
|
|
343
|
+
const newlineIndex = response.indexOf('\n');
|
|
344
|
+
if (newlineIndex === -1) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
const result = JSON.parse(response.slice(0, newlineIndex));
|
|
349
|
+
finish(null, Boolean(result && typeof result === 'object' && result.ok === true));
|
|
350
|
+
} catch (error) {
|
|
351
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
socket.once('close', () => finish(new Error('MCP process control connection closed.')));
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @param {string} input
|
|
360
|
+
* @returns {{ action: string, token: string } | null}
|
|
361
|
+
*/
|
|
362
|
+
function parseControlRequest(input) {
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(input);
|
|
365
|
+
if (
|
|
366
|
+
!parsed ||
|
|
367
|
+
typeof parsed !== 'object' ||
|
|
368
|
+
typeof parsed.action !== 'string' ||
|
|
369
|
+
typeof parsed.token !== 'string'
|
|
370
|
+
) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
return parsed;
|
|
374
|
+
} catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @param {string} registrationPath
|
|
381
|
+
*/
|
|
382
|
+
function removeRegistrationSync(registrationPath) {
|
|
383
|
+
try {
|
|
384
|
+
fs.rmSync(registrationPath, { force: true });
|
|
385
|
+
} catch {
|
|
386
|
+
// Cleanup is best-effort during process shutdown.
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {unknown} error
|
|
392
|
+
* @returns {boolean}
|
|
393
|
+
*/
|
|
394
|
+
function isMissingFileError(error) {
|
|
395
|
+
return Boolean(
|
|
396
|
+
error && typeof error === 'object' && 'code' in error && String(error.code) === 'ENOENT'
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* @param {number} pid
|
|
402
|
+
* @returns {boolean}
|
|
403
|
+
*/
|
|
404
|
+
function isProcessRunning(pid) {
|
|
405
|
+
try {
|
|
406
|
+
process.kill(pid, 0);
|
|
407
|
+
return true;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return Boolean(
|
|
410
|
+
error && typeof error === 'object' && 'code' in error && String(error.code) === 'EPERM'
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
@@ -32,11 +32,14 @@ import {
|
|
|
32
32
|
import {
|
|
33
33
|
BUDGET_PRESETS,
|
|
34
34
|
DEFAULT_CONSOLE_LIMIT,
|
|
35
|
+
DEFAULT_EVAL_TIMEOUT_MS,
|
|
36
|
+
DEFAULT_LOG_TAIL_LIMIT,
|
|
35
37
|
DEFAULT_MAX_DEPTH,
|
|
36
38
|
DEFAULT_MAX_HTML_LENGTH,
|
|
37
39
|
DEFAULT_MAX_NODES,
|
|
38
40
|
DEFAULT_PAGE_TEXT_BUDGET,
|
|
39
41
|
DEFAULT_TEXT_BUDGET,
|
|
42
|
+
DEFAULT_NAV_TIMEOUT_MS,
|
|
40
43
|
DEFAULT_WAIT_TIMEOUT_MS,
|
|
41
44
|
getMethodsByMaxComplexity,
|
|
42
45
|
} from '../../protocol/src/index.js';
|
|
@@ -113,9 +116,12 @@ export function createBridgeMcpServer() {
|
|
|
113
116
|
{
|
|
114
117
|
title: 'Browser Bridge Status',
|
|
115
118
|
description:
|
|
116
|
-
'Check bridge readiness: daemon connectivity, extension state, and window access.
|
|
119
|
+
'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.',
|
|
117
120
|
inputSchema: {
|
|
118
|
-
destinationId: z
|
|
121
|
+
destinationId: z
|
|
122
|
+
.string()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('Inspect one destination instead of aggregating all configured destinations.'),
|
|
119
125
|
},
|
|
120
126
|
},
|
|
121
127
|
handleStatusTool
|
|
@@ -147,7 +153,7 @@ export function createBridgeMcpServer() {
|
|
|
147
153
|
.int()
|
|
148
154
|
.positive()
|
|
149
155
|
.optional()
|
|
150
|
-
.describe(`Maximum log entries to return (default: ${
|
|
156
|
+
.describe(`Maximum log entries to return (default: ${DEFAULT_LOG_TAIL_LIMIT})`),
|
|
151
157
|
budgetPreset: z
|
|
152
158
|
.enum(['quick', 'normal', 'deep'])
|
|
153
159
|
.optional()
|
|
@@ -175,15 +181,23 @@ export function createBridgeMcpServer() {
|
|
|
175
181
|
{
|
|
176
182
|
title: 'Browser Tabs',
|
|
177
183
|
description:
|
|
178
|
-
'List, create, or
|
|
184
|
+
'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.',
|
|
179
185
|
inputSchema: {
|
|
180
186
|
action: z
|
|
181
|
-
.enum(['list', 'create', 'close'])
|
|
182
|
-
.describe('"list" (preferred), "create" (only when needed), or "
|
|
187
|
+
.enum(['list', 'create', 'close', 'activate'])
|
|
188
|
+
.describe('"list" (preferred), "create" (only when needed), "close", or "activate"'),
|
|
183
189
|
url: z.string().optional().describe('URL for create action'),
|
|
184
190
|
active: z.boolean().optional().describe('Focus the new tab (default: true)'),
|
|
185
|
-
tabId: z
|
|
186
|
-
|
|
191
|
+
tabId: z
|
|
192
|
+
.number()
|
|
193
|
+
.int()
|
|
194
|
+
.positive()
|
|
195
|
+
.optional()
|
|
196
|
+
.describe('Tab ID (required for close/activate)'),
|
|
197
|
+
destinationId: z
|
|
198
|
+
.string()
|
|
199
|
+
.optional()
|
|
200
|
+
.describe('Target one destination; list without it aggregates configured destinations.'),
|
|
187
201
|
},
|
|
188
202
|
},
|
|
189
203
|
handleTabsTool
|
|
@@ -194,7 +208,7 @@ export function createBridgeMcpServer() {
|
|
|
194
208
|
{
|
|
195
209
|
title: 'Browser DOM',
|
|
196
210
|
description:
|
|
197
|
-
'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
|
|
211
|
+
'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 - use query/find first.',
|
|
198
212
|
inputSchema: {
|
|
199
213
|
action: z
|
|
200
214
|
.enum([
|
|
@@ -270,7 +284,9 @@ export function createBridgeMcpServer() {
|
|
|
270
284
|
state: z
|
|
271
285
|
.enum(['attached', 'detached', 'visible', 'hidden'])
|
|
272
286
|
.optional()
|
|
273
|
-
.describe(
|
|
287
|
+
.describe(
|
|
288
|
+
'Expected element state (for wait action); hidden also matches complete absence'
|
|
289
|
+
),
|
|
274
290
|
timeoutMs: z
|
|
275
291
|
.number()
|
|
276
292
|
.int()
|
|
@@ -334,7 +350,7 @@ export function createBridgeMcpServer() {
|
|
|
334
350
|
{
|
|
335
351
|
title: 'Browser Page State',
|
|
336
352
|
description:
|
|
337
|
-
'Read page-level data: state (URL/title), evaluate (JS), console, storage, text, network, or performance. For element-level reads, use browser_dom. evaluate and performance are debugger-backed
|
|
353
|
+
'Read page-level data: state (URL/title), evaluate (JS), console, storage, text, network, or performance. For element-level reads, use browser_dom. evaluate and performance are debugger-backed - prefer lighter reads first.',
|
|
338
354
|
inputSchema: {
|
|
339
355
|
action: z
|
|
340
356
|
.enum([
|
|
@@ -364,15 +380,19 @@ export function createBridgeMcpServer() {
|
|
|
364
380
|
.int()
|
|
365
381
|
.positive()
|
|
366
382
|
.optional()
|
|
367
|
-
.describe(
|
|
383
|
+
.describe(
|
|
384
|
+
`Timeout (default: ${DEFAULT_EVAL_TIMEOUT_MS} for evaluate, ${DEFAULT_NAV_TIMEOUT_MS} for wait_for_load)`
|
|
385
|
+
),
|
|
368
386
|
returnByValue: z
|
|
369
|
-
.
|
|
387
|
+
.literal(true)
|
|
370
388
|
.optional()
|
|
371
|
-
.describe('Return
|
|
389
|
+
.describe('Return serializable values (only true is supported; default: true)'),
|
|
372
390
|
level: z
|
|
373
|
-
.
|
|
391
|
+
.enum(['all', 'debug', 'log', 'info', 'warn', 'error', 'exception', 'rejection'])
|
|
374
392
|
.optional()
|
|
375
|
-
.describe(
|
|
393
|
+
.describe(
|
|
394
|
+
'Minimum console severity (default: all); exception/rejection select that exact stream'
|
|
395
|
+
),
|
|
376
396
|
clear: z.boolean().optional().describe('Clear buffer after reading (default: false)'),
|
|
377
397
|
limit: z
|
|
378
398
|
.number()
|
|
@@ -405,7 +425,7 @@ export function createBridgeMcpServer() {
|
|
|
405
425
|
{
|
|
406
426
|
title: 'Browser Navigation',
|
|
407
427
|
description:
|
|
408
|
-
'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed
|
|
428
|
+
'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed - use only for exact viewport overrides.',
|
|
409
429
|
inputSchema: {
|
|
410
430
|
action: z
|
|
411
431
|
.enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
|
|
@@ -423,7 +443,7 @@ export function createBridgeMcpServer() {
|
|
|
423
443
|
.int()
|
|
424
444
|
.positive()
|
|
425
445
|
.optional()
|
|
426
|
-
.describe(
|
|
446
|
+
.describe(`Timeout for navigation (default: ${DEFAULT_NAV_TIMEOUT_MS})`),
|
|
427
447
|
top: z.number().optional().describe('Scroll target Y position (pixels)'),
|
|
428
448
|
left: z.number().optional().describe('Scroll target X position (pixels)'),
|
|
429
449
|
behavior: z.enum(['auto', 'smooth']).optional().describe('Scroll behavior (default: auto)'),
|
|
@@ -456,6 +476,7 @@ export function createBridgeMcpServer() {
|
|
|
456
476
|
'click',
|
|
457
477
|
'focus',
|
|
458
478
|
'type',
|
|
479
|
+
'fill',
|
|
459
480
|
'press_key',
|
|
460
481
|
'cdp_press_key',
|
|
461
482
|
'set_checked',
|
|
@@ -488,6 +509,11 @@ export function createBridgeMcpServer() {
|
|
|
488
509
|
.optional()
|
|
489
510
|
.describe('Click count (1=single, 2=double)'),
|
|
490
511
|
text: z.string().max(100000).optional().describe('Text to type (for type action)'),
|
|
512
|
+
value: z.string().max(100000).optional().describe('Value to set (for fill action)'),
|
|
513
|
+
mode: z
|
|
514
|
+
.enum(['auto', 'setter', 'keystrokes'])
|
|
515
|
+
.optional()
|
|
516
|
+
.describe('Fill strategy (default: auto)'),
|
|
491
517
|
clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
|
|
492
518
|
submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
|
|
493
519
|
key: z
|
|
@@ -517,7 +543,7 @@ export function createBridgeMcpServer() {
|
|
|
517
543
|
.int()
|
|
518
544
|
.nonnegative()
|
|
519
545
|
.optional()
|
|
520
|
-
.describe('Hover duration in ms (default:
|
|
546
|
+
.describe('Hover duration in ms (default: 0)'),
|
|
521
547
|
sourceElementRef: z.string().optional().describe('Drag source element (for drag action)'),
|
|
522
548
|
sourceSelector: z
|
|
523
549
|
.string()
|
|
@@ -565,19 +591,12 @@ export function createBridgeMcpServer() {
|
|
|
565
591
|
.describe('CSS property: value pairs (for apply_styles)'),
|
|
566
592
|
important: z.boolean().optional().describe('Add !important flag (default: false)'),
|
|
567
593
|
operation: z
|
|
568
|
-
.enum([
|
|
569
|
-
'setAttribute',
|
|
570
|
-
'removeAttribute',
|
|
571
|
-
'addClass',
|
|
572
|
-
'removeClass',
|
|
573
|
-
'setTextContent',
|
|
574
|
-
'setProperty',
|
|
575
|
-
])
|
|
594
|
+
.enum(['setAttribute', 'removeAttribute', 'addClass', 'removeClass', 'setTextContent'])
|
|
576
595
|
.optional()
|
|
577
596
|
.describe('DOM mutation type'),
|
|
578
597
|
value: z.unknown().optional().describe('Value for the DOM operation'),
|
|
579
598
|
name: z.string().optional().describe('Attribute/class/property name (for apply_dom)'),
|
|
580
|
-
patchId: z.string().optional().describe('Patch ID
|
|
599
|
+
patchId: z.string().optional().describe('Patch ID (required for rollback)'),
|
|
581
600
|
verify: z
|
|
582
601
|
.boolean()
|
|
583
602
|
.optional()
|
|
@@ -594,7 +613,7 @@ export function createBridgeMcpServer() {
|
|
|
594
613
|
{
|
|
595
614
|
title: 'Browser Capture',
|
|
596
615
|
description:
|
|
597
|
-
'Capture screenshots or CDP snapshots. Debugger-backed and token-expensive
|
|
616
|
+
'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.',
|
|
598
617
|
inputSchema: {
|
|
599
618
|
action: z
|
|
600
619
|
.enum([
|
|
@@ -645,7 +664,13 @@ export function createBridgeMcpServer() {
|
|
|
645
664
|
{
|
|
646
665
|
title: 'Browser Bridge Batch',
|
|
647
666
|
description:
|
|
648
|
-
'Execute
|
|
667
|
+
'Execute independent read-only bridge calls in parallel to reduce round-trips. Mutations and destructive reads are rejected; run them sequentially with specialized tools or browser_call. Preserves call order in the response.',
|
|
668
|
+
annotations: {
|
|
669
|
+
readOnlyHint: true,
|
|
670
|
+
destructiveHint: false,
|
|
671
|
+
idempotentHint: true,
|
|
672
|
+
openWorldHint: true,
|
|
673
|
+
},
|
|
649
674
|
inputSchema: {
|
|
650
675
|
calls: z
|
|
651
676
|
.array(
|
|
@@ -685,6 +710,10 @@ export function createBridgeMcpServer() {
|
|
|
685
710
|
.describe('Method parameters as object'),
|
|
686
711
|
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
687
712
|
destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
|
|
713
|
+
budgetPreset: z
|
|
714
|
+
.enum(['quick', 'normal', 'deep'])
|
|
715
|
+
.optional()
|
|
716
|
+
.describe(BUDGET_PRESET_DESCRIPTION),
|
|
688
717
|
},
|
|
689
718
|
},
|
|
690
719
|
handleRawCallTool
|
|
@@ -602,7 +602,9 @@ export class BridgeDaemon {
|
|
|
602
602
|
|
|
603
603
|
if (message?.type === 'agent.request') {
|
|
604
604
|
const isExtensionSetupRequest =
|
|
605
|
-
socket.__role === 'extension' &&
|
|
605
|
+
socket.__role === 'extension' &&
|
|
606
|
+
(message.request?.method === 'setup.get_status' ||
|
|
607
|
+
message.request?.method === 'setup.install');
|
|
606
608
|
if (socket.__role !== 'agent' && !isExtensionSetupRequest) {
|
|
607
609
|
return this.rejectMessageForRole(socket, message);
|
|
608
610
|
}
|
|
@@ -64,7 +64,7 @@ export const METHOD_CAPABILITIES = Object.freeze({
|
|
|
64
64
|
'page.get_storage': CAPABILITIES.PAGE_READ,
|
|
65
65
|
'page.get_text': CAPABILITIES.PAGE_READ,
|
|
66
66
|
'page.get_network': CAPABILITIES.NETWORK_READ,
|
|
67
|
-
// Interception mutates traffic (block/forge responses)
|
|
67
|
+
// Interception mutates traffic (block/forge responses) - distinct from
|
|
68
68
|
// passive network reads so hosts can gate it independently.
|
|
69
69
|
'network.intercept.add': CAPABILITIES.NETWORK_INTERCEPT,
|
|
70
70
|
'network.intercept.remove': CAPABILITIES.NETWORK_INTERCEPT,
|
|
@@ -20,6 +20,8 @@ export const DEFAULT_CONSOLE_LIMIT = 50;
|
|
|
20
20
|
export const DEFAULT_VIEWPORT_WIDTH = 1280;
|
|
21
21
|
export const DEFAULT_VIEWPORT_HEIGHT = 720;
|
|
22
22
|
export const DEFAULT_DEVICE_SCALE_FACTOR = 0;
|
|
23
|
+
/** @type {'continue'} */
|
|
24
|
+
export const DEFAULT_NETWORK_INTERCEPT_ACTION = 'continue';
|
|
23
25
|
|
|
24
26
|
/** Maximum size of a Chrome native messaging message in bytes. */
|
|
25
27
|
export const MAX_NATIVE_MESSAGE_BYTES = 1_048_576;
|