viyv-browser-mcp 0.10.1 → 0.10.3

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
@@ -2988,7 +2988,7 @@ var require_compile = __commonJS({
2988
2988
  const schOrFunc = root.refs[ref];
2989
2989
  if (schOrFunc)
2990
2990
  return schOrFunc;
2991
- let _sch = resolve3.call(this, root, ref);
2991
+ let _sch = resolve4.call(this, root, ref);
2992
2992
  if (_sch === void 0) {
2993
2993
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2994
2994
  const { schemaId } = this.opts;
@@ -3015,7 +3015,7 @@ var require_compile = __commonJS({
3015
3015
  function sameSchemaEnv(s1, s2) {
3016
3016
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3017
3017
  }
3018
- function resolve3(root, ref) {
3018
+ function resolve4(root, ref) {
3019
3019
  let sch;
3020
3020
  while (typeof (sch = this.refs[ref]) == "string")
3021
3021
  ref = sch;
@@ -3590,7 +3590,7 @@ var require_fast_uri = __commonJS({
3590
3590
  }
3591
3591
  return uri;
3592
3592
  }
3593
- function resolve3(baseURI, relativeURI, options) {
3593
+ function resolve4(baseURI, relativeURI, options) {
3594
3594
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3595
3595
  const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
3596
3596
  schemelessOptions.skipEscape = true;
@@ -3817,7 +3817,7 @@ var require_fast_uri = __commonJS({
3817
3817
  var fastUri = {
3818
3818
  SCHEMES,
3819
3819
  normalize: normalize2,
3820
- resolve: resolve3,
3820
+ resolve: resolve4,
3821
3821
  resolveComponent,
3822
3822
  equal,
3823
3823
  serialize,
@@ -11452,10 +11452,10 @@ var require_raw_body = __commonJS({
11452
11452
  if (done) {
11453
11453
  return readStream(stream, encoding, length, limit, wrap(done));
11454
11454
  }
11455
- return new Promise(function executor(resolve3, reject) {
11455
+ return new Promise(function executor(resolve4, reject) {
11456
11456
  readStream(stream, encoding, length, limit, function onRead(err, buf) {
11457
11457
  if (err) return reject(err);
11458
- resolve3(buf);
11458
+ resolve4(buf);
11459
11459
  });
11460
11460
  });
11461
11461
  }
@@ -11695,7 +11695,7 @@ var require_content_type = __commonJS({
11695
11695
 
11696
11696
  // src/native-host/bridge.ts
11697
11697
  import { randomUUID } from "crypto";
11698
- import { readFileSync as readFileSync3 } from "fs";
11698
+ import { readFileSync as readFileSync4 } from "fs";
11699
11699
  import { createServer as createServer2 } from "net";
11700
11700
  import { join as join2 } from "path";
11701
11701
 
@@ -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,59 +11811,185 @@ 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
+
11843
+ // ../shared/dist/util/semver.js
11844
+ function compareSemver(a, b) {
11845
+ const pa = a.split(".").map((s) => Number.parseInt(s, 10));
11846
+ const pb = b.split(".").map((s) => Number.parseInt(s, 10));
11847
+ for (let i = 0; i < 3; i++) {
11848
+ const ai = Number.isFinite(pa[i]) ? pa[i] : 0;
11849
+ const bi = Number.isFinite(pb[i]) ? pb[i] : 0;
11850
+ const diff = ai - bi;
11851
+ if (diff !== 0)
11852
+ return diff;
11853
+ }
11854
+ return 0;
11855
+ }
11856
+
11806
11857
  // src/native-host-updater.ts
11807
- import {
11808
- chmodSync,
11809
- copyFileSync,
11810
- existsSync,
11811
- mkdirSync,
11812
- readFileSync,
11813
- renameSync,
11814
- unlinkSync
11815
- } from "fs";
11858
+ import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
11859
+ import { resolve as resolve2 } from "path";
11860
+ import { fileURLToPath } from "url";
11861
+
11862
+ // src/native-host/deployment-state.ts
11863
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
11816
11864
  import { homedir } from "os";
11817
11865
  import { resolve } from "path";
11818
- import { fileURLToPath } from "url";
11866
+ function getNativeHostDir() {
11867
+ return resolve(homedir(), ".viyv-browser", "native-host");
11868
+ }
11869
+ function getDeploymentBinaryPath() {
11870
+ return resolve(getNativeHostDir(), "index.js");
11871
+ }
11872
+ function getDeploymentVersionPath() {
11873
+ return resolve(getNativeHostDir(), "version");
11874
+ }
11875
+ var SEMVER_CORE = /^\d+\.\d+\.\d+/;
11876
+ function readDeploymentState() {
11877
+ const versionPath = getDeploymentVersionPath();
11878
+ if (!existsSync(versionPath)) {
11879
+ return { version: null };
11880
+ }
11881
+ try {
11882
+ const raw = readFileSync(versionPath, "utf-8").trim();
11883
+ if (!raw || !SEMVER_CORE.test(raw)) {
11884
+ return { version: null };
11885
+ }
11886
+ return { version: raw };
11887
+ } catch {
11888
+ return { version: null };
11889
+ }
11890
+ }
11891
+ function writeDeploymentVersion(version2) {
11892
+ const dir = getNativeHostDir();
11893
+ mkdirSync(dir, { recursive: true });
11894
+ const target = getDeploymentVersionPath();
11895
+ const tmp = resolve(dir, `version.tmp.${process.pid}`);
11896
+ try {
11897
+ writeFileSync(tmp, `${version2}
11898
+ `, "utf-8");
11899
+ renameSync(tmp, target);
11900
+ } catch (err) {
11901
+ try {
11902
+ unlinkSync(tmp);
11903
+ } catch {
11904
+ }
11905
+ throw err;
11906
+ }
11907
+ }
11908
+
11909
+ // src/native-host/sync-policy.ts
11910
+ function decideSyncAction(state, selfVersion) {
11911
+ if (state.version === null) {
11912
+ return {
11913
+ shouldSync: true,
11914
+ reason: "no-deploy-version",
11915
+ deployedVersion: null,
11916
+ selfVersion
11917
+ };
11918
+ }
11919
+ const cmp = compareSemver(selfVersion, state.version);
11920
+ if (cmp > 0) {
11921
+ return {
11922
+ shouldSync: true,
11923
+ reason: "upgrade",
11924
+ deployedVersion: state.version,
11925
+ selfVersion
11926
+ };
11927
+ }
11928
+ if (cmp === 0) {
11929
+ return {
11930
+ shouldSync: true,
11931
+ reason: "same-version-idempotent",
11932
+ deployedVersion: state.version,
11933
+ selfVersion
11934
+ };
11935
+ }
11936
+ return {
11937
+ shouldSync: false,
11938
+ reason: "avoid-downgrade",
11939
+ deployedVersion: state.version,
11940
+ selfVersion
11941
+ };
11942
+ }
11943
+
11944
+ // src/native-host-updater.ts
11819
11945
  var PKG_VERSION = (() => {
11820
- if (true) return "0.10.1";
11946
+ if (true) return "0.10.3";
11821
11947
  try {
11822
11948
  const here = fileURLToPath(import.meta.url);
11823
- const pkgPath = resolve(here, "..", "..", "package.json");
11824
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
11949
+ const pkgPath = resolve2(here, "..", "..", "package.json");
11950
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
11825
11951
  return pkg.version ?? "0.0.0-test";
11826
11952
  } catch {
11827
11953
  return "0.0.0-test";
11828
11954
  }
11829
11955
  })();
11830
- var NATIVE_HOST_DIR = resolve(homedir(), ".viyv-browser", "native-host");
11831
11956
  function getSourceBinaryPath() {
11832
11957
  return fileURLToPath(import.meta.url);
11833
11958
  }
11834
11959
  function getNativeHostBinaryPath() {
11835
- return resolve(NATIVE_HOST_DIR, "index.js");
11960
+ return getDeploymentBinaryPath();
11836
11961
  }
11837
11962
  function isNativeHostDeployed() {
11838
- return existsSync(getNativeHostBinaryPath());
11963
+ return existsSync2(getNativeHostBinaryPath());
11839
11964
  }
11840
11965
  function syncNativeHostBinary() {
11841
11966
  const source = getSourceBinaryPath();
11842
- if (!existsSync(source)) {
11967
+ if (!existsSync2(source)) {
11843
11968
  throw new Error(`Source binary not found: ${source}`);
11844
11969
  }
11845
- mkdirSync(NATIVE_HOST_DIR, { recursive: true });
11970
+ const dir = getNativeHostDir();
11971
+ mkdirSync2(dir, { recursive: true });
11846
11972
  const target = getNativeHostBinaryPath();
11847
- const tmp = resolve(NATIVE_HOST_DIR, `index.js.tmp.${process.pid}`);
11973
+ const tmp = resolve2(dir, `index.js.tmp.${process.pid}`);
11848
11974
  try {
11849
11975
  copyFileSync(source, tmp);
11850
11976
  chmodSync(tmp, 493);
11851
- renameSync(tmp, target);
11977
+ renameSync2(tmp, target);
11852
11978
  } catch (err) {
11853
11979
  try {
11854
- unlinkSync(tmp);
11980
+ unlinkSync2(tmp);
11855
11981
  } catch {
11856
11982
  }
11857
11983
  throw err;
11858
11984
  }
11985
+ writeDeploymentVersion(PKG_VERSION);
11986
+ }
11987
+ function syncNativeHostBinaryIfNeeded() {
11988
+ const decision = decideSyncAction(readDeploymentState(), PKG_VERSION);
11989
+ if (decision.shouldSync) {
11990
+ syncNativeHostBinary();
11991
+ }
11992
+ return decision;
11859
11993
  }
11860
11994
  function shouldUpdateBridge(bridgeVersion) {
11861
11995
  return compareSemver(PKG_VERSION, bridgeVersion) > 0;
@@ -11863,20 +11997,11 @@ function shouldUpdateBridge(bridgeVersion) {
11863
11997
  function getPackageVersion() {
11864
11998
  return PKG_VERSION;
11865
11999
  }
11866
- function compareSemver(a, b) {
11867
- const pa = a.split(".").map(Number);
11868
- const pb = b.split(".").map(Number);
11869
- for (let i = 0; i < 3; i++) {
11870
- const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
11871
- if (diff !== 0) return diff;
11872
- }
11873
- return 0;
11874
- }
11875
12000
 
11876
12001
  // src/native-host/bridge-control.ts
11877
12002
  import { randomBytes } from "crypto";
11878
12003
  import { createServer } from "net";
11879
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
12004
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
11880
12005
  import { dirname } from "path";
11881
12006
  var LOG_PREFIX = "[viyv-browser:native-host:control]";
11882
12007
  function startBridgeControl(opts) {
@@ -11886,9 +12011,9 @@ function startBridgeControl(opts) {
11886
12011
  const { deps, onError } = opts;
11887
12012
  let cookie;
11888
12013
  try {
11889
- mkdirSync2(dirname(cookiePath), { recursive: true });
12014
+ mkdirSync3(dirname(cookiePath), { recursive: true });
11890
12015
  cookie = randomBytes(BRIDGE.CONTROL_COOKIE_BYTES);
11891
- writeFileSync(cookiePath, cookie.toString("hex"), { mode: 384 });
12016
+ writeFileSync2(cookiePath, cookie.toString("hex"), { mode: 384 });
11892
12017
  try {
11893
12018
  chmodSync2(cookiePath, 384);
11894
12019
  } catch {
@@ -12026,7 +12151,7 @@ function startBridgeControl(opts) {
12026
12151
  }
12027
12152
 
12028
12153
  // src/native-host/bridge-relay.ts
12029
- import { readFileSync as readFileSync2 } from "fs";
12154
+ import { readFileSync as readFileSync3 } from "fs";
12030
12155
  import { createConnection } from "net";
12031
12156
  import { join } from "path";
12032
12157
 
@@ -12128,7 +12253,7 @@ function readProfileIdFromEnv() {
12128
12253
  const dir = process.env.VIYV_USER_DATA_DIR;
12129
12254
  if (!dir) return null;
12130
12255
  try {
12131
- const raw = readFileSync2(join(dir, "viyv-profile.id"), "utf8").trim();
12256
+ const raw = readFileSync3(join(dir, "viyv-profile.id"), "utf8").trim();
12132
12257
  return raw || null;
12133
12258
  } catch {
12134
12259
  return null;
@@ -12460,7 +12585,7 @@ function startBridge(options) {
12460
12585
  const DRAIN_TIMEOUT = 5e3;
12461
12586
  const drainAndExit = () => {
12462
12587
  if (requestOrigin.size === 0) {
12463
- gracefulExit("Bridge shutdown requested for version update");
12588
+ gracefulExit("shutdown-request", "version update");
12464
12589
  return;
12465
12590
  }
12466
12591
  process.stderr.write(
@@ -12471,7 +12596,7 @@ function startBridge(options) {
12471
12596
  const check2 = setInterval(() => {
12472
12597
  if (requestOrigin.size === 0 || Date.now() - start > DRAIN_TIMEOUT) {
12473
12598
  clearInterval(check2);
12474
- gracefulExit("Bridge shutdown requested for version update");
12599
+ gracefulExit("shutdown-request", "version update");
12475
12600
  }
12476
12601
  }, 200);
12477
12602
  };
@@ -12536,6 +12661,7 @@ function startBridge(options) {
12536
12661
  } else if (type === "browser_event") {
12537
12662
  const agentId = message2.agentId;
12538
12663
  if (agentId) broadcastToAgent(agentId, message2);
12664
+ } else if (type === "sw_keepalive") {
12539
12665
  } else {
12540
12666
  process.stderr.write(`${LOG_PREFIX3} Unknown message type from Chrome: ${type}, dropping
12541
12667
  `);
@@ -12565,19 +12691,22 @@ function startBridge(options) {
12565
12691
  let primaryStdinClosed = false;
12566
12692
  const ORPHAN_TIMEOUT = 6e4;
12567
12693
  let orphanTimer = null;
12568
- function gracefulExit(reason) {
12569
- process.stderr.write(`${LOG_PREFIX3} ${reason}, exiting
12694
+ function gracefulExit(reason, detail) {
12695
+ const suffix = detail ? ` (${detail})` : "";
12696
+ process.stderr.write(`${LOG_PREFIX3} exiting reason=${reason}${suffix}
12570
12697
  `);
12571
12698
  if (orphanTimer) {
12572
12699
  clearTimeout(orphanTimer);
12573
12700
  orphanTimer = null;
12574
12701
  }
12575
- const closingMsg = JSON.stringify({ type: "bridge_closing", timestamp: Date.now() });
12576
- for (const [, conn] of mcpConnections) {
12577
- try {
12578
- conn.socket.write(`${closingMsg}
12702
+ if (shouldBroadcastClosing(reason)) {
12703
+ const closingMsg = JSON.stringify({ type: "bridge_closing", timestamp: Date.now() });
12704
+ for (const [, conn] of mcpConnections) {
12705
+ try {
12706
+ conn.socket.write(`${closingMsg}
12579
12707
  `);
12580
- } catch {
12708
+ } catch {
12709
+ }
12581
12710
  }
12582
12711
  }
12583
12712
  for (const [, conn] of mcpConnections) {
@@ -12603,7 +12732,7 @@ function startBridge(options) {
12603
12732
  return;
12604
12733
  }
12605
12734
  if (chromeConnections.size === 0 && mcpConnections.size === 0) {
12606
- gracefulExit("No connections remaining");
12735
+ gracefulExit("all-empty", "no connections remaining");
12607
12736
  }
12608
12737
  if (chromeConnections.size === 0 && mcpConnections.size > 0 && !orphanTimer) {
12609
12738
  process.stderr.write(
@@ -12800,7 +12929,7 @@ function startBridge(options) {
12800
12929
  const primaryUserDataDir = process.env.VIYV_USER_DATA_DIR;
12801
12930
  if (primaryUserDataDir) {
12802
12931
  try {
12803
- const raw = readFileSync3(join2(primaryUserDataDir, "viyv-profile.id"), "utf8").trim();
12932
+ const raw = readFileSync4(join2(primaryUserDataDir, "viyv-profile.id"), "utf8").trim();
12804
12933
  if (raw) {
12805
12934
  setProfileId(primaryChromeId, raw);
12806
12935
  process.stderr.write(
@@ -12819,8 +12948,8 @@ function startBridge(options) {
12819
12948
  onError: (e) => onError?.(e)
12820
12949
  });
12821
12950
  notifyChromeConnected(false);
12822
- process.on("SIGINT", () => gracefulExit("SIGINT received"));
12823
- process.on("SIGTERM", () => gracefulExit("SIGTERM received"));
12951
+ process.on("SIGINT", () => gracefulExit("sigint"));
12952
+ process.on("SIGTERM", () => gracefulExit("chrome-sigterm"));
12824
12953
  process.stdin.on("end", () => {
12825
12954
  process.stderr.write(`${LOG_PREFIX3} Primary Chrome stdin closed
12826
12955
  `);
@@ -12833,9 +12962,8 @@ function startBridge(options) {
12833
12962
 
12834
12963
  // src/server.ts
12835
12964
  import { randomUUID as randomUUID5 } from "crypto";
12836
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
12965
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync5, statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
12837
12966
  import http from "http";
12838
- import { createConnection as createConnection2 } from "net";
12839
12967
  import { dirname as dirname2 } from "path";
12840
12968
 
12841
12969
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
@@ -24912,7 +25040,7 @@ var Protocol = class {
24912
25040
  return;
24913
25041
  }
24914
25042
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
24915
- await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
25043
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
24916
25044
  options?.signal?.throwIfAborted();
24917
25045
  }
24918
25046
  } catch (error2) {
@@ -24929,7 +25057,7 @@ var Protocol = class {
24929
25057
  */
24930
25058
  request(request, resultSchema, options) {
24931
25059
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
24932
- return new Promise((resolve3, reject) => {
25060
+ return new Promise((resolve4, reject) => {
24933
25061
  const earlyReject = (error2) => {
24934
25062
  reject(error2);
24935
25063
  };
@@ -25007,7 +25135,7 @@ var Protocol = class {
25007
25135
  if (!parseResult.success) {
25008
25136
  reject(parseResult.error);
25009
25137
  } else {
25010
- resolve3(parseResult.data);
25138
+ resolve4(parseResult.data);
25011
25139
  }
25012
25140
  } catch (error2) {
25013
25141
  reject(error2);
@@ -25268,12 +25396,12 @@ var Protocol = class {
25268
25396
  }
25269
25397
  } catch {
25270
25398
  }
25271
- return new Promise((resolve3, reject) => {
25399
+ return new Promise((resolve4, reject) => {
25272
25400
  if (signal.aborted) {
25273
25401
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
25274
25402
  return;
25275
25403
  }
25276
- const timeoutId = setTimeout(resolve3, interval);
25404
+ const timeoutId = setTimeout(resolve4, interval);
25277
25405
  signal.addEventListener("abort", () => {
25278
25406
  clearTimeout(timeoutId);
25279
25407
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -26232,7 +26360,7 @@ var McpServer = class {
26232
26360
  let task = createTaskResult.task;
26233
26361
  const pollInterval = task.pollInterval ?? 5e3;
26234
26362
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
26235
- await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
26363
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
26236
26364
  const updatedTask = await extra.taskStore.getTask(taskId);
26237
26365
  if (!updatedTask) {
26238
26366
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -27020,12 +27148,12 @@ var StdioServerTransport = class {
27020
27148
  this.onclose?.();
27021
27149
  }
27022
27150
  send(message2) {
27023
- return new Promise((resolve3) => {
27151
+ return new Promise((resolve4) => {
27024
27152
  const json = serializeMessage(message2);
27025
27153
  if (this._stdout.write(json)) {
27026
- resolve3();
27154
+ resolve4();
27027
27155
  } else {
27028
- this._stdout.once("drain", resolve3);
27156
+ this._stdout.once("drain", resolve4);
27029
27157
  }
27030
27158
  });
27031
27159
  }
@@ -27439,7 +27567,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
27439
27567
  });
27440
27568
  if (!chunk) {
27441
27569
  if (i === 1) {
27442
- await new Promise((resolve3) => setTimeout(resolve3));
27570
+ await new Promise((resolve4) => setTimeout(resolve4));
27443
27571
  maxReadCount = 3;
27444
27572
  continue;
27445
27573
  }
@@ -27921,9 +28049,9 @@ data:
27921
28049
  const initRequest = messages.find((m) => isInitializeRequest(m));
27922
28050
  const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
27923
28051
  if (this._enableJsonResponse) {
27924
- return new Promise((resolve3) => {
28052
+ return new Promise((resolve4) => {
27925
28053
  this._streamMapping.set(streamId, {
27926
- resolveJson: resolve3,
28054
+ resolveJson: resolve4,
27927
28055
  cleanup: () => {
27928
28056
  this._streamMapping.delete(streamId);
27929
28057
  }
@@ -28249,6 +28377,408 @@ var StreamableHTTPServerTransport = class {
28249
28377
  }
28250
28378
  };
28251
28379
 
28380
+ // src/native-host/bridge-client/index.ts
28381
+ import { createConnection as createConnection2 } from "net";
28382
+
28383
+ // src/native-host/bridge-client/connection-state.ts
28384
+ function transition(state, event) {
28385
+ if (event.kind === "stop") return "disconnected";
28386
+ switch (state) {
28387
+ case "disconnected":
28388
+ if (event.kind === "start") return "connecting";
28389
+ return state;
28390
+ case "connecting":
28391
+ if (event.kind === "socket-connected") return "handshaking";
28392
+ if (event.kind === "socket-closed") return "disconnected";
28393
+ return state;
28394
+ case "handshaking":
28395
+ if (event.kind === "bridge-status-true") return "ready";
28396
+ if (event.kind === "bridge-status-false") return "handshaking";
28397
+ if (event.kind === "bridge-closing") return "draining";
28398
+ if (event.kind === "socket-closed") return "disconnected";
28399
+ return state;
28400
+ case "ready":
28401
+ if (event.kind === "bridge-status-false") return "handshaking";
28402
+ if (event.kind === "bridge-closing") return "draining";
28403
+ if (event.kind === "socket-closed") return "disconnected";
28404
+ return state;
28405
+ case "draining":
28406
+ if (event.kind === "socket-closed") return "disconnected";
28407
+ return state;
28408
+ default: {
28409
+ const _exhaustive = state;
28410
+ return _exhaustive;
28411
+ }
28412
+ }
28413
+ }
28414
+ function isReady(state) {
28415
+ return state === "ready";
28416
+ }
28417
+
28418
+ // src/native-host/bridge-client/pending-queue.ts
28419
+ var PendingQueue = class {
28420
+ constructor(max, ttlMs, timers) {
28421
+ this.max = max;
28422
+ this.ttlMs = ttlMs;
28423
+ this.timers = timers;
28424
+ if (max < 1) throw new Error("PendingQueue max must be >= 1");
28425
+ if (ttlMs < 0) throw new Error("PendingQueue ttlMs must be >= 0");
28426
+ }
28427
+ entries = [];
28428
+ /** Try to enqueue. Returns `true` on success, `false` when rejected (onReject already fired). */
28429
+ enqueue(entry) {
28430
+ if (this.entries.length >= this.max) {
28431
+ entry.onReject("queue-full");
28432
+ return false;
28433
+ }
28434
+ const stored = { ...entry, timer: null };
28435
+ stored.timer = this.timers.setTimer(() => this.expire(stored), this.ttlMs);
28436
+ this.entries.push(stored);
28437
+ return true;
28438
+ }
28439
+ /** Flush every queued entry in FIFO order. */
28440
+ flushAll() {
28441
+ const snapshot = this.entries.splice(0, this.entries.length);
28442
+ for (const entry of snapshot) {
28443
+ this.timers.clearTimer(entry.timer);
28444
+ entry.onFlush(entry.payload);
28445
+ }
28446
+ }
28447
+ /** Reject every queued entry with the given reason. Used on `stop()`. */
28448
+ rejectAll(reason) {
28449
+ const snapshot = this.entries.splice(0, this.entries.length);
28450
+ for (const entry of snapshot) {
28451
+ this.timers.clearTimer(entry.timer);
28452
+ entry.onReject(reason);
28453
+ }
28454
+ }
28455
+ size() {
28456
+ return this.entries.length;
28457
+ }
28458
+ expire(entry) {
28459
+ const idx = this.entries.indexOf(entry);
28460
+ if (idx === -1) return;
28461
+ this.entries.splice(idx, 1);
28462
+ entry.onReject("timeout");
28463
+ }
28464
+ };
28465
+
28466
+ // src/native-host/bridge-client/index.ts
28467
+ var LOG_PREFIX4 = "[viyv-browser:mcp:bridge]";
28468
+ var METRIC_PREFIX = "[viyv-browser:mcp:metric]";
28469
+ var LINE_TERMINATOR = "\n";
28470
+ var BRIDGE_RECONNECT_CAP_MS = 3e3;
28471
+ var defaultTimers = {
28472
+ setTimer: (fn, ms) => setTimeout(fn, ms),
28473
+ clearTimer: (h) => clearTimeout(h)
28474
+ };
28475
+ var defaultSchedule = (fn, ms) => {
28476
+ const h = setTimeout(fn, ms);
28477
+ return () => clearTimeout(h);
28478
+ };
28479
+ var BridgeClient = class {
28480
+ constructor(options) {
28481
+ this.options = options;
28482
+ this.log = options.log ?? ((line) => process.stderr.write(`${line}
28483
+ `));
28484
+ this.metric = options.metric ?? ((line) => process.stderr.write(`${line}
28485
+ `));
28486
+ this.now = options.now ?? (() => Date.now());
28487
+ this.schedule = options.schedule ?? defaultSchedule;
28488
+ this.random = options.random ?? Math.random;
28489
+ this.queue = new PendingQueue(
28490
+ BRIDGE_READINESS.PENDING_QUEUE_MAX,
28491
+ BRIDGE_READINESS.TIMEOUT_MS,
28492
+ options.timers ?? defaultTimers
28493
+ );
28494
+ }
28495
+ state = "disconnected";
28496
+ socket = null;
28497
+ reconnectAttempt = 0;
28498
+ cancelReconnect = null;
28499
+ messageHandlers = /* @__PURE__ */ new Set();
28500
+ stateHandlers = /* @__PURE__ */ new Set();
28501
+ queue;
28502
+ /** ms since epoch when the current handshake started; `null` when not measuring. */
28503
+ handshakeStartMs = null;
28504
+ started = false;
28505
+ log;
28506
+ metric;
28507
+ now;
28508
+ schedule;
28509
+ random;
28510
+ start() {
28511
+ if (this.started) return;
28512
+ this.started = true;
28513
+ this.apply({ kind: "start" });
28514
+ this.connect();
28515
+ }
28516
+ stop() {
28517
+ this.started = false;
28518
+ this.cancelReconnect?.();
28519
+ this.cancelReconnect = null;
28520
+ this.queue.rejectAll("stopped");
28521
+ if (this.socket && !this.socket.destroyed) {
28522
+ this.socket.destroy();
28523
+ }
28524
+ this.socket = null;
28525
+ this.apply({ kind: "stop" });
28526
+ }
28527
+ /**
28528
+ * Drop the current socket and reconnect immediately. Used by switch_browser
28529
+ * to pivot to a fresh Chrome. Pending tool_calls are drained with the
28530
+ * SWITCH_IN_PROGRESS error by the caller before this is invoked.
28531
+ */
28532
+ forceReconnect() {
28533
+ if (this.socket && !this.socket.destroyed) {
28534
+ this.socket.destroy();
28535
+ }
28536
+ this.socket = null;
28537
+ this.apply({ kind: "socket-closed" });
28538
+ if (this.started) {
28539
+ this.reconnectAttempt = 0;
28540
+ this.scheduleReconnect();
28541
+ }
28542
+ }
28543
+ getState() {
28544
+ return this.state;
28545
+ }
28546
+ isReady() {
28547
+ return isReady(this.state);
28548
+ }
28549
+ /**
28550
+ * Return the socket when it is TCP-live (handshaking, ready, or draining).
28551
+ * Ancillary writes like session_init / session_close tolerate writes during
28552
+ * handshake — the bridge buffers them until Chrome is reachable.
28553
+ * Returns null when the socket is not safe to write to.
28554
+ */
28555
+ getSocket() {
28556
+ if (!this.socket || this.socket.destroyed) return null;
28557
+ if (this.state === "handshaking" || this.state === "ready" || this.state === "draining") {
28558
+ return this.socket;
28559
+ }
28560
+ return null;
28561
+ }
28562
+ onMessage(fn) {
28563
+ this.messageHandlers.add(fn);
28564
+ return () => this.messageHandlers.delete(fn);
28565
+ }
28566
+ onStateChange(fn) {
28567
+ this.stateHandlers.add(fn);
28568
+ return () => this.stateHandlers.delete(fn);
28569
+ }
28570
+ /**
28571
+ * Send a tool_call line. If the bridge is `ready`, writes immediately.
28572
+ * Otherwise enqueues until readiness, failing with `queue-full` if capacity
28573
+ * is exhausted or `timeout` if the handshake takes too long.
28574
+ */
28575
+ enqueueToolCall(line) {
28576
+ return new Promise((resolve4, reject) => {
28577
+ if (this.state === "ready" && this.socket && !this.socket.destroyed) {
28578
+ try {
28579
+ this.socket.write(line.endsWith(LINE_TERMINATOR) ? line : line + LINE_TERMINATOR);
28580
+ resolve4();
28581
+ } catch (err) {
28582
+ reject({ reason: "write-failed", detail: err instanceof Error ? err.message : String(err) });
28583
+ }
28584
+ return;
28585
+ }
28586
+ const enqueued = this.queue.enqueue({
28587
+ payload: {
28588
+ line: line.endsWith(LINE_TERMINATOR) ? line : line + LINE_TERMINATOR,
28589
+ resolve: resolve4,
28590
+ reject
28591
+ },
28592
+ onFlush: (entry) => {
28593
+ if (this.socket && !this.socket.destroyed) {
28594
+ try {
28595
+ this.socket.write(entry.line);
28596
+ entry.resolve();
28597
+ } catch (err) {
28598
+ entry.reject({
28599
+ reason: "write-failed",
28600
+ detail: err instanceof Error ? err.message : String(err)
28601
+ });
28602
+ }
28603
+ } else {
28604
+ entry.reject({ reason: "write-failed", detail: "socket unavailable at flush" });
28605
+ }
28606
+ },
28607
+ onReject: (reason) => {
28608
+ reject({ reason });
28609
+ }
28610
+ });
28611
+ if (enqueued) {
28612
+ this.emitMetric("queue.enqueue", { depth: this.queue.size() });
28613
+ }
28614
+ });
28615
+ }
28616
+ connect() {
28617
+ if (!this.started) return;
28618
+ this.cancelReconnect = null;
28619
+ this.apply({ kind: "start" });
28620
+ if (this.state !== "connecting") {
28621
+ this.forceState("connecting");
28622
+ }
28623
+ const sock = createConnection2({ host: this.options.host, port: this.options.port });
28624
+ this.socket = sock;
28625
+ sock.on("connect", () => {
28626
+ this.handshakeStartMs = this.now();
28627
+ sock.setKeepAlive(true, 3e4);
28628
+ this.reconnectAttempt = 0;
28629
+ this.apply({ kind: "socket-connected" });
28630
+ });
28631
+ sock.on("error", (err) => {
28632
+ this.log(`${LOG_PREFIX4} socket error: ${err.message}`);
28633
+ });
28634
+ sock.on("close", () => {
28635
+ const wasReady = this.state === "ready";
28636
+ if (this.socket === sock) this.socket = null;
28637
+ this.apply({ kind: "socket-closed" });
28638
+ if (wasReady) this.log(`${LOG_PREFIX4} bridge disconnected`);
28639
+ if (this.started) this.scheduleReconnect();
28640
+ });
28641
+ let lineBuffer = "";
28642
+ sock.on("data", (data) => {
28643
+ lineBuffer += data.toString("utf-8");
28644
+ const lines = lineBuffer.split(LINE_TERMINATOR);
28645
+ lineBuffer = lines.pop() ?? "";
28646
+ for (const line of lines) {
28647
+ if (!line) continue;
28648
+ try {
28649
+ const parsed = JSON.parse(line);
28650
+ this.handleIncoming(parsed);
28651
+ } catch (err) {
28652
+ this.log(
28653
+ `${LOG_PREFIX4} parse error: ${err instanceof Error ? err.message : String(err)}`
28654
+ );
28655
+ }
28656
+ }
28657
+ });
28658
+ }
28659
+ handleIncoming(msg) {
28660
+ if (msg && typeof msg === "object") {
28661
+ const type = msg.type;
28662
+ if (type === "bridge_closing") {
28663
+ this.log(`${LOG_PREFIX4} received bridge_closing, will reconnect`);
28664
+ this.apply({ kind: "bridge-closing" });
28665
+ if (this.socket && !this.socket.destroyed) this.socket.destroy();
28666
+ return;
28667
+ }
28668
+ if (type === "bridge_status") {
28669
+ const connected = msg.connected === true;
28670
+ this.apply({ kind: connected ? "bridge-status-true" : "bridge-status-false" });
28671
+ }
28672
+ }
28673
+ for (const h of this.messageHandlers) {
28674
+ try {
28675
+ h(msg);
28676
+ } catch (err) {
28677
+ this.log(
28678
+ `${LOG_PREFIX4} handler threw: ${err instanceof Error ? err.message : String(err)}`
28679
+ );
28680
+ }
28681
+ }
28682
+ }
28683
+ scheduleReconnect() {
28684
+ if (!this.started) return;
28685
+ if (this.cancelReconnect) return;
28686
+ this.reconnectAttempt++;
28687
+ const base = Math.min(
28688
+ RECONNECT.INITIAL_DELAY * RECONNECT.MULTIPLIER ** (this.reconnectAttempt - 1),
28689
+ BRIDGE_RECONNECT_CAP_MS
28690
+ );
28691
+ const delay = applyJitter(base, RECONNECT.JITTER_RATIO, this.random);
28692
+ this.emitMetric("reconnect.scheduled", { attempt: this.reconnectAttempt, delayMs: delay });
28693
+ this.log(`${LOG_PREFIX4} reconnect in ${delay}ms (attempt ${this.reconnectAttempt})`);
28694
+ this.cancelReconnect = this.schedule(() => {
28695
+ this.cancelReconnect = null;
28696
+ this.connect();
28697
+ }, delay);
28698
+ }
28699
+ apply(event) {
28700
+ const next = transition(this.state, event);
28701
+ if (next === this.state) return;
28702
+ const prev = this.state;
28703
+ this.state = next;
28704
+ this.emitMetric("state.transition", { from: prev, to: next, event: event.kind });
28705
+ if (next === "ready") {
28706
+ if (this.handshakeStartMs !== null) {
28707
+ this.emitMetric("handshake.complete", {
28708
+ elapsedMs: this.now() - this.handshakeStartMs
28709
+ });
28710
+ this.handshakeStartMs = null;
28711
+ }
28712
+ this.queue.flushAll();
28713
+ }
28714
+ for (const h of this.stateHandlers) {
28715
+ try {
28716
+ h(next, prev);
28717
+ } catch (err) {
28718
+ this.log(
28719
+ `${LOG_PREFIX4} state handler threw: ${err instanceof Error ? err.message : String(err)}`
28720
+ );
28721
+ }
28722
+ }
28723
+ }
28724
+ /** Force a specific state (used only for recovering from impossible transitions). */
28725
+ forceState(to) {
28726
+ if (this.state === to) return;
28727
+ const prev = this.state;
28728
+ this.state = to;
28729
+ this.emitMetric("state.force", { from: prev, to });
28730
+ for (const h of this.stateHandlers) {
28731
+ try {
28732
+ h(to, prev);
28733
+ } catch {
28734
+ }
28735
+ }
28736
+ }
28737
+ emitMetric(name, fields) {
28738
+ this.metric(
28739
+ `${METRIC_PREFIX} ${name} ${Object.entries(fields).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ")}`
28740
+ );
28741
+ }
28742
+ };
28743
+ function toolCallRejectionToErrorCode(rej) {
28744
+ switch (rej.reason) {
28745
+ case "queue-full":
28746
+ return {
28747
+ code: "EXTENSION_NOT_CONNECTED",
28748
+ message: "Bridge readiness queue is full; try again shortly"
28749
+ };
28750
+ case "timeout":
28751
+ return {
28752
+ code: "EXTENSION_NOT_CONNECTED",
28753
+ message: "Timed out waiting for bridge readiness"
28754
+ };
28755
+ case "stopped":
28756
+ return {
28757
+ code: "EXTENSION_NOT_CONNECTED",
28758
+ message: "Bridge client was stopped"
28759
+ };
28760
+ case "write-failed":
28761
+ return {
28762
+ code: "EXTENSION_NOT_CONNECTED",
28763
+ message: `Socket write failed: ${rej.detail}`
28764
+ };
28765
+ }
28766
+ }
28767
+
28768
+ // src/bridge-protocol/shutdown-request-gate.ts
28769
+ var SHUTDOWN_REQUEST_COOLDOWN_MS = 3e4;
28770
+ function createShutdownGateState() {
28771
+ return { lastSentAt: null, lastVersion: null };
28772
+ }
28773
+ function shouldSendShutdownRequest(state, observedVersion, now, cooldownMs = SHUTDOWN_REQUEST_COOLDOWN_MS) {
28774
+ if (state.lastSentAt === null) return true;
28775
+ if (state.lastVersion !== observedVersion) return true;
28776
+ return now - state.lastSentAt >= cooldownMs;
28777
+ }
28778
+ function markSent(state, version2, now) {
28779
+ return { lastSentAt: now, lastVersion: version2 };
28780
+ }
28781
+
28252
28782
  // ../shared/dist/bearer-equals.js
28253
28783
  import { timingSafeEqual } from "crypto";
28254
28784
  function bearerEquals(header, expectedToken) {
@@ -33775,8 +34305,12 @@ function computeToolTimeout(tool, input) {
33775
34305
 
33776
34306
  // src/server.ts
33777
34307
  var pendingRequests = /* @__PURE__ */ new Map();
33778
- var extensionSocket = null;
34308
+ var bridgeClient = null;
33779
34309
  var bridgeUpdateInfo = null;
34310
+ var shutdownGate = createShutdownGateState();
34311
+ function getBridgeSocket() {
34312
+ return bridgeClient?.getSocket() ?? null;
34313
+ }
33780
34314
  function readJwtConfigFromEnv() {
33781
34315
  const alg = process.env.VIYV_JWT_ALG ?? "none";
33782
34316
  return {
@@ -33844,8 +34378,9 @@ function bridgeRegisterAgent(agentId, sessionId) {
33844
34378
  bridgeAgentRefs.set(agentId, set);
33845
34379
  }
33846
34380
  set.add(sessionId);
33847
- if (isFirst && extensionSocket && !extensionSocket.destroyed) {
33848
- sendSessionInit(extensionSocket, agentId);
34381
+ if (isFirst) {
34382
+ const sock = getBridgeSocket();
34383
+ if (sock) sendSessionInit(sock, agentId);
33849
34384
  }
33850
34385
  }
33851
34386
  function bridgeUnregisterAgent(agentId, sessionId) {
@@ -33854,8 +34389,9 @@ function bridgeUnregisterAgent(agentId, sessionId) {
33854
34389
  set.delete(sessionId);
33855
34390
  if (set.size === 0) {
33856
34391
  bridgeAgentRefs.delete(agentId);
33857
- if (extensionSocket && !extensionSocket.destroyed) {
33858
- extensionSocket.write(
34392
+ const sock = getBridgeSocket();
34393
+ if (sock) {
34394
+ sock.write(
33859
34395
  `${JSON.stringify({
33860
34396
  id: randomUUID5(),
33861
34397
  type: "session_close",
@@ -33892,7 +34428,7 @@ function coerceShape(shape) {
33892
34428
  return result;
33893
34429
  }
33894
34430
  function handleFileExport(filePath, result) {
33895
- mkdirSync3(dirname2(filePath), { recursive: true });
34431
+ mkdirSync4(dirname2(filePath), { recursive: true });
33896
34432
  let content;
33897
34433
  let metadata;
33898
34434
  if (typeof result.data === "string") {
@@ -33912,7 +34448,7 @@ function handleFileExport(filePath, result) {
33912
34448
  const pages = Array.isArray(result.pages) ? result.pages : [];
33913
34449
  metadata = { ok: true, file_path: filePath, page_count: pages.length };
33914
34450
  }
33915
- writeFileSync2(filePath, content, "utf-8");
34451
+ writeFileSync3(filePath, content, "utf-8");
33916
34452
  metadata.file_size = Buffer.byteLength(content, "utf-8");
33917
34453
  return metadata;
33918
34454
  }
@@ -34006,7 +34542,8 @@ async function startMcpServer(agentName, options) {
34006
34542
  setDefaultAgentId(agentName);
34007
34543
  }
34008
34544
  configuredChromeProfile = options?.chromeProfile;
34009
- connectToBridge();
34545
+ bridgeClient = initBridgeClient();
34546
+ bridgeClient.start();
34010
34547
  if (options?.transport === "sse") {
34011
34548
  const sessions2 = /* @__PURE__ */ new Map();
34012
34549
  const httpServer = http.createServer();
@@ -34044,7 +34581,7 @@ async function startMcpServer(agentName, options) {
34044
34581
  }
34045
34582
  httpServer.close(() => {
34046
34583
  });
34047
- extensionSocket?.destroy();
34584
+ bridgeClient?.stop();
34048
34585
  process.exit(0);
34049
34586
  };
34050
34587
  process.on("SIGINT", () => {
@@ -34119,7 +34656,7 @@ async function startMcpServer(agentName, options) {
34119
34656
  }
34120
34657
  httpServer.close(() => {
34121
34658
  });
34122
- extensionSocket?.destroy();
34659
+ bridgeClient?.stop();
34123
34660
  process.exit(0);
34124
34661
  };
34125
34662
  process.on("SIGINT", () => {
@@ -34138,13 +34675,13 @@ async function startMcpServer(agentName, options) {
34138
34675
  );
34139
34676
  process.stdin.on("end", () => {
34140
34677
  process.stderr.write("[viyv-browser:mcp] stdin closed, shutting down\n");
34141
- extensionSocket?.destroy();
34678
+ bridgeClient?.stop();
34142
34679
  process.exit(0);
34143
34680
  });
34144
34681
  process.on("SIGINT", () => process.exit(0));
34145
34682
  process.on("SIGTERM", () => process.exit(0));
34146
34683
  process.on("exit", () => {
34147
- extensionSocket?.destroy();
34684
+ bridgeClient?.stop();
34148
34685
  });
34149
34686
  }
34150
34687
  }
@@ -34299,7 +34836,7 @@ async function handleStreamableHttpRequest(req, res, sessions2) {
34299
34836
  }
34300
34837
  var MAX_BODY_SIZE = 4 * 1024 * 1024;
34301
34838
  function parseJsonBody(req) {
34302
- return new Promise((resolve3, reject) => {
34839
+ return new Promise((resolve4, reject) => {
34303
34840
  const chunks = [];
34304
34841
  let size = 0;
34305
34842
  let destroyed = false;
@@ -34317,7 +34854,7 @@ function parseJsonBody(req) {
34317
34854
  req.on("end", () => {
34318
34855
  if (destroyed) return;
34319
34856
  try {
34320
- resolve3(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
34857
+ resolve4(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
34321
34858
  } catch (error2) {
34322
34859
  reject(new Error(`Invalid JSON: ${error2.message}`));
34323
34860
  }
@@ -34327,65 +34864,6 @@ function parseJsonBody(req) {
34327
34864
  });
34328
34865
  });
34329
34866
  }
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
34867
  function sendSessionInit(socket, agentId) {
34390
34868
  const initMsg = {
34391
34869
  id: randomUUID5(),
@@ -34400,65 +34878,79 @@ function sendSessionInit(socket, agentId) {
34400
34878
  socket.write(`${JSON.stringify(initMsg)}
34401
34879
  `);
34402
34880
  }
34403
- function setupBridgeConnection(socket, onBridgeClosing) {
34404
- process.stderr.write(
34405
- `[viyv-browser:mcp] Connected to bridge at ${BRIDGE.TCP_HOST}:${BRIDGE.TCP_PORT}
34881
+ function initBridgeClient() {
34882
+ const client = new BridgeClient({ host: BRIDGE.TCP_HOST, port: BRIDGE.TCP_PORT });
34883
+ let heartbeatInterval = null;
34884
+ client.onMessage((rawMsg) => {
34885
+ if (!rawMsg || typeof rawMsg !== "object") return;
34886
+ let parsed = rawMsg;
34887
+ if (parsed.type === "compressed" && typeof parsed.data === "string") {
34888
+ try {
34889
+ const decompressed = decompressPayload(parsed.data, true);
34890
+ parsed = JSON.parse(decompressed);
34891
+ } catch (err) {
34892
+ process.stderr.write(
34893
+ `[viyv-browser:mcp] Decompress error: ${err instanceof Error ? err.message : String(err)}
34406
34894
  `
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;
34895
+ );
34896
+ return;
34897
+ }
34418
34898
  }
34419
- socket.write(
34420
- `${JSON.stringify({
34421
- id: randomUUID5(),
34422
- type: "session_heartbeat",
34423
- agentId,
34424
- timestamp: Date.now()
34425
- })}
34899
+ handleExtensionMessage(parsed);
34900
+ });
34901
+ client.onStateChange((next, prev) => {
34902
+ if (next === "ready" && prev !== "ready") {
34903
+ process.stderr.write(
34904
+ `[viyv-browser:mcp] Connected to bridge at ${BRIDGE.TCP_HOST}:${BRIDGE.TCP_PORT}
34426
34905
  `
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
- `);
34906
+ );
34907
+ setExtensionConnected(true);
34908
+ const sock = client.getSocket();
34909
+ if (sock) {
34910
+ const agentId = getDefaultAgentId();
34911
+ sendSessionInit(sock, agentId);
34912
+ bridgeResyncAgents(sock);
34913
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
34914
+ heartbeatInterval = setInterval(() => {
34915
+ const s = client.getSocket();
34916
+ if (!s || s.destroyed) {
34917
+ if (heartbeatInterval) {
34918
+ clearInterval(heartbeatInterval);
34919
+ heartbeatInterval = null;
34920
+ }
34921
+ return;
34922
+ }
34923
+ s.write(
34924
+ `${JSON.stringify({
34925
+ id: randomUUID5(),
34926
+ type: "session_heartbeat",
34927
+ agentId,
34928
+ timestamp: Date.now()
34929
+ })}
34930
+ `
34931
+ );
34932
+ }, TIMEOUTS.HEARTBEAT);
34933
+ }
34934
+ }
34935
+ if (prev === "ready" && next !== "ready") {
34936
+ setExtensionConnected(false);
34937
+ if (heartbeatInterval) {
34938
+ clearInterval(heartbeatInterval);
34939
+ heartbeatInterval = null;
34940
+ }
34941
+ for (const [id, pending] of pendingRequests) {
34942
+ clearTimeout(pending.timer);
34943
+ pendingRequests.delete(id);
34944
+ pending.resolve({
34945
+ error: {
34946
+ code: "EXTENSION_NOT_CONNECTED",
34947
+ message: "Bridge disconnected while request was pending"
34948
+ }
34949
+ });
34452
34950
  }
34453
34951
  }
34454
34952
  });
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
- };
34953
+ return client;
34462
34954
  }
34463
34955
  function handleExtensionMessage(message2) {
34464
34956
  if (!message2 || typeof message2 !== "object") return;
@@ -34516,9 +35008,10 @@ function handleExtensionMessage(message2) {
34516
35008
  } else {
34517
35009
  setExtensionConnected(true);
34518
35010
  recordHeartbeat();
34519
- if (extensionSocket && !extensionSocket.destroyed && listSessions().length === 0) {
35011
+ const sockForReinit = getBridgeSocket();
35012
+ if (sockForReinit && listSessions().length === 0) {
34520
35013
  const agentId = getDefaultAgentId();
34521
- sendSessionInit(extensionSocket, agentId);
35014
+ sendSessionInit(sockForReinit, agentId);
34522
35015
  process.stderr.write(
34523
35016
  `[viyv-browser:mcp] Session re-initialized after Chrome reconnect: ${agentId}
34524
35017
  `
@@ -34529,14 +35022,24 @@ function handleExtensionMessage(message2) {
34529
35022
  const bridgeVersion = typeof msg.bridgeVersion === "string" ? msg.bridgeVersion : null;
34530
35023
  if (bridgeVersion && shouldUpdateBridge(bridgeVersion)) {
34531
35024
  const serverVersion = getPackageVersion();
34532
- process.stderr.write(
34533
- `[viyv-browser:mcp] Bridge version mismatch (bridge=${bridgeVersion}, server=${serverVersion}), requesting restart
35025
+ const nowMs = Date.now();
35026
+ if (shouldSendShutdownRequest(shutdownGate, bridgeVersion, nowMs)) {
35027
+ process.stderr.write(
35028
+ `[viyv-browser:mcp] Bridge version mismatch (bridge=${bridgeVersion}, server=${serverVersion}), requesting restart
34534
35029
  `
34535
- );
34536
- bridgeUpdateInfo = { from: bridgeVersion, to: serverVersion };
34537
- if (extensionSocket && !extensionSocket.destroyed) {
34538
- extensionSocket.write(
34539
- `${JSON.stringify({ type: "bridge_shutdown_request", timestamp: Date.now() })}
35030
+ );
35031
+ bridgeUpdateInfo = { from: bridgeVersion, to: serverVersion };
35032
+ const shutdownSock = getBridgeSocket();
35033
+ if (shutdownSock) {
35034
+ shutdownSock.write(
35035
+ `${JSON.stringify({ type: "bridge_shutdown_request", timestamp: nowMs })}
35036
+ `
35037
+ );
35038
+ shutdownGate = markSent(shutdownGate, bridgeVersion, nowMs);
35039
+ }
35040
+ } else {
35041
+ process.stderr.write(
35042
+ `[viyv-browser:mcp] Skipping bridge_shutdown_request \u2014 same version ${bridgeVersion} within cooldown (orphan MCP or external tampering is re-seeding the deploy binary; check for stale viyv-browser-mcp processes)
34540
35043
  `
34541
35044
  );
34542
35045
  }
@@ -34665,7 +35168,7 @@ async function callExtensionTool(tool, input) {
34665
35168
  }
34666
35169
  if (tool === "browser_health") {
34667
35170
  const health = getHealthStatus();
34668
- if (!health.extensionConnected || !extensionSocket || extensionSocket.destroyed) {
35171
+ if (!health.extensionConnected || !getBridgeSocket()) {
34669
35172
  return {
34670
35173
  content: [{ type: "text", text: JSON.stringify(health) }]
34671
35174
  };
@@ -34679,7 +35182,7 @@ async function callExtensionTool(tool, input) {
34679
35182
  if (paths) {
34680
35183
  for (const p of paths) {
34681
35184
  try {
34682
- if (!existsSync2(p) || !statSync(p).isFile()) {
35185
+ if (!existsSync3(p) || !statSync(p).isFile()) {
34683
35186
  return {
34684
35187
  content: [
34685
35188
  {
@@ -34711,7 +35214,7 @@ async function callExtensionTool(tool, input) {
34711
35214
  let csvData;
34712
35215
  if (typeof input.file_path === "string") {
34713
35216
  const fp = input.file_path;
34714
- if (!existsSync2(fp) || !statSync(fp).isFile()) {
35217
+ if (!existsSync3(fp) || !statSync(fp).isFile()) {
34715
35218
  return {
34716
35219
  content: [
34717
35220
  {
@@ -34723,7 +35226,7 @@ async function callExtensionTool(tool, input) {
34723
35226
  ]
34724
35227
  };
34725
35228
  }
34726
- csvData = readFileSync4(fp, "utf-8");
35229
+ csvData = readFileSync5(fp, "utf-8");
34727
35230
  } else if (typeof input.data === "string") {
34728
35231
  csvData = input.data;
34729
35232
  }
@@ -34749,7 +35252,7 @@ async function callExtensionTool(tool, input) {
34749
35252
  }
34750
35253
  }
34751
35254
  }
34752
- if (!extensionSocket || extensionSocket.destroyed) {
35255
+ if (!bridgeClient) {
34753
35256
  return {
34754
35257
  content: [
34755
35258
  {
@@ -34757,7 +35260,7 @@ async function callExtensionTool(tool, input) {
34757
35260
  text: JSON.stringify({
34758
35261
  error: {
34759
35262
  code: "EXTENSION_NOT_CONNECTED",
34760
- message: "Chrome Extension is not connected. Please open Chrome and click the Viyv Browser extension icon."
35263
+ message: "Bridge client not initialized"
34761
35264
  }
34762
35265
  })
34763
35266
  }
@@ -34767,36 +35270,15 @@ async function callExtensionTool(tool, input) {
34767
35270
  const requestId = randomUUID5();
34768
35271
  const agentId = getCurrentAgentId(getDefaultAgentId);
34769
35272
  touchSession(agentId);
34770
- const sock = extensionSocket;
35273
+ const client = bridgeClient;
34771
35274
  const toolTimeout = computeToolTimeout(tool, input);
34772
- 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
- };
35275
+ return new Promise((resolve4) => {
34793
35276
  const removeErrorListener = () => {
34794
- sock.removeListener("error", onError);
34795
35277
  };
34796
35278
  const timer = setTimeout(() => {
34797
35279
  pendingRequests.delete(requestId);
34798
35280
  removeErrorListener();
34799
- resolve3({
35281
+ resolve4({
34800
35282
  content: [
34801
35283
  {
34802
35284
  type: "text",
@@ -34823,12 +35305,12 @@ async function callExtensionTool(tool, input) {
34823
35305
  if (FILE_EXPORT_TOOLS.has(tool) && typeof filePath === "string" && ("data" in result || "pages" in result)) {
34824
35306
  try {
34825
35307
  const metadata = handleFileExport(filePath, result);
34826
- resolve3({
35308
+ resolve4({
34827
35309
  content: [{ type: "text", text: JSON.stringify(metadata) }]
34828
35310
  });
34829
35311
  } catch (e) {
34830
35312
  const msg = e instanceof Error ? e.message : String(e);
34831
- resolve3({
35313
+ resolve4({
34832
35314
  content: [
34833
35315
  {
34834
35316
  type: "text",
@@ -34847,9 +35329,9 @@ async function callExtensionTool(tool, input) {
34847
35329
  if (RESULT_EXPORT_TOOLS.has(tool) && typeof filePath === "string") {
34848
35330
  try {
34849
35331
  const content = JSON.stringify(result, null, 2);
34850
- mkdirSync3(dirname2(filePath), { recursive: true });
34851
- writeFileSync2(filePath, content, "utf-8");
34852
- resolve3({
35332
+ mkdirSync4(dirname2(filePath), { recursive: true });
35333
+ writeFileSync3(filePath, content, "utf-8");
35334
+ resolve4({
34853
35335
  content: [
34854
35336
  {
34855
35337
  type: "text",
@@ -34862,7 +35344,7 @@ async function callExtensionTool(tool, input) {
34862
35344
  });
34863
35345
  } catch (e) {
34864
35346
  const msg = e instanceof Error ? e.message : String(e);
34865
- resolve3({
35347
+ resolve4({
34866
35348
  content: [
34867
35349
  {
34868
35350
  type: "text",
@@ -34889,21 +35371,21 @@ async function callExtensionTool(tool, input) {
34889
35371
  }
34890
35372
  if (tempImportFile) {
34891
35373
  try {
34892
- unlinkSync2(tempImportFile);
35374
+ unlinkSync3(tempImportFile);
34893
35375
  } catch {
34894
35376
  }
34895
35377
  }
34896
- resolve3({ content: buildResponseContent(tool, finalResult) });
35378
+ resolve4({ content: buildResponseContent(tool, finalResult) });
34897
35379
  },
34898
35380
  reject: (error2) => {
34899
35381
  removeErrorListener();
34900
35382
  if (tempImportFile) {
34901
35383
  try {
34902
- unlinkSync2(tempImportFile);
35384
+ unlinkSync3(tempImportFile);
34903
35385
  } catch {
34904
35386
  }
34905
35387
  }
34906
- resolve3({
35388
+ resolve4({
34907
35389
  content: [
34908
35390
  {
34909
35391
  type: "text",
@@ -34937,13 +35419,17 @@ async function callExtensionTool(tool, input) {
34937
35419
  `[viyv-browser:mcp] tool_call id=${requestId} agent=${agentId} pid=${ctxLog?.claims?.pid ?? chromeProfile ?? "-"} tool=${tool}
34938
35420
  `
34939
35421
  );
34940
- const written = sock.write(`${JSON.stringify(request)}
34941
- `);
34942
- if (!written) {
34943
- sock.once("drain", () => {
35422
+ client.enqueueToolCall(`${JSON.stringify(request)}
35423
+ `).catch((rej) => {
35424
+ const pending = pendingRequests.get(requestId);
35425
+ if (!pending) return;
35426
+ clearTimeout(pending.timer);
35427
+ pendingRequests.delete(requestId);
35428
+ const { code, message: message2 } = toolCallRejectionToErrorCode(rej);
35429
+ resolve4({
35430
+ content: [{ type: "text", text: JSON.stringify({ error: { code, message: message2 } }) }]
34944
35431
  });
34945
- }
34946
- sock.once("error", onError);
35432
+ });
34947
35433
  });
34948
35434
  }
34949
35435
  async function handleSwitchBrowser() {
@@ -34958,19 +35444,18 @@ async function handleSwitchBrowser() {
34958
35444
  }
34959
35445
  });
34960
35446
  }
34961
- if (extensionSocket && !extensionSocket.destroyed) {
35447
+ if (bridgeClient) {
34962
35448
  process.stderr.write("[viyv-browser:mcp] switch_browser: closing current connection\n");
34963
- extensionSocket.destroy();
34964
- extensionSocket = null;
35449
+ bridgeClient.forceReconnect();
34965
35450
  setExtensionConnected(false);
34966
35451
  }
34967
35452
  process.stderr.write("[viyv-browser:mcp] switch_browser: waiting for bridge reconnection...\n");
34968
- return new Promise((resolve3) => {
35453
+ return new Promise((resolve4) => {
34969
35454
  const checkInterval = setInterval(() => {
34970
- if (extensionSocket && !extensionSocket.destroyed && isExtensionConnected()) {
35455
+ if (bridgeClient?.isReady() && isExtensionConnected()) {
34971
35456
  clearInterval(checkInterval);
34972
35457
  clearTimeout(timer);
34973
- resolve3({
35458
+ resolve4({
34974
35459
  content: [
34975
35460
  {
34976
35461
  type: "text",
@@ -34985,7 +35470,7 @@ async function handleSwitchBrowser() {
34985
35470
  }, 500);
34986
35471
  const timer = setTimeout(() => {
34987
35472
  clearInterval(checkInterval);
34988
- resolve3({
35473
+ resolve4({
34989
35474
  content: [
34990
35475
  {
34991
35476
  type: "text",
@@ -35004,9 +35489,9 @@ async function handleSwitchBrowser() {
35004
35489
 
35005
35490
  // src/setup.ts
35006
35491
  import { execSync } from "child_process";
35007
- import { chmodSync as chmodSync3, existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
35492
+ import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
35008
35493
  import { homedir as homedir2, platform } from "os";
35009
- import { dirname as dirname3, resolve as resolve2 } from "path";
35494
+ import { dirname as dirname3, resolve as resolve3 } from "path";
35010
35495
  function runSetup(options = {}) {
35011
35496
  const os = platform();
35012
35497
  console.log("Viyv Browser MCP - Native Messaging Host Setup");
@@ -35038,8 +35523,8 @@ function runSetup(options = {}) {
35038
35523
  console.log(`Wrapper: ${wrapperPath}`);
35039
35524
  for (const manifestPath of manifestPaths) {
35040
35525
  console.log(`Manifest path: ${manifestPath}`);
35041
- mkdirSync4(dirname3(manifestPath), { recursive: true });
35042
- writeFileSync3(manifestPath, JSON.stringify(manifest, null, 2));
35526
+ mkdirSync5(dirname3(manifestPath), { recursive: true });
35527
+ writeFileSync4(manifestPath, JSON.stringify(manifest, null, 2));
35043
35528
  chmodSync3(manifestPath, 420);
35044
35529
  }
35045
35530
  console.log("\nNative Messaging Host registered successfully!");
@@ -35053,17 +35538,17 @@ function runSetup(options = {}) {
35053
35538
  }
35054
35539
  function createNativeHostWrapper(os, binaryPath, dockerMode) {
35055
35540
  const manifestDir = dockerMode ? "/etc/chromium/native-messaging-hosts" : dirname3(getManifestPath(os));
35056
- mkdirSync4(manifestDir, { recursive: true });
35541
+ mkdirSync5(manifestDir, { recursive: true });
35057
35542
  const nodePath = getNodePath();
35058
35543
  if (os === "win32") {
35059
- const wrapperPath2 = resolve2(manifestDir, `${NATIVE_HOST_NAME}.bat`);
35060
- writeFileSync3(wrapperPath2, `@echo off\r
35544
+ const wrapperPath2 = resolve3(manifestDir, `${NATIVE_HOST_NAME}.bat`);
35545
+ writeFileSync4(wrapperPath2, `@echo off\r
35061
35546
  "${nodePath}" "${binaryPath}" --native-host\r
35062
35547
  `);
35063
35548
  return wrapperPath2;
35064
35549
  }
35065
- const wrapperPath = resolve2(manifestDir, `${NATIVE_HOST_NAME}.sh`);
35066
- writeFileSync3(wrapperPath, `#!/bin/bash
35550
+ const wrapperPath = resolve3(manifestDir, `${NATIVE_HOST_NAME}.sh`);
35551
+ writeFileSync4(wrapperPath, `#!/bin/bash
35067
35552
  exec "${nodePath}" "${binaryPath}" --native-host
35068
35553
  `);
35069
35554
  chmodSync3(wrapperPath, 493);
@@ -35078,23 +35563,23 @@ function getNodePath() {
35078
35563
  }
35079
35564
  function getDockerManifestPaths() {
35080
35565
  return [
35081
- resolve2("/etc/chromium/native-messaging-hosts", `${NATIVE_HOST_NAME}.json`),
35082
- resolve2(homedir2(), ".config/chromium/NativeMessagingHosts", `${NATIVE_HOST_NAME}.json`)
35566
+ resolve3("/etc/chromium/native-messaging-hosts", `${NATIVE_HOST_NAME}.json`),
35567
+ resolve3(homedir2(), ".config/chromium/NativeMessagingHosts", `${NATIVE_HOST_NAME}.json`)
35083
35568
  ];
35084
35569
  }
35085
35570
  function getManifestPath(os) {
35086
35571
  const home = homedir2();
35087
35572
  switch (os) {
35088
35573
  case "darwin":
35089
- return resolve2(
35574
+ return resolve3(
35090
35575
  home,
35091
35576
  "Library/Application Support/Google/Chrome/NativeMessagingHosts",
35092
35577
  `${NATIVE_HOST_NAME}.json`
35093
35578
  );
35094
35579
  case "linux":
35095
- return resolve2(home, ".config/google-chrome/NativeMessagingHosts", `${NATIVE_HOST_NAME}.json`);
35580
+ return resolve3(home, ".config/google-chrome/NativeMessagingHosts", `${NATIVE_HOST_NAME}.json`);
35096
35581
  case "win32":
35097
- return resolve2(
35582
+ return resolve3(
35098
35583
  home,
35099
35584
  "AppData/Local/Google/Chrome/User Data/NativeMessagingHosts",
35100
35585
  `${NATIVE_HOST_NAME}.json`
@@ -35114,9 +35599,9 @@ function setupClaudeDesktopConfig() {
35114
35599
  const configPath = getClaudeDesktopConfigPath();
35115
35600
  console.log(`Config path: ${configPath}`);
35116
35601
  let config2 = {};
35117
- if (existsSync3(configPath)) {
35602
+ if (existsSync4(configPath)) {
35118
35603
  try {
35119
- config2 = JSON.parse(readFileSync5(configPath, "utf-8"));
35604
+ config2 = JSON.parse(readFileSync6(configPath, "utf-8"));
35120
35605
  } catch {
35121
35606
  console.error(
35122
35607
  `ERROR: Failed to parse ${configPath}. Fix the JSON manually or delete the file.`
@@ -35136,8 +35621,8 @@ function setupClaudeDesktopConfig() {
35136
35621
  env: { PATH: envPath }
35137
35622
  };
35138
35623
  const configDir = dirname3(configPath);
35139
- mkdirSync4(configDir, { recursive: true });
35140
- writeFileSync3(configPath, JSON.stringify(config2, null, 2));
35624
+ mkdirSync5(configDir, { recursive: true });
35625
+ writeFileSync4(configPath, JSON.stringify(config2, null, 2));
35141
35626
  console.log(`npx path: ${npxPath}`);
35142
35627
  console.log("\nClaude Desktop config updated successfully!");
35143
35628
  console.log("Please restart Claude Desktop for changes to take effect.");
@@ -35154,13 +35639,13 @@ function getClaudeDesktopConfigPath() {
35154
35639
  const home = homedir2();
35155
35640
  switch (process.platform) {
35156
35641
  case "darwin":
35157
- return resolve2(home, "Library/Application Support/Claude/claude_desktop_config.json");
35642
+ return resolve3(home, "Library/Application Support/Claude/claude_desktop_config.json");
35158
35643
  case "linux":
35159
- return resolve2(home, ".config/Claude/claude_desktop_config.json");
35644
+ return resolve3(home, ".config/Claude/claude_desktop_config.json");
35160
35645
  case "win32":
35161
- return resolve2(home, "AppData/Roaming/Claude/claude_desktop_config.json");
35646
+ return resolve3(home, "AppData/Roaming/Claude/claude_desktop_config.json");
35162
35647
  default:
35163
- return resolve2(home, ".config/Claude/claude_desktop_config.json");
35648
+ return resolve3(home, ".config/Claude/claude_desktop_config.json");
35164
35649
  }
35165
35650
  }
35166
35651
 
@@ -35202,7 +35687,18 @@ if (args.includes("setup")) {
35202
35687
  const authBearer = authIdx >= 0 ? args[authIdx + 1] : process.env.VIYV_MCP_AUTH_BEARER;
35203
35688
  if (isNativeHostDeployed()) {
35204
35689
  try {
35205
- syncNativeHostBinary();
35690
+ const decision = syncNativeHostBinaryIfNeeded();
35691
+ if (decision.shouldSync) {
35692
+ process.stderr.write(
35693
+ `[viyv-browser:mcp] Native host binary synced (self=${decision.selfVersion}, was=${decision.deployedVersion ?? "unknown"}, reason=${decision.reason})
35694
+ `
35695
+ );
35696
+ } else {
35697
+ process.stderr.write(
35698
+ `[viyv-browser:mcp] Native host sync skipped \u2014 deployed v${decision.deployedVersion} is newer than self v${decision.selfVersion} (preventing downgrade by orphan MCP)
35699
+ `
35700
+ );
35701
+ }
35206
35702
  } catch (err) {
35207
35703
  process.stderr.write(
35208
35704
  `[viyv-browser:mcp] WARNING: Failed to update native host binary: ${err.message}