@browserbridge/bbx 1.7.4 → 1.7.6
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 +6 -6
- package/packages/agent-client/src/atomic-write.js +99 -0
- package/packages/agent-client/src/cli-args.js +152 -0
- package/packages/agent-client/src/cli-batch.js +140 -0
- package/packages/agent-client/src/cli-helpers.js +20 -3
- package/packages/agent-client/src/cli-output.js +57 -0
- package/packages/agent-client/src/cli-proxy-remote.js +350 -0
- package/packages/agent-client/src/cli-setup-commands.js +404 -0
- package/packages/agent-client/src/cli.js +63 -906
- 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 -4
- package/packages/mcp-server/src/bin.js +10 -1
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +8 -5
- package/packages/mcp-server/src/handlers-dom.js +2 -2
- package/packages/mcp-server/src/handlers-navigation.js +20 -9
- package/packages/mcp-server/src/handlers-page.js +111 -111
- package/packages/mcp-server/src/handlers-utils.js +268 -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 +27 -19
- package/packages/native-host/src/auth-token.js +1 -1
- package/packages/native-host/src/config.js +9 -4
- package/packages/native-host/src/daemon-process.js +8 -1
- package/packages/protocol/src/capabilities.js +33 -110
- package/packages/protocol/src/capability-values.js +45 -0
- package/packages/protocol/src/defaults.js +19 -2
- package/packages/protocol/src/protocol.js +106 -5
- package/packages/protocol/src/registry.js +299 -86
- package/packages/protocol/src/types.ts +19 -1
- package/skills/browser-bridge/SKILL.md +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
import {
|
|
4
|
+
import { DEFAULT_LOG_TAIL_LIMIT, createRuntimeContext } from '../../protocol/src/index.js';
|
|
5
5
|
import { collectSetupStatus } from '../../agent-client/src/setup-status.js';
|
|
6
6
|
import {
|
|
7
7
|
callBridgeTool,
|
|
@@ -82,15 +82,17 @@ export async function handleStatusTool(args = {}) {
|
|
|
82
82
|
);
|
|
83
83
|
const result = await callRemoteHealth(requestedDestinationId);
|
|
84
84
|
return createToolResult(
|
|
85
|
-
result.
|
|
86
|
-
? `Browser Bridge destination "${requestedDestinationId}" is
|
|
87
|
-
:
|
|
85
|
+
result.routeReady
|
|
86
|
+
? `Browser Bridge destination "${requestedDestinationId}" is ready.`
|
|
87
|
+
: result.reachable
|
|
88
|
+
? `Browser Bridge destination "${requestedDestinationId}" is reachable, but its browser route is not ready.`
|
|
89
|
+
: `Browser Bridge destination "${requestedDestinationId}" is not reachable.`,
|
|
88
90
|
{
|
|
89
|
-
ok: result.
|
|
91
|
+
ok: result.routeReady === true,
|
|
90
92
|
destination: destination ?? { id: requestedDestinationId, local: false },
|
|
91
93
|
...result,
|
|
92
94
|
},
|
|
93
|
-
result.
|
|
95
|
+
result.routeReady !== true
|
|
94
96
|
);
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -131,17 +133,36 @@ export async function handleStatusTool(args = {}) {
|
|
|
131
133
|
*/
|
|
132
134
|
async function callRemoteHealth(destinationId) {
|
|
133
135
|
const result = await callBridgeTool('health.ping', {}, { destinationId });
|
|
136
|
+
const evidence =
|
|
137
|
+
result.structuredContent.evidence && typeof result.structuredContent.evidence === 'object'
|
|
138
|
+
? /** @type {Record<string, unknown>} */ (result.structuredContent.evidence)
|
|
139
|
+
: {};
|
|
140
|
+
const access =
|
|
141
|
+
evidence.access && typeof evidence.access === 'object'
|
|
142
|
+
? /** @type {Record<string, unknown>} */ (evidence.access)
|
|
143
|
+
: {};
|
|
144
|
+
const reachable = result.structuredContent.ok === true && evidence.daemon === 'ok';
|
|
134
145
|
return {
|
|
135
|
-
reachable
|
|
146
|
+
reachable,
|
|
147
|
+
daemonReachable: reachable,
|
|
148
|
+
extensionConnected: evidence.extensionConnected === true,
|
|
149
|
+
accessEnabled: access.enabled === true,
|
|
150
|
+
routeReady: reachable && access.routeReady === true,
|
|
151
|
+
routeTabId: typeof access.routeTabId === 'number' ? access.routeTabId : null,
|
|
152
|
+
routeReason: typeof access.reason === 'string' ? access.reason : null,
|
|
136
153
|
health: result.structuredContent,
|
|
137
154
|
};
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
/**
|
|
158
|
+
* @param {{ destinationId?: string }} [args]
|
|
141
159
|
* @returns {Promise<ToolResult>}
|
|
142
160
|
*/
|
|
143
|
-
export async function handleSkillTool() {
|
|
161
|
+
export async function handleSkillTool(args = {}) {
|
|
144
162
|
try {
|
|
163
|
+
if (typeof args.destinationId === 'string') {
|
|
164
|
+
return callBridgeTool('skill.get_runtime_context', {}, { destinationId: args.destinationId });
|
|
165
|
+
}
|
|
145
166
|
const ctx = createRuntimeContext();
|
|
146
167
|
return createToolResult('Runtime context retrieved.', {
|
|
147
168
|
ok: true,
|
|
@@ -176,13 +197,13 @@ export async function handleSetupTool(args) {
|
|
|
176
197
|
export async function handleLogTool(args) {
|
|
177
198
|
const normalizedArgs = applyLimitBudgetPreset(args, {
|
|
178
199
|
quick: 10,
|
|
179
|
-
normal:
|
|
200
|
+
normal: DEFAULT_LOG_TAIL_LIMIT,
|
|
180
201
|
deep: 100,
|
|
181
202
|
});
|
|
182
203
|
return callBridgeTool(
|
|
183
204
|
'log.tail',
|
|
184
205
|
{
|
|
185
|
-
limit: normalizedArgs.limit ??
|
|
206
|
+
limit: normalizedArgs.limit ?? DEFAULT_LOG_TAIL_LIMIT,
|
|
186
207
|
},
|
|
187
208
|
{
|
|
188
209
|
tokenBudget: getToolTokenBudget(normalizedArgs),
|
|
@@ -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,7 +181,7 @@ export function createBridgeMcpServer() {
|
|
|
175
181
|
{
|
|
176
182
|
title: 'Browser Tabs',
|
|
177
183
|
description:
|
|
178
|
-
'List, create, close, or activate browser tabs.
|
|
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
187
|
.enum(['list', 'create', 'close', 'activate'])
|
|
@@ -188,7 +194,10 @@ export function createBridgeMcpServer() {
|
|
|
188
194
|
.positive()
|
|
189
195
|
.optional()
|
|
190
196
|
.describe('Tab ID (required for close/activate)'),
|
|
191
|
-
destinationId: z
|
|
197
|
+
destinationId: z
|
|
198
|
+
.string()
|
|
199
|
+
.optional()
|
|
200
|
+
.describe('Target one destination; list without it aggregates configured destinations.'),
|
|
192
201
|
},
|
|
193
202
|
},
|
|
194
203
|
handleTabsTool
|
|
@@ -371,11 +380,13 @@ export function createBridgeMcpServer() {
|
|
|
371
380
|
.int()
|
|
372
381
|
.positive()
|
|
373
382
|
.optional()
|
|
374
|
-
.describe(
|
|
383
|
+
.describe(
|
|
384
|
+
`Timeout (default: ${DEFAULT_EVAL_TIMEOUT_MS} for evaluate, ${DEFAULT_NAV_TIMEOUT_MS} for wait_for_load)`
|
|
385
|
+
),
|
|
375
386
|
returnByValue: z
|
|
376
|
-
.
|
|
387
|
+
.literal(true)
|
|
377
388
|
.optional()
|
|
378
|
-
.describe('Return
|
|
389
|
+
.describe('Return serializable values (only true is supported; default: true)'),
|
|
379
390
|
level: z
|
|
380
391
|
.enum(['all', 'debug', 'log', 'info', 'warn', 'error', 'exception', 'rejection'])
|
|
381
392
|
.optional()
|
|
@@ -432,7 +443,7 @@ export function createBridgeMcpServer() {
|
|
|
432
443
|
.int()
|
|
433
444
|
.positive()
|
|
434
445
|
.optional()
|
|
435
|
-
.describe(
|
|
446
|
+
.describe(`Timeout for navigation (default: ${DEFAULT_NAV_TIMEOUT_MS})`),
|
|
436
447
|
top: z.number().optional().describe('Scroll target Y position (pixels)'),
|
|
437
448
|
left: z.number().optional().describe('Scroll target X position (pixels)'),
|
|
438
449
|
behavior: z.enum(['auto', 'smooth']).optional().describe('Scroll behavior (default: auto)'),
|
|
@@ -532,7 +543,7 @@ export function createBridgeMcpServer() {
|
|
|
532
543
|
.int()
|
|
533
544
|
.nonnegative()
|
|
534
545
|
.optional()
|
|
535
|
-
.describe('Hover duration in ms (default:
|
|
546
|
+
.describe('Hover duration in ms (default: 0)'),
|
|
536
547
|
sourceElementRef: z.string().optional().describe('Drag source element (for drag action)'),
|
|
537
548
|
sourceSelector: z
|
|
538
549
|
.string()
|
|
@@ -580,19 +591,12 @@ export function createBridgeMcpServer() {
|
|
|
580
591
|
.describe('CSS property: value pairs (for apply_styles)'),
|
|
581
592
|
important: z.boolean().optional().describe('Add !important flag (default: false)'),
|
|
582
593
|
operation: z
|
|
583
|
-
.enum([
|
|
584
|
-
'setAttribute',
|
|
585
|
-
'removeAttribute',
|
|
586
|
-
'addClass',
|
|
587
|
-
'removeClass',
|
|
588
|
-
'setTextContent',
|
|
589
|
-
'setProperty',
|
|
590
|
-
])
|
|
594
|
+
.enum(['setAttribute', 'removeAttribute', 'addClass', 'removeClass', 'setTextContent'])
|
|
591
595
|
.optional()
|
|
592
596
|
.describe('DOM mutation type'),
|
|
593
597
|
value: z.unknown().optional().describe('Value for the DOM operation'),
|
|
594
598
|
name: z.string().optional().describe('Attribute/class/property name (for apply_dom)'),
|
|
595
|
-
patchId: z.string().optional().describe('Patch ID
|
|
599
|
+
patchId: z.string().optional().describe('Patch ID (required for rollback)'),
|
|
596
600
|
verify: z
|
|
597
601
|
.boolean()
|
|
598
602
|
.optional()
|
|
@@ -706,6 +710,10 @@ export function createBridgeMcpServer() {
|
|
|
706
710
|
.describe('Method parameters as object'),
|
|
707
711
|
tabId: z.number().int().positive().optional().describe(TAB_ID_DESCRIPTION),
|
|
708
712
|
destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
|
|
713
|
+
budgetPreset: z
|
|
714
|
+
.enum(['quick', 'normal', 'deep'])
|
|
715
|
+
.optional()
|
|
716
|
+
.describe(BUDGET_PRESET_DESCRIPTION),
|
|
709
717
|
},
|
|
710
718
|
},
|
|
711
719
|
handleRawCallTool
|
|
@@ -60,7 +60,7 @@ export async function readBridgeAuthTokenOverride(options = {}) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* @param {{ tokenPath?: string, readFile?: typeof fs.promises.readFile }} [options={}]
|
|
63
|
+
* @param {{ env?: NodeJS.ProcessEnv, tokenPath?: string, readFile?: typeof fs.promises.readFile }} [options={}]
|
|
64
64
|
* @returns {Promise<string | null>}
|
|
65
65
|
*/
|
|
66
66
|
export async function readBridgeAuthToken(options = {}) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
|
|
7
8
|
export const APP_NAME = 'com.browserbridge.browser_bridge';
|
|
8
9
|
export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
|
|
@@ -62,15 +63,19 @@ export function getBridgeDir(env = process.env) {
|
|
|
62
63
|
* connect to. On Windows we use a Named Pipe by default because Node's AF_UNIX
|
|
63
64
|
* support fails with EACCES on listen() under recent Node + Windows 11
|
|
64
65
|
* combinations, while Named Pipes are the historical and reliable Windows IPC
|
|
65
|
-
* mechanism.
|
|
66
|
-
*
|
|
66
|
+
* mechanism. Custom bridge homes use a stable suffix so isolated daemon and
|
|
67
|
+
* test setups do not contend for the default pipe.
|
|
67
68
|
*
|
|
68
69
|
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
69
70
|
* @returns {string}
|
|
70
71
|
*/
|
|
71
72
|
export function getSocketPath(env = process.env) {
|
|
72
|
-
if (os.platform() === 'win32'
|
|
73
|
-
|
|
73
|
+
if (os.platform() === 'win32') {
|
|
74
|
+
const bridgeHome = env[BRIDGE_HOME_ENV];
|
|
75
|
+
const suffix = bridgeHome
|
|
76
|
+
? `-${createHash('sha256').update(bridgeHome.toLowerCase()).digest('hex').slice(0, 16)}`
|
|
77
|
+
: '';
|
|
78
|
+
return `\\\\.\\pipe\\${APP_NAME}${suffix}`;
|
|
74
79
|
}
|
|
75
80
|
return path.join(getBridgeDir(env), 'bridge.sock');
|
|
76
81
|
}
|
|
@@ -407,10 +407,17 @@ async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
|
|
|
407
407
|
throw new Error('Timed out waiting for Browser Bridge daemon to start.');
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
let pid = await readPidFn(pidPath);
|
|
411
|
+
if (pid === null) {
|
|
412
|
+
// The daemon starts listening immediately before it persists its PID.
|
|
413
|
+
await sleepFn(pollIntervalMs);
|
|
414
|
+
pid = await readPidFn(pidPath);
|
|
415
|
+
}
|
|
416
|
+
|
|
410
417
|
return {
|
|
411
418
|
...stopResult,
|
|
412
419
|
pidPath,
|
|
413
|
-
pid
|
|
420
|
+
pid,
|
|
414
421
|
};
|
|
415
422
|
}
|
|
416
423
|
|