@browserbridge/bbx 1.8.0 → 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 +29 -3
- package/README.md +10 -3
- package/SECURITY.md +12 -0
- package/package.json +7 -1
- package/packages/agent-client/src/cli-args.js +35 -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 +227 -19
- package/packages/agent-client/src/client.js +156 -3
- package/packages/agent-client/src/command-registry.js +8 -2
- package/packages/agent-client/src/config.js +195 -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 +97 -5
- package/packages/agent-client/src/types.ts +38 -0
- package/packages/mcp-server/src/handlers-capture.js +29 -2
- package/packages/mcp-server/src/handlers-dom.js +54 -3
- package/packages/mcp-server/src/handlers-page.js +180 -60
- package/packages/mcp-server/src/handlers-utils.js +35 -4
- package/packages/mcp-server/src/handlers.js +10 -3
- package/packages/mcp-server/src/server.js +75 -6
- 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 +543 -27
- package/packages/native-host/src/native-host.js +25 -2
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +28 -0
- package/packages/protocol/src/errors.js +48 -5
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/protocol.js +316 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +143 -6
- package/packages/protocol/src/summary.js +116 -25
- package/packages/protocol/src/types.ts +489 -4
- package/skills/browser-bridge/SKILL.md +22 -8
- package/skills/browser-bridge/references/protocol.md +117 -13
- package/skills/browser-bridge/references/token-efficiency.md +17 -7
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
BridgeMethod,
|
|
5
5
|
BridgeRequestSource,
|
|
6
6
|
BridgeResponse,
|
|
7
|
+
RecoveryEventKind,
|
|
8
|
+
RecoveryTelemetrySummary,
|
|
7
9
|
ScreenshotResult,
|
|
8
10
|
SetupStatus,
|
|
9
11
|
} from '../../protocol/src/types.js';
|
|
@@ -30,6 +32,26 @@ export type McpClientName =
|
|
|
30
32
|
|
|
31
33
|
export type SupportedTarget = McpClientName;
|
|
32
34
|
|
|
35
|
+
export type AutoUpdatePolicy = 'off' | 'compatible';
|
|
36
|
+
|
|
37
|
+
export interface BrowserBridgeConfig {
|
|
38
|
+
autoUpdate: AutoUpdatePolicy;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NpmUpdateResult {
|
|
43
|
+
updated: boolean;
|
|
44
|
+
reason:
|
|
45
|
+
| 'updated'
|
|
46
|
+
| 'invalid_extension_version'
|
|
47
|
+
| 'invalid_installed_version'
|
|
48
|
+
| 'extension_not_newer'
|
|
49
|
+
| 'not_global_install'
|
|
50
|
+
| 'no_compatible_update';
|
|
51
|
+
previousVersion?: string;
|
|
52
|
+
version?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
export type Detector = () => boolean | Promise<boolean>;
|
|
34
56
|
|
|
35
57
|
export interface InstallAgentOptions {
|
|
@@ -51,6 +73,8 @@ export interface SetupStatusOptions {
|
|
|
51
73
|
|
|
52
74
|
export interface ProtocolHealthResult {
|
|
53
75
|
extensionConnected?: boolean;
|
|
76
|
+
extensionVersion?: string;
|
|
77
|
+
daemonVersion?: string;
|
|
54
78
|
supported_versions?: string[];
|
|
55
79
|
extension_supported_versions?: string[];
|
|
56
80
|
daemon_supported_versions?: string[];
|
|
@@ -91,6 +115,12 @@ export interface BridgeClientOptions {
|
|
|
91
115
|
checkProtocolOnConnect?: boolean;
|
|
92
116
|
restartDaemonOnVersionMismatch?: boolean;
|
|
93
117
|
restartDaemonFn?: typeof restartBridgeDaemon;
|
|
118
|
+
updateNpmOnCompatibleVersion?: boolean;
|
|
119
|
+
exitProcessOnNpmUpdate?: boolean;
|
|
120
|
+
updateCompatibleNpmPackageFn?: (options: {
|
|
121
|
+
extensionVersion: string;
|
|
122
|
+
supportedVersions: readonly string[];
|
|
123
|
+
}) => Promise<NpmUpdateResult>;
|
|
94
124
|
authToken?: string | null;
|
|
95
125
|
}
|
|
96
126
|
|
|
@@ -182,6 +212,13 @@ export interface DoctorDaemonMetrics {
|
|
|
182
212
|
requestsProcessed: number;
|
|
183
213
|
requestsFailed: number;
|
|
184
214
|
avgResponseTimeMs: number;
|
|
215
|
+
recovery: RecoveryTelemetrySummary | null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface DoctorRecoveryDiagnostics {
|
|
219
|
+
daemon: RecoveryTelemetrySummary | null;
|
|
220
|
+
routedExtension: RecoveryTelemetrySummary | null;
|
|
221
|
+
activeLoops: RecoveryEventKind[];
|
|
185
222
|
}
|
|
186
223
|
|
|
187
224
|
export interface DoctorRecentEvent {
|
|
@@ -246,6 +283,7 @@ export interface DoctorReport {
|
|
|
246
283
|
connections: DoctorConnectionDiagnostics;
|
|
247
284
|
protocol: DoctorProtocolDiagnostics;
|
|
248
285
|
debugger: DoctorDebuggerDiagnostics;
|
|
286
|
+
recovery: DoctorRecoveryDiagnostics;
|
|
249
287
|
metrics: DoctorDaemonMetrics | null;
|
|
250
288
|
recentEvents: DoctorRecentEvent[];
|
|
251
289
|
recentCauses: DoctorRecentEvent['cause'][];
|
|
@@ -26,6 +26,8 @@ export const CAPTURE_ACTIONS = {
|
|
|
26
26
|
elementRef: r,
|
|
27
27
|
...(a.format ? { format: a.format } : {}),
|
|
28
28
|
...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
|
|
29
|
+
delivery: a.delivery ?? 'auto',
|
|
30
|
+
...(typeof a.scale === 'number' ? { scale: a.scale } : {}),
|
|
29
31
|
}),
|
|
30
32
|
},
|
|
31
33
|
region: {
|
|
@@ -35,6 +37,8 @@ export const CAPTURE_ACTIONS = {
|
|
|
35
37
|
.../** @type {Record<string, unknown>} */ (a.rect || {}),
|
|
36
38
|
...(a.format ? { format: a.format } : {}),
|
|
37
39
|
...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
|
|
40
|
+
delivery: a.delivery ?? 'auto',
|
|
41
|
+
...(typeof a.scale === 'number' ? { scale: a.scale } : {}),
|
|
38
42
|
}),
|
|
39
43
|
},
|
|
40
44
|
full_page: {
|
|
@@ -43,13 +47,15 @@ export const CAPTURE_ACTIONS = {
|
|
|
43
47
|
params: (a) => ({
|
|
44
48
|
...(a.format ? { format: a.format } : {}),
|
|
45
49
|
...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
|
|
50
|
+
delivery: a.delivery ?? 'auto',
|
|
51
|
+
...(typeof a.scale === 'number' ? { scale: a.scale } : {}),
|
|
46
52
|
}),
|
|
47
53
|
},
|
|
48
54
|
cdp_document: { ref: false, method: 'cdp.get_document', params: () => ({}) },
|
|
49
55
|
cdp_dom_snapshot: {
|
|
50
56
|
ref: false,
|
|
51
57
|
method: 'cdp.get_dom_snapshot',
|
|
52
|
-
params: () => ({}),
|
|
58
|
+
params: (a) => ({ computedStyles: a.computedStyles }),
|
|
53
59
|
},
|
|
54
60
|
cdp_box_model: {
|
|
55
61
|
ref: false,
|
|
@@ -93,7 +99,7 @@ function isValidCaptureRegion(rect) {
|
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
/**
|
|
96
|
-
* @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, format?: 'png' | 'jpeg' | 'webp', quality?: number, nodeId?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
102
|
+
* @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, format?: 'png' | 'jpeg' | 'webp', quality?: number, delivery?: 'auto' | 'inline' | 'artifact', scale?: number, nodeId?: number, computedStyles?: string[], tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
97
103
|
* @returns {Promise<ToolResult>}
|
|
98
104
|
*/
|
|
99
105
|
export async function handleCaptureTool(args) {
|
|
@@ -141,6 +147,27 @@ export async function handleCaptureTool(args) {
|
|
|
141
147
|
*/
|
|
142
148
|
export function createScreenshotResult(response, method) {
|
|
143
149
|
const result = toRecord(response.result);
|
|
150
|
+
if (result.delivery === 'artifact') {
|
|
151
|
+
const artifact = toRecord(result.artifact);
|
|
152
|
+
if (typeof artifact.artifactId !== 'string' || typeof artifact.byteLength !== 'number') {
|
|
153
|
+
return summarizeToolError(new Error(`${method} returned invalid artifact metadata.`));
|
|
154
|
+
}
|
|
155
|
+
return createToolResult(
|
|
156
|
+
`Captured screenshot artifact (${artifact.byteLength} bytes, expires ${artifact.expiresAt ?? 'soon'}).`,
|
|
157
|
+
{
|
|
158
|
+
ok: true,
|
|
159
|
+
method,
|
|
160
|
+
delivery: 'artifact',
|
|
161
|
+
artifact,
|
|
162
|
+
mimeType: result.mimeType,
|
|
163
|
+
byteLength: result.byteLength,
|
|
164
|
+
dimensions: result.dimensions,
|
|
165
|
+
rect: boundedRect(result.rect),
|
|
166
|
+
complete: result.complete === true,
|
|
167
|
+
clipped: result.clipped === true,
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
144
171
|
if (typeof result.image !== 'string') {
|
|
145
172
|
return summarizeToolError(new Error(`${method} returned no image data.`));
|
|
146
173
|
}
|
|
@@ -15,6 +15,32 @@ import {
|
|
|
15
15
|
|
|
16
16
|
/** @type {Record<string, ToolAction>} */
|
|
17
17
|
export const DOM_ACTIONS = {
|
|
18
|
+
baseline_create: {
|
|
19
|
+
ref: false,
|
|
20
|
+
method: 'dom.baseline.create',
|
|
21
|
+
params: (a) => ({
|
|
22
|
+
selector: a.selector,
|
|
23
|
+
maxNodes: a.maxNodes,
|
|
24
|
+
maxDepth: a.maxDepth,
|
|
25
|
+
textBudget: a.textBudget,
|
|
26
|
+
attributeAllowlist: a.attributeAllowlist,
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
baseline_compare: {
|
|
30
|
+
ref: false,
|
|
31
|
+
method: 'dom.baseline.compare',
|
|
32
|
+
params: (a) => ({ baselineId: a.baselineId, maxChanges: a.maxChanges }),
|
|
33
|
+
},
|
|
34
|
+
baseline_describe: {
|
|
35
|
+
ref: false,
|
|
36
|
+
method: 'dom.baseline.describe',
|
|
37
|
+
params: (a) => ({ baselineId: a.baselineId }),
|
|
38
|
+
},
|
|
39
|
+
baseline_release: {
|
|
40
|
+
ref: false,
|
|
41
|
+
method: 'dom.baseline.release',
|
|
42
|
+
params: (a) => ({ baselineId: a.baselineId }),
|
|
43
|
+
},
|
|
18
44
|
query: {
|
|
19
45
|
ref: false,
|
|
20
46
|
method: 'dom.query',
|
|
@@ -96,7 +122,7 @@ export const DOM_ACTIONS = {
|
|
|
96
122
|
};
|
|
97
123
|
|
|
98
124
|
/**
|
|
99
|
-
* @param {{ action: string, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, compact?: boolean, interactiveOnly?: boolean, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
125
|
+
* @param {{ action: string, baselineId?: string, maxChanges?: number, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, compact?: boolean, interactiveOnly?: boolean, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
100
126
|
* @returns {Promise<ToolResult>}
|
|
101
127
|
*/
|
|
102
128
|
export async function handleDomTool(args) {
|
|
@@ -109,7 +135,17 @@ export async function handleDomTool(args) {
|
|
|
109
135
|
if (args.action === 'find_role' && !hasText(args.role)) {
|
|
110
136
|
return summarizeToolError('role is required for dom.find_by_role.');
|
|
111
137
|
}
|
|
112
|
-
if (
|
|
138
|
+
if (
|
|
139
|
+
['baseline_compare', 'baseline_describe', 'baseline_release'].includes(args.action) &&
|
|
140
|
+
!hasText(args.baselineId)
|
|
141
|
+
) {
|
|
142
|
+
return summarizeToolError('baselineId is required for this DOM baseline action.');
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
args.action === 'query' ||
|
|
146
|
+
args.action === 'accessibility_tree' ||
|
|
147
|
+
args.action === 'baseline_create'
|
|
148
|
+
) {
|
|
113
149
|
const inferred =
|
|
114
150
|
args.action === 'accessibility_tree' ? 'normal' : inferBudgetFromSelector(args);
|
|
115
151
|
const withBudget = inferred ? { ...args, budgetPreset: args.budgetPreset ?? inferred } : args;
|
|
@@ -118,6 +154,18 @@ export async function handleDomTool(args) {
|
|
|
118
154
|
if (args.action === 'text') {
|
|
119
155
|
return dispatchToolAction(DOM_ACTIONS, applyTextBudgetPreset(args), 'DOM');
|
|
120
156
|
}
|
|
157
|
+
if (args.action === 'baseline_compare') {
|
|
158
|
+
const byPreset = { quick: 10, normal: 50, deep: 100 };
|
|
159
|
+
return dispatchToolAction(
|
|
160
|
+
DOM_ACTIONS,
|
|
161
|
+
{
|
|
162
|
+
...args,
|
|
163
|
+
maxChanges:
|
|
164
|
+
args.maxChanges ?? (args.budgetPreset ? byPreset[args.budgetPreset] : undefined),
|
|
165
|
+
},
|
|
166
|
+
'DOM'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
121
169
|
if (args.action === 'html') {
|
|
122
170
|
return dispatchToolAction(DOM_ACTIONS, applyHtmlBudgetPreset(args), 'DOM');
|
|
123
171
|
}
|
|
@@ -189,11 +237,14 @@ export const PATCH_ACTIONS = {
|
|
|
189
237
|
removeAttribute: 'remove_attribute',
|
|
190
238
|
addClass: 'add_class',
|
|
191
239
|
removeClass: 'remove_class',
|
|
240
|
+
toggleClass: 'toggle_class',
|
|
192
241
|
setTextContent: 'set_text',
|
|
193
242
|
};
|
|
194
243
|
const normalizedOperation = opMap[operation] || operation;
|
|
195
244
|
const value =
|
|
196
|
-
normalizedOperation === 'add_class' ||
|
|
245
|
+
normalizedOperation === 'add_class' ||
|
|
246
|
+
normalizedOperation === 'remove_class' ||
|
|
247
|
+
normalizedOperation === 'toggle_class'
|
|
197
248
|
? (a.value ?? a.name)
|
|
198
249
|
: a.value;
|
|
199
250
|
return {
|
|
@@ -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,63 +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.extract_content',
|
|
53
|
-
'page.get_network',
|
|
54
|
-
'dom.query',
|
|
55
|
-
'dom.describe',
|
|
56
|
-
'dom.get_text',
|
|
57
|
-
'dom.get_attributes',
|
|
58
|
-
'dom.wait_for',
|
|
59
|
-
'dom.find_by_text',
|
|
60
|
-
'dom.find_by_role',
|
|
61
|
-
'dom.get_html',
|
|
62
|
-
'dom.get_accessibility_tree',
|
|
63
|
-
'layout.get_box_model',
|
|
64
|
-
'layout.hit_test',
|
|
65
|
-
'styles.get_computed',
|
|
66
|
-
'styles.get_matched_rules',
|
|
67
|
-
'network.intercept.list',
|
|
68
|
-
'patch.list',
|
|
69
|
-
'performance.get_metrics',
|
|
70
|
-
]);
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @param {BridgeMethod} method
|
|
74
|
-
* @param {Record<string, unknown>} params
|
|
75
|
-
* @returns {boolean}
|
|
76
|
-
*/
|
|
77
|
-
export function isBatchSafeBridgeCall(method, params) {
|
|
78
|
-
if (!BATCH_SAFE_METHODS.has(method)) return false;
|
|
79
|
-
if ((method === 'page.get_console' || method === 'page.get_network') && params.clear === true) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
if (
|
|
83
|
-
method === 'page.get_network' &&
|
|
84
|
-
params.source === 'cdp' &&
|
|
85
|
-
params.capture !== undefined &&
|
|
86
|
-
params.capture !== 'read'
|
|
87
|
-
) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
40
|
/** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
|
|
94
41
|
export const PAGE_ACTIONS = {
|
|
95
42
|
state: { method: 'page.get_state', params: () => ({}) },
|
|
@@ -152,11 +99,19 @@ export const PAGE_ACTIONS = {
|
|
|
152
99
|
capture: a.capture,
|
|
153
100
|
}),
|
|
154
101
|
},
|
|
102
|
+
har: {
|
|
103
|
+
method: 'network.export_har',
|
|
104
|
+
params: (a) => ({
|
|
105
|
+
limit: a.limit,
|
|
106
|
+
urlPattern: a.urlPattern,
|
|
107
|
+
delivery: a.delivery,
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
155
110
|
performance: { method: 'performance.get_metrics', params: () => ({}) },
|
|
156
111
|
};
|
|
157
112
|
|
|
158
113
|
/**
|
|
159
|
-
* @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', dialogAction?: string, promptText?: string, expectedDialogId?: string, waitForLoad?: boolean, url?: string, urlMatch?: 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
|
|
160
115
|
* @returns {Promise<ToolResult>}
|
|
161
116
|
*/
|
|
162
117
|
export async function handlePageTool(args) {
|
|
@@ -175,6 +130,12 @@ export async function handlePageTool(args) {
|
|
|
175
130
|
normal: DEFAULT_NETWORK_LIMIT,
|
|
176
131
|
deep: 100,
|
|
177
132
|
});
|
|
133
|
+
} else if (args.action === 'har') {
|
|
134
|
+
normalizedArgs = applyLimitBudgetPreset(args, {
|
|
135
|
+
quick: 20,
|
|
136
|
+
normal: DEFAULT_HAR_LIMIT,
|
|
137
|
+
deep: 100,
|
|
138
|
+
});
|
|
178
139
|
}
|
|
179
140
|
const entry = PAGE_ACTIONS[normalizedArgs.action];
|
|
180
141
|
if (!entry) return summarizeToolError(`Unsupported page action "${args.action}".`);
|
|
@@ -184,13 +145,53 @@ export async function handlePageTool(args) {
|
|
|
184
145
|
) {
|
|
185
146
|
return summarizeToolError('expression is required for page evaluate.');
|
|
186
147
|
}
|
|
187
|
-
|
|
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, {
|
|
188
163
|
tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
|
|
189
164
|
tokenBudget: getToolTokenBudget(normalizedArgs),
|
|
190
165
|
destinationId: normalizedArgs.destinationId ?? null,
|
|
191
166
|
});
|
|
192
167
|
}
|
|
193
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
|
+
|
|
194
195
|
/**
|
|
195
196
|
* @param {{ calls?: Array<{ method?: string, params?: Record<string, unknown>, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }> }} args
|
|
196
197
|
* @returns {Promise<ToolResult>}
|
|
@@ -361,17 +362,136 @@ export async function handleRawCallTool(args) {
|
|
|
361
362
|
const response = await requestBridgeWithRetry(client, method, params, {
|
|
362
363
|
tabId: typeof args.tabId === 'number' ? args.tabId : null,
|
|
363
364
|
source: REQUEST_SOURCE,
|
|
364
|
-
tokenBudget:
|
|
365
|
+
tokenBudget:
|
|
366
|
+
method.startsWith('screenshot.') || method === 'sensitive.read'
|
|
367
|
+
? null
|
|
368
|
+
: getToolTokenBudget(args),
|
|
365
369
|
});
|
|
366
370
|
if (response.ok && method.startsWith('screenshot.')) {
|
|
367
371
|
return createScreenshotResult(response, method);
|
|
368
372
|
}
|
|
373
|
+
if (method === 'network.export_har') {
|
|
374
|
+
return createHarExportResult(response);
|
|
375
|
+
}
|
|
376
|
+
if (method === 'sensitive.read') {
|
|
377
|
+
return createSensitiveReadResult(response);
|
|
378
|
+
}
|
|
369
379
|
return summarizeToolResponse(response, method, params);
|
|
370
380
|
},
|
|
371
381
|
{ destinationId: args.destinationId ?? null }
|
|
372
382
|
);
|
|
373
383
|
}
|
|
374
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
|
+
|
|
375
495
|
/**
|
|
376
496
|
* @typedef {{ method: BridgeMethod, params: (args: Record<string, unknown>) => Record<string, unknown> }} InvestigateStep
|
|
377
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([
|
|
@@ -47,6 +51,8 @@ const RETRY_SAFE_METHODS = new Set([
|
|
|
47
51
|
'page.get_text',
|
|
48
52
|
'page.extract_content',
|
|
49
53
|
'dom.query',
|
|
54
|
+
'dom.baseline.compare',
|
|
55
|
+
'dom.baseline.describe',
|
|
50
56
|
'dom.describe',
|
|
51
57
|
'dom.get_text',
|
|
52
58
|
'dom.get_attributes',
|
|
@@ -198,10 +204,14 @@ export function summarizeToolError(error) {
|
|
|
198
204
|
export async function withToolClient(callback, options = {}) {
|
|
199
205
|
try {
|
|
200
206
|
if (!options.destinationId) {
|
|
201
|
-
return await withBridgeClient(callback, {
|
|
207
|
+
return await withBridgeClient(callback, {
|
|
208
|
+
checkProtocolOnConnect: false,
|
|
209
|
+
clientId: MCP_CLIENT_ID,
|
|
210
|
+
});
|
|
202
211
|
}
|
|
203
212
|
const client = await createBridgeClientForDestination(options.destinationId, {
|
|
204
213
|
checkProtocolOnConnect: false,
|
|
214
|
+
clientId: MCP_CLIENT_ID,
|
|
205
215
|
});
|
|
206
216
|
await client.connect();
|
|
207
217
|
try {
|
|
@@ -367,9 +377,21 @@ export function applyHtmlBudgetPreset(args) {
|
|
|
367
377
|
*/
|
|
368
378
|
export function applyMethodBudgetPreset(method, params, budgetPreset) {
|
|
369
379
|
const args = { ...params, budgetPreset };
|
|
380
|
+
/** @type {Record<string, unknown>} */
|
|
370
381
|
let normalized = args;
|
|
371
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
method === 'dom.query' ||
|
|
384
|
+
method === 'dom.get_accessibility_tree' ||
|
|
385
|
+
method === 'dom.baseline.create'
|
|
386
|
+
) {
|
|
372
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
|
+
};
|
|
373
395
|
} else if (method === 'dom.get_text') {
|
|
374
396
|
normalized = applyTextBudgetPreset(args);
|
|
375
397
|
} else if (method === 'dom.get_html') {
|
|
@@ -388,6 +410,12 @@ export function applyMethodBudgetPreset(method, params, budgetPreset) {
|
|
|
388
410
|
normal: DEFAULT_NETWORK_LIMIT,
|
|
389
411
|
deep: 100,
|
|
390
412
|
});
|
|
413
|
+
} else if (method === 'network.export_har') {
|
|
414
|
+
normalized = applyLimitBudgetPreset(args, {
|
|
415
|
+
quick: 20,
|
|
416
|
+
normal: DEFAULT_HAR_LIMIT,
|
|
417
|
+
deep: 100,
|
|
418
|
+
});
|
|
391
419
|
} else if (method === 'log.tail') {
|
|
392
420
|
normalized = applyLimitBudgetPreset(args, {
|
|
393
421
|
quick: 10,
|
|
@@ -637,7 +665,7 @@ function compactDomNode(value) {
|
|
|
637
665
|
* @param {import('../../agent-client/src/client.js').BridgeClient} client
|
|
638
666
|
* @param {BridgeMethod} method
|
|
639
667
|
* @param {Record<string, unknown>} params
|
|
640
|
-
* @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
|
|
641
669
|
* @returns {Promise<BridgeResponse>}
|
|
642
670
|
*/
|
|
643
671
|
export async function requestBridgeWithRetry(client, method, params, options) {
|
|
@@ -652,7 +680,10 @@ export async function requestBridgeWithRetry(client, method, params, options) {
|
|
|
652
680
|
`[bbx-mcp] Retrying ${method} after ${delay}ms (${response.error.code})\n`
|
|
653
681
|
);
|
|
654
682
|
await new Promise((r) => setTimeout(r, delay));
|
|
655
|
-
return requestBridge(client, method, params,
|
|
683
|
+
return requestBridge(client, method, params, {
|
|
684
|
+
...options,
|
|
685
|
+
automaticRetry: 'mcp_second_attempt',
|
|
686
|
+
});
|
|
656
687
|
}
|
|
657
688
|
return response;
|
|
658
689
|
}
|
|
@@ -54,15 +54,17 @@ 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
|
|
|
@@ -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,
|