@browserbridge/bbx 1.7.8 → 1.9.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/PRIVACY.md +57 -12
- package/README.md +19 -8
- package/SECURITY.md +12 -0
- package/package.json +15 -6
- package/packages/agent-client/src/cli-args.js +59 -0
- package/packages/agent-client/src/cli-batch.js +121 -58
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli.js +272 -70
- package/packages/agent-client/src/client.js +296 -8
- package/packages/agent-client/src/command-registry.js +64 -55
- package/packages/agent-client/src/config.js +195 -0
- package/packages/agent-client/src/content-extract.js +198 -0
- package/packages/agent-client/src/npm-self-update.js +305 -0
- package/packages/agent-client/src/remotes.js +4 -2
- package/packages/agent-client/src/runtime.js +864 -39
- package/packages/agent-client/src/types.ts +169 -6
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +69 -6
- package/packages/mcp-server/src/handlers-dom.js +61 -4
- package/packages/mcp-server/src/handlers-navigation.js +2 -1
- package/packages/mcp-server/src/handlers-page.js +211 -54
- package/packages/mcp-server/src/handlers-utils.js +101 -10
- package/packages/mcp-server/src/handlers.js +14 -7
- package/packages/mcp-server/src/server.js +176 -11
- package/packages/native-host/src/artifact-store.js +353 -0
- package/packages/native-host/src/config.js +9 -1
- package/packages/native-host/src/daemon-logger.js +3 -1
- package/packages/native-host/src/daemon.js +701 -33
- package/packages/native-host/src/native-host.js +28 -3
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +31 -0
- package/packages/protocol/src/errors.js +102 -6
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/protocol.js +612 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +206 -38
- package/packages/protocol/src/summary.js +203 -30
- package/packages/protocol/src/types.ts +721 -2
- package/skills/browser-bridge/SKILL.md +54 -19
- package/skills/browser-bridge/references/interaction.md +55 -11
- package/skills/browser-bridge/references/patch-workflow.md +2 -0
- package/skills/browser-bridge/references/protocol.md +265 -86
- package/skills/browser-bridge/references/token-efficiency.md +29 -13
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_CONSOLE_LIMIT,
|
|
5
|
+
DEFAULT_HAR_LIMIT,
|
|
5
6
|
DEFAULT_NETWORK_LIMIT,
|
|
6
7
|
ERROR_CODES,
|
|
7
8
|
getErrorRecovery,
|
|
9
|
+
isBatchSafeBridgeCall,
|
|
10
|
+
MAX_BATCH_CALLS,
|
|
11
|
+
MAX_BATCH_CONCURRENCY,
|
|
8
12
|
METHOD_SET,
|
|
9
13
|
} from '../../protocol/src/index.js';
|
|
10
14
|
import { createBridgeClientForDestination } from '../../agent-client/src/remotes.js';
|
|
@@ -33,54 +37,6 @@ import { createScreenshotResult } from './handlers-capture.js';
|
|
|
33
37
|
/** @typedef {import('../../agent-client/src/client.js').BridgeClient} BridgeClient */
|
|
34
38
|
/** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
|
|
35
39
|
|
|
36
|
-
export const MAX_BATCH_CALLS = 20;
|
|
37
|
-
export const MAX_BATCH_CONCURRENCY = 5;
|
|
38
|
-
|
|
39
|
-
/** @type {ReadonlySet<BridgeMethod>} */
|
|
40
|
-
const BATCH_SAFE_METHODS = new Set([
|
|
41
|
-
'health.ping',
|
|
42
|
-
'daemon.metrics',
|
|
43
|
-
'tabs.list',
|
|
44
|
-
'skill.get_runtime_context',
|
|
45
|
-
'setup.get_status',
|
|
46
|
-
'log.tail',
|
|
47
|
-
'page.get_state',
|
|
48
|
-
'page.get_console',
|
|
49
|
-
'page.wait_for_load_state',
|
|
50
|
-
'page.get_storage',
|
|
51
|
-
'page.get_text',
|
|
52
|
-
'page.get_network',
|
|
53
|
-
'dom.query',
|
|
54
|
-
'dom.describe',
|
|
55
|
-
'dom.get_text',
|
|
56
|
-
'dom.get_attributes',
|
|
57
|
-
'dom.wait_for',
|
|
58
|
-
'dom.find_by_text',
|
|
59
|
-
'dom.find_by_role',
|
|
60
|
-
'dom.get_html',
|
|
61
|
-
'dom.get_accessibility_tree',
|
|
62
|
-
'layout.get_box_model',
|
|
63
|
-
'layout.hit_test',
|
|
64
|
-
'styles.get_computed',
|
|
65
|
-
'styles.get_matched_rules',
|
|
66
|
-
'network.intercept.list',
|
|
67
|
-
'patch.list',
|
|
68
|
-
'performance.get_metrics',
|
|
69
|
-
]);
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* @param {BridgeMethod} method
|
|
73
|
-
* @param {Record<string, unknown>} params
|
|
74
|
-
* @returns {boolean}
|
|
75
|
-
*/
|
|
76
|
-
export function isBatchSafeBridgeCall(method, params) {
|
|
77
|
-
if (!BATCH_SAFE_METHODS.has(method)) return false;
|
|
78
|
-
if ((method === 'page.get_console' || method === 'page.get_network') && params.clear === true) {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
40
|
/** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
|
|
85
41
|
export const PAGE_ACTIONS = {
|
|
86
42
|
state: { method: 'page.get_state', params: () => ({}) },
|
|
@@ -97,9 +53,22 @@ export const PAGE_ACTIONS = {
|
|
|
97
53
|
method: 'page.get_console',
|
|
98
54
|
params: (a) => ({ level: a.level, clear: a.clear, limit: a.limit }),
|
|
99
55
|
},
|
|
56
|
+
handle_dialog: {
|
|
57
|
+
method: 'page.handle_dialog',
|
|
58
|
+
params: (a) => ({
|
|
59
|
+
action: a.dialogAction,
|
|
60
|
+
promptText: a.promptText,
|
|
61
|
+
expectedDialogId: a.expectedDialogId,
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
100
64
|
wait_for_load: {
|
|
101
65
|
method: 'page.wait_for_load_state',
|
|
102
|
-
params: (a) => ({
|
|
66
|
+
params: (a) => ({
|
|
67
|
+
timeoutMs: a.timeoutMs,
|
|
68
|
+
waitForLoad: a.waitForLoad,
|
|
69
|
+
url: a.url,
|
|
70
|
+
urlMatch: a.urlMatch,
|
|
71
|
+
}),
|
|
103
72
|
},
|
|
104
73
|
storage: {
|
|
105
74
|
method: 'page.get_storage',
|
|
@@ -109,24 +78,45 @@ export const PAGE_ACTIONS = {
|
|
|
109
78
|
method: 'page.get_text',
|
|
110
79
|
params: (a) => ({ textBudget: a.textBudget }),
|
|
111
80
|
},
|
|
81
|
+
extract_content: {
|
|
82
|
+
method: 'page.extract_content',
|
|
83
|
+
params: (a) => ({
|
|
84
|
+
format: a.format,
|
|
85
|
+
selector: a.selector,
|
|
86
|
+
includeMetadata: a.includeMetadata,
|
|
87
|
+
consistency: a.consistency,
|
|
88
|
+
textBudget: a.textBudget,
|
|
89
|
+
settleTimeoutMs: a.settleTimeoutMs,
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
112
92
|
network: {
|
|
113
93
|
method: 'page.get_network',
|
|
114
94
|
params: (a) => ({
|
|
115
95
|
clear: a.clear,
|
|
116
96
|
limit: a.limit,
|
|
117
97
|
urlPattern: a.urlPattern,
|
|
98
|
+
source: a.source,
|
|
99
|
+
capture: a.capture,
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
har: {
|
|
103
|
+
method: 'network.export_har',
|
|
104
|
+
params: (a) => ({
|
|
105
|
+
limit: a.limit,
|
|
106
|
+
urlPattern: a.urlPattern,
|
|
107
|
+
delivery: a.delivery,
|
|
118
108
|
}),
|
|
119
109
|
},
|
|
120
110
|
performance: { method: 'performance.get_metrics', params: () => ({}) },
|
|
121
111
|
};
|
|
122
112
|
|
|
123
113
|
/**
|
|
124
|
-
* @param {{ action: string, expression?: string, awaitPromise?: boolean, timeoutMs?: number, returnByValue?: boolean, level?: string, clear?: boolean, limit?: number, type?: string, keys?: string[], textBudget?: number, urlPattern?: string, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
114
|
+
* @param {{ action: string, expression?: string, awaitPromise?: boolean, timeoutMs?: number, returnByValue?: boolean, level?: string, clear?: boolean, limit?: number, type?: string, keys?: string[], textBudget?: number, format?: 'text' | 'markdown', selector?: string, includeMetadata?: boolean, consistency?: 'best_effort' | 'settled', settleTimeoutMs?: number, urlPattern?: string, source?: 'fetch-xhr' | 'cdp', capture?: 'read' | 'start' | 'clear' | 'stop', delivery?: 'auto' | 'inline' | 'artifact', dialogAction?: string, promptText?: string, expectedDialogId?: string, waitForLoad?: boolean, url?: string, urlMatch?: string, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
125
115
|
* @returns {Promise<ToolResult>}
|
|
126
116
|
*/
|
|
127
117
|
export async function handlePageTool(args) {
|
|
128
118
|
let normalizedArgs = args;
|
|
129
|
-
if (args.action === 'text') {
|
|
119
|
+
if (args.action === 'text' || args.action === 'extract_content') {
|
|
130
120
|
normalizedArgs = applyPageTextBudgetPreset(args);
|
|
131
121
|
} else if (args.action === 'console') {
|
|
132
122
|
normalizedArgs = applyLimitBudgetPreset(args, {
|
|
@@ -140,6 +130,12 @@ export async function handlePageTool(args) {
|
|
|
140
130
|
normal: DEFAULT_NETWORK_LIMIT,
|
|
141
131
|
deep: 100,
|
|
142
132
|
});
|
|
133
|
+
} else if (args.action === 'har') {
|
|
134
|
+
normalizedArgs = applyLimitBudgetPreset(args, {
|
|
135
|
+
quick: 20,
|
|
136
|
+
normal: DEFAULT_HAR_LIMIT,
|
|
137
|
+
deep: 100,
|
|
138
|
+
});
|
|
143
139
|
}
|
|
144
140
|
const entry = PAGE_ACTIONS[normalizedArgs.action];
|
|
145
141
|
if (!entry) return summarizeToolError(`Unsupported page action "${args.action}".`);
|
|
@@ -149,13 +145,53 @@ export async function handlePageTool(args) {
|
|
|
149
145
|
) {
|
|
150
146
|
return summarizeToolError('expression is required for page evaluate.');
|
|
151
147
|
}
|
|
152
|
-
|
|
148
|
+
const params = entry.params(normalizedArgs);
|
|
149
|
+
if (entry.method === 'network.export_har') {
|
|
150
|
+
return withToolClient(
|
|
151
|
+
async (client) => {
|
|
152
|
+
const response = await requestBridgeWithRetry(client, entry.method, params, {
|
|
153
|
+
tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
|
|
154
|
+
source: REQUEST_SOURCE,
|
|
155
|
+
tokenBudget: getToolTokenBudget(normalizedArgs),
|
|
156
|
+
});
|
|
157
|
+
return createHarExportResult(response);
|
|
158
|
+
},
|
|
159
|
+
{ destinationId: normalizedArgs.destinationId ?? null }
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return callBridgeTool(entry.method, params, {
|
|
153
163
|
tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
|
|
154
164
|
tokenBudget: getToolTokenBudget(normalizedArgs),
|
|
155
165
|
destinationId: normalizedArgs.destinationId ?? null,
|
|
156
166
|
});
|
|
157
167
|
}
|
|
158
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Deliberately read one exact storage value without retries, budgeting, or
|
|
171
|
+
* value-derived delivery metrics.
|
|
172
|
+
*
|
|
173
|
+
* @param {{ source: 'local_storage' | 'session_storage', key: string, tabId?: number, destinationId?: string }} args
|
|
174
|
+
* @returns {Promise<ToolResult>}
|
|
175
|
+
*/
|
|
176
|
+
export async function handleSensitiveReadTool(args) {
|
|
177
|
+
return withToolClient(
|
|
178
|
+
async (client) => {
|
|
179
|
+
const response = await requestBridgeWithRetry(
|
|
180
|
+
client,
|
|
181
|
+
'sensitive.read',
|
|
182
|
+
{ source: args.source, key: args.key },
|
|
183
|
+
{
|
|
184
|
+
tabId: typeof args.tabId === 'number' ? args.tabId : null,
|
|
185
|
+
source: REQUEST_SOURCE,
|
|
186
|
+
tokenBudget: null,
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
return createSensitiveReadResult(response);
|
|
190
|
+
},
|
|
191
|
+
{ destinationId: args.destinationId ?? null }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
159
195
|
/**
|
|
160
196
|
* @param {{ calls?: Array<{ method?: string, params?: Record<string, unknown>, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }> }} args
|
|
161
197
|
* @returns {Promise<ToolResult>}
|
|
@@ -201,7 +237,9 @@ export async function handleBatchTool(args) {
|
|
|
201
237
|
/** @type {unknown} */
|
|
202
238
|
let callError = null;
|
|
203
239
|
try {
|
|
204
|
-
callClient = await createBridgeClientForDestination(destinationId
|
|
240
|
+
callClient = await createBridgeClientForDestination(destinationId, {
|
|
241
|
+
checkProtocolOnConnect: false,
|
|
242
|
+
});
|
|
205
243
|
await callClient.connect();
|
|
206
244
|
const params = applyMethodBudgetPreset(method, call.params || {}, call.budgetPreset);
|
|
207
245
|
response = await requestBridgeWithRetry(callClient, method, params, {
|
|
@@ -324,17 +362,136 @@ export async function handleRawCallTool(args) {
|
|
|
324
362
|
const response = await requestBridgeWithRetry(client, method, params, {
|
|
325
363
|
tabId: typeof args.tabId === 'number' ? args.tabId : null,
|
|
326
364
|
source: REQUEST_SOURCE,
|
|
327
|
-
tokenBudget:
|
|
365
|
+
tokenBudget:
|
|
366
|
+
method.startsWith('screenshot.') || method === 'sensitive.read'
|
|
367
|
+
? null
|
|
368
|
+
: getToolTokenBudget(args),
|
|
328
369
|
});
|
|
329
370
|
if (response.ok && method.startsWith('screenshot.')) {
|
|
330
371
|
return createScreenshotResult(response, method);
|
|
331
372
|
}
|
|
373
|
+
if (method === 'network.export_har') {
|
|
374
|
+
return createHarExportResult(response);
|
|
375
|
+
}
|
|
376
|
+
if (method === 'sensitive.read') {
|
|
377
|
+
return createSensitiveReadResult(response);
|
|
378
|
+
}
|
|
332
379
|
return summarizeToolResponse(response, method, params);
|
|
333
380
|
},
|
|
334
381
|
{ destinationId: args.destinationId ?? null }
|
|
335
382
|
);
|
|
336
383
|
}
|
|
337
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Preserve the atomic HAR payload exactly. Generic evidence bounding must not
|
|
387
|
+
* turn a valid HAR document into a structurally invalid partial document.
|
|
388
|
+
*
|
|
389
|
+
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
390
|
+
* @returns {ToolResult}
|
|
391
|
+
*/
|
|
392
|
+
export function createHarExportResult(response) {
|
|
393
|
+
if (!response.ok) return summarizeToolResponse(response, 'network.export_har', {});
|
|
394
|
+
const result =
|
|
395
|
+
response.result && typeof response.result === 'object' && !Array.isArray(response.result)
|
|
396
|
+
? /** @type {Record<string, unknown>} */ (response.result)
|
|
397
|
+
: {};
|
|
398
|
+
const numericFields = [
|
|
399
|
+
'byteLength',
|
|
400
|
+
'entryCount',
|
|
401
|
+
'totalEntries',
|
|
402
|
+
'dropped',
|
|
403
|
+
'abandoned',
|
|
404
|
+
'inflight',
|
|
405
|
+
];
|
|
406
|
+
const captureState = result.captureState;
|
|
407
|
+
const validMetadata =
|
|
408
|
+
result.format === 'har' &&
|
|
409
|
+
result.harVersion === '1.2' &&
|
|
410
|
+
result.mimeType === 'application/json' &&
|
|
411
|
+
numericFields.every((field) => typeof result[field] === 'number') &&
|
|
412
|
+
(typeof result.startedAt === 'number' || result.startedAt === null) &&
|
|
413
|
+
(captureState === 'armed' ||
|
|
414
|
+
captureState === 'stop_failed' ||
|
|
415
|
+
captureState === 'stopped' ||
|
|
416
|
+
captureState === 'instrumented') &&
|
|
417
|
+
typeof result.truncated === 'boolean' &&
|
|
418
|
+
result.truncation !== null &&
|
|
419
|
+
typeof result.truncation === 'object';
|
|
420
|
+
if (!validMetadata) {
|
|
421
|
+
return summarizeToolError('network.export_har returned invalid HAR metadata.');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (result.delivery === 'inline') {
|
|
425
|
+
const har =
|
|
426
|
+
result.har && typeof result.har === 'object' && !Array.isArray(result.har)
|
|
427
|
+
? /** @type {Record<string, unknown>} */ (result.har)
|
|
428
|
+
: {};
|
|
429
|
+
const log =
|
|
430
|
+
har.log && typeof har.log === 'object' && !Array.isArray(har.log)
|
|
431
|
+
? /** @type {Record<string, unknown>} */ (har.log)
|
|
432
|
+
: {};
|
|
433
|
+
if (log.version !== '1.2' || !Array.isArray(log.entries)) {
|
|
434
|
+
return summarizeToolError('network.export_har returned an invalid inline HAR document.');
|
|
435
|
+
}
|
|
436
|
+
} else if (result.delivery === 'artifact') {
|
|
437
|
+
const artifact =
|
|
438
|
+
result.artifact && typeof result.artifact === 'object' && !Array.isArray(result.artifact)
|
|
439
|
+
? /** @type {Record<string, unknown>} */ (result.artifact)
|
|
440
|
+
: {};
|
|
441
|
+
if (
|
|
442
|
+
artifact.kind !== 'har' ||
|
|
443
|
+
artifact.mimeType !== 'application/json' ||
|
|
444
|
+
typeof artifact.artifactId !== 'string' ||
|
|
445
|
+
typeof artifact.byteLength !== 'number' ||
|
|
446
|
+
typeof artifact.sha256 !== 'string' ||
|
|
447
|
+
!/^[a-f0-9]{64}$/u.test(artifact.sha256) ||
|
|
448
|
+
typeof artifact.chunkSize !== 'number' ||
|
|
449
|
+
typeof artifact.chunkCount !== 'number' ||
|
|
450
|
+
typeof artifact.createdAt !== 'string' ||
|
|
451
|
+
typeof artifact.expiresAt !== 'string'
|
|
452
|
+
) {
|
|
453
|
+
return summarizeToolError('network.export_har returned invalid artifact metadata.');
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
return summarizeToolError('network.export_har returned an invalid delivery mode.');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const summary = summarizeBridgeResponse(response, 'network.export_har');
|
|
460
|
+
return createToolResult(summary.summary, result);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
465
|
+
* @returns {ToolResult}
|
|
466
|
+
*/
|
|
467
|
+
function createSensitiveReadResult(response) {
|
|
468
|
+
if (!response.ok) return summarizeToolResponse(response, 'sensitive.read', {});
|
|
469
|
+
const result =
|
|
470
|
+
response.result && typeof response.result === 'object'
|
|
471
|
+
? /** @type {Record<string, unknown>} */ (response.result)
|
|
472
|
+
: {};
|
|
473
|
+
if (
|
|
474
|
+
(result.source !== 'local_storage' && result.source !== 'session_storage') ||
|
|
475
|
+
typeof result.value !== 'string' ||
|
|
476
|
+
result.exact !== true
|
|
477
|
+
) {
|
|
478
|
+
return summarizeToolError('sensitive.read returned an invalid exact-value response.');
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
content: [
|
|
482
|
+
{
|
|
483
|
+
type: 'text',
|
|
484
|
+
text: 'Sensitive storage read succeeded. The exact value is available in structuredContent.value.',
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
structuredContent: {
|
|
488
|
+
source: result.source,
|
|
489
|
+
value: result.value,
|
|
490
|
+
exact: true,
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
338
495
|
/**
|
|
339
496
|
* @typedef {{ method: BridgeMethod, params: (args: Record<string, unknown>) => Record<string, unknown> }} InvestigateStep
|
|
340
497
|
*/
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
3
5
|
import {
|
|
4
6
|
BridgeError,
|
|
5
7
|
bridgeMethodNeedsTab,
|
|
6
8
|
DEFAULT_CONSOLE_LIMIT,
|
|
9
|
+
DEFAULT_HAR_LIMIT,
|
|
7
10
|
DEFAULT_LOG_TAIL_LIMIT,
|
|
8
11
|
DEFAULT_MAX_HTML_LENGTH,
|
|
9
12
|
DEFAULT_MAX_NODES,
|
|
@@ -33,6 +36,7 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-clie
|
|
|
33
36
|
/** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
|
|
34
37
|
|
|
35
38
|
export const REQUEST_SOURCE = 'mcp';
|
|
39
|
+
const MCP_CLIENT_ID = `mcp_${randomUUID()}`;
|
|
36
40
|
|
|
37
41
|
/** @type {ReadonlySet<BridgeMethod>} */
|
|
38
42
|
const RETRY_SAFE_METHODS = new Set([
|
|
@@ -45,7 +49,10 @@ const RETRY_SAFE_METHODS = new Set([
|
|
|
45
49
|
'page.get_state',
|
|
46
50
|
'page.get_storage',
|
|
47
51
|
'page.get_text',
|
|
52
|
+
'page.extract_content',
|
|
48
53
|
'dom.query',
|
|
54
|
+
'dom.baseline.compare',
|
|
55
|
+
'dom.baseline.describe',
|
|
49
56
|
'dom.describe',
|
|
50
57
|
'dom.get_text',
|
|
51
58
|
'dom.get_attributes',
|
|
@@ -75,7 +82,13 @@ const RETRY_SAFE_METHODS = new Set([
|
|
|
75
82
|
*/
|
|
76
83
|
export function isRetrySafeBridgeMethod(method, params) {
|
|
77
84
|
if (method === 'page.get_console' || method === 'page.get_network') {
|
|
78
|
-
return
|
|
85
|
+
return (
|
|
86
|
+
params.clear !== true &&
|
|
87
|
+
(method !== 'page.get_network' ||
|
|
88
|
+
params.source !== 'cdp' ||
|
|
89
|
+
params.capture === undefined ||
|
|
90
|
+
params.capture === 'read')
|
|
91
|
+
);
|
|
79
92
|
}
|
|
80
93
|
return RETRY_SAFE_METHODS.has(method);
|
|
81
94
|
}
|
|
@@ -136,6 +149,7 @@ export function summarizeToolResponse(response, method, params = {}) {
|
|
|
136
149
|
if (response.ok) {
|
|
137
150
|
const evidence = getRequestAwareEvidence(response.result, method, params, summary.evidence);
|
|
138
151
|
summary.evidence = evidence.value;
|
|
152
|
+
if (evidence.metadata) summaryRecord.evidenceMeta = evidence.metadata;
|
|
139
153
|
if (evidence.truncated) {
|
|
140
154
|
summaryRecord.outputTruncated = true;
|
|
141
155
|
summaryRecord.outputLimit = evidence.limit;
|
|
@@ -190,9 +204,15 @@ export function summarizeToolError(error) {
|
|
|
190
204
|
export async function withToolClient(callback, options = {}) {
|
|
191
205
|
try {
|
|
192
206
|
if (!options.destinationId) {
|
|
193
|
-
return await withBridgeClient(callback
|
|
207
|
+
return await withBridgeClient(callback, {
|
|
208
|
+
checkProtocolOnConnect: false,
|
|
209
|
+
clientId: MCP_CLIENT_ID,
|
|
210
|
+
});
|
|
194
211
|
}
|
|
195
|
-
const client = await createBridgeClientForDestination(options.destinationId
|
|
212
|
+
const client = await createBridgeClientForDestination(options.destinationId, {
|
|
213
|
+
checkProtocolOnConnect: false,
|
|
214
|
+
clientId: MCP_CLIENT_ID,
|
|
215
|
+
});
|
|
196
216
|
await client.connect();
|
|
197
217
|
try {
|
|
198
218
|
return await callback(client);
|
|
@@ -357,14 +377,26 @@ export function applyHtmlBudgetPreset(args) {
|
|
|
357
377
|
*/
|
|
358
378
|
export function applyMethodBudgetPreset(method, params, budgetPreset) {
|
|
359
379
|
const args = { ...params, budgetPreset };
|
|
380
|
+
/** @type {Record<string, unknown>} */
|
|
360
381
|
let normalized = args;
|
|
361
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
method === 'dom.query' ||
|
|
384
|
+
method === 'dom.get_accessibility_tree' ||
|
|
385
|
+
method === 'dom.baseline.create'
|
|
386
|
+
) {
|
|
362
387
|
normalized = applyTreeBudgetPreset(args);
|
|
388
|
+
} else if (method === 'dom.baseline.compare') {
|
|
389
|
+
const preset = getBudgetPresetName(budgetPreset);
|
|
390
|
+
const defaults = { quick: 10, normal: 50, deep: 100 };
|
|
391
|
+
normalized = {
|
|
392
|
+
...args,
|
|
393
|
+
maxChanges: params.maxChanges ?? (preset ? defaults[preset] : undefined),
|
|
394
|
+
};
|
|
363
395
|
} else if (method === 'dom.get_text') {
|
|
364
396
|
normalized = applyTextBudgetPreset(args);
|
|
365
397
|
} else if (method === 'dom.get_html') {
|
|
366
398
|
normalized = applyHtmlBudgetPreset(args);
|
|
367
|
-
} else if (method === 'page.get_text') {
|
|
399
|
+
} else if (method === 'page.get_text' || method === 'page.extract_content') {
|
|
368
400
|
normalized = applyPageTextBudgetPreset(args);
|
|
369
401
|
} else if (method === 'page.get_console') {
|
|
370
402
|
normalized = applyLimitBudgetPreset(args, {
|
|
@@ -378,6 +410,12 @@ export function applyMethodBudgetPreset(method, params, budgetPreset) {
|
|
|
378
410
|
normal: DEFAULT_NETWORK_LIMIT,
|
|
379
411
|
deep: 100,
|
|
380
412
|
});
|
|
413
|
+
} else if (method === 'network.export_har') {
|
|
414
|
+
normalized = applyLimitBudgetPreset(args, {
|
|
415
|
+
quick: 20,
|
|
416
|
+
normal: DEFAULT_HAR_LIMIT,
|
|
417
|
+
deep: 100,
|
|
418
|
+
});
|
|
381
419
|
} else if (method === 'log.tail') {
|
|
382
420
|
normalized = applyLimitBudgetPreset(args, {
|
|
383
421
|
quick: 10,
|
|
@@ -392,7 +430,7 @@ export function applyMethodBudgetPreset(method, params, budgetPreset) {
|
|
|
392
430
|
/**
|
|
393
431
|
* @param {unknown} input
|
|
394
432
|
* @param {{ maxEntries?: number, maxCharacters?: number, maxStringLength?: number }} [options]
|
|
395
|
-
* @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
|
|
433
|
+
* @returns {{ value: unknown, truncated: boolean, limit: Record<string, number>, metadata?: Record<string, unknown> }}
|
|
396
434
|
*/
|
|
397
435
|
export function boundToolValue(input, options = {}) {
|
|
398
436
|
const maxEntries = options.maxEntries ?? 500;
|
|
@@ -451,13 +489,27 @@ export function boundToolValue(input, options = {}) {
|
|
|
451
489
|
* @param {string | undefined} method
|
|
452
490
|
* @param {Record<string, unknown>} params
|
|
453
491
|
* @param {unknown} fallback
|
|
454
|
-
* @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
|
|
492
|
+
* @returns {{ value: unknown, truncated: boolean, limit: Record<string, number>, metadata?: Record<string, unknown> }}
|
|
455
493
|
*/
|
|
456
494
|
function getRequestAwareEvidence(rawResult, method, params, fallback) {
|
|
457
495
|
const result =
|
|
458
496
|
rawResult && typeof rawResult === 'object' && !Array.isArray(rawResult)
|
|
459
497
|
? /** @type {Record<string, unknown>} */ (rawResult)
|
|
460
498
|
: {};
|
|
499
|
+
if (method === 'page.extract_content' && typeof result.content === 'string') {
|
|
500
|
+
const requested = positiveInteger(params.textBudget) ?? DEFAULT_PAGE_TEXT_BUDGET;
|
|
501
|
+
const maxCharacters = Math.min(requested, 16_000);
|
|
502
|
+
const content = result.content.slice(0, maxCharacters);
|
|
503
|
+
return {
|
|
504
|
+
value: {
|
|
505
|
+
...result,
|
|
506
|
+
content,
|
|
507
|
+
returnedChars: content.length,
|
|
508
|
+
},
|
|
509
|
+
truncated: result.content.length > maxCharacters,
|
|
510
|
+
limit: { maxCharacters },
|
|
511
|
+
};
|
|
512
|
+
}
|
|
461
513
|
if (method === 'page.get_text' || method === 'dom.get_text') {
|
|
462
514
|
const text = typeof result.text === 'string' ? result.text : result.value;
|
|
463
515
|
if (typeof text === 'string') {
|
|
@@ -506,14 +558,30 @@ function getRequestAwareEvidence(rawResult, method, params, fallback) {
|
|
|
506
558
|
const node =
|
|
507
559
|
value && typeof value === 'object' ? /** @type {Record<string, unknown>} */ (value) : {};
|
|
508
560
|
return {
|
|
561
|
+
nodeId: node.nodeId,
|
|
509
562
|
role: node.role,
|
|
510
563
|
name: node.name,
|
|
511
564
|
...(node.interactive === true ? { interactive: true } : {}),
|
|
565
|
+
...(node.semanticInteractive === true ? { semanticInteractive: true } : {}),
|
|
566
|
+
...(node.focusableAndEnabled === true ? { focusableAndEnabled: true } : {}),
|
|
512
567
|
...(node.value !== undefined ? { value: node.value } : {}),
|
|
568
|
+
childIds: Array.isArray(node.childIds) ? node.childIds : [],
|
|
513
569
|
};
|
|
514
570
|
});
|
|
515
571
|
return {
|
|
516
572
|
value: nodes,
|
|
573
|
+
metadata: {
|
|
574
|
+
rootIds: Array.isArray(result.rootIds) ? result.rootIds : [],
|
|
575
|
+
count: result.count,
|
|
576
|
+
total: result.total,
|
|
577
|
+
rawTotal: result.rawTotal,
|
|
578
|
+
source: result.source,
|
|
579
|
+
compact: result.compact,
|
|
580
|
+
interactiveOnly: result.interactiveOnly,
|
|
581
|
+
truncated: result.truncated,
|
|
582
|
+
truncation: result.truncation,
|
|
583
|
+
continuationHint: result.continuationHint,
|
|
584
|
+
},
|
|
517
585
|
truncated: result.truncated === true || result.nodes.length > maxEntries,
|
|
518
586
|
limit: { maxEntries },
|
|
519
587
|
};
|
|
@@ -535,7 +603,27 @@ function getRequestAwareEvidence(rawResult, method, params, fallback) {
|
|
|
535
603
|
: result.entries.slice(0, maxEntries);
|
|
536
604
|
const bounded = boundToolValue(selected, { maxEntries: 500, maxCharacters: 20_000 });
|
|
537
605
|
return {
|
|
538
|
-
value:
|
|
606
|
+
value:
|
|
607
|
+
method === 'page.get_network'
|
|
608
|
+
? {
|
|
609
|
+
entries: bounded.value,
|
|
610
|
+
count: result.count,
|
|
611
|
+
total: result.total,
|
|
612
|
+
filteredTotal: result.filteredTotal,
|
|
613
|
+
dropped: result.dropped,
|
|
614
|
+
abandoned: result.abandoned,
|
|
615
|
+
source: result.source,
|
|
616
|
+
capture: result.capture,
|
|
617
|
+
armed: result.armed,
|
|
618
|
+
armedDuringCapture: result.armedDuringCapture,
|
|
619
|
+
captureState: result.captureState,
|
|
620
|
+
startedAt: result.startedAt,
|
|
621
|
+
inflight: result.inflight,
|
|
622
|
+
ownershipHeld: result.ownershipHeld,
|
|
623
|
+
truncated: result.truncated,
|
|
624
|
+
truncation: result.truncation,
|
|
625
|
+
}
|
|
626
|
+
: bounded.value,
|
|
539
627
|
truncated: result.entries.length > maxEntries || bounded.truncated,
|
|
540
628
|
limit: { maxEntries, maxCharacters: 20_000 },
|
|
541
629
|
};
|
|
@@ -577,7 +665,7 @@ function compactDomNode(value) {
|
|
|
577
665
|
* @param {import('../../agent-client/src/client.js').BridgeClient} client
|
|
578
666
|
* @param {BridgeMethod} method
|
|
579
667
|
* @param {Record<string, unknown>} params
|
|
580
|
-
* @param {{ tabId?: number | null, source?: import('../../protocol/src/types.js').BridgeRequestSource, tokenBudget?: number | null }} options
|
|
668
|
+
* @param {{ tabId?: number | null, source?: import('../../protocol/src/types.js').BridgeRequestSource, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} options
|
|
581
669
|
* @returns {Promise<BridgeResponse>}
|
|
582
670
|
*/
|
|
583
671
|
export async function requestBridgeWithRetry(client, method, params, options) {
|
|
@@ -592,7 +680,10 @@ export async function requestBridgeWithRetry(client, method, params, options) {
|
|
|
592
680
|
`[bbx-mcp] Retrying ${method} after ${delay}ms (${response.error.code})\n`
|
|
593
681
|
);
|
|
594
682
|
await new Promise((r) => setTimeout(r, delay));
|
|
595
|
-
return requestBridge(client, method, params,
|
|
683
|
+
return requestBridge(client, method, params, {
|
|
684
|
+
...options,
|
|
685
|
+
automaticRetry: 'mcp_second_attempt',
|
|
686
|
+
});
|
|
596
687
|
}
|
|
597
688
|
return response;
|
|
598
689
|
}
|
|
@@ -54,22 +54,24 @@ export {
|
|
|
54
54
|
} from './handlers-capture.js';
|
|
55
55
|
|
|
56
56
|
export {
|
|
57
|
-
MAX_BATCH_CALLS,
|
|
58
|
-
MAX_BATCH_CONCURRENCY,
|
|
59
57
|
PAGE_ACTIONS,
|
|
60
58
|
handlePageTool,
|
|
59
|
+
handleSensitiveReadTool,
|
|
61
60
|
handleBatchTool,
|
|
62
61
|
handleRawCallTool,
|
|
62
|
+
createHarExportResult,
|
|
63
63
|
handleInvestigateTool,
|
|
64
64
|
} from './handlers-page.js';
|
|
65
65
|
|
|
66
|
+
export { MAX_BATCH_CALLS, MAX_BATCH_CONCURRENCY } from '../../protocol/src/index.js';
|
|
67
|
+
|
|
66
68
|
/** @typedef {import('./handlers-utils.js').ToolAction} ToolAction */
|
|
67
69
|
/** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
|
|
68
70
|
|
|
69
71
|
const HOME_DIR = os.homedir();
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
|
-
* @param {{ destinationId?: string }} [args]
|
|
74
|
+
* @param {{ destinationId?: string, intent?: import('../../protocol/src/types.js').AccessIntent }} [args]
|
|
73
75
|
* @returns {Promise<ToolResult>}
|
|
74
76
|
*/
|
|
75
77
|
export async function handleStatusTool(args = {}) {
|
|
@@ -97,8 +99,13 @@ export async function handleStatusTool(args = {}) {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
const report = await getDoctorReport();
|
|
102
|
+
const configuredDestinations = await getBridgeDestinations();
|
|
103
|
+
const destinationsToInspect =
|
|
104
|
+
requestedDestinationId === 'local'
|
|
105
|
+
? configuredDestinations.filter((destination) => destination.local)
|
|
106
|
+
: configuredDestinations;
|
|
100
107
|
const destinations = await Promise.all(
|
|
101
|
-
|
|
108
|
+
destinationsToInspect.map(async (destination) => {
|
|
102
109
|
if (destination.local) {
|
|
103
110
|
return {
|
|
104
111
|
...destination,
|
|
@@ -155,7 +162,7 @@ async function callRemoteHealth(destinationId) {
|
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
/**
|
|
158
|
-
* @param {{ destinationId?: string }} [args]
|
|
165
|
+
* @param {{ destinationId?: string, intent?: import('../../protocol/src/types.js').AccessIntent }} [args]
|
|
159
166
|
* @returns {Promise<ToolResult>}
|
|
160
167
|
*/
|
|
161
168
|
export async function handleSkillTool(args = {}) {
|
|
@@ -231,13 +238,13 @@ export async function handleHealthTool(args = {}) {
|
|
|
231
238
|
}
|
|
232
239
|
|
|
233
240
|
/**
|
|
234
|
-
* @param {{ destinationId?: string }} [args]
|
|
241
|
+
* @param {{ destinationId?: string, intent?: import('../../protocol/src/types.js').AccessIntent }} [args]
|
|
235
242
|
* @returns {Promise<ToolResult>}
|
|
236
243
|
*/
|
|
237
244
|
export async function handleAccessTool(args = {}) {
|
|
238
245
|
return callBridgeTool(
|
|
239
246
|
'access.request',
|
|
240
|
-
{},
|
|
247
|
+
{ intent: args.intent },
|
|
241
248
|
{
|
|
242
249
|
destinationId: typeof args.destinationId === 'string' ? args.destinationId : null,
|
|
243
250
|
}
|