viyv-browser-mcp 0.10.0 → 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.0";
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) {
@@ -31488,11 +31916,12 @@ abort:
31488
31916
 
31489
31917
  Supported scenario step types: navigate, action, fetch, wait, loop. Loop variable sources: static, previous_fetch, target_options (max nesting 3, max iterations 500, cartesian product across variables).
31490
31918
 
31491
- HttpFetch groundwork (F1 \u2014 executor support lands in F2):
31919
+ HttpFetch runtime (F2):
31492
31920
  - Fetch definitions may carry type_name='http_fetch' + http_config instead of DOM extraction fields. Loop-merged params resolve {{var}} in url / body_template with per-value URL-encoding.
31493
31921
  - extractor_code (when set) is eval-equivalent in the page MAIN world \u2014 bundles must originate from the trusted authoring pipeline gated by MCP JWT auth. Extraction precedence: extractor_code > selector > raw responseText.
31922
+ - Response shape: results[result_key] = { rows: unknown[] } \u2014 extractor array, selector string[], or [responseText] in raw fallback. Scraper should consume result.data.rows.
31923
+ - HTTP 4xx/5xx are forwarded as ok:true with httpStatus populated (caller decides). Network / extractor failures surface as ok:false with prefixed error strings ('fetch failed: ...' / 'extractor threw: ...' / 'scripting failed: ...' / 'async extractor not supported in v1').
31494
31924
  - sm_probe_inline automatically skips http_fetch (no DOM locators to probe).
31495
- - Calling an http_fetch at runtime in this release returns an error; runtime dispatch lands in F2.
31496
31925
  - ScenarioFetchStep.result_key_mode ('overwrite' | 'append' | 'concat', default 'overwrite') controls how results[result_key] accumulates across loop iterations. Runtime dispatch lands in F3; pre-F3 all modes behave as 'overwrite'.
31497
31926
 
31498
31927
  Full reference: docs/inline-sm-execution.md`;
@@ -33774,8 +34203,11 @@ function computeToolTimeout(tool, input) {
33774
34203
 
33775
34204
  // src/server.ts
33776
34205
  var pendingRequests = /* @__PURE__ */ new Map();
33777
- var extensionSocket = null;
34206
+ var bridgeClient = null;
33778
34207
  var bridgeUpdateInfo = null;
34208
+ function getBridgeSocket() {
34209
+ return bridgeClient?.getSocket() ?? null;
34210
+ }
33779
34211
  function readJwtConfigFromEnv() {
33780
34212
  const alg = process.env.VIYV_JWT_ALG ?? "none";
33781
34213
  return {
@@ -33843,8 +34275,9 @@ function bridgeRegisterAgent(agentId, sessionId) {
33843
34275
  bridgeAgentRefs.set(agentId, set);
33844
34276
  }
33845
34277
  set.add(sessionId);
33846
- if (isFirst && extensionSocket && !extensionSocket.destroyed) {
33847
- sendSessionInit(extensionSocket, agentId);
34278
+ if (isFirst) {
34279
+ const sock = getBridgeSocket();
34280
+ if (sock) sendSessionInit(sock, agentId);
33848
34281
  }
33849
34282
  }
33850
34283
  function bridgeUnregisterAgent(agentId, sessionId) {
@@ -33853,8 +34286,9 @@ function bridgeUnregisterAgent(agentId, sessionId) {
33853
34286
  set.delete(sessionId);
33854
34287
  if (set.size === 0) {
33855
34288
  bridgeAgentRefs.delete(agentId);
33856
- if (extensionSocket && !extensionSocket.destroyed) {
33857
- extensionSocket.write(
34289
+ const sock = getBridgeSocket();
34290
+ if (sock) {
34291
+ sock.write(
33858
34292
  `${JSON.stringify({
33859
34293
  id: randomUUID5(),
33860
34294
  type: "session_close",
@@ -34005,7 +34439,8 @@ async function startMcpServer(agentName, options) {
34005
34439
  setDefaultAgentId(agentName);
34006
34440
  }
34007
34441
  configuredChromeProfile = options?.chromeProfile;
34008
- connectToBridge();
34442
+ bridgeClient = initBridgeClient();
34443
+ bridgeClient.start();
34009
34444
  if (options?.transport === "sse") {
34010
34445
  const sessions2 = /* @__PURE__ */ new Map();
34011
34446
  const httpServer = http.createServer();
@@ -34043,7 +34478,7 @@ async function startMcpServer(agentName, options) {
34043
34478
  }
34044
34479
  httpServer.close(() => {
34045
34480
  });
34046
- extensionSocket?.destroy();
34481
+ bridgeClient?.stop();
34047
34482
  process.exit(0);
34048
34483
  };
34049
34484
  process.on("SIGINT", () => {
@@ -34118,7 +34553,7 @@ async function startMcpServer(agentName, options) {
34118
34553
  }
34119
34554
  httpServer.close(() => {
34120
34555
  });
34121
- extensionSocket?.destroy();
34556
+ bridgeClient?.stop();
34122
34557
  process.exit(0);
34123
34558
  };
34124
34559
  process.on("SIGINT", () => {
@@ -34137,13 +34572,13 @@ async function startMcpServer(agentName, options) {
34137
34572
  );
34138
34573
  process.stdin.on("end", () => {
34139
34574
  process.stderr.write("[viyv-browser:mcp] stdin closed, shutting down\n");
34140
- extensionSocket?.destroy();
34575
+ bridgeClient?.stop();
34141
34576
  process.exit(0);
34142
34577
  });
34143
34578
  process.on("SIGINT", () => process.exit(0));
34144
34579
  process.on("SIGTERM", () => process.exit(0));
34145
34580
  process.on("exit", () => {
34146
- extensionSocket?.destroy();
34581
+ bridgeClient?.stop();
34147
34582
  });
34148
34583
  }
34149
34584
  }
@@ -34326,65 +34761,6 @@ function parseJsonBody(req) {
34326
34761
  });
34327
34762
  });
34328
34763
  }
34329
- function connectToBridge() {
34330
- let retryCount = 0;
34331
- let bridgeClosingReceived = false;
34332
- let cleanup = null;
34333
- function connect() {
34334
- bridgeClosingReceived = false;
34335
- const socket = createConnection2({ port: BRIDGE.TCP_PORT, host: BRIDGE.TCP_HOST });
34336
- socket.on("connect", () => {
34337
- retryCount = 0;
34338
- cleanup = setupBridgeConnection(socket, () => {
34339
- bridgeClosingReceived = true;
34340
- });
34341
- });
34342
- socket.on("error", () => {
34343
- });
34344
- socket.on("close", () => {
34345
- const wasConnected = cleanup !== null;
34346
- cleanup?.();
34347
- cleanup = null;
34348
- if (wasConnected) {
34349
- process.stderr.write("[viyv-browser:mcp] Bridge disconnected\n");
34350
- }
34351
- if (extensionSocket === socket) {
34352
- extensionSocket = null;
34353
- setExtensionConnected(false);
34354
- }
34355
- for (const [id, pending] of pendingRequests) {
34356
- clearTimeout(pending.timer);
34357
- pendingRequests.delete(id);
34358
- pending.resolve({
34359
- error: {
34360
- code: "EXTENSION_NOT_CONNECTED",
34361
- message: "Bridge disconnected while request was pending"
34362
- }
34363
- });
34364
- }
34365
- scheduleReconnect();
34366
- });
34367
- }
34368
- function scheduleReconnect() {
34369
- if (bridgeClosingReceived) {
34370
- retryCount = 0;
34371
- setTimeout(connect, RECONNECT.INITIAL_DELAY);
34372
- return;
34373
- }
34374
- retryCount++;
34375
- const BRIDGE_RECONNECT_CAP = 3e3;
34376
- const delay = Math.min(
34377
- RECONNECT.INITIAL_DELAY * RECONNECT.MULTIPLIER ** (retryCount - 1),
34378
- BRIDGE_RECONNECT_CAP
34379
- );
34380
- process.stderr.write(
34381
- `[viyv-browser:mcp] Reconnecting to bridge in ${delay}ms (attempt ${retryCount})
34382
- `
34383
- );
34384
- setTimeout(connect, delay);
34385
- }
34386
- connect();
34387
- }
34388
34764
  function sendSessionInit(socket, agentId) {
34389
34765
  const initMsg = {
34390
34766
  id: randomUUID5(),
@@ -34399,65 +34775,79 @@ function sendSessionInit(socket, agentId) {
34399
34775
  socket.write(`${JSON.stringify(initMsg)}
34400
34776
  `);
34401
34777
  }
34402
- function setupBridgeConnection(socket, onBridgeClosing) {
34403
- process.stderr.write(
34404
- `[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)}
34405
34791
  `
34406
- );
34407
- extensionSocket = socket;
34408
- socket.setKeepAlive(true, 3e4);
34409
- setExtensionConnected(true);
34410
- const agentId = getDefaultAgentId();
34411
- sendSessionInit(socket, agentId);
34412
- bridgeResyncAgents(socket);
34413
- const heartbeatInterval = setInterval(() => {
34414
- if (socket.destroyed) {
34415
- clearInterval(heartbeatInterval);
34416
- return;
34792
+ );
34793
+ return;
34794
+ }
34417
34795
  }
34418
- socket.write(
34419
- `${JSON.stringify({
34420
- id: randomUUID5(),
34421
- type: "session_heartbeat",
34422
- agentId,
34423
- timestamp: Date.now()
34424
- })}
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}
34425
34802
  `
34426
- );
34427
- }, TIMEOUTS.HEARTBEAT);
34428
- let lineBuffer = "";
34429
- socket.on("data", (data) => {
34430
- lineBuffer += data.toString("utf-8");
34431
- const lines = lineBuffer.split("\n");
34432
- lineBuffer = lines.pop() ?? "";
34433
- for (const line of lines) {
34434
- if (!line) continue;
34435
- try {
34436
- let parsed = JSON.parse(line);
34437
- if (parsed.type === "bridge_closing") {
34438
- process.stderr.write("[viyv-browser:mcp] Received bridge_closing, will reconnect\n");
34439
- onBridgeClosing();
34440
- socket.destroy();
34441
- return;
34442
- }
34443
- if (parsed.type === "compressed" && typeof parsed.data === "string") {
34444
- const decompressed = decompressPayload(parsed.data, true);
34445
- parsed = JSON.parse(decompressed);
34446
- }
34447
- handleExtensionMessage(parsed);
34448
- } catch (error2) {
34449
- process.stderr.write(`[viyv-browser:mcp] Parse error: ${error2.message}
34450
- `);
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
+ });
34451
34847
  }
34452
34848
  }
34453
34849
  });
