viyv-browser-mcp 0.10.1 → 0.10.2

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
@@ -11762,7 +11762,15 @@ var RECONNECT = {
11762
11762
  /** Max delay (ms) */
11763
11763
  MAX_DELAY: 3e4,
11764
11764
  /** Backoff multiplier */
11765
- MULTIPLIER: 2
11765
+ MULTIPLIER: 2,
11766
+ /** Symmetric jitter ratio applied to each reconnect delay. ±30% spreads synchronized retries. */
11767
+ JITTER_RATIO: 0.3
11768
+ };
11769
+ var BRIDGE_READINESS = {
11770
+ /** Max time a pre-handshake tool_call waits for `bridge_status { connected:true }` */
11771
+ TIMEOUT_MS: 8e3,
11772
+ /** Max number of tool_calls queued while the bridge client is below `ready` state */
11773
+ PENDING_QUEUE_MAX: 10
11766
11774
  };
11767
11775
  var BRIDGE = {
11768
11776
  /** TCP port for Native Host bridge (TCP server) */
@@ -11803,6 +11811,35 @@ function matchSubscription(sub, event) {
11803
11811
  return matchEventType(sub.eventTypes, event.eventType) && matchUrlPattern(sub.urlPattern, event.url);
11804
11812
  }
11805
11813
 
11814
+ // ../shared/dist/util/jitter.js
11815
+ function applyJitter(baseMs, ratio, random = Math.random) {
11816
+ if (!Number.isFinite(baseMs) || baseMs < 0)
11817
+ return 0;
11818
+ if (ratio <= 0)
11819
+ return baseMs;
11820
+ const clampedRatio = Math.min(ratio, 1);
11821
+ const offset = (random() * 2 - 1) * clampedRatio * baseMs;
11822
+ const result = baseMs + offset;
11823
+ return Math.max(0, Math.round(result));
11824
+ }
11825
+
11826
+ // ../shared/dist/util/exit-reason.js
11827
+ function shouldBroadcastClosing(reason) {
11828
+ switch (reason) {
11829
+ case "chrome-sigterm":
11830
+ case "sigint":
11831
+ return false;
11832
+ case "all-empty":
11833
+ case "relay-exhausted":
11834
+ case "shutdown-request":
11835
+ return true;
11836
+ default: {
11837
+ const _exhaustive = reason;
11838
+ return _exhaustive;
11839
+ }
11840
+ }
11841
+ }
11842
+
11806
11843
  // src/native-host-updater.ts
11807
11844
  import {
11808
11845
  chmodSync,
@@ -11817,7 +11854,7 @@ import { homedir } from "os";
11817
11854
  import { resolve } from "path";
11818
11855
  import { fileURLToPath } from "url";
11819
11856
  var PKG_VERSION = (() => {
11820
- if (true) return "0.10.1";
11857
+ if (true) return "0.10.2";
11821
11858
  try {
11822
11859
  const here = fileURLToPath(import.meta.url);
11823
11860
  const pkgPath = resolve(here, "..", "..", "package.json");
@@ -12460,7 +12497,7 @@ function startBridge(options) {
12460
12497
  const DRAIN_TIMEOUT = 5e3;
12461
12498
  const drainAndExit = () => {
12462
12499
  if (requestOrigin.size === 0) {
12463
- gracefulExit("Bridge shutdown requested for version update");
12500
+ gracefulExit("shutdown-request", "version update");
12464
12501
  return;
12465
12502
  }
12466
12503
  process.stderr.write(
@@ -12471,7 +12508,7 @@ function startBridge(options) {
12471
12508
  const check2 = setInterval(() => {
12472
12509
  if (requestOrigin.size === 0 || Date.now() - start > DRAIN_TIMEOUT) {
12473
12510
  clearInterval(check2);
12474
- gracefulExit("Bridge shutdown requested for version update");
12511
+ gracefulExit("shutdown-request", "version update");
12475
12512
  }
12476
12513
  }, 200);
12477
12514
  };
@@ -12536,6 +12573,7 @@ function startBridge(options) {
12536
12573
  } else if (type === "browser_event") {
12537
12574
  const agentId = message2.agentId;
12538
12575
  if (agentId) broadcastToAgent(agentId, message2);
12576
+ } else if (type === "sw_keepalive") {
12539
12577
  } else {
12540
12578
  process.stderr.write(`${LOG_PREFIX3} Unknown message type from Chrome: ${type}, dropping
12541
12579
  `);
@@ -12565,19 +12603,22 @@ function startBridge(options) {
12565
12603
  let primaryStdinClosed = false;
12566
12604
  const ORPHAN_TIMEOUT = 6e4;
12567
12605
  let orphanTimer = null;
12568
- function gracefulExit(reason) {
12569
- process.stderr.write(`${LOG_PREFIX3} ${reason}, exiting
12606
+ function gracefulExit(reason, detail) {
12607
+ const suffix = detail ? ` (${detail})` : "";
12608
+ process.stderr.write(`${LOG_PREFIX3} exiting reason=${reason}${suffix}
12570
12609
  `);
12571
12610
  if (orphanTimer) {
12572
12611
  clearTimeout(orphanTimer);
12573
12612
  orphanTimer = null;
12574
12613
  }
12575
- const closingMsg = JSON.stringify({ type: "bridge_closing", timestamp: Date.now() });
12576
- for (const [, conn] of mcpConnections) {
12577
- try {
12578
- conn.socket.write(`${closingMsg}
12614
+ if (shouldBroadcastClosing(reason)) {
12615
+ const closingMsg = JSON.stringify({ type: "bridge_closing", timestamp: Date.now() });
12616
+ for (const [, conn] of mcpConnections) {
12617
+ try {
12618
+ conn.socket.write(`${closingMsg}
12579
12619
  `);
12580
- } catch {
12620
+ } catch {
12621
+ }
12581
12622
  }
12582
12623
  }
12583
12624
  for (const [, conn] of mcpConnections) {
@@ -12603,7 +12644,7 @@ function startBridge(options) {
12603
12644
  return;
12604
12645
  }
12605
12646
  if (chromeConnections.size === 0 && mcpConnections.size === 0) {
12606
- gracefulExit("No connections remaining");
12647
+ gracefulExit("all-empty", "no connections remaining");
12607
12648
  }
12608
12649
  if (chromeConnections.size === 0 && mcpConnections.size > 0 && !orphanTimer) {
12609
12650
  process.stderr.write(
@@ -12819,8 +12860,8 @@ function startBridge(options) {
12819
12860
  onError: (e) => onError?.(e)
12820
12861
  });
12821
12862
  notifyChromeConnected(false);
12822
- process.on("SIGINT", () => gracefulExit("SIGINT received"));
12823
- process.on("SIGTERM", () => gracefulExit("SIGTERM received"));
12863
+ process.on("SIGINT", () => gracefulExit("sigint"));
12864
+ process.on("SIGTERM", () => gracefulExit("chrome-sigterm"));
12824
12865
  process.stdin.on("end", () => {
12825
12866
  process.stderr.write(`${LOG_PREFIX3} Primary Chrome stdin closed
12826
12867
  `);
@@ -12835,7 +12876,6 @@ function startBridge(options) {
12835
12876
  import { randomUUID as randomUUID5 } from "crypto";
12836
12877
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
12837
12878
  import http from "http";
12838
- import { createConnection as createConnection2 } from "net";
12839
12879
  import { dirname as dirname2 } from "path";
12840
12880
 
12841
12881
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
@@ -28249,6 +28289,394 @@ var StreamableHTTPServerTransport = class {
28249
28289
  }
28250
28290
  };
28251
28291
 
28292
+ // src/native-host/bridge-client/index.ts
28293
+ import { createConnection as createConnection2 } from "net";
28294
+
28295
+ // src/native-host/bridge-client/connection-state.ts
28296
+ function transition(state, event) {
28297
+ if (event.kind === "stop") return "disconnected";
28298
+ switch (state) {
28299
+ case "disconnected":
28300
+ if (event.kind === "start") return "connecting";
28301
+ return state;
28302
+ case "connecting":
28303
+ if (event.kind === "socket-connected") return "handshaking";
28304
+ if (event.kind === "socket-closed") return "disconnected";
28305
+ return state;
28306
+ case "handshaking":
28307
+ if (event.kind === "bridge-status-true") return "ready";
28308
+ if (event.kind === "bridge-status-false") return "handshaking";
28309
+ if (event.kind === "bridge-closing") return "draining";
28310
+ if (event.kind === "socket-closed") return "disconnected";
28311
+ return state;
28312
+ case "ready":
28313
+ if (event.kind === "bridge-status-false") return "handshaking";
28314
+ if (event.kind === "bridge-closing") return "draining";
28315
+ if (event.kind === "socket-closed") return "disconnected";
28316
+ return state;
28317
+ case "draining":
28318
+ if (event.kind === "socket-closed") return "disconnected";
28319
+ return state;
28320
+ default: {
28321
+ const _exhaustive = state;
28322
+ return _exhaustive;
28323
+ }
28324
+ }
28325
+ }
28326
+ function isReady(state) {
28327
+ return state === "ready";
28328
+ }
28329
+
28330
+ // src/native-host/bridge-client/pending-queue.ts
28331
+ var PendingQueue = class {
28332
+ constructor(max, ttlMs, timers) {
28333
+ this.max = max;
28334
+ this.ttlMs = ttlMs;
28335
+ this.timers = timers;
28336
+ if (max < 1) throw new Error("PendingQueue max must be >= 1");
28337
+ if (ttlMs < 0) throw new Error("PendingQueue ttlMs must be >= 0");
28338
+ }
28339
+ entries = [];
28340
+ /** Try to enqueue. Returns `true` on success, `false` when rejected (onReject already fired). */
28341
+ enqueue(entry) {
28342
+ if (this.entries.length >= this.max) {
28343
+ entry.onReject("queue-full");
28344
+ return false;
28345
+ }
28346
+ const stored = { ...entry, timer: null };
28347
+ stored.timer = this.timers.setTimer(() => this.expire(stored), this.ttlMs);
28348
+ this.entries.push(stored);
28349
+ return true;
28350
+ }
28351
+ /** Flush every queued entry in FIFO order. */
28352
+ flushAll() {
28353
+ const snapshot = this.entries.splice(0, this.entries.length);
28354
+ for (const entry of snapshot) {
28355
+ this.timers.clearTimer(entry.timer);
28356
+ entry.onFlush(entry.payload);
28357
+ }
28358
+ }
28359
+ /** Reject every queued entry with the given reason. Used on `stop()`. */
28360
+ rejectAll(reason) {
28361
+ const snapshot = this.entries.splice(0, this.entries.length);
28362
+ for (const entry of snapshot) {
28363
+ this.timers.clearTimer(entry.timer);
28364
+ entry.onReject(reason);
28365
+ }
28366
+ }
28367
+ size() {
28368
+ return this.entries.length;
28369
+ }
28370
+ expire(entry) {
28371
+ const idx = this.entries.indexOf(entry);
28372
+ if (idx === -1) return;
28373
+ this.entries.splice(idx, 1);
28374
+ entry.onReject("timeout");
28375
+ }
28376
+ };
28377
+
28378
+ // src/native-host/bridge-client/index.ts
28379
+ var LOG_PREFIX4 = "[viyv-browser:mcp:bridge]";
28380
+ var METRIC_PREFIX = "[viyv-browser:mcp:metric]";
28381
+ var LINE_TERMINATOR = "\n";
28382
+ var BRIDGE_RECONNECT_CAP_MS = 3e3;
28383
+ var defaultTimers = {
28384
+ setTimer: (fn, ms) => setTimeout(fn, ms),
28385
+ clearTimer: (h) => clearTimeout(h)
28386
+ };
28387
+ var defaultSchedule = (fn, ms) => {
28388
+ const h = setTimeout(fn, ms);
28389
+ return () => clearTimeout(h);
28390
+ };
28391
+ var BridgeClient = class {
28392
+ constructor(options) {
28393
+ this.options = options;
28394
+ this.log = options.log ?? ((line) => process.stderr.write(`${line}
28395
+ `));
28396
+ this.metric = options.metric ?? ((line) => process.stderr.write(`${line}
28397
+ `));
28398
+ this.now = options.now ?? (() => Date.now());
28399
+ this.schedule = options.schedule ?? defaultSchedule;
28400
+ this.random = options.random ?? Math.random;
28401
+ this.queue = new PendingQueue(
28402
+ BRIDGE_READINESS.PENDING_QUEUE_MAX,
28403
+ BRIDGE_READINESS.TIMEOUT_MS,
28404
+ options.timers ?? defaultTimers
28405
+ );
28406
+ }
28407
+ state = "disconnected";
28408
+ socket = null;
28409
+ reconnectAttempt = 0;
28410
+ cancelReconnect = null;
28411
+ messageHandlers = /* @__PURE__ */ new Set();
28412
+ stateHandlers = /* @__PURE__ */ new Set();
28413
+ queue;
28414
+ /** ms since epoch when the current handshake started; `null` when not measuring. */
28415
+ handshakeStartMs = null;
28416
+ started = false;
28417
+ log;
28418
+ metric;
28419
+ now;
28420
+ schedule;
28421
+ random;
28422
+ start() {
28423
+ if (this.started) return;
28424
+ this.started = true;
28425
+ this.apply({ kind: "start" });
28426
+ this.connect();
28427
+ }
28428
+ stop() {
28429
+ this.started = false;
28430
+ this.cancelReconnect?.();
28431
+ this.cancelReconnect = null;
28432
+ this.queue.rejectAll("stopped");
28433
+ if (this.socket && !this.socket.destroyed) {
28434
+ this.socket.destroy();
28435
+ }
28436
+ this.socket = null;
28437
+ this.apply({ kind: "stop" });
28438
+ }
28439
+ /**
28440
+ * Drop the current socket and reconnect immediately. Used by switch_browser
28441
+ * to pivot to a fresh Chrome. Pending tool_calls are drained with the
28442
+ * SWITCH_IN_PROGRESS error by the caller before this is invoked.
28443
+ */
28444
+ forceReconnect() {
28445
+ if (this.socket && !this.socket.destroyed) {
28446
+ this.socket.destroy();
28447
+ }
28448
+ this.socket = null;
28449
+ this.apply({ kind: "socket-closed" });
28450
+ if (this.started) {
28451
+ this.reconnectAttempt = 0;
28452
+ this.scheduleReconnect();
28453
+ }
28454
+ }
28455
+ getState() {
28456
+ return this.state;
28457
+ }
28458
+ isReady() {
28459
+ return isReady(this.state);
28460
+ }
28461
+ /**
28462
+ * Return the socket when it is TCP-live (handshaking, ready, or draining).
28463
+ * Ancillary writes like session_init / session_close tolerate writes during
28464
+ * handshake — the bridge buffers them until Chrome is reachable.
28465
+ * Returns null when the socket is not safe to write to.
28466
+ */
28467
+ getSocket() {
28468
+ if (!this.socket || this.socket.destroyed) return null;
28469
+ if (this.state === "handshaking" || this.state === "ready" || this.state === "draining") {
28470
+ return this.socket;
28471
+ }
28472
+ return null;
28473
+ }
28474
+ onMessage(fn) {
28475
+ this.messageHandlers.add(fn);
28476
+ return () => this.messageHandlers.delete(fn);
28477
+ }
28478
+ onStateChange(fn) {
28479
+ this.stateHandlers.add(fn);
28480
+ return () => this.stateHandlers.delete(fn);
28481
+ }
28482
+ /**
28483
+ * Send a tool_call line. If the bridge is `ready`, writes immediately.
28484
+ * Otherwise enqueues until readiness, failing with `queue-full` if capacity
28485
+ * is exhausted or `timeout` if the handshake takes too long.
28486
+ */
28487
+ enqueueToolCall(line) {
28488
+ return new Promise((resolve3, reject) => {
28489
+ if (this.state === "ready" && this.socket && !this.socket.destroyed) {
28490
+ try {
28491
+ this.socket.write(line.endsWith(LINE_TERMINATOR) ? line : line + LINE_TERMINATOR);
28492
+ resolve3();
28493
+ } catch (err) {
28494
+ reject({ reason: "write-failed", detail: err instanceof Error ? err.message : String(err) });
28495
+ }
28496
+ return;
28497
+ }
28498
+ const enqueued = this.queue.enqueue({
28499
+ payload: {
28500
+ line: line.endsWith(LINE_TERMINATOR) ? line : line + LINE_TERMINATOR,
28501
+ resolve: resolve3,
28502
+ reject
28503
+ },
28504
+ onFlush: (entry) => {
28505
+ if (this.socket && !this.socket.destroyed) {
28506
+ try {
28507
+ this.socket.write(entry.line);
28508
+ entry.resolve();
28509
+ } catch (err) {
28510
+ entry.reject({
28511
+ reason: "write-failed",
28512
+ detail: err instanceof Error ? err.message : String(err)
28513
+ });
28514
+ }
28515
+ } else {
28516
+ entry.reject({ reason: "write-failed", detail: "socket unavailable at flush" });
28517
+ }
28518
+ },
28519
+ onReject: (reason) => {
28520
+ reject({ reason });
28521
+ }
28522
+ });
28523
+ if (enqueued) {
28524
+ this.emitMetric("queue.enqueue", { depth: this.queue.size() });
28525
+ }
28526
+ });
28527
+ }
28528
+ connect() {
28529
+ if (!this.started) return;
28530
+ this.cancelReconnect = null;
28531
+ this.apply({ kind: "start" });
28532
+ if (this.state !== "connecting") {
28533
+ this.forceState("connecting");
28534
+ }
28535
+ const sock = createConnection2({ host: this.options.host, port: this.options.port });
28536
+ this.socket = sock;
28537
+ sock.on("connect", () => {
28538
+ this.handshakeStartMs = this.now();
28539
+ sock.setKeepAlive(true, 3e4);
28540
+ this.reconnectAttempt = 0;
28541
+ this.apply({ kind: "socket-connected" });
28542
+ });
28543
+ sock.on("error", (err) => {
28544
+ this.log(`${LOG_PREFIX4} socket error: ${err.message}`);
28545
+ });
28546
+ sock.on("close", () => {
28547
+ const wasReady = this.state === "ready";
28548
+ if (this.socket === sock) this.socket = null;
28549
+ this.apply({ kind: "socket-closed" });
28550
+ if (wasReady) this.log(`${LOG_PREFIX4} bridge disconnected`);
28551
+ if (this.started) this.scheduleReconnect();
28552
+ });
28553
+ let lineBuffer = "";
28554
+ sock.on("data", (data) => {
28555
+ lineBuffer += data.toString("utf-8");
28556
+ const lines = lineBuffer.split(LINE_TERMINATOR);
28557
+ lineBuffer = lines.pop() ?? "";
28558
+ for (const line of lines) {
28559
+ if (!line) continue;
28560
+ try {
28561
+ const parsed = JSON.parse(line);
28562
+ this.handleIncoming(parsed);
28563
+ } catch (err) {
28564
+ this.log(
28565
+ `${LOG_PREFIX4} parse error: ${err instanceof Error ? err.message : String(err)}`
28566
+ );
28567
+ }
28568
+ }
28569
+ });
28570
+ }
28571
+ handleIncoming(msg) {
28572
+ if (msg && typeof msg === "object") {
28573
+ const type = msg.type;
28574
+ if (type === "bridge_closing") {
28575
+ this.log(`${LOG_PREFIX4} received bridge_closing, will reconnect`);
28576
+ this.apply({ kind: "bridge-closing" });
28577
+ if (this.socket && !this.socket.destroyed) this.socket.destroy();
28578
+ return;
28579
+ }
28580
+ if (type === "bridge_status") {
28581
+ const connected = msg.connected === true;
28582
+ this.apply({ kind: connected ? "bridge-status-true" : "bridge-status-false" });
28583
+ }
28584
+ }
28585
+ for (const h of this.messageHandlers) {
28586
+ try {
28587
+ h(msg);
28588
+ } catch (err) {
28589
+ this.log(
28590
+ `${LOG_PREFIX4} handler threw: ${err instanceof Error ? err.message : String(err)}`
28591
+ );
28592
+ }
28593
+ }
28594
+ }
28595
+ scheduleReconnect() {
28596
+ if (!this.started) return;
28597
+ if (this.cancelReconnect) return;
28598
+ this.reconnectAttempt++;
28599
+ const base = Math.min(
28600
+ RECONNECT.INITIAL_DELAY * RECONNECT.MULTIPLIER ** (this.reconnectAttempt - 1),
28601
+ BRIDGE_RECONNECT_CAP_MS
28602
+ );
28603
+ const delay = applyJitter(base, RECONNECT.JITTER_RATIO, this.random);
28604
+ this.emitMetric("reconnect.scheduled", { attempt: this.reconnectAttempt, delayMs: delay });
28605
+ this.log(`${LOG_PREFIX4} reconnect in ${delay}ms (attempt ${this.reconnectAttempt})`);
28606
+ this.cancelReconnect = this.schedule(() => {
28607
+ this.cancelReconnect = null;
28608
+ this.connect();
28609
+ }, delay);
28610
+ }
28611
+ apply(event) {
28612
+ const next = transition(this.state, event);
28613
+ if (next === this.state) return;
28614
+ const prev = this.state;
28615
+ this.state = next;
28616
+ this.emitMetric("state.transition", { from: prev, to: next, event: event.kind });
28617
+ if (next === "ready") {
28618
+ if (this.handshakeStartMs !== null) {
28619
+ this.emitMetric("handshake.complete", {
28620
+ elapsedMs: this.now() - this.handshakeStartMs
28621
+ });
28622
+ this.handshakeStartMs = null;
28623
+ }
28624
+ this.queue.flushAll();
28625
+ }
28626
+ for (const h of this.stateHandlers) {
28627
+ try {
28628
+ h(next, prev);
28629
+ } catch (err) {
28630
+ this.log(
28631
+ `${LOG_PREFIX4} state handler threw: ${err instanceof Error ? err.message : String(err)}`
28632
+ );
28633
+ }
28634
+ }
28635
+ }
28636
+ /** Force a specific state (used only for recovering from impossible transitions). */
28637
+ forceState(to) {
28638
+ if (this.state === to) return;
28639
+ const prev = this.state;
28640
+ this.state = to;
28641
+ this.emitMetric("state.force", { from: prev, to });
28642
+ for (const h of this.stateHandlers) {
28643
+ try {
28644
+ h(to, prev);
28645
+ } catch {
28646
+ }
28647
+ }
28648
+ }
28649
+ emitMetric(name, fields) {
28650
+ this.metric(
28651
+ `${METRIC_PREFIX} ${name} ${Object.entries(fields).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ")}`
28652
+ );
28653
+ }
28654
+ };
28655
+ function toolCallRejectionToErrorCode(rej) {
28656
+ switch (rej.reason) {
28657
+ case "queue-full":
28658
+ return {
28659
+ code: "EXTENSION_NOT_CONNECTED",
28660
+ message: "Bridge readiness queue is full; try again shortly"
28661
+ };
28662
+ case "timeout":
28663
+ return {
28664
+ code: "EXTENSION_NOT_CONNECTED",
28665
+ message: "Timed out waiting for bridge readiness"
28666
+ };
28667
+ case "stopped":
28668
+ return {
28669
+ code: "EXTENSION_NOT_CONNECTED",
28670
+ message: "Bridge client was stopped"
28671
+ };
28672
+ case "write-failed":
28673
+ return {
28674
+ code: "EXTENSION_NOT_CONNECTED",
28675
+ message: `Socket write failed: ${rej.detail}`
28676
+ };
28677
+ }
28678
+ }
28679
+
28252
28680
  // ../shared/dist/bearer-equals.js
28253
28681
  import { timingSafeEqual } from "crypto";
28254
28682
  function bearerEquals(header, expectedToken) {
@@ -33775,8 +34203,11 @@ function computeToolTimeout(tool, input) {
33775
34203
 
33776
34204
  // src/server.ts
33777
34205
  var pendingRequests = /* @__PURE__ */ new Map();
33778
- var extensionSocket = null;
34206
+ var bridgeClient = null;
33779
34207
  var bridgeUpdateInfo = null;
34208
+ function getBridgeSocket() {
34209
+ return bridgeClient?.getSocket() ?? null;
34210
+ }
33780
34211
  function readJwtConfigFromEnv() {
33781
34212
  const alg = process.env.VIYV_JWT_ALG ?? "none";
33782
34213
  return {
@@ -33844,8 +34275,9 @@ function bridgeRegisterAgent(agentId, sessionId) {
33844
34275
  bridgeAgentRefs.set(agentId, set);
33845
34276
  }
33846
34277
  set.add(sessionId);
33847
- if (isFirst && extensionSocket && !extensionSocket.destroyed) {
33848
- sendSessionInit(extensionSocket, agentId);
34278
+ if (isFirst) {
34279
+ const sock = getBridgeSocket();
34280
+ if (sock) sendSessionInit(sock, agentId);
33849
34281
  }
33850
34282
  }
33851
34283
  function bridgeUnregisterAgent(agentId, sessionId) {
@@ -33854,8 +34286,9 @@ function bridgeUnregisterAgent(agentId, sessionId) {
33854
34286
  set.delete(sessionId);
33855
34287
  if (set.size === 0) {
33856
34288
  bridgeAgentRefs.delete(agentId);
33857
- if (extensionSocket && !extensionSocket.destroyed) {
33858
- extensionSocket.write(
34289
+ const sock = getBridgeSocket();
34290
+ if (sock) {
34291
+ sock.write(
33859
34292
  `${JSON.stringify({
33860
34293
  id: randomUUID5(),
33861
34294
  type: "session_close",
@@ -34006,7 +34439,8 @@ async function startMcpServer(agentName, options) {
34006
34439
  setDefaultAgentId(agentName);
34007
34440
  }
34008
34441
  configuredChromeProfile = options?.chromeProfile;
34009
- connectToBridge();
34442
+ bridgeClient = initBridgeClient();
34443
+ bridgeClient.start();
34010
34444
  if (options?.transport === "sse") {
34011
34445
  const sessions2 = /* @__PURE__ */ new Map();
34012
34446
  const httpServer = http.createServer();
@@ -34044,7 +34478,7 @@ async function startMcpServer(agentName, options) {
34044
34478
  }
34045
34479
  httpServer.close(() => {
34046
34480
  });
34047
- extensionSocket?.destroy();
34481
+ bridgeClient?.stop();
34048
34482
  process.exit(0);
34049
34483
  };
34050
34484
  process.on("SIGINT", () => {
@@ -34119,7 +34553,7 @@ async function startMcpServer(agentName, options) {
34119
34553
  }
34120
34554
  httpServer.close(() => {
34121
34555
  });
34122
- extensionSocket?.destroy();
34556
+ bridgeClient?.stop();
34123
34557
  process.exit(0);
34124
34558
  };
34125
34559
  process.on("SIGINT", () => {
@@ -34138,13 +34572,13 @@ async function startMcpServer(agentName, options) {
34138
34572
  );
34139
34573
  process.stdin.on("end", () => {
34140
34574
  process.stderr.write("[viyv-browser:mcp] stdin closed, shutting down\n");
34141
- extensionSocket?.destroy();
34575
+ bridgeClient?.stop();
34142
34576
  process.exit(0);
34143
34577
  });
34144
34578
  process.on("SIGINT", () => process.exit(0));
34145
34579
  process.on("SIGTERM", () => process.exit(0));
34146
34580
  process.on("exit", () => {
34147
- extensionSocket?.destroy();
34581
+ bridgeClient?.stop();
34148
34582
  });
34149
34583
  }
34150
34584
  }
@@ -34327,65 +34761,6 @@ function parseJsonBody(req) {
34327
34761
  });
34328
34762
  });
34329
34763
  }
34330
- function connectToBridge() {
34331
- let retryCount = 0;
34332
- let bridgeClosingReceived = false;
34333
- let cleanup = null;
34334
- function connect() {
34335
- bridgeClosingReceived = false;
34336
- const socket = createConnection2({ port: BRIDGE.TCP_PORT, host: BRIDGE.TCP_HOST });
34337
- socket.on("connect", () => {
34338
- retryCount = 0;
34339
- cleanup = setupBridgeConnection(socket, () => {
34340
- bridgeClosingReceived = true;
34341
- });
34342
- });
34343
- socket.on("error", () => {
34344
- });
34345
- socket.on("close", () => {
34346
- const wasConnected = cleanup !== null;
34347
- cleanup?.();
34348
- cleanup = null;
34349
- if (wasConnected) {
34350
- process.stderr.write("[viyv-browser:mcp] Bridge disconnected\n");
34351
- }
34352
- if (extensionSocket === socket) {
34353
- extensionSocket = null;
34354
- setExtensionConnected(false);
34355
- }
34356
- for (const [id, pending] of pendingRequests) {
34357
- clearTimeout(pending.timer);
34358
- pendingRequests.delete(id);
34359
- pending.resolve({
34360
- error: {
34361
- code: "EXTENSION_NOT_CONNECTED",
34362
- message: "Bridge disconnected while request was pending"
34363
- }
34364
- });
34365
- }
34366
- scheduleReconnect();
34367
- });
34368
- }
34369
- function scheduleReconnect() {
34370
- if (bridgeClosingReceived) {
34371
- retryCount = 0;
34372
- setTimeout(connect, RECONNECT.INITIAL_DELAY);
34373
- return;
34374
- }
34375
- retryCount++;
34376
- const BRIDGE_RECONNECT_CAP = 3e3;
34377
- const delay = Math.min(
34378
- RECONNECT.INITIAL_DELAY * RECONNECT.MULTIPLIER ** (retryCount - 1),
34379
- BRIDGE_RECONNECT_CAP
34380
- );
34381
- process.stderr.write(
34382
- `[viyv-browser:mcp] Reconnecting to bridge in ${delay}ms (attempt ${retryCount})
34383
- `
34384
- );
34385
- setTimeout(connect, delay);
34386
- }
34387
- connect();
34388
- }
34389
34764
  function sendSessionInit(socket, agentId) {
34390
34765
  const initMsg = {
34391
34766
  id: randomUUID5(),
@@ -34400,65 +34775,79 @@ function sendSessionInit(socket, agentId) {
34400
34775
  socket.write(`${JSON.stringify(initMsg)}
34401
34776
  `);
34402
34777
  }
34403
- function setupBridgeConnection(socket, onBridgeClosing) {
34404
- process.stderr.write(
34405
- `[viyv-browser:mcp] Connected to bridge at ${BRIDGE.TCP_HOST}:${BRIDGE.TCP_PORT}
34778
+ function initBridgeClient() {
34779
+ const client = new BridgeClient({ host: BRIDGE.TCP_HOST, port: BRIDGE.TCP_PORT });
34780
+ let heartbeatInterval = null;
34781
+ client.onMessage((rawMsg) => {
34782
+ if (!rawMsg || typeof rawMsg !== "object") return;
34783
+ let parsed = rawMsg;
34784
+ if (parsed.type === "compressed" && typeof parsed.data === "string") {
34785
+ try {
34786
+ const decompressed = decompressPayload(parsed.data, true);
34787
+ parsed = JSON.parse(decompressed);
34788
+ } catch (err) {
34789
+ process.stderr.write(
34790
+ `[viyv-browser:mcp] Decompress error: ${err instanceof Error ? err.message : String(err)}
34406
34791
  `
34407
- );
34408
- extensionSocket = socket;
34409
- socket.setKeepAlive(true, 3e4);
34410
- setExtensionConnected(true);
34411
- const agentId = getDefaultAgentId();
34412
- sendSessionInit(socket, agentId);
34413
- bridgeResyncAgents(socket);
34414
- const heartbeatInterval = setInterval(() => {
34415
- if (socket.destroyed) {
34416
- clearInterval(heartbeatInterval);
34417
- return;
34792
+ );
34793
+ return;
34794
+ }
34418
34795
  }
34419
- socket.write(
34420
- `${JSON.stringify({
34421
- id: randomUUID5(),
34422
- type: "session_heartbeat",
34423
- agentId,
34424
- timestamp: Date.now()
34425
- })}
34796
+ handleExtensionMessage(parsed);
34797
+ });
34798
+ client.onStateChange((next, prev) => {
34799
+ if (next === "ready" && prev !== "ready") {
34800
+ process.stderr.write(
34801
+ `[viyv-browser:mcp] Connected to bridge at ${BRIDGE.TCP_HOST}:${BRIDGE.TCP_PORT}
34426
34802
  `
34427
- );
34428
- }, TIMEOUTS.HEARTBEAT);
34429
- let lineBuffer = "";
34430
- socket.on("data", (data) => {
34431
- lineBuffer += data.toString("utf-8");
34432
- const lines = lineBuffer.split("\n");
34433
- lineBuffer = lines.pop() ?? "";
34434
- for (const line of lines) {
34435
- if (!line) continue;
34436
- try {
34437
- let parsed = JSON.parse(line);
34438
- if (parsed.type === "bridge_closing") {
34439
- process.stderr.write("[viyv-browser:mcp] Received bridge_closing, will reconnect\n");
34440
- onBridgeClosing();
34441
- socket.destroy();
34442
- return;
34443
- }
34444
- if (parsed.type === "compressed" && typeof parsed.data === "string") {
34445
- const decompressed = decompressPayload(parsed.data, true);
34446
- parsed = JSON.parse(decompressed);
34447
- }
34448
- handleExtensionMessage(parsed);
34449
- } catch (error2) {
34450
- process.stderr.write(`[viyv-browser:mcp] Parse error: ${error2.message}
34451
- `);
34803
+ );
34804
+ setExtensionConnected(true);
34805
+ const sock = client.getSocket();
34806
+ if (sock) {
34807
+ const agentId = getDefaultAgentId();
34808
+ sendSessionInit(sock, agentId);
34809
+ bridgeResyncAgents(sock);
34810
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
34811
+ heartbeatInterval = setInterval(() => {
34812
+ const s = client.getSocket();
34813
+ if (!s || s.destroyed) {
34814
+ if (heartbeatInterval) {
34815
+ clearInterval(heartbeatInterval);
34816
+ heartbeatInterval = null;
34817
+ }
34818
+ return;
34819
+ }
34820
+ s.write(
34821
+ `${JSON.stringify({
34822
+ id: randomUUID5(),
34823
+ type: "session_heartbeat",
34824
+ agentId,
34825
+ timestamp: Date.now()
34826
+ })}
34827
+ `
34828
+ );
34829
+ }, TIMEOUTS.HEARTBEAT);
34830
+ }
34831
+ }
34832
+ if (prev === "ready" && next !== "ready") {
34833
+ setExtensionConnected(false);
34834
+ if (heartbeatInterval) {
34835
+ clearInterval(heartbeatInterval);
34836
+ heartbeatInterval = null;
34837
+ }
34838
+ for (const [id, pending] of pendingRequests) {
34839
+ clearTimeout(pending.timer);
34840
+ pendingRequests.delete(id);
34841
+ pending.resolve({
34842
+ error: {
34843
+ code: "EXTENSION_NOT_CONNECTED",
34844
+ message: "Bridge disconnected while request was pending"
34845
+ }
34846
+ });
34452
34847
  }
34453
34848
  }
34454
34849
  });
34455
- socket.on("error", (error2) => {
34456
- process.stderr.write(`[viyv-browser:mcp] Bridge socket error: ${error2.message}
34457
- `);
34458
- });
34459
- return () => {
34460
- clearInterval(heartbeatInterval);
34461
- };
34850
+ return client;
34462
34851
  }
34463
34852
  function handleExtensionMessage(message2) {
34464
34853
  if (!message2 || typeof message2 !== "object") return;
@@ -34516,9 +34905,10 @@ function handleExtensionMessage(message2) {
34516
34905
  } else {
34517
34906
  setExtensionConnected(true);
34518
34907
  recordHeartbeat();
34519
- if (extensionSocket && !extensionSocket.destroyed && listSessions().length === 0) {
34908
+ const sockForReinit = getBridgeSocket();
34909
+ if (sockForReinit && listSessions().length === 0) {
34520
34910
  const agentId = getDefaultAgentId();
34521
- sendSessionInit(extensionSocket, agentId);
34911
+ sendSessionInit(sockForReinit, agentId);
34522
34912
  process.stderr.write(
34523
34913
  `[viyv-browser:mcp] Session re-initialized after Chrome reconnect: ${agentId}
34524
34914
  `
@@ -34534,8 +34924,9 @@ function handleExtensionMessage(message2) {
34534
34924
  `
34535
34925
  );
34536
34926
  bridgeUpdateInfo = { from: bridgeVersion, to: serverVersion };
34537
- if (extensionSocket && !extensionSocket.destroyed) {
34538
- extensionSocket.write(
34927
+ const shutdownSock = getBridgeSocket();
34928
+ if (shutdownSock) {
34929
+ shutdownSock.write(
34539
34930
  `${JSON.stringify({ type: "bridge_shutdown_request", timestamp: Date.now() })}
34540
34931
  `
34541
34932
  );
@@ -34665,7 +35056,7 @@ async function callExtensionTool(tool, input) {
34665
35056
  }
34666
35057
  if (tool === "browser_health") {
34667
35058
  const health = getHealthStatus();
34668
- if (!health.extensionConnected || !extensionSocket || extensionSocket.destroyed) {
35059
+ if (!health.extensionConnected || !getBridgeSocket()) {
34669
35060
  return {
34670
35061
  content: [{ type: "text", text: JSON.stringify(health) }]
34671
35062
  };
@@ -34749,7 +35140,7 @@ async function callExtensionTool(tool, input) {
34749
35140
  }
34750
35141
  }
34751
35142
  }
34752
- if (!extensionSocket || extensionSocket.destroyed) {
35143
+ if (!bridgeClient) {
34753
35144
  return {
34754
35145
  content: [
34755
35146
  {
@@ -34757,7 +35148,7 @@ async function callExtensionTool(tool, input) {
34757
35148
  text: JSON.stringify({
34758
35149
  error: {
34759
35150
  code: "EXTENSION_NOT_CONNECTED",
34760
- message: "Chrome Extension is not connected. Please open Chrome and click the Viyv Browser extension icon."
35151
+ message: "Bridge client not initialized"
34761
35152
  }
34762
35153
  })
34763
35154
  }
@@ -34767,31 +35158,10 @@ async function callExtensionTool(tool, input) {
34767
35158
  const requestId = randomUUID5();
34768
35159
  const agentId = getCurrentAgentId(getDefaultAgentId);
34769
35160
  touchSession(agentId);
34770
- const sock = extensionSocket;
35161
+ const client = bridgeClient;
34771
35162
  const toolTimeout = computeToolTimeout(tool, input);
34772
35163
  return new Promise((resolve3) => {
34773
- const onError = () => {
34774
- const pending = pendingRequests.get(requestId);
34775
- if (pending) {
34776
- clearTimeout(pending.timer);
34777
- pendingRequests.delete(requestId);
34778
- resolve3({
34779
- content: [
34780
- {
34781
- type: "text",
34782
- text: JSON.stringify({
34783
- error: {
34784
- code: "EXTENSION_NOT_CONNECTED",
34785
- message: "Socket write failed"
34786
- }
34787
- })
34788
- }
34789
- ]
34790
- });
34791
- }
34792
- };
34793
35164
  const removeErrorListener = () => {
34794
- sock.removeListener("error", onError);
34795
35165
  };
34796
35166
  const timer = setTimeout(() => {
34797
35167
  pendingRequests.delete(requestId);
@@ -34937,13 +35307,17 @@ async function callExtensionTool(tool, input) {
34937
35307
  `[viyv-browser:mcp] tool_call id=${requestId} agent=${agentId} pid=${ctxLog?.claims?.pid ?? chromeProfile ?? "-"} tool=${tool}
34938
35308
  `
34939
35309
  );
34940
- const written = sock.write(`${JSON.stringify(request)}
34941
- `);
34942
- if (!written) {
34943
- sock.once("drain", () => {
35310
+ client.enqueueToolCall(`${JSON.stringify(request)}
35311
+ `).catch((rej) => {
35312
+ const pending = pendingRequests.get(requestId);
35313
+ if (!pending) return;
35314
+ clearTimeout(pending.timer);
35315
+ pendingRequests.delete(requestId);
35316
+ const { code, message: message2 } = toolCallRejectionToErrorCode(rej);
35317
+ resolve3({
35318
+ content: [{ type: "text", text: JSON.stringify({ error: { code, message: message2 } }) }]
34944
35319
  });
34945
- }
34946
- sock.once("error", onError);
35320
+ });
34947
35321
  });
34948
35322
  }
34949
35323
  async function handleSwitchBrowser() {
@@ -34958,16 +35332,15 @@ async function handleSwitchBrowser() {
34958
35332
  }
34959
35333
  });
34960
35334
  }
34961
- if (extensionSocket && !extensionSocket.destroyed) {
35335
+ if (bridgeClient) {
34962
35336
  process.stderr.write("[viyv-browser:mcp] switch_browser: closing current connection\n");
34963
- extensionSocket.destroy();
34964
- extensionSocket = null;
35337
+ bridgeClient.forceReconnect();
34965
35338
  setExtensionConnected(false);
34966
35339
  }
34967
35340
  process.stderr.write("[viyv-browser:mcp] switch_browser: waiting for bridge reconnection...\n");
34968
35341
  return new Promise((resolve3) => {
34969
35342
  const checkInterval = setInterval(() => {
34970
- if (extensionSocket && !extensionSocket.destroyed && isExtensionConnected()) {
35343
+ if (bridgeClient?.isReady() && isExtensionConnected()) {
34971
35344
  clearInterval(checkInterval);
34972
35345
  clearTimeout(timer);
34973
35346
  resolve3({