@browserbridge/bbx 1.7.1 → 1.7.3
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 +4 -3
- package/packages/agent-client/src/cli.js +6 -0
- package/packages/agent-client/src/mcp-config.js +64 -65
- package/packages/agent-client/src/runtime.js +19 -0
- package/packages/mcp-server/src/guidance.js +4 -4
- package/packages/mcp-server/src/handlers-capture.js +166 -2
- package/packages/mcp-server/src/handlers-page.js +132 -72
- package/packages/mcp-server/src/handlers-utils.js +16 -66
- package/packages/mcp-server/src/handlers.js +2 -0
- package/packages/mcp-server/src/server.js +5 -1
- package/packages/native-host/src/daemon-process.js +16 -8
- package/packages/native-host/src/daemon.js +199 -79
- package/packages/protocol/src/defaults.js +17 -0
- package/packages/protocol/src/protocol.js +45 -2
- package/packages/protocol/src/registry.js +1 -1
- package/skills/browser-bridge/SKILL.md +12 -8
- package/skills/browser-bridge/references/interaction.md +10 -7
- package/skills/browser-bridge/references/protocol.md +6 -2
- package/skills/browser-bridge/references/token-efficiency.md +5 -4
- package/skills/browser-bridge/references/ui-workflows.md +11 -9
|
@@ -24,8 +24,12 @@ import {
|
|
|
24
24
|
} from './handlers-utils.js';
|
|
25
25
|
|
|
26
26
|
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
27
|
+
/** @typedef {import('../../agent-client/src/client.js').BridgeClient} BridgeClient */
|
|
27
28
|
/** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
|
|
28
29
|
|
|
30
|
+
export const MAX_BATCH_CALLS = 20;
|
|
31
|
+
export const MAX_BATCH_CONCURRENCY = 5;
|
|
32
|
+
|
|
29
33
|
/** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
|
|
30
34
|
export const PAGE_ACTIONS = {
|
|
31
35
|
state: { method: 'page.get_state', params: () => ({}) },
|
|
@@ -109,86 +113,111 @@ export async function handleBatchTool(args) {
|
|
|
109
113
|
if (!Array.isArray(args.calls) || args.calls.length === 0) {
|
|
110
114
|
return summarizeToolError('calls must be a non-empty array.');
|
|
111
115
|
}
|
|
116
|
+
if (args.calls.length > MAX_BATCH_CALLS) {
|
|
117
|
+
return summarizeToolError(`calls must contain at most ${MAX_BATCH_CALLS} entries.`);
|
|
118
|
+
}
|
|
112
119
|
|
|
113
120
|
const calls = args.calls;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
/** @param {BridgeClient | null} client */
|
|
122
|
+
const executeBatch = async (client) => {
|
|
123
|
+
const results = await mapWithConcurrency(calls, MAX_BATCH_CONCURRENCY, async (call) => {
|
|
124
|
+
if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
|
|
125
|
+
return {
|
|
126
|
+
method: '',
|
|
127
|
+
tabId: null,
|
|
128
|
+
ok: false,
|
|
129
|
+
summary: 'INVALID_REQUEST: Each batch call needs a method.',
|
|
130
|
+
evidence: null,
|
|
131
|
+
error: {
|
|
132
|
+
code: 'INVALID_REQUEST',
|
|
133
|
+
message: 'Each batch call needs a method.',
|
|
134
|
+
},
|
|
135
|
+
response: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
|
|
140
|
+
return {
|
|
141
|
+
method: call.method,
|
|
142
|
+
tabId: null,
|
|
143
|
+
ok: false,
|
|
144
|
+
summary: `INVALID_REQUEST: Unknown bridge method "${call.method}".`,
|
|
145
|
+
evidence: null,
|
|
146
|
+
error: {
|
|
147
|
+
code: 'INVALID_REQUEST',
|
|
148
|
+
message: `Unknown bridge method "${call.method}".`,
|
|
149
|
+
},
|
|
150
|
+
response: null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
:
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
if (call.method === 'setup.install') {
|
|
155
|
+
return {
|
|
156
|
+
method: call.method,
|
|
157
|
+
tabId: null,
|
|
158
|
+
ok: false,
|
|
159
|
+
summary: 'INVALID_REQUEST: Use browser_setup for setup changes.',
|
|
160
|
+
evidence: null,
|
|
161
|
+
error: {
|
|
162
|
+
code: 'INVALID_REQUEST',
|
|
163
|
+
message: 'Use browser_setup for setup changes.',
|
|
164
|
+
},
|
|
165
|
+
response: null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
155
168
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
const method = /** @type {BridgeMethod} */ (call.method);
|
|
170
|
+
const tabId = bridgeMethodNeedsTab(method)
|
|
171
|
+
? typeof call.tabId === 'number'
|
|
172
|
+
? call.tabId
|
|
173
|
+
: null
|
|
174
|
+
: null;
|
|
175
|
+
const tokenBudget = getToolTokenBudget(call);
|
|
176
|
+
const destinationId = typeof call.destinationId === 'string' ? call.destinationId : null;
|
|
177
|
+
|
|
178
|
+
const startTime = Date.now();
|
|
179
|
+
const callClient = destinationId
|
|
180
|
+
? await createBridgeClientForDestination(destinationId)
|
|
181
|
+
: client;
|
|
182
|
+
if (!callClient) {
|
|
183
|
+
return summarizeBatchErrorItem({
|
|
184
|
+
method,
|
|
185
|
+
tabId,
|
|
186
|
+
error: new Error('A local bridge client is required for calls without destinationId.'),
|
|
187
|
+
durationMs: Date.now() - startTime,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
if (destinationId) {
|
|
192
|
+
await callClient.connect();
|
|
193
|
+
}
|
|
194
|
+
const response = await requestBridgeWithRetry(callClient, method, call.params || {}, {
|
|
195
|
+
tabId,
|
|
196
|
+
source: REQUEST_SOURCE,
|
|
197
|
+
tokenBudget,
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
destinationId,
|
|
201
|
+
...summarizeBatchResponseItem({
|
|
180
202
|
method,
|
|
181
203
|
tabId,
|
|
182
|
-
|
|
204
|
+
response,
|
|
183
205
|
durationMs: Date.now() - startTime,
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
206
|
+
}),
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return summarizeBatchErrorItem({
|
|
210
|
+
method,
|
|
211
|
+
tabId,
|
|
212
|
+
error,
|
|
213
|
+
durationMs: Date.now() - startTime,
|
|
214
|
+
});
|
|
215
|
+
} finally {
|
|
216
|
+
if (destinationId) {
|
|
217
|
+
await callClient.close();
|
|
189
218
|
}
|
|
190
|
-
}
|
|
191
|
-
);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
192
221
|
|
|
193
222
|
const failureCount = results.filter((result) => !result.ok).length;
|
|
194
223
|
const summary =
|
|
@@ -203,7 +232,35 @@ export async function handleBatchTool(args) {
|
|
|
203
232
|
},
|
|
204
233
|
failureCount > 0
|
|
205
234
|
);
|
|
206
|
-
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return calls.every((call) => typeof call?.destinationId === 'string')
|
|
238
|
+
? executeBatch(null)
|
|
239
|
+
: withToolClient(executeBatch);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @template T, R
|
|
244
|
+
* @param {T[]} values
|
|
245
|
+
* @param {number} concurrency
|
|
246
|
+
* @param {(value: T, index: number) => Promise<R>} callback
|
|
247
|
+
* @returns {Promise<R[]>}
|
|
248
|
+
*/
|
|
249
|
+
async function mapWithConcurrency(values, concurrency, callback) {
|
|
250
|
+
/** @type {R[]} */
|
|
251
|
+
const results = new Array(values.length);
|
|
252
|
+
let nextIndex = 0;
|
|
253
|
+
const worker = async () => {
|
|
254
|
+
while (nextIndex < values.length) {
|
|
255
|
+
const index = nextIndex;
|
|
256
|
+
nextIndex += 1;
|
|
257
|
+
results[index] = await callback(values[index], index);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
await Promise.all(
|
|
261
|
+
Array.from({ length: Math.min(concurrency, values.length) }, async () => worker())
|
|
262
|
+
);
|
|
263
|
+
return results;
|
|
207
264
|
}
|
|
208
265
|
|
|
209
266
|
/**
|
|
@@ -214,6 +271,9 @@ export async function handleRawCallTool(args) {
|
|
|
214
271
|
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
|
|
215
272
|
return summarizeToolError(`Unknown bridge method "${args.method}".`);
|
|
216
273
|
}
|
|
274
|
+
if (args.method === 'setup.install') {
|
|
275
|
+
return summarizeToolError('Use browser_setup for setup changes.');
|
|
276
|
+
}
|
|
217
277
|
|
|
218
278
|
return withToolClient(
|
|
219
279
|
async (client) => {
|
|
@@ -76,7 +76,10 @@ export function isRetrySafeBridgeMethod(method, params) {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* @typedef {{
|
|
79
|
-
* content:
|
|
79
|
+
* content: [
|
|
80
|
+
* { type: 'text', text: string },
|
|
81
|
+
* ...Array<{ type: 'image', data: string, mimeType: string }>
|
|
82
|
+
* ],
|
|
80
83
|
* structuredContent: Record<string, unknown>,
|
|
81
84
|
* isError?: boolean
|
|
82
85
|
* }} ToolResult
|
|
@@ -86,11 +89,20 @@ export function isRetrySafeBridgeMethod(method, params) {
|
|
|
86
89
|
* @param {string} summary
|
|
87
90
|
* @param {Record<string, unknown>} [structuredContent={}]
|
|
88
91
|
* @param {boolean} [isError=false]
|
|
92
|
+
* @param {Array<{ type: 'image', data: string, mimeType: string }>} [additionalContent=[]]
|
|
89
93
|
* @returns {ToolResult}
|
|
90
94
|
*/
|
|
91
|
-
export function createToolResult(
|
|
95
|
+
export function createToolResult(
|
|
96
|
+
summary,
|
|
97
|
+
structuredContent = {},
|
|
98
|
+
isError = false,
|
|
99
|
+
additionalContent = []
|
|
100
|
+
) {
|
|
92
101
|
const toolResult = {
|
|
93
|
-
content:
|
|
102
|
+
content: /** @type {ToolResult['content']} */ ([
|
|
103
|
+
{ type: 'text', text: summary },
|
|
104
|
+
...additionalContent,
|
|
105
|
+
]),
|
|
94
106
|
structuredContent,
|
|
95
107
|
...(isError ? { isError: true } : {}),
|
|
96
108
|
};
|
|
@@ -132,67 +144,6 @@ export function summarizeToolError(error) {
|
|
|
132
144
|
);
|
|
133
145
|
}
|
|
134
146
|
|
|
135
|
-
/**
|
|
136
|
-
* @param {ToolResult} result
|
|
137
|
-
* @returns {boolean}
|
|
138
|
-
*/
|
|
139
|
-
function shouldTryRemoteFallback(result) {
|
|
140
|
-
if (!result.isError && result.structuredContent.ok !== false) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
const text = result.content[0]?.text ?? '';
|
|
144
|
-
return /\b(ACCESS_DENIED|DAEMON_OFFLINE|EXTENSION_DISCONNECTED|NATIVE_HOST_UNAVAILABLE|CONNECTION_LOST)\b|\b(ENOENT|ECONNREFUSED|ETIMEDOUT)\b|ERROR: connect\b/u.test(
|
|
145
|
-
text
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* @param {ToolResult} result
|
|
151
|
-
* @param {string} destinationId
|
|
152
|
-
* @returns {ToolResult}
|
|
153
|
-
*/
|
|
154
|
-
function annotateAutoDestination(result, destinationId) {
|
|
155
|
-
return {
|
|
156
|
-
...result,
|
|
157
|
-
structuredContent: {
|
|
158
|
-
...result.structuredContent,
|
|
159
|
-
destinationId,
|
|
160
|
-
autoSelectedDestinationId: destinationId,
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @param {(client: import('../../agent-client/src/client.js').BridgeClient) => Promise<ToolResult>} callback
|
|
167
|
-
* @param {ToolResult} localResult
|
|
168
|
-
* @returns {Promise<ToolResult>}
|
|
169
|
-
*/
|
|
170
|
-
async function tryRemoteFallback(callback, localResult) {
|
|
171
|
-
const remotes = (await listBridgeDestinations()).filter((destination) => !destination.local);
|
|
172
|
-
if (remotes.length === 0) {
|
|
173
|
-
return localResult;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
let lastFallbackResult = localResult;
|
|
177
|
-
for (const remote of remotes) {
|
|
178
|
-
const client = await createBridgeClientForDestination(remote.id);
|
|
179
|
-
try {
|
|
180
|
-
await client.connect();
|
|
181
|
-
const result = annotateAutoDestination(await callback(client), remote.id);
|
|
182
|
-
lastFallbackResult = result;
|
|
183
|
-
if (!shouldTryRemoteFallback(result)) {
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
186
|
-
} catch (error) {
|
|
187
|
-
lastFallbackResult = annotateAutoDestination(summarizeToolError(error), remote.id);
|
|
188
|
-
} finally {
|
|
189
|
-
await client.close();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return lastFallbackResult;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
147
|
/**
|
|
197
148
|
* @param {(client: import('../../agent-client/src/client.js').BridgeClient) => Promise<ToolResult>} callback
|
|
198
149
|
* @param {{ destinationId?: string | null }} [options]
|
|
@@ -201,8 +152,7 @@ async function tryRemoteFallback(callback, localResult) {
|
|
|
201
152
|
export async function withToolClient(callback, options = {}) {
|
|
202
153
|
try {
|
|
203
154
|
if (!options.destinationId) {
|
|
204
|
-
|
|
205
|
-
return shouldTryRemoteFallback(result) ? await tryRemoteFallback(callback, result) : result;
|
|
155
|
+
return await withBridgeClient(callback);
|
|
206
156
|
}
|
|
207
157
|
const client = await createBridgeClientForDestination(options.destinationId);
|
|
208
158
|
await client.connect();
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
handleStylesLayoutTool,
|
|
28
28
|
handleTabsTool,
|
|
29
29
|
handleInvestigateTool,
|
|
30
|
+
MAX_BATCH_CALLS,
|
|
30
31
|
} from './handlers.js';
|
|
31
32
|
import {
|
|
32
33
|
BUDGET_PRESETS,
|
|
@@ -40,7 +41,7 @@ import {
|
|
|
40
41
|
getMethodsByMaxComplexity,
|
|
41
42
|
} from '../../protocol/src/index.js';
|
|
42
43
|
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
43
|
-
import { MCP_SERVER_INSTRUCTIONS } from './guidance.js';
|
|
44
|
+
import { MCP_SERVER_INSTRUCTIONS, registerBridgeMcpGuidance } from './guidance.js';
|
|
44
45
|
|
|
45
46
|
export const BUDGET_PRESET_DESCRIPTION = `Budget preset: "quick", "normal", or "deep" (defaults: query ${BUDGET_PRESETS.normal.maxNodes} nodes / depth ${BUDGET_PRESETS.normal.maxDepth} / text ${BUDGET_PRESETS.normal.textBudget}). Numeric fields override the preset when both are provided.`;
|
|
46
47
|
export const TAB_ID_DESCRIPTION =
|
|
@@ -105,6 +106,8 @@ export function createBridgeMcpServer() {
|
|
|
105
106
|
}
|
|
106
107
|
);
|
|
107
108
|
|
|
109
|
+
registerBridgeMcpGuidance(server);
|
|
110
|
+
|
|
108
111
|
server.registerTool(
|
|
109
112
|
'browser_status',
|
|
110
113
|
{
|
|
@@ -661,6 +664,7 @@ export function createBridgeMcpServer() {
|
|
|
661
664
|
})
|
|
662
665
|
)
|
|
663
666
|
.min(1)
|
|
667
|
+
.max(MAX_BATCH_CALLS)
|
|
664
668
|
.describe('Calls to execute in parallel'),
|
|
665
669
|
},
|
|
666
670
|
},
|
|
@@ -249,11 +249,20 @@ export async function stopBridgeDaemon(options = {}) {
|
|
|
249
249
|
resolvedTransport.type === 'socket' ? resolvedTransport.socketPath : '';
|
|
250
250
|
|
|
251
251
|
const pidFromFile = await readPidFn(pidPath);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
252
|
+
const reachable = await safePingDaemon(resolvedTransport, pingDaemonFn);
|
|
253
|
+
const endpointOwnerPid =
|
|
254
|
+
pidFromFile !== null || reachable ? await findPidByTransportFn(resolvedTransport) : null;
|
|
255
|
+
/** @type {number | null} */
|
|
256
|
+
let previousPid = null;
|
|
257
|
+
let previouslyRunning = false;
|
|
258
|
+
|
|
259
|
+
if (endpointOwnerPid !== null) {
|
|
260
|
+
previousPid = endpointOwnerPid;
|
|
261
|
+
previouslyRunning = true;
|
|
262
|
+
} else if (reachable) {
|
|
263
|
+
// TCP endpoints do not expose an owner pid. In that case daemon
|
|
264
|
+
// reachability is the evidence supporting the recorded pid.
|
|
265
|
+
previousPid = pidFromFile;
|
|
257
266
|
previouslyRunning = true;
|
|
258
267
|
}
|
|
259
268
|
|
|
@@ -275,9 +284,8 @@ export async function stopBridgeDaemon(options = {}) {
|
|
|
275
284
|
sleepFn,
|
|
276
285
|
});
|
|
277
286
|
if (!stopped) {
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
// different pid owns the socket. Target the socket owner before giving up.
|
|
287
|
+
// Endpoint ownership may become discoverable after a recorded daemon pid
|
|
288
|
+
// fails to stop. Only target the verified owner before giving up.
|
|
281
289
|
const socketOwnerPid = await findPidByTransportFn(resolvedTransport);
|
|
282
290
|
if (socketOwnerPid !== null && socketOwnerPid !== previousPid) {
|
|
283
291
|
try {
|