34454
- socket.on("error", (error2) => {
34455
- process.stderr.write(`[viyv-browser:mcp] Bridge socket error: ${error2.message}
34456
- `);
34457
- });
34458
- return () => {
34459
- clearInterval(heartbeatInterval);
34460
- };
34850
+ return client;
34461
34851
  }
34462
34852
  function handleExtensionMessage(message2) {
34463
34853
  if (!message2 || typeof message2 !== "object") return;
@@ -34515,9 +34905,10 @@ function handleExtensionMessage(message2) {
34515
34905
  } else {
34516
34906
  setExtensionConnected(true);
34517
34907
  recordHeartbeat();
34518
- if (extensionSocket && !extensionSocket.destroyed && listSessions().length === 0) {
34908
+ const sockForReinit = getBridgeSocket();
34909
+ if (sockForReinit && listSessions().length === 0) {
34519
34910
  const agentId = getDefaultAgentId();
34520
- sendSessionInit(extensionSocket, agentId);
34911
+ sendSessionInit(sockForReinit, agentId);
34521
34912
  process.stderr.write(
34522
34913
  `[viyv-browser:mcp] Session re-initialized after Chrome reconnect: ${agentId}
34523
34914
  `
@@ -34533,8 +34924,9 @@ function handleExtensionMessage(message2) {
34533
34924
  `
34534
34925
  );
34535
34926
  bridgeUpdateInfo = { from: bridgeVersion, to: serverVersion };
34536
- if (extensionSocket && !extensionSocket.destroyed) {
34537
- extensionSocket.write(
34927
+ const shutdownSock = getBridgeSocket();
34928
+ if (shutdownSock) {
34929
+ shutdownSock.write(
34538
34930
  `${JSON.stringify({ type: "bridge_shutdown_request", timestamp: Date.now() })}
34539
34931
  `
34540
34932
  );
@@ -34664,7 +35056,7 @@ async function callExtensionTool(tool, input) {
34664
35056
  }
34665
35057
  if (tool === "browser_health") {
34666
35058
  const health = getHealthStatus();
34667
- if (!health.extensionConnected || !extensionSocket || extensionSocket.destroyed) {
35059
+ if (!health.extensionConnected || !getBridgeSocket()) {
34668
35060
  return {
34669
35061
  content: [{ type: "text", text: JSON.stringify(health) }]
34670
35062
  };
@@ -34748,7 +35140,7 @@ async function callExtensionTool(tool, input) {
34748
35140
  }
34749
35141
  }
34750
35142
  }
34751
- if (!extensionSocket || extensionSocket.destroyed) {
35143
+ if (!bridgeClient) {
34752
35144
  return {
34753
35145
  content: [
34754
35146
  {
@@ -34756,7 +35148,7 @@ async function callExtensionTool(tool, input) {
34756
35148
  text: JSON.stringify({
34757
35149
  error: {
34758
35150
  code: "EXTENSION_NOT_CONNECTED",
34759
- message: "Chrome Extension is not connected. Please open Chrome and click the Viyv Browser extension icon."
35151
+ message: "Bridge client not initialized"
34760
35152
  }
34761
35153
  })
34762
35154
  }
@@ -34766,31 +35158,10 @@ async function callExtensionTool(tool, input) {
34766
35158
  const requestId = randomUUID5();
34767
35159
  const agentId = getCurrentAgentId(getDefaultAgentId);
34768
35160
  touchSession(agentId);
34769
- const sock = extensionSocket;
35161
+ const client = bridgeClient;
34770
35162
  const toolTimeout = computeToolTimeout(tool, input);
34771
35163
  return new Promise((resolve3) => {
34772
- const onError = () => {
34773
- const pending = pendingRequests.get(requestId);
34774
- if (pending) {
34775
- clearTimeout(pending.timer);
34776
- pendingRequests.delete(requestId);
34777
- resolve3({
34778
- content: [
34779
- {
34780
- type: "text",
34781
- text: JSON.stringify({
34782
- error: {
34783
- code: "EXTENSION_NOT_CONNECTED",
34784
- message: "Socket write failed"
34785
- }
34786
- })
34787
- }
34788
- ]
34789
- });
34790
- }
34791
- };
34792
35164
  const removeErrorListener = () => {
34793
- sock.removeListener("error", onError);
34794
35165
  };
34795
35166
  const timer = setTimeout(() => {
34796
35167
  pendingRequests.delete(requestId);
@@ -34936,13 +35307,17 @@ async function callExtensionTool(tool, input) {
34936
35307
  `[viyv-browser:mcp] tool_call id=${requestId} agent=${agentId} pid=${ctxLog?.claims?.pid ?? chromeProfile ?? "-"} tool=${tool}
34937
35308
  `
34938
35309
  );
34939
- const written = sock.write(`${JSON.stringify(request)}
34940
- `);
34941
- if (!written) {
34942
- 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 } }) }]
34943
35319
  });
34944
- }
34945
- sock.once("error", onError);
35320
+ });
34946
35321
  });
34947
35322
  }
34948
35323
  async function handleSwitchBrowser() {
@@ -34957,16 +35332,15 @@ async function handleSwitchBrowser() {
34957
35332
  }
34958
35333
  });
34959
35334
  }
34960
- if (extensionSocket && !extensionSocket.destroyed) {
35335
+ if (bridgeClient) {
34961
35336
  process.stderr.write("[viyv-browser:mcp] switch_browser: closing current connection\n");
34962
- extensionSocket.destroy();
34963
- extensionSocket = null;
35337
+ bridgeClient.forceReconnect();
34964
35338
  setExtensionConnected(false);
34965
35339
  }
34966
35340
  process.stderr.write("[viyv-browser:mcp] switch_browser: waiting for bridge reconnection...\n");
34967
35341
  return new Promise((resolve3) => {
34968
35342
  const checkInterval = setInterval(() => {
34969
- if (extensionSocket && !extensionSocket.destroyed && isExtensionConnected()) {
35343
+ if (bridgeClient?.isReady() && isExtensionConnected()) {
34970
35344
  clearInterval(checkInterval);
34971
35345
  clearTimeout(timer);
34972
35346
  resolve3({