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 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 notifyChromeConnected(connected) {
167
- writeMessage(process.stdout, {
168
- type: "bridge_status",
169
- connected,
170
- timestamp: Date.now()
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
- writeMessage(process.stdout, message);
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
- sendToChrome(message);
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
- `${LOG_PREFIX} WARNING: agentId '${agentId}' re-registered from conn ${existingConnId} \u2192 ${connId}
368
+ `${LOG_PREFIX2} WARNING: agentId '${agentId}' re-registered from conn ${existingConnId} \u2192 ${connId}
205
369
  `
206
370
  );
207
- const oldConn = mcpConnections.get(existingConnId);
208
- if (oldConn) oldConn.agentIds.delete(agentId);
371
+ mcpConnections.get(existingConnId)?.agentIds.delete(agentId);
209
372
  }
210
373
  agentToConn.set(agentId, connId);
211
- const conn = mcpConnections.get(connId);
212
- if (conn) conn.agentIds.add(agentId);
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
- agentToConn.delete(agentId);
219
- const conn = mcpConnections.get(connId);
220
- if (conn) conn.agentIds.delete(agentId);
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(`${LOG_PREFIX} Ignoring ${type} from MCP Server ${connId}
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
- `${LOG_PREFIX} No routing entry for tool_result id=${id}, dropping
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
- `${LOG_PREFIX} No routing entry for chunk requestId=${requestId}, dropping
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
- agentToConn.delete(agentId);
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(`${LOG_PREFIX} Unknown message type from Chrome: ${type}, dropping
454
+ process.stderr.write(`${LOG_PREFIX2} Unknown message type from Chrome: ${type}, dropping
291
455
  `);
292
456
  }
293
457
  }
294
- function handleMcpConnection(socket) {
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
- `${LOG_PREFIX} Max connections (${BRIDGE.MAX_CONNECTIONS}) reached, rejecting
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, lineBuffer: "", agentIds: /* @__PURE__ */ new Set() };
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
- `${LOG_PREFIX} MCP Server connected: ${connId} (total: ${mcpConnections.size})
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
- socket.on("data", (data) => {
321
- conn.lineBuffer += data.toString("utf-8");
322
- const lines = conn.lineBuffer.split("\n");
323
- conn.lineBuffer = lines.pop() ?? "";
324
- for (const line of lines) {
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(`${LOG_PREFIX} MCP Server socket error (${connId}): ${error.message}
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
- `${LOG_PREFIX} MCP Server disconnected: ${connId} (remaining: ${mcpConnections.size - 1})
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
- agentToConn.delete(agentId);
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
- createMessageReader(
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
- `${LOG_PREFIX} TCP port ${port} already in use. Another Native Host may be running.
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(`${LOG_PREFIX} TCP server error: ${error.message} (${error.code})
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(`${LOG_PREFIX} TCP server listening on ${host}:${port}
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
- function shutdown() {
406
- const closingMsg = JSON.stringify({ type: "bridge_closing", timestamp: Date.now() });
407
- for (const [, conn] of mcpConnections) {
408
- try {
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
- } catch {
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
- for (const [, conn] of mcpConnections) {
415
- conn.socket.destroy();
416
- }
417
- mcpConnections.clear();
418
- agentToConn.clear();
419
- requestOrigin.clear();
420
- clearInterval(cleanupTimer2);
421
- server?.close();
422
- server = null;
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
- shutdown();
436
- process.exit(0);
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
- server.tool(
2446
- tool.name,
2447
- tool.description,
2448
- coerceShape(shape),
2449
- async (params) => {
2450
- const result = await callExtensionTool(tool.name, params);
2451
- const first = result.content[0];
2452
- if (tool.name === "browser_event_subscribe" && first?.type === "text") {
2453
- try {
2454
- const parsed = JSON.parse(first.text);
2455
- if (parsed.subscriptionId) {
2456
- const p = params;
2457
- addSubscription({
2458
- id: parsed.subscriptionId,
2459
- agentId: getDefaultAgentId(),
2460
- eventTypes: p.eventTypes ?? [],
2461
- urlPattern: p.urlPattern,
2462
- createdAt: Date.now()
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
- } else if (tool.name === "browser_event_unsubscribe" && first?.type === "text") {
2468
- try {
2469
- const parsed = JSON.parse(first.text);
2470
- if (parsed.subscriptionId) {
2471
- removeSubscription(parsed.subscriptionId);
2472
- }
2473
- } catch {
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 = createConnection({ port: BRIDGE.TCP_PORT, host: BRIDGE.TCP_HOST });
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
- startMcpServer(agentName, { transport: transportMode, port });
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