viyv-browser-mcp 0.5.0 → 0.5.4
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/dist/index.js +505 -158
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -80,6 +80,9 @@ var MCP_SERVER = {
|
|
|
80
80
|
SOCKET_PATH_TEMPLATE: "/tmp/viyv-browser-{pid}.sock"
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
// src/native-host/bridge-relay.ts
|
|
84
|
+
import { createConnection } from "net";
|
|
85
|
+
|
|
83
86
|
// src/native-host/compression.ts
|
|
84
87
|
import { gunzipSync, gzipSync } from "zlib";
|
|
85
88
|
var CHUNK_SIZE = 768 * 1024;
|
|
@@ -144,16 +147,127 @@ function writeMessage(stream, message) {
|
|
|
144
147
|
const encoded = encodeMessage(message);
|
|
145
148
|
stream.write(encoded);
|
|
146
149
|
}
|
|
150
|
+
function decompressIfNeeded(message) {
|
|
151
|
+
if (message.type === "compressed" && typeof message.data === "string") {
|
|
152
|
+
return JSON.parse(decompressPayload(message.data, true));
|
|
153
|
+
}
|
|
154
|
+
return message;
|
|
155
|
+
}
|
|
156
|
+
function createTcpLineReader(onMessage, onError) {
|
|
157
|
+
let lineBuffer = "";
|
|
158
|
+
return (data) => {
|
|
159
|
+
lineBuffer += data.toString("utf-8");
|
|
160
|
+
const lines = lineBuffer.split("\n");
|
|
161
|
+
lineBuffer = lines.pop() ?? "";
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
if (!line) continue;
|
|
164
|
+
try {
|
|
165
|
+
const message = decompressIfNeeded(JSON.parse(line));
|
|
166
|
+
onMessage(message);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
onError?.(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
147
173
|
|
|
148
|
-
// src/native-host/bridge.ts
|
|
174
|
+
// src/native-host/bridge-relay.ts
|
|
149
175
|
var LOG_PREFIX = "[viyv-browser:native-host]";
|
|
176
|
+
var RELAY_MAX_RETRIES = 10;
|
|
177
|
+
function startRelayMode(port, host, onError) {
|
|
178
|
+
let retryCount = 0;
|
|
179
|
+
let relaySocket = null;
|
|
180
|
+
createMessageReader(
|
|
181
|
+
process.stdin,
|
|
182
|
+
(raw) => {
|
|
183
|
+
if (!raw || typeof raw !== "object") return;
|
|
184
|
+
const message = decompressIfNeeded(raw);
|
|
185
|
+
if (relaySocket && !relaySocket.destroyed) {
|
|
186
|
+
relaySocket.write(`${JSON.stringify(message)}
|
|
187
|
+
`);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
onError
|
|
191
|
+
);
|
|
192
|
+
function connect() {
|
|
193
|
+
const socket = createConnection({ port, host });
|
|
194
|
+
socket.on("connect", () => {
|
|
195
|
+
retryCount = 0;
|
|
196
|
+
relaySocket = socket;
|
|
197
|
+
process.stderr.write(`${LOG_PREFIX} Relay connected to bridge at ${host}:${port}
|
|
198
|
+
`);
|
|
199
|
+
socket.write(`${JSON.stringify({ type: "chrome_relay_init", timestamp: Date.now() })}
|
|
200
|
+
`);
|
|
201
|
+
const processData = createTcpLineReader((message) => {
|
|
202
|
+
if (message.type === "chrome_relay_ack") {
|
|
203
|
+
process.stderr.write(`${LOG_PREFIX} Relay assigned chromeId: ${message.chromeId}
|
|
204
|
+
`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
writeMessage(process.stdout, message);
|
|
208
|
+
}, onError);
|
|
209
|
+
socket.on("data", processData);
|
|
210
|
+
});
|
|
211
|
+
socket.on("error", (error) => {
|
|
212
|
+
process.stderr.write(`${LOG_PREFIX} Relay connection error: ${error.message}
|
|
213
|
+
`);
|
|
214
|
+
});
|
|
215
|
+
socket.on("close", () => {
|
|
216
|
+
relaySocket = null;
|
|
217
|
+
retryCount++;
|
|
218
|
+
if (retryCount > RELAY_MAX_RETRIES) {
|
|
219
|
+
process.stderr.write(
|
|
220
|
+
`${LOG_PREFIX} Relay max retries (${RELAY_MAX_RETRIES}) reached. Exiting.
|
|
221
|
+
`
|
|
222
|
+
);
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
const delay = Math.min(
|
|
226
|
+
RECONNECT.INITIAL_DELAY * RECONNECT.MULTIPLIER ** (retryCount - 1),
|
|
227
|
+
RECONNECT.MAX_DELAY
|
|
228
|
+
);
|
|
229
|
+
process.stderr.write(
|
|
230
|
+
`${LOG_PREFIX} Relay reconnecting in ${delay}ms (attempt ${retryCount}/${RELAY_MAX_RETRIES})
|
|
231
|
+
`
|
|
232
|
+
);
|
|
233
|
+
setTimeout(connect, delay);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
connect();
|
|
237
|
+
process.stdin.on("end", () => {
|
|
238
|
+
process.stderr.write(`${LOG_PREFIX} Relay: stdin closed, shutting down
|
|
239
|
+
`);
|
|
240
|
+
relaySocket?.destroy();
|
|
241
|
+
process.exit(0);
|
|
242
|
+
});
|
|
243
|
+
process.on("SIGINT", () => {
|
|
244
|
+
relaySocket?.destroy();
|
|
245
|
+
process.exit(0);
|
|
246
|
+
});
|
|
247
|
+
process.on("SIGTERM", () => {
|
|
248
|
+
relaySocket?.destroy();
|
|
249
|
+
process.exit(0);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/native-host/bridge.ts
|
|
254
|
+
var LOG_PREFIX2 = "[viyv-browser:native-host]";
|
|
150
255
|
var ROUTING_CLEANUP_INTERVAL = 3e4;
|
|
151
256
|
function startBridge(options) {
|
|
152
257
|
const { port = BRIDGE.TCP_PORT, host = BRIDGE.TCP_HOST, onError } = options;
|
|
153
258
|
const mcpConnections = /* @__PURE__ */ new Map();
|
|
154
259
|
const requestOrigin = /* @__PURE__ */ new Map();
|
|
155
260
|
const agentToConn = /* @__PURE__ */ new Map();
|
|
261
|
+
const chromeConnections = /* @__PURE__ */ new Map();
|
|
262
|
+
const agentToChrome = /* @__PURE__ */ new Map();
|
|
263
|
+
let nextChromeIndex = 0;
|
|
156
264
|
let server = null;
|
|
265
|
+
const primaryChromeId = `chrome-${nextChromeIndex++}`;
|
|
266
|
+
chromeConnections.set(primaryChromeId, {
|
|
267
|
+
chromeId: primaryChromeId,
|
|
268
|
+
send: (msg) => writeMessage(process.stdout, msg),
|
|
269
|
+
agentIds: /* @__PURE__ */ new Set()
|
|
270
|
+
});
|
|
157
271
|
const cleanupTimer2 = setInterval(() => {
|
|
158
272
|
const now = Date.now();
|
|
159
273
|
for (const [id, entry] of requestOrigin) {
|
|
@@ -163,12 +277,11 @@ function startBridge(options) {
|
|
|
163
277
|
}
|
|
164
278
|
}, ROUTING_CLEANUP_INTERVAL);
|
|
165
279
|
cleanupTimer2.unref();
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
connected
|
|
170
|
-
|
|
171
|
-
});
|
|
280
|
+
function getChromeProfilesList() {
|
|
281
|
+
return Array.from(chromeConnections.values()).map((c) => ({
|
|
282
|
+
chromeId: c.chromeId,
|
|
283
|
+
connected: true
|
|
284
|
+
}));
|
|
172
285
|
}
|
|
173
286
|
function sendToMcp(connId, message) {
|
|
174
287
|
const conn = mcpConnections.get(connId);
|
|
@@ -180,53 +293,110 @@ function startBridge(options) {
|
|
|
180
293
|
onError?.(error);
|
|
181
294
|
}
|
|
182
295
|
}
|
|
183
|
-
function sendToChrome(message) {
|
|
184
|
-
|
|
296
|
+
function sendToChrome(chromeId, message) {
|
|
297
|
+
const chrome = chromeConnections.get(chromeId);
|
|
298
|
+
if (chrome) {
|
|
299
|
+
chrome.send(message);
|
|
300
|
+
} else {
|
|
301
|
+
process.stderr.write(
|
|
302
|
+
`${LOG_PREFIX2} WARNING: chromeId '${chromeId}' not found, falling back to primary
|
|
303
|
+
`
|
|
304
|
+
);
|
|
305
|
+
chromeConnections.get(primaryChromeId)?.send(message);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function getChromeForAgent(agentId) {
|
|
309
|
+
return agentToChrome.get(agentId) ?? primaryChromeId;
|
|
185
310
|
}
|
|
186
311
|
function connIdForAgent(agentId) {
|
|
187
312
|
return agentToConn.get(agentId);
|
|
188
313
|
}
|
|
314
|
+
function cleanupAgent(agentId, connId) {
|
|
315
|
+
agentToConn.delete(agentId);
|
|
316
|
+
mcpConnections.get(connId)?.agentIds.delete(agentId);
|
|
317
|
+
const chromeId = agentToChrome.get(agentId);
|
|
318
|
+
if (chromeId) {
|
|
319
|
+
chromeConnections.get(chromeId)?.agentIds.delete(agentId);
|
|
320
|
+
}
|
|
321
|
+
agentToChrome.delete(agentId);
|
|
322
|
+
}
|
|
323
|
+
function notifyChromeConnected(connected) {
|
|
324
|
+
const msg = { type: "bridge_status", connected, timestamp: Date.now() };
|
|
325
|
+
for (const chrome of chromeConnections.values()) {
|
|
326
|
+
chrome.send(msg);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function broadcastChromeProfiles() {
|
|
330
|
+
const msg = {
|
|
331
|
+
type: "bridge_status",
|
|
332
|
+
connected: true,
|
|
333
|
+
chromeProfiles: getChromeProfilesList(),
|
|
334
|
+
timestamp: Date.now()
|
|
335
|
+
};
|
|
336
|
+
for (const [connId] of mcpConnections) {
|
|
337
|
+
sendToMcp(connId, msg);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
189
340
|
function handleMcpMessage(connId, message) {
|
|
190
341
|
const type = message.type;
|
|
191
342
|
if (!type) return;
|
|
343
|
+
const agentId = message.agentId;
|
|
192
344
|
if (type === "tool_call") {
|
|
193
345
|
const id = message.id;
|
|
194
346
|
if (id) {
|
|
195
347
|
requestOrigin.set(id, { connId, timestamp: Date.now() });
|
|
196
348
|
}
|
|
197
|
-
|
|
349
|
+
const perCallProfile = message.chromeProfile;
|
|
350
|
+
let targetChrome;
|
|
351
|
+
if (perCallProfile && chromeConnections.has(perCallProfile)) {
|
|
352
|
+
targetChrome = perCallProfile;
|
|
353
|
+
} else {
|
|
354
|
+
if (perCallProfile && !chromeConnections.has(perCallProfile)) {
|
|
355
|
+
process.stderr.write(
|
|
356
|
+
`${LOG_PREFIX2} WARNING: chromeProfile '${perCallProfile}' not found, using agent default
|
|
357
|
+
`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
targetChrome = agentId ? getChromeForAgent(agentId) : primaryChromeId;
|
|
361
|
+
}
|
|
362
|
+
sendToChrome(targetChrome, message);
|
|
198
363
|
} else if (type === "session_init") {
|
|
199
|
-
const agentId = message.agentId;
|
|
200
364
|
if (agentId) {
|
|
201
365
|
const existingConnId = agentToConn.get(agentId);
|
|
202
366
|
if (existingConnId && existingConnId !== connId) {
|
|
203
367
|
process.stderr.write(
|
|
204
|
-
`${
|
|
368
|
+
`${LOG_PREFIX2} WARNING: agentId '${agentId}' re-registered from conn ${existingConnId} \u2192 ${connId}
|
|
205
369
|
`
|
|
206
370
|
);
|
|
207
|
-
|
|
208
|
-
if (oldConn) oldConn.agentIds.delete(agentId);
|
|
371
|
+
mcpConnections.get(existingConnId)?.agentIds.delete(agentId);
|
|
209
372
|
}
|
|
210
373
|
agentToConn.set(agentId, connId);
|
|
211
|
-
|
|
212
|
-
|
|
374
|
+
mcpConnections.get(connId)?.agentIds.add(agentId);
|
|
375
|
+
const chromeProfile = message.chromeProfile;
|
|
376
|
+
const resolvedChrome = chromeProfile && chromeConnections.has(chromeProfile) ? chromeProfile : primaryChromeId;
|
|
377
|
+
if (chromeProfile && !chromeConnections.has(chromeProfile)) {
|
|
378
|
+
process.stderr.write(
|
|
379
|
+
`${LOG_PREFIX2} WARNING: chromeProfile '${chromeProfile}' not found, using primary
|
|
380
|
+
`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
agentToChrome.set(agentId, resolvedChrome);
|
|
384
|
+
chromeConnections.get(resolvedChrome)?.agentIds.add(agentId);
|
|
213
385
|
}
|
|
214
|
-
sendToChrome(message);
|
|
386
|
+
sendToChrome(agentId ? getChromeForAgent(agentId) : primaryChromeId, message);
|
|
215
387
|
} else if (type === "session_close") {
|
|
216
|
-
const agentId = message.agentId;
|
|
217
388
|
if (agentId && agentToConn.get(agentId) === connId) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
389
|
+
const targetChrome = getChromeForAgent(agentId);
|
|
390
|
+
cleanupAgent(agentId, connId);
|
|
391
|
+
sendToChrome(targetChrome, message);
|
|
221
392
|
}
|
|
222
|
-
sendToChrome(message);
|
|
223
393
|
} else if (type === "session_heartbeat" || type === "session_recovery") {
|
|
224
|
-
sendToChrome(message);
|
|
394
|
+
sendToChrome(agentId ? getChromeForAgent(agentId) : primaryChromeId, message);
|
|
225
395
|
} else if (type === "bridge_status" || type === "bridge_closing") {
|
|
226
|
-
process.stderr.write(`${
|
|
396
|
+
process.stderr.write(`${LOG_PREFIX2} Ignoring ${type} from MCP Server ${connId}
|
|
227
397
|
`);
|
|
228
398
|
} else {
|
|
229
|
-
sendToChrome(message);
|
|
399
|
+
sendToChrome(agentId ? getChromeForAgent(agentId) : primaryChromeId, message);
|
|
230
400
|
}
|
|
231
401
|
}
|
|
232
402
|
function handleChromeMessage(message) {
|
|
@@ -241,7 +411,7 @@ function startBridge(options) {
|
|
|
241
411
|
sendToMcp(entry.connId, message);
|
|
242
412
|
} else {
|
|
243
413
|
process.stderr.write(
|
|
244
|
-
`${
|
|
414
|
+
`${LOG_PREFIX2} No routing entry for tool_result id=${id}, dropping
|
|
245
415
|
`
|
|
246
416
|
);
|
|
247
417
|
}
|
|
@@ -254,7 +424,7 @@ function startBridge(options) {
|
|
|
254
424
|
sendToMcp(entry.connId, message);
|
|
255
425
|
} else {
|
|
256
426
|
process.stderr.write(
|
|
257
|
-
`${
|
|
427
|
+
`${LOG_PREFIX2} No routing entry for chunk requestId=${requestId}, dropping
|
|
258
428
|
`
|
|
259
429
|
);
|
|
260
430
|
}
|
|
@@ -264,9 +434,7 @@ function startBridge(options) {
|
|
|
264
434
|
if (agentId) {
|
|
265
435
|
const connId = connIdForAgent(agentId);
|
|
266
436
|
if (connId) {
|
|
267
|
-
|
|
268
|
-
const conn = mcpConnections.get(connId);
|
|
269
|
-
if (conn) conn.agentIds.delete(agentId);
|
|
437
|
+
cleanupAgent(agentId, connId);
|
|
270
438
|
sendToMcp(connId, message);
|
|
271
439
|
}
|
|
272
440
|
}
|
|
@@ -274,38 +442,133 @@ function startBridge(options) {
|
|
|
274
442
|
const agentId = message.agentId;
|
|
275
443
|
if (agentId) {
|
|
276
444
|
const connId = connIdForAgent(agentId);
|
|
277
|
-
if (connId)
|
|
278
|
-
sendToMcp(connId, message);
|
|
279
|
-
}
|
|
445
|
+
if (connId) sendToMcp(connId, message);
|
|
280
446
|
}
|
|
281
447
|
} else if (type === "browser_event") {
|
|
282
448
|
const agentId = message.agentId;
|
|
283
449
|
if (agentId) {
|
|
284
450
|
const connId = connIdForAgent(agentId);
|
|
285
|
-
if (connId)
|
|
286
|
-
sendToMcp(connId, message);
|
|
287
|
-
}
|
|
451
|
+
if (connId) sendToMcp(connId, message);
|
|
288
452
|
}
|
|
289
453
|
} else {
|
|
290
|
-
process.stderr.write(`${
|
|
454
|
+
process.stderr.write(`${LOG_PREFIX2} Unknown message type from Chrome: ${type}, dropping
|
|
291
455
|
`);
|
|
292
456
|
}
|
|
293
457
|
}
|
|
294
|
-
function
|
|
458
|
+
function setupRelayConnection(socket) {
|
|
459
|
+
const chromeId = `chrome-${nextChromeIndex++}`;
|
|
460
|
+
const chrome = {
|
|
461
|
+
chromeId,
|
|
462
|
+
send: (msg) => {
|
|
463
|
+
if (!socket.destroyed) {
|
|
464
|
+
try {
|
|
465
|
+
socket.write(`${JSON.stringify(msg)}
|
|
466
|
+
`);
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
agentIds: /* @__PURE__ */ new Set()
|
|
472
|
+
};
|
|
473
|
+
chromeConnections.set(chromeId, chrome);
|
|
474
|
+
process.stderr.write(
|
|
475
|
+
`${LOG_PREFIX2} Chrome relay connected: ${chromeId} (total chromes: ${chromeConnections.size})
|
|
476
|
+
`
|
|
477
|
+
);
|
|
478
|
+
socket.write(
|
|
479
|
+
`${JSON.stringify({ type: "chrome_relay_ack", chromeId, timestamp: Date.now() })}
|
|
480
|
+
`
|
|
481
|
+
);
|
|
482
|
+
if (mcpConnections.size > 0) {
|
|
483
|
+
chrome.send({ type: "bridge_status", connected: true, timestamp: Date.now() });
|
|
484
|
+
}
|
|
485
|
+
broadcastChromeProfiles();
|
|
486
|
+
const processData = createTcpLineReader((message) => handleChromeMessage(message), onError);
|
|
487
|
+
socket.on("data", processData);
|
|
488
|
+
socket.on("error", (error) => {
|
|
489
|
+
process.stderr.write(`${LOG_PREFIX2} Relay socket error (${chromeId}): ${error.message}
|
|
490
|
+
`);
|
|
491
|
+
});
|
|
492
|
+
socket.on("close", () => {
|
|
493
|
+
process.stderr.write(
|
|
494
|
+
`${LOG_PREFIX2} Chrome relay disconnected: ${chromeId} (remaining chromes: ${chromeConnections.size - 1})
|
|
495
|
+
`
|
|
496
|
+
);
|
|
497
|
+
for (const agentId of [...chrome.agentIds]) {
|
|
498
|
+
const connId = connIdForAgent(agentId);
|
|
499
|
+
if (connId) {
|
|
500
|
+
sendToMcp(connId, {
|
|
501
|
+
id: randomUUID(),
|
|
502
|
+
type: "session_close",
|
|
503
|
+
agentId,
|
|
504
|
+
timestamp: Date.now()
|
|
505
|
+
});
|
|
506
|
+
cleanupAgent(agentId, connId);
|
|
507
|
+
} else {
|
|
508
|
+
agentToChrome.delete(agentId);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
chromeConnections.delete(chromeId);
|
|
512
|
+
broadcastChromeProfiles();
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function handleTcpConnection(socket) {
|
|
516
|
+
let lineBuffer = "";
|
|
517
|
+
let identified = false;
|
|
518
|
+
const identifyTimeout = setTimeout(() => {
|
|
519
|
+
if (!identified) {
|
|
520
|
+
identified = true;
|
|
521
|
+
socket.removeListener("data", onData);
|
|
522
|
+
handleMcpConnection(socket, lineBuffer);
|
|
523
|
+
}
|
|
524
|
+
}, 5e3);
|
|
525
|
+
const onData = (data) => {
|
|
526
|
+
if (identified) return;
|
|
527
|
+
lineBuffer += data.toString("utf-8");
|
|
528
|
+
const newlineIdx = lineBuffer.indexOf("\n");
|
|
529
|
+
if (newlineIdx === -1) return;
|
|
530
|
+
identified = true;
|
|
531
|
+
clearTimeout(identifyTimeout);
|
|
532
|
+
socket.removeListener("data", onData);
|
|
533
|
+
const firstLine = lineBuffer.substring(0, newlineIdx);
|
|
534
|
+
const remaining = lineBuffer.substring(newlineIdx + 1);
|
|
535
|
+
try {
|
|
536
|
+
const firstMsg = JSON.parse(firstLine);
|
|
537
|
+
if (firstMsg.type === "chrome_relay_init") {
|
|
538
|
+
setupRelayConnection(socket);
|
|
539
|
+
if (remaining) {
|
|
540
|
+
socket.emit("data", Buffer.from(remaining, "utf-8"));
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
handleMcpConnection(socket, lineBuffer);
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
handleMcpConnection(socket, lineBuffer);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
socket.on("data", onData);
|
|
550
|
+
socket.on("error", () => {
|
|
551
|
+
clearTimeout(identifyTimeout);
|
|
552
|
+
});
|
|
553
|
+
socket.on("close", () => {
|
|
554
|
+
clearTimeout(identifyTimeout);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
function handleMcpConnection(socket, initialData) {
|
|
295
558
|
if (mcpConnections.size >= BRIDGE.MAX_CONNECTIONS) {
|
|
296
559
|
process.stderr.write(
|
|
297
|
-
`${
|
|
560
|
+
`${LOG_PREFIX2} Max connections (${BRIDGE.MAX_CONNECTIONS}) reached, rejecting
|
|
298
561
|
`
|
|
299
562
|
);
|
|
300
563
|
socket.destroy();
|
|
301
564
|
return;
|
|
302
565
|
}
|
|
303
566
|
const connId = randomUUID();
|
|
304
|
-
const conn = { socket,
|
|
567
|
+
const conn = { socket, agentIds: /* @__PURE__ */ new Set() };
|
|
305
568
|
const wasEmpty = mcpConnections.size === 0;
|
|
306
569
|
mcpConnections.set(connId, conn);
|
|
307
570
|
process.stderr.write(
|
|
308
|
-
`${
|
|
571
|
+
`${LOG_PREFIX2} MCP Server connected: ${connId} (total: ${mcpConnections.size})
|
|
309
572
|
`
|
|
310
573
|
);
|
|
311
574
|
socket.setKeepAlive(true, 3e4);
|
|
@@ -315,44 +578,33 @@ function startBridge(options) {
|
|
|
315
578
|
sendToMcp(connId, {
|
|
316
579
|
type: "bridge_status",
|
|
317
580
|
connected: true,
|
|
581
|
+
chromeProfiles: getChromeProfilesList(),
|
|
318
582
|
timestamp: Date.now()
|
|
319
583
|
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (!line) continue;
|
|
326
|
-
try {
|
|
327
|
-
let message = JSON.parse(line);
|
|
328
|
-
if (message.type === "compressed" && typeof message.data === "string") {
|
|
329
|
-
const decompressed = decompressPayload(message.data, true);
|
|
330
|
-
message = JSON.parse(decompressed);
|
|
331
|
-
}
|
|
332
|
-
handleMcpMessage(connId, message);
|
|
333
|
-
} catch (error) {
|
|
334
|
-
onError?.(error);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
});
|
|
584
|
+
const processData = createTcpLineReader((message) => handleMcpMessage(connId, message), onError);
|
|
585
|
+
socket.on("data", processData);
|
|
586
|
+
if (initialData) {
|
|
587
|
+
processData(Buffer.from(initialData, "utf-8"));
|
|
588
|
+
}
|
|
338
589
|
socket.on("error", (error) => {
|
|
339
|
-
process.stderr.write(`${
|
|
590
|
+
process.stderr.write(`${LOG_PREFIX2} MCP Server socket error (${connId}): ${error.message}
|
|
340
591
|
`);
|
|
341
592
|
});
|
|
342
593
|
socket.on("close", () => {
|
|
343
594
|
process.stderr.write(
|
|
344
|
-
`${
|
|
595
|
+
`${LOG_PREFIX2} MCP Server disconnected: ${connId} (remaining: ${mcpConnections.size - 1})
|
|
345
596
|
`
|
|
346
597
|
);
|
|
347
|
-
for (const agentId of conn.agentIds) {
|
|
598
|
+
for (const agentId of [...conn.agentIds]) {
|
|
348
599
|
if (agentToConn.get(agentId) === connId) {
|
|
349
|
-
|
|
350
|
-
sendToChrome({
|
|
600
|
+
const targetChrome = getChromeForAgent(agentId);
|
|
601
|
+
sendToChrome(targetChrome, {
|
|
351
602
|
id: randomUUID(),
|
|
352
603
|
type: "session_close",
|
|
353
604
|
agentId,
|
|
354
605
|
timestamp: Date.now()
|
|
355
606
|
});
|
|
607
|
+
cleanupAgent(agentId, connId);
|
|
356
608
|
}
|
|
357
609
|
}
|
|
358
610
|
for (const [id, entry] of requestOrigin) {
|
|
@@ -366,74 +618,66 @@ function startBridge(options) {
|
|
|
366
618
|
}
|
|
367
619
|
});
|
|
368
620
|
}
|
|
369
|
-
|
|
370
|
-
process.stdin,
|
|
371
|
-
(raw) => {
|
|
372
|
-
if (!raw || typeof raw !== "object") return;
|
|
373
|
-
let message = raw;
|
|
374
|
-
if (message.type === "compressed" && typeof message.data === "string") {
|
|
375
|
-
try {
|
|
376
|
-
const decompressed = decompressPayload(message.data, true);
|
|
377
|
-
message = JSON.parse(decompressed);
|
|
378
|
-
} catch (error) {
|
|
379
|
-
onError?.(error);
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
handleChromeMessage(message);
|
|
384
|
-
},
|
|
385
|
-
onError
|
|
386
|
-
);
|
|
387
|
-
server = createServer((socket) => handleMcpConnection(socket));
|
|
621
|
+
server = createServer((socket) => handleTcpConnection(socket));
|
|
388
622
|
server.on("error", (error) => {
|
|
389
623
|
if (error.code === "EADDRINUSE") {
|
|
390
|
-
process.stderr.write(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
);
|
|
394
|
-
process.exit(1);
|
|
624
|
+
process.stderr.write(`${LOG_PREFIX2} Port ${port} in use. Starting relay mode.
|
|
625
|
+
`);
|
|
626
|
+
clearInterval(cleanupTimer2);
|
|
627
|
+
startRelayMode(port, host, onError);
|
|
395
628
|
} else {
|
|
396
|
-
process.stderr.write(`${
|
|
629
|
+
process.stderr.write(`${LOG_PREFIX2} TCP server error: ${error.message} (${error.code})
|
|
397
630
|
`);
|
|
398
631
|
}
|
|
399
632
|
});
|
|
400
633
|
server.listen(port, host, () => {
|
|
401
|
-
process.stderr.write(`${
|
|
634
|
+
process.stderr.write(`${LOG_PREFIX2} TCP server listening on ${host}:${port}
|
|
402
635
|
`);
|
|
636
|
+
createMessageReader(
|
|
637
|
+
process.stdin,
|
|
638
|
+
(raw) => {
|
|
639
|
+
if (!raw || typeof raw !== "object") return;
|
|
640
|
+
const message = decompressIfNeeded(raw);
|
|
641
|
+
handleChromeMessage(message);
|
|
642
|
+
},
|
|
643
|
+
onError
|
|
644
|
+
);
|
|
403
645
|
notifyChromeConnected(false);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
conn.socket.write(`${closingMsg}
|
|
646
|
+
function shutdown() {
|
|
647
|
+
const closingMsg = JSON.stringify({ type: "bridge_closing", timestamp: Date.now() });
|
|
648
|
+
for (const [, conn] of mcpConnections) {
|
|
649
|
+
try {
|
|
650
|
+
conn.socket.write(`${closingMsg}
|
|
410
651
|
`);
|
|
411
|
-
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
412
654
|
}
|
|
655
|
+
for (const [, conn] of mcpConnections) {
|
|
656
|
+
conn.socket.destroy();
|
|
657
|
+
}
|
|
658
|
+
mcpConnections.clear();
|
|
659
|
+
agentToConn.clear();
|
|
660
|
+
agentToChrome.clear();
|
|
661
|
+
chromeConnections.clear();
|
|
662
|
+
requestOrigin.clear();
|
|
663
|
+
clearInterval(cleanupTimer2);
|
|
664
|
+
server?.close();
|
|
665
|
+
server = null;
|
|
413
666
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
process.on("SIGINT", () => {
|
|
425
|
-
shutdown();
|
|
426
|
-
process.exit(0);
|
|
427
|
-
});
|
|
428
|
-
process.on("SIGTERM", () => {
|
|
429
|
-
shutdown();
|
|
430
|
-
process.exit(0);
|
|
431
|
-
});
|
|
432
|
-
process.stdin.on("end", () => {
|
|
433
|
-
process.stderr.write(`${LOG_PREFIX} stdin closed, shutting down
|
|
667
|
+
process.on("SIGINT", () => {
|
|
668
|
+
shutdown();
|
|
669
|
+
process.exit(0);
|
|
670
|
+
});
|
|
671
|
+
process.on("SIGTERM", () => {
|
|
672
|
+
shutdown();
|
|
673
|
+
process.exit(0);
|
|
674
|
+
});
|
|
675
|
+
process.stdin.on("end", () => {
|
|
676
|
+
process.stderr.write(`${LOG_PREFIX2} stdin closed, shutting down
|
|
434
677
|
`);
|
|
435
|
-
|
|
436
|
-
|
|
678
|
+
shutdown();
|
|
679
|
+
process.exit(0);
|
|
680
|
+
});
|
|
437
681
|
});
|
|
438
682
|
}
|
|
439
683
|
|
|
@@ -441,7 +685,7 @@ function startBridge(options) {
|
|
|
441
685
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
442
686
|
import { existsSync, statSync } from "fs";
|
|
443
687
|
import http from "http";
|
|
444
|
-
import { createConnection } from "net";
|
|
688
|
+
import { createConnection as createConnection2 } from "net";
|
|
445
689
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
446
690
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
447
691
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -554,6 +798,7 @@ function processEvent(event) {
|
|
|
554
798
|
// src/health.ts
|
|
555
799
|
var extensionConnected = false;
|
|
556
800
|
var lastHeartbeat = null;
|
|
801
|
+
var chromeProfiles = [];
|
|
557
802
|
function setExtensionConnected(connected) {
|
|
558
803
|
extensionConnected = connected;
|
|
559
804
|
if (connected) lastHeartbeat = Date.now();
|
|
@@ -561,6 +806,9 @@ function setExtensionConnected(connected) {
|
|
|
561
806
|
function recordHeartbeat() {
|
|
562
807
|
lastHeartbeat = Date.now();
|
|
563
808
|
}
|
|
809
|
+
function setChromeProfiles(profiles) {
|
|
810
|
+
chromeProfiles = profiles;
|
|
811
|
+
}
|
|
564
812
|
var HEARTBEAT_STALENESS_MS = 6e4;
|
|
565
813
|
function isExtensionConnected() {
|
|
566
814
|
if (!extensionConnected) return false;
|
|
@@ -574,7 +822,8 @@ function getHealthStatus() {
|
|
|
574
822
|
extensionConnected,
|
|
575
823
|
lastHeartbeat,
|
|
576
824
|
uptime: process.uptime(),
|
|
577
|
-
memoryUsage: process.memoryUsage().heapUsed
|
|
825
|
+
memoryUsage: process.memoryUsage().heapUsed,
|
|
826
|
+
chromeProfiles
|
|
578
827
|
};
|
|
579
828
|
}
|
|
580
829
|
|
|
@@ -790,6 +1039,38 @@ Returns details of HTTP requests and responses including URL, method,
|
|
|
790
1039
|
status code, headers, and timing information. Supports filtering
|
|
791
1040
|
by URL pattern or resource type.`;
|
|
792
1041
|
|
|
1042
|
+
// src/tools/sheets/descriptions.ts
|
|
1043
|
+
var SHEETS_READ_DESCRIPTION = `Read data from the active Google Sheets spreadsheet.
|
|
1044
|
+
|
|
1045
|
+
Uses the gviz/tq endpoint with session cookies \u2014 works with private sheets without OAuth.
|
|
1046
|
+
Supports range selection, tq query language for filtering/aggregation, and row limits.
|
|
1047
|
+
|
|
1048
|
+
tq query examples:
|
|
1049
|
+
- "SELECT A, B WHERE C > 100" \u2014 filter rows
|
|
1050
|
+
- "SELECT A, SUM(B) GROUP BY A" \u2014 aggregate
|
|
1051
|
+
- "SELECT * ORDER BY C DESC LIMIT 5" \u2014 sort and limit
|
|
1052
|
+
|
|
1053
|
+
Set formulas=true to read raw formulas (e.g., =IMAGE(), =HYPERLINK()) instead of computed values.
|
|
1054
|
+
Formula mode reads cell-by-cell via the formula bar, limited to 200 cells.
|
|
1055
|
+
The tab must be on a Google Sheets page (docs.google.com/spreadsheets/).`;
|
|
1056
|
+
var SHEETS_WRITE_DESCRIPTION = `Write data to the active Google Sheets spreadsheet.
|
|
1057
|
+
|
|
1058
|
+
For single cell: navigates to the cell via Name Box and types the value.
|
|
1059
|
+
For multiple cells: converts 2D array to TSV, copies to clipboard via Offscreen Document, then pastes with Ctrl+V. Sheets auto-expands TSV into multiple cells.
|
|
1060
|
+
|
|
1061
|
+
Provide either "value" (single cell string) or "values" (2D array), not both.
|
|
1062
|
+
The tab must be on a Google Sheets page.`;
|
|
1063
|
+
var SHEETS_INFO_DESCRIPTION = `Get metadata about the active Google Sheets spreadsheet.
|
|
1064
|
+
|
|
1065
|
+
Returns spreadsheet ID, document title, sheet list (name, index, row/col counts, active status), active cell reference, and URL.
|
|
1066
|
+
Uses DOM inspection and gviz/tq queries for sheet dimensions.
|
|
1067
|
+
The tab must be on a Google Sheets page.`;
|
|
1068
|
+
var SHEETS_NAVIGATE_DESCRIPTION = `Navigate within the active Google Sheets spreadsheet.
|
|
1069
|
+
|
|
1070
|
+
Jump to a specific cell (e.g., "A1", "Z100") via the Name Box, or switch to a different sheet tab by name.
|
|
1071
|
+
Both cell and sheet can be specified together.
|
|
1072
|
+
The tab must be on a Google Sheets page.`;
|
|
1073
|
+
|
|
793
1074
|
// src/tools/semantic/sm-add-action.ts
|
|
794
1075
|
var SM_ADD_ACTION_DESCRIPTION = `Define a semantic action (step sequence) for a registered page.
|
|
795
1076
|
|
|
@@ -1284,6 +1565,7 @@ across multiple pages. Loop steps support cartesian product iteration over varia
|
|
|
1284
1565
|
|
|
1285
1566
|
Parameters:
|
|
1286
1567
|
- label: scenario name (e.g., "Price Table Collection")
|
|
1568
|
+
- start_url: the URL to open when running this scenario (required)
|
|
1287
1569
|
- description: optional LLM-facing description
|
|
1288
1570
|
- params: global parameter definitions (available as {{param_name}} templates)
|
|
1289
1571
|
- steps: ordered step sequence
|
|
@@ -1360,6 +1642,7 @@ Supports partial updates - only provide fields you want to change.
|
|
|
1360
1642
|
Parameters:
|
|
1361
1643
|
- scenario_id: the scenario to update
|
|
1362
1644
|
- label: new name
|
|
1645
|
+
- start_url: new starting URL
|
|
1363
1646
|
- description: new description
|
|
1364
1647
|
- params: new parameter definitions
|
|
1365
1648
|
- steps: new step sequence
|
|
@@ -1926,7 +2209,7 @@ var smAddActionTool = {
|
|
|
1926
2209
|
).optional().describe("Parameter definitions"),
|
|
1927
2210
|
steps: z.array(
|
|
1928
2211
|
z.object({
|
|
1929
|
-
type: z.enum(["click", "type", "select", "wait", "scroll", "key", "navigate", "file_upload"]).describe("Step type"),
|
|
2212
|
+
type: z.enum(["click", "type", "select", "wait", "scroll", "key", "navigate", "file_upload", "script"]).describe("Step type"),
|
|
1930
2213
|
target_id: z.string().optional().describe("Target to act on"),
|
|
1931
2214
|
params: z.object({
|
|
1932
2215
|
text: z.string().optional(),
|
|
@@ -1939,7 +2222,7 @@ var smAddActionTool = {
|
|
|
1939
2222
|
file_paths: z.array(z.string()).optional().describe("File paths for file_upload step")
|
|
1940
2223
|
}).optional().describe("Step parameters"),
|
|
1941
2224
|
pre_wait: z.object({
|
|
1942
|
-
strategy: z.enum(["selector", "navigation", "time"]),
|
|
2225
|
+
strategy: z.enum(["selector", "navigation", "time", "network_idle"]),
|
|
1943
2226
|
selector: z.string().optional(),
|
|
1944
2227
|
timeout_ms: z.coerce.number().default(5e3)
|
|
1945
2228
|
}).optional().describe("Pre-step wait rule"),
|
|
@@ -2079,7 +2362,7 @@ var scenarioStepSchema = z.lazy(
|
|
|
2079
2362
|
z.object({
|
|
2080
2363
|
type: z.literal("wait"),
|
|
2081
2364
|
label: z.string().optional(),
|
|
2082
|
-
strategy: z.enum(["time", "selector", "navigation"]),
|
|
2365
|
+
strategy: z.enum(["time", "selector", "navigation", "network_idle"]),
|
|
2083
2366
|
selector: z.string().optional(),
|
|
2084
2367
|
timeout_ms: z.coerce.number()
|
|
2085
2368
|
})
|
|
@@ -2090,6 +2373,7 @@ var smScenarioCreateTool = {
|
|
|
2090
2373
|
description: SM_SCENARIO_CREATE_DESCRIPTION,
|
|
2091
2374
|
inputSchema: z.object({
|
|
2092
2375
|
label: z.string().describe("Scenario name"),
|
|
2376
|
+
start_url: z.string().url().describe("Starting URL to open when running this scenario"),
|
|
2093
2377
|
description: z.string().optional().describe("LLM-facing description"),
|
|
2094
2378
|
params: z.array(
|
|
2095
2379
|
z.object({
|
|
@@ -2112,6 +2396,7 @@ var smScenarioUpdateTool = {
|
|
|
2112
2396
|
inputSchema: z.object({
|
|
2113
2397
|
scenario_id: z.string().describe("Scenario ID to update"),
|
|
2114
2398
|
label: z.string().optional(),
|
|
2399
|
+
start_url: z.string().url().optional().describe("Starting URL to open when running this scenario"),
|
|
2115
2400
|
description: z.string().optional(),
|
|
2116
2401
|
params: z.array(
|
|
2117
2402
|
z.object({
|
|
@@ -2325,6 +2610,47 @@ var smCustomViewGetTool = {
|
|
|
2325
2610
|
include_data: z.boolean().optional().describe("Include resolved data in response (default: false)")
|
|
2326
2611
|
})
|
|
2327
2612
|
};
|
|
2613
|
+
var sheetsReadTool = {
|
|
2614
|
+
name: "sheets_read",
|
|
2615
|
+
description: SHEETS_READ_DESCRIPTION,
|
|
2616
|
+
inputSchema: z.object({
|
|
2617
|
+
tabId: z.coerce.number().describe("Tab ID of the Google Sheets page"),
|
|
2618
|
+
range: z.string().optional().describe('Cell range (e.g., "A1:D10", "Sheet2!B:B"). Omit for all data.'),
|
|
2619
|
+
query: z.string().optional().describe('gviz tq query (e.g., "SELECT A,B WHERE C > 100 ORDER BY A")'),
|
|
2620
|
+
headers: z.coerce.number().min(0).max(10).optional().describe("Number of header rows (default: 1)"),
|
|
2621
|
+
sheet: z.string().optional().describe("Sheet name (omit for active sheet)"),
|
|
2622
|
+
max_rows: z.coerce.number().min(1).max(1e4).optional().describe("Max rows to return (default: 500)"),
|
|
2623
|
+
formulas: z.boolean().optional().describe(
|
|
2624
|
+
'Read cell formulas instead of computed values (default: false). Requires "range". Reads cell-by-cell via formula bar, limited to 200 cells. Use for =IMAGE(), =HYPERLINK(), etc.'
|
|
2625
|
+
)
|
|
2626
|
+
})
|
|
2627
|
+
};
|
|
2628
|
+
var sheetsWriteTool = {
|
|
2629
|
+
name: "sheets_write",
|
|
2630
|
+
description: SHEETS_WRITE_DESCRIPTION,
|
|
2631
|
+
inputSchema: z.object({
|
|
2632
|
+
tabId: z.coerce.number().describe("Tab ID of the Google Sheets page"),
|
|
2633
|
+
cell: z.string().describe('Target cell reference (e.g., "A1", "Sheet2!B5")'),
|
|
2634
|
+
value: z.string().optional().describe("Single cell value (exclusive with values)"),
|
|
2635
|
+
values: z.array(z.array(z.string())).optional().describe("2D array of values (exclusive with value)")
|
|
2636
|
+
})
|
|
2637
|
+
};
|
|
2638
|
+
var sheetsInfoTool = {
|
|
2639
|
+
name: "sheets_info",
|
|
2640
|
+
description: SHEETS_INFO_DESCRIPTION,
|
|
2641
|
+
inputSchema: z.object({
|
|
2642
|
+
tabId: z.coerce.number().describe("Tab ID of the Google Sheets page")
|
|
2643
|
+
})
|
|
2644
|
+
};
|
|
2645
|
+
var sheetsNavigateTool = {
|
|
2646
|
+
name: "sheets_navigate",
|
|
2647
|
+
description: SHEETS_NAVIGATE_DESCRIPTION,
|
|
2648
|
+
inputSchema: z.object({
|
|
2649
|
+
tabId: z.coerce.number().describe("Tab ID of the Google Sheets page"),
|
|
2650
|
+
cell: z.string().optional().describe('Cell to jump to (e.g., "A1", "Z100")'),
|
|
2651
|
+
sheet: z.string().optional().describe("Sheet name to switch to")
|
|
2652
|
+
})
|
|
2653
|
+
};
|
|
2328
2654
|
var allTools = [
|
|
2329
2655
|
// Core (16)
|
|
2330
2656
|
navigateTool,
|
|
@@ -2409,12 +2735,18 @@ var allTools = [
|
|
|
2409
2735
|
smCustomViewUpdateTool,
|
|
2410
2736
|
smCustomViewDeleteTool,
|
|
2411
2737
|
smCustomViewListTool,
|
|
2412
|
-
smCustomViewGetTool
|
|
2738
|
+
smCustomViewGetTool,
|
|
2739
|
+
// Google Sheets (4)
|
|
2740
|
+
sheetsReadTool,
|
|
2741
|
+
sheetsWriteTool,
|
|
2742
|
+
sheetsInfoTool,
|
|
2743
|
+
sheetsNavigateTool
|
|
2413
2744
|
];
|
|
2414
2745
|
|
|
2415
2746
|
// src/server.ts
|
|
2416
2747
|
var pendingRequests = /* @__PURE__ */ new Map();
|
|
2417
2748
|
var extensionSocket = null;
|
|
2749
|
+
var configuredChromeProfile;
|
|
2418
2750
|
function coerceValue(v) {
|
|
2419
2751
|
if (typeof v !== "string") return v;
|
|
2420
2752
|
if (v.startsWith("[") && v.endsWith("]") || v.startsWith("{") && v.endsWith("}")) {
|
|
@@ -2442,40 +2774,40 @@ function createConfiguredMcpServer() {
|
|
|
2442
2774
|
for (const tool of allTools) {
|
|
2443
2775
|
const def = tool.inputSchema._def;
|
|
2444
2776
|
const shape = def.shape?.() ?? {};
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
}
|
|
2465
|
-
} catch {
|
|
2777
|
+
const coerced = coerceShape(shape);
|
|
2778
|
+
coerced.chromeProfile = z2.preprocess(
|
|
2779
|
+
coerceValue,
|
|
2780
|
+
z2.string().optional().describe('Target Chrome profile ID (e.g. "chrome-0", "chrome-1")')
|
|
2781
|
+
);
|
|
2782
|
+
server.tool(tool.name, tool.description, coerced, async (params) => {
|
|
2783
|
+
const result = await callExtensionTool(tool.name, params);
|
|
2784
|
+
const first = result.content[0];
|
|
2785
|
+
if (tool.name === "browser_event_subscribe" && first?.type === "text") {
|
|
2786
|
+
try {
|
|
2787
|
+
const parsed = JSON.parse(first.text);
|
|
2788
|
+
if (parsed.subscriptionId) {
|
|
2789
|
+
const p = params;
|
|
2790
|
+
addSubscription({
|
|
2791
|
+
id: parsed.subscriptionId,
|
|
2792
|
+
agentId: getDefaultAgentId(),
|
|
2793
|
+
eventTypes: p.eventTypes ?? [],
|
|
2794
|
+
urlPattern: p.urlPattern,
|
|
2795
|
+
createdAt: Date.now()
|
|
2796
|
+
});
|
|
2466
2797
|
}
|
|
2467
|
-
}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2798
|
+
} catch {
|
|
2799
|
+
}
|
|
2800
|
+
} else if (tool.name === "browser_event_unsubscribe" && first?.type === "text") {
|
|
2801
|
+
try {
|
|
2802
|
+
const parsed = JSON.parse(first.text);
|
|
2803
|
+
if (parsed.subscriptionId) {
|
|
2804
|
+
removeSubscription(parsed.subscriptionId);
|
|
2474
2805
|
}
|
|
2806
|
+
} catch {
|
|
2475
2807
|
}
|
|
2476
|
-
return result;
|
|
2477
2808
|
}
|
|
2478
|
-
|
|
2809
|
+
return result;
|
|
2810
|
+
});
|
|
2479
2811
|
}
|
|
2480
2812
|
const listener = (event) => {
|
|
2481
2813
|
server.sendLoggingMessage({
|
|
@@ -2494,6 +2826,7 @@ async function startMcpServer(agentName, options) {
|
|
|
2494
2826
|
if (agentName) {
|
|
2495
2827
|
setDefaultAgentId(agentName);
|
|
2496
2828
|
}
|
|
2829
|
+
configuredChromeProfile = options?.chromeProfile;
|
|
2497
2830
|
connectToBridge();
|
|
2498
2831
|
if (options?.transport === "sse") {
|
|
2499
2832
|
const sessions2 = /* @__PURE__ */ new Map();
|
|
@@ -2797,7 +3130,7 @@ function connectToBridge() {
|
|
|
2797
3130
|
let cleanup = null;
|
|
2798
3131
|
function connect() {
|
|
2799
3132
|
bridgeClosingReceived = false;
|
|
2800
|
-
const socket =
|
|
3133
|
+
const socket = createConnection2({ port: BRIDGE.TCP_PORT, host: BRIDGE.TCP_HOST });
|
|
2801
3134
|
socket.on("connect", () => {
|
|
2802
3135
|
retryCount = 0;
|
|
2803
3136
|
cleanup = setupBridgeConnection(socket, () => {
|
|
@@ -2866,6 +3199,9 @@ function setupBridgeConnection(socket, onBridgeClosing) {
|
|
|
2866
3199
|
protocolVersion: PROTOCOL_VERSION,
|
|
2867
3200
|
timestamp: Date.now()
|
|
2868
3201
|
};
|
|
3202
|
+
if (configuredChromeProfile) {
|
|
3203
|
+
initMsg.chromeProfile = configuredChromeProfile;
|
|
3204
|
+
}
|
|
2869
3205
|
socket.write(`${JSON.stringify(initMsg)}
|
|
2870
3206
|
`);
|
|
2871
3207
|
const heartbeatInterval = setInterval(() => {
|
|
@@ -2964,6 +3300,11 @@ function handleExtensionMessage(message) {
|
|
|
2964
3300
|
`
|
|
2965
3301
|
);
|
|
2966
3302
|
}
|
|
3303
|
+
} else if (type === "bridge_status") {
|
|
3304
|
+
const profiles = msg.chromeProfiles;
|
|
3305
|
+
if (profiles) {
|
|
3306
|
+
setChromeProfiles(profiles);
|
|
3307
|
+
}
|
|
2967
3308
|
} else if (type === "browser_event") {
|
|
2968
3309
|
process.stderr.write(`[viyv-browser:mcp] Browser event: ${String(msg.eventType)}
|
|
2969
3310
|
`);
|
|
@@ -3120,14 +3461,18 @@ async function callExtensionTool(tool, input) {
|
|
|
3120
3461
|
},
|
|
3121
3462
|
timer
|
|
3122
3463
|
});
|
|
3464
|
+
const { chromeProfile, ...cleanInput } = input;
|
|
3123
3465
|
const request = {
|
|
3124
3466
|
id: requestId,
|
|
3125
3467
|
type: "tool_call",
|
|
3126
3468
|
agentId,
|
|
3127
3469
|
tool,
|
|
3128
|
-
input,
|
|
3470
|
+
input: cleanInput,
|
|
3129
3471
|
timestamp: Date.now()
|
|
3130
3472
|
};
|
|
3473
|
+
if (typeof chromeProfile === "string" && chromeProfile) {
|
|
3474
|
+
request.chromeProfile = chromeProfile;
|
|
3475
|
+
}
|
|
3131
3476
|
const written = sock.write(`${JSON.stringify(request)}
|
|
3132
3477
|
`);
|
|
3133
3478
|
if (!written) {
|
|
@@ -3385,6 +3730,8 @@ if (args.includes("setup")) {
|
|
|
3385
3730
|
}
|
|
3386
3731
|
const portIdx = args.indexOf("--port");
|
|
3387
3732
|
const port = portIdx >= 0 ? Number(args[portIdx + 1]) : void 0;
|
|
3388
|
-
|
|
3733
|
+
const chromeProfileIdx = args.indexOf("--chrome-profile");
|
|
3734
|
+
const chromeProfile = chromeProfileIdx >= 0 ? args[chromeProfileIdx + 1] : process.env.VIYV_CHROME_PROFILE;
|
|
3735
|
+
startMcpServer(agentName, { transport: transportMode, port, chromeProfile });
|
|
3389
3736
|
}
|
|
3390
3737
|
//# sourceMappingURL=index.js.map
|