ofw-mcp 2.2.0 → 2.3.1

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/bundle.js CHANGED
@@ -7395,14 +7395,14 @@ var require_permessage_deflate = __commonJS({
7395
7395
  }
7396
7396
  };
7397
7397
  module.exports = PerMessageDeflate2;
7398
- function deflateOnData(chunk) {
7399
- this[kBuffers].push(chunk);
7400
- this[kTotalLength] += chunk.length;
7398
+ function deflateOnData(chunk2) {
7399
+ this[kBuffers].push(chunk2);
7400
+ this[kTotalLength] += chunk2.length;
7401
7401
  }
7402
- function inflateOnData(chunk) {
7403
- this[kTotalLength] += chunk.length;
7402
+ function inflateOnData(chunk2) {
7403
+ this[kTotalLength] += chunk2.length;
7404
7404
  if (this[kPerMessageDeflate]._maxPayload < 1 || this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload) {
7405
- this[kBuffers].push(chunk);
7405
+ this[kBuffers].push(chunk2);
7406
7406
  return;
7407
7407
  }
7408
7408
  this[kError] = new RangeError("Max payload size exceeded");
@@ -7702,7 +7702,7 @@ var require_receiver = __commonJS({
7702
7702
  * @param {Function} cb Callback
7703
7703
  * @private
7704
7704
  */
7705
- _write(chunk, encoding, cb) {
7705
+ _write(chunk2, encoding, cb) {
7706
7706
  if (this._opcode === 8 && this._state == GET_INFO) return cb();
7707
7707
  if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
7708
7708
  cb(
@@ -7716,8 +7716,8 @@ var require_receiver = __commonJS({
7716
7716
  );
7717
7717
  return;
7718
7718
  }
7719
- this._bufferedBytes += chunk.length;
7720
- this._buffers.push(chunk);
7719
+ this._bufferedBytes += chunk2.length;
7720
+ this._buffers.push(chunk2);
7721
7721
  this.startLoop(cb);
7722
7722
  }
7723
7723
  /**
@@ -9991,8 +9991,8 @@ var require_websocket = __commonJS({
9991
9991
  this.removeListener("end", socketOnEnd);
9992
9992
  websocket._readyState = WebSocket2.CLOSING;
9993
9993
  if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) {
9994
- const chunk = this.read(this._readableState.length);
9995
- websocket._receiver.write(chunk);
9994
+ const chunk2 = this.read(this._readableState.length);
9995
+ websocket._receiver.write(chunk2);
9996
9996
  }
9997
9997
  websocket._receiver.end();
9998
9998
  this[kWebSocket] = void 0;
@@ -10004,8 +10004,8 @@ var require_websocket = __commonJS({
10004
10004
  websocket._receiver.on("finish", receiverOnFinish);
10005
10005
  }
10006
10006
  }
10007
- function socketOnData(chunk) {
10008
- if (!this[kWebSocket]._receiver.write(chunk)) {
10007
+ function socketOnData(chunk2) {
10008
+ if (!this[kWebSocket]._receiver.write(chunk2)) {
10009
10009
  this.pause();
10010
10010
  }
10011
10011
  }
@@ -10108,14 +10108,14 @@ var require_stream = __commonJS({
10108
10108
  duplex._read = function() {
10109
10109
  if (ws.isPaused) ws.resume();
10110
10110
  };
10111
- duplex._write = function(chunk, encoding, callback) {
10111
+ duplex._write = function(chunk2, encoding, callback) {
10112
10112
  if (ws.readyState === ws.CONNECTING) {
10113
10113
  ws.once("open", function open() {
10114
- duplex._write(chunk, encoding, callback);
10114
+ duplex._write(chunk2, encoding, callback);
10115
10115
  });
10116
10116
  return;
10117
10117
  }
10118
- ws.send(chunk, callback);
10118
+ ws.send(chunk2, callback);
10119
10119
  };
10120
10120
  duplex.on("end", duplexOnEnd);
10121
10121
  duplex.on("error", duplexOnError);
@@ -10342,10 +10342,10 @@ var require_websocket_server = __commonJS({
10342
10342
  process.nextTick(emitClose, this);
10343
10343
  }
10344
10344
  } else {
10345
- const server2 = this._server;
10345
+ const server = this._server;
10346
10346
  this._removeListeners();
10347
10347
  this._removeListeners = this._server = null;
10348
- server2.close(() => {
10348
+ server.close(() => {
10349
10349
  emitClose(this);
10350
10350
  });
10351
10351
  }
@@ -10530,17 +10530,17 @@ var require_websocket_server = __commonJS({
10530
10530
  }
10531
10531
  };
10532
10532
  module.exports = WebSocketServer2;
10533
- function addListeners(server2, map2) {
10534
- for (const event of Object.keys(map2)) server2.on(event, map2[event]);
10533
+ function addListeners(server, map2) {
10534
+ for (const event of Object.keys(map2)) server.on(event, map2[event]);
10535
10535
  return function removeListeners() {
10536
10536
  for (const event of Object.keys(map2)) {
10537
- server2.removeListener(event, map2[event]);
10537
+ server.removeListener(event, map2[event]);
10538
10538
  }
10539
10539
  };
10540
10540
  }
10541
- function emitClose(server2) {
10542
- server2._state = CLOSED;
10543
- server2.emit("close");
10541
+ function emitClose(server) {
10542
+ server._state = CLOSED;
10543
+ server.emit("close");
10544
10544
  }
10545
10545
  function socketOnError() {
10546
10546
  this.destroy();
@@ -10559,11 +10559,11 @@ var require_websocket_server = __commonJS({
10559
10559
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
10560
10560
  );
10561
10561
  }
10562
- function abortHandshakeOrEmitwsClientError(server2, req, socket, code, message, headers) {
10563
- if (server2.listenerCount("wsClientError")) {
10562
+ function abortHandshakeOrEmitwsClientError(server, req, socket, code, message, headers) {
10563
+ if (server.listenerCount("wsClientError")) {
10564
10564
  const err = new Error(message);
10565
10565
  Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
10566
- server2.emit("wsClientError", err, socket, req);
10566
+ server.emit("wsClientError", err, socket, req);
10567
10567
  } else {
10568
10568
  abortHandshake(socket, code, message, headers);
10569
10569
  }
@@ -32189,11 +32189,11 @@ var Protocol = class {
32189
32189
  *
32190
32190
  * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
32191
32191
  */
32192
- async connect(transport2) {
32192
+ async connect(transport) {
32193
32193
  if (this._transport) {
32194
32194
  throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
32195
32195
  }
32196
- this._transport = transport2;
32196
+ this._transport = transport;
32197
32197
  const _onclose = this.transport?.onclose;
32198
32198
  this._transport.onclose = () => {
32199
32199
  _onclose?.();
@@ -33783,8 +33783,8 @@ var McpServer = class {
33783
33783
  *
33784
33784
  * The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
33785
33785
  */
33786
- async connect(transport2) {
33787
- return await this.server.connect(transport2);
33786
+ async connect(transport) {
33787
+ return await this.server.connect(transport);
33788
33788
  }
33789
33789
  /**
33790
33790
  * Closes the connection.
@@ -34547,8 +34547,8 @@ import process3 from "node:process";
34547
34547
 
34548
34548
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
34549
34549
  var ReadBuffer = class {
34550
- append(chunk) {
34551
- this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
34550
+ append(chunk2) {
34551
+ this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
34552
34552
  }
34553
34553
  readMessage() {
34554
34554
  if (!this._buffer) {
@@ -34580,8 +34580,8 @@ var StdioServerTransport = class {
34580
34580
  this._stdout = _stdout;
34581
34581
  this._readBuffer = new ReadBuffer();
34582
34582
  this._started = false;
34583
- this._ondata = (chunk) => {
34584
- this._readBuffer.append(chunk);
34583
+ this._ondata = (chunk2) => {
34584
+ this._readBuffer.append(chunk2);
34585
34585
  this.processReadBuffer();
34586
34586
  };
34587
34587
  this._onerror = (error51) => {
@@ -34634,8 +34634,152 @@ var StdioServerTransport = class {
34634
34634
  }
34635
34635
  };
34636
34636
 
34637
+ // node_modules/@chrischall/mcp-utils/dist/server/index.js
34638
+ async function createMcpServer(opts) {
34639
+ const server = new McpServer({ name: opts.name, version: opts.version });
34640
+ if (opts.banner !== void 0) {
34641
+ console.error(opts.banner);
34642
+ }
34643
+ const deps = opts.deps;
34644
+ for (const register of opts.tools) {
34645
+ await register(server, deps);
34646
+ }
34647
+ return server;
34648
+ }
34649
+ function withGracefulShutdown(server, opts = {}) {
34650
+ const shouldExit = opts.exit ?? true;
34651
+ let shuttingDown = false;
34652
+ const handler = (signal) => {
34653
+ if (shuttingDown)
34654
+ return;
34655
+ shuttingDown = true;
34656
+ void (async () => {
34657
+ try {
34658
+ if (opts.onSignal)
34659
+ await opts.onSignal(signal);
34660
+ await server.close();
34661
+ } catch (err) {
34662
+ console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34663
+ } finally {
34664
+ if (shouldExit)
34665
+ process.exit(0);
34666
+ }
34667
+ })();
34668
+ };
34669
+ process.on("SIGINT", () => handler("SIGINT"));
34670
+ process.on("SIGTERM", () => handler("SIGTERM"));
34671
+ }
34672
+ async function runMcp(opts) {
34673
+ const server = await createMcpServer(opts);
34674
+ const shutdown = opts.shutdown ?? true;
34675
+ if (shutdown !== false) {
34676
+ withGracefulShutdown(server, shutdown === true ? {} : shutdown);
34677
+ }
34678
+ const spec = opts.transport ?? "stdio";
34679
+ const transport = spec === "stdio" ? new StdioServerTransport() : spec;
34680
+ await server.connect(transport);
34681
+ return server;
34682
+ }
34683
+
34684
+ // node_modules/@chrischall/mcp-utils/dist/response/index.js
34685
+ function textResult(data) {
34686
+ return {
34687
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
34688
+ };
34689
+ }
34690
+ function rawTextResult(text) {
34691
+ return { content: [{ type: "text", text }] };
34692
+ }
34693
+
34694
+ // node_modules/@chrischall/mcp-utils/dist/config/index.js
34695
+ import { homedir } from "node:os";
34696
+ import { isAbsolute, join, resolve } from "node:path";
34697
+ var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
34698
+ function readEnvVar(key, opts = {}) {
34699
+ const env = opts.env ?? process.env;
34700
+ const raw = env[key];
34701
+ if (typeof raw === "string") {
34702
+ const trimmed = raw.trim();
34703
+ if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
34704
+ return trimmed;
34705
+ }
34706
+ }
34707
+ return opts.default;
34708
+ }
34709
+ var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
34710
+ var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
34711
+ function parseBoolEnv(key, opts = {}) {
34712
+ const fallback = opts.default ?? false;
34713
+ const raw = readEnvVar(key, { env: opts.env });
34714
+ if (raw === void 0)
34715
+ return fallback;
34716
+ const token = raw.toLowerCase();
34717
+ if (TRUE_TOKENS.has(token))
34718
+ return true;
34719
+ if (FALSE_TOKENS.has(token))
34720
+ return false;
34721
+ return fallback;
34722
+ }
34723
+ function expandPath(p) {
34724
+ let expanded = p;
34725
+ if (p === "~") {
34726
+ expanded = homedir();
34727
+ } else if (p.startsWith("~/")) {
34728
+ expanded = join(homedir(), p.slice(2));
34729
+ }
34730
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
34731
+ }
34732
+ async function loadDotenvSafely(opts = {}) {
34733
+ try {
34734
+ const mod = await import(
34735
+ /* @vite-ignore */
34736
+ "dotenv"
34737
+ );
34738
+ const result = mod.config({
34739
+ ...opts.path !== void 0 ? { path: opts.path } : {},
34740
+ override: opts.override ?? false,
34741
+ quiet: true
34742
+ });
34743
+ return result.error === void 0;
34744
+ } catch {
34745
+ return false;
34746
+ }
34747
+ }
34748
+
34749
+ // node_modules/@chrischall/mcp-utils/dist/fs/index.js
34750
+ import { openAsBlob } from "node:fs";
34751
+ async function fileBlob(path, opts = {}) {
34752
+ let blob;
34753
+ try {
34754
+ blob = await openAsBlob(path, opts.type !== void 0 ? { type: opts.type } : void 0);
34755
+ } catch {
34756
+ throw new Error(`Cannot read file for upload: ${path}`);
34757
+ }
34758
+ if (opts.maxBytes !== void 0 && blob.size > opts.maxBytes) {
34759
+ throw new Error(`${opts.label ?? "File"} is ${blob.size} bytes, over the ${opts.maxBytes}-byte limit: ${path}`);
34760
+ }
34761
+ return blob;
34762
+ }
34763
+
34764
+ // node_modules/@chrischall/mcp-utils/dist/zod/index.js
34765
+ var PositiveInt = external_exports.number().int().positive();
34766
+ var NonNegInt = external_exports.number().int().nonnegative();
34767
+ var NonEmptyString = external_exports.string().min(1);
34768
+ var IsoDate = external_exports.iso.date();
34769
+ var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
34770
+ var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
34771
+ var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
34772
+ var paginationSchema = {
34773
+ offset: NonNegInt.default(0).describe("Number of items to skip (0-based)."),
34774
+ limit: external_exports.number().int().min(1).max(200).default(50).describe("Maximum number of items to return (1-200).")
34775
+ };
34776
+ var pageSchema = {
34777
+ page_num: PositiveInt.default(1).describe("1-based page number."),
34778
+ page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
34779
+ };
34780
+
34637
34781
  // src/client.ts
34638
- import { dirname, join as join3 } from "path";
34782
+ import { dirname, join as join4 } from "path";
34639
34783
  import { fileURLToPath } from "url";
34640
34784
 
34641
34785
  // node_modules/@fetchproxy/protocol/dist/frames.js
@@ -35494,13 +35638,13 @@ async function openEncryptedFrame(sessionKey, frame) {
35494
35638
  // node_modules/@fetchproxy/server/dist/election.js
35495
35639
  import { createServer, Server as HttpServer } from "node:http";
35496
35640
  async function electRole(opts) {
35497
- const server2 = createServer();
35641
+ const server = createServer();
35498
35642
  return new Promise((resolve2, reject) => {
35499
35643
  const onError = (e) => {
35500
- server2.removeListener("listening", onListening);
35644
+ server.removeListener("listening", onListening);
35501
35645
  if (e.code === "EADDRINUSE") {
35502
35646
  try {
35503
- server2.close();
35647
+ server.close();
35504
35648
  } catch {
35505
35649
  }
35506
35650
  resolve2({ role: "peer" });
@@ -35509,12 +35653,12 @@ async function electRole(opts) {
35509
35653
  }
35510
35654
  };
35511
35655
  const onListening = () => {
35512
- server2.removeListener("error", onError);
35513
- resolve2({ role: "host", server: server2 });
35656
+ server.removeListener("error", onError);
35657
+ resolve2({ role: "host", server });
35514
35658
  };
35515
- server2.once("error", onError);
35516
- server2.once("listening", onListening);
35517
- server2.listen(opts.port, opts.host);
35659
+ server.once("error", onError);
35660
+ server.once("listening", onListening);
35661
+ server.listen(opts.port, opts.host);
35518
35662
  });
35519
35663
  }
35520
35664
 
@@ -35924,19 +36068,19 @@ async function startPeer(opts) {
35924
36068
 
35925
36069
  // node_modules/@fetchproxy/server/dist/identity.js
35926
36070
  import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
35927
- import { join } from "node:path";
35928
- import { homedir } from "node:os";
36071
+ import { join as join2 } from "node:path";
36072
+ import { homedir as homedir2 } from "node:os";
35929
36073
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
35930
36074
  var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
35931
36075
  function defaultIdentityDir() {
35932
- return join(homedir(), ".fetchproxy", "identity");
36076
+ return join2(homedir2(), ".fetchproxy", "identity");
35933
36077
  }
35934
36078
  async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
35935
36079
  if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
35936
36080
  throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
35937
36081
  }
35938
36082
  const safeFile = serverName.replace(/\//g, "_");
35939
- const path = join(dir, `${safeFile}.json`);
36083
+ const path = join2(dir, `${safeFile}.json`);
35940
36084
  await mkdir(dir, { recursive: true, mode: 448 });
35941
36085
  try {
35942
36086
  const raw = await readFile(path, "utf8");
@@ -35999,6 +36143,19 @@ function classifyFetchError(error51) {
35999
36143
  return "other";
36000
36144
  }
36001
36145
 
36146
+ // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
36147
+ function classifyBridgeError(err) {
36148
+ if (err instanceof FetchproxyTimeoutError)
36149
+ return "timeout";
36150
+ if (err instanceof FetchproxyBridgeDownError)
36151
+ return "bridge_down";
36152
+ if (err instanceof FetchproxyHttpError)
36153
+ return "http";
36154
+ if (err instanceof FetchproxyProtocolError)
36155
+ return "protocol";
36156
+ return "other";
36157
+ }
36158
+
36002
36159
  // node_modules/@fetchproxy/server/dist/ws-server.js
36003
36160
  var FetchproxyProtocolError = class extends Error {
36004
36161
  constructor(message) {
@@ -36028,7 +36185,7 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
36028
36185
  const retryAttempted = args.retryAttempted ?? false;
36029
36186
  const op = args.op ?? "fetch";
36030
36187
  const retryClause = retryAttempted ? `Server already burned a one-shot lazy-revive retry; SW is still down. ` : `Server lazy-revive retry was disabled (bridgeReviveDelayMs unset/0). `;
36031
- const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Wake it by clicking the fetchproxy extension toolbar icon, then retry. If it keeps happening, reload the extension from chrome://extensions.`;
36188
+ const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Make sure a tab for this domain is open, fully loaded, and signed in (the bridge fetches through that tab) \u2014 then retry. If it keeps happening, reload the extension from chrome://extensions and reload the tab.`;
36032
36189
  super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
36033
36190
  this.name = "FetchproxyBridgeDownError";
36034
36191
  this.originalError = args.originalError;
@@ -36050,6 +36207,14 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36050
36207
  port;
36051
36208
  /** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
36052
36209
  elapsedMs;
36210
+ /**
36211
+ * 0.11.0+ (#90/#91): true when the server's lazy-revive retry path
36212
+ * fired for this timeout (a cold-start `timeout` symptom followed by
36213
+ * a warm-and-retry that also timed out). False when the retry was
36214
+ * disabled (`bridgeReviveDelayMs` unset/0) so the timeout surfaced on
36215
+ * the first attempt.
36216
+ */
36217
+ retryAttempted;
36053
36218
  constructor(args) {
36054
36219
  super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
36055
36220
  this.name = "FetchproxyTimeoutError";
@@ -36058,6 +36223,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36058
36223
  this.role = args.role ?? null;
36059
36224
  this.port = args.port ?? 0;
36060
36225
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
36226
+ this.retryAttempted = args.retryAttempted ?? false;
36061
36227
  }
36062
36228
  };
36063
36229
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -36131,6 +36297,25 @@ var FetchproxyServer = class {
36131
36297
  // for "we're connecting right now" so two parallel first-calls don't
36132
36298
  // race the port bind.
36133
36299
  connectingPromise = null;
36300
+ // 0.8.1+ (#67): server-initiated keep-alive ping. Active when
36301
+ // `keepAliveIntervalMs` is set AND we've seen recent activity
36302
+ // (fetch/capture success or failure, or markActive()) within
36303
+ // `keepAliveMaxIdleMs`. The interval handle is created lazily on
36304
+ // first activity and torn down on close() / extension disconnect.
36305
+ keepAliveTimer = null;
36306
+ lastActiveAt = null;
36307
+ // 0.10.0+ (#73): observability counters surfaced via
36308
+ // bridgeHealth().keepAlive / .swEviction. Monotonic across the process
36309
+ // lifetime so a downstream healthcheck tool can verify the keep-alive
36310
+ // is actually preventing SW eviction. `lastPingAt` and `totalPings`
36311
+ // are stamped from `startKeepaliveIfIdle`'s tick. `lazyRevive*` and
36312
+ // `lastEvictionDetectedAt` are stamped from the lazy-revive code path
36313
+ // in fetch() / captureRequestHeader().
36314
+ lastPingAt = null;
36315
+ totalPings = 0;
36316
+ lazyReviveAttempts = 0;
36317
+ lazyReviveSuccesses = 0;
36318
+ lastEvictionDetectedAt = null;
36134
36319
  constructor(opts) {
36135
36320
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
36136
36321
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
@@ -36183,6 +36368,22 @@ var FetchproxyServer = class {
36183
36368
  // the legacy hang-forever / fail-once-on-SW-eviction behavior.
36184
36369
  fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
36185
36370
  bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
36371
+ // 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
36372
+ // wave showed every Pattern A consumer was opting into this same
36373
+ // value. Pass `0` to disable; the existing `<= 0` guards in
36374
+ // `startKeepaliveIfIdle` / `noteActivityForKeepalive` honour that.
36375
+ //
36376
+ // #90 (P1-1): tightened to 20s. 25s left only ~5s of slack under
36377
+ // Chrome's ~30s SW-eviction window — slack that timer drift, a
36378
+ // busy host event loop (CPU-bound response parsing between calls),
36379
+ // and the ping's own round-trip latency routinely ate, so the SW
36380
+ // evicted before the next ping landed and the next call cold-
36381
+ // started. 20s restores real margin. (The extension
36382
+ // `chrome.alarms` backstop is clamped by Chrome to a 30s minimum
36383
+ // period, firing *at* the edge — it can't rescue a sub-30s race;
36384
+ // the server ping is the real defense.)
36385
+ keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
36386
+ keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
36186
36387
  identityDir: opts.identityDir,
36187
36388
  onPairCode: opts.onPairCode
36188
36389
  };
@@ -36283,7 +36484,10 @@ var FetchproxyServer = class {
36283
36484
  onPairCode: this.opts.onPairCode
36284
36485
  });
36285
36486
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
36286
- this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
36487
+ this.hostHandle.onExtensionDisconnect(() => {
36488
+ this.stopKeepalive();
36489
+ this.rejectAllPending();
36490
+ });
36287
36491
  this.hostHandle.onPendingPair((code) => {
36288
36492
  this.rejectAllPending(this.pairingErrorMessage(code));
36289
36493
  });
@@ -36307,7 +36511,10 @@ var FetchproxyServer = class {
36307
36511
  sessionStoragePointers: this.opts.sessionStoragePointers
36308
36512
  });
36309
36513
  this.peerHandle.onInner((inner) => this.onInner(inner));
36310
- this.peerHandle.onRenegotiate(() => this.rejectAllPending());
36514
+ this.peerHandle.onRenegotiate(() => {
36515
+ this.stopKeepalive();
36516
+ this.rejectAllPending();
36517
+ });
36311
36518
  this.peerHandle.onPendingPair((code) => {
36312
36519
  this.rejectAllPending(this.pairingErrorMessage(code));
36313
36520
  });
@@ -36348,13 +36555,18 @@ var FetchproxyServer = class {
36348
36555
  }
36349
36556
  const first = await this._fetchOnceWithTimeout(init);
36350
36557
  const reviveMs = this.opts.bridgeReviveDelayMs;
36351
- let final = first;
36352
- if (!first.ok && first.kind === "content_script_unreachable" && reviveMs !== void 0 && reviveMs > 0) {
36558
+ const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
36559
+ if (isColdStartSymptom) {
36560
+ this.lastEvictionDetectedAt = Date.now();
36561
+ }
36562
+ if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
36563
+ this.lazyReviveAttempts += 1;
36353
36564
  await new Promise((r) => setTimeout(r, reviveMs));
36354
36565
  const second = await this._fetchOnceWithTimeout(init);
36355
- if (second.ok)
36566
+ if (second.ok) {
36567
+ this.lazyReviveSuccesses += 1;
36356
36568
  this.recordSuccess();
36357
- else
36569
+ } else
36358
36570
  this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
36359
36571
  return { ...second, retryAttempted: true };
36360
36572
  }
@@ -36376,6 +36588,9 @@ var FetchproxyServer = class {
36376
36588
  * call (addresses #23 ask 4).
36377
36589
  */
36378
36590
  bridgeHealth() {
36591
+ const intervalMs = this.opts.keepAliveIntervalMs;
36592
+ const maxIdleMs = this.opts.keepAliveMaxIdleMs;
36593
+ const idleSinceMs = this.lastActiveAt === null ? null : Date.now() - this.lastActiveAt;
36379
36594
  return {
36380
36595
  role: this.role,
36381
36596
  port: this.opts.port,
@@ -36386,17 +36601,85 @@ var FetchproxyServer = class {
36386
36601
  lastFailureAt: this.lastFailureAt,
36387
36602
  lastFailureReason: this.lastFailureReason,
36388
36603
  consecutiveFailures: this.consecutiveFailures,
36389
- lastExtensionMessageAt: this.lastExtensionMessageAt
36604
+ lastExtensionMessageAt: this.lastExtensionMessageAt,
36605
+ keepAlive: {
36606
+ enabled: intervalMs > 0,
36607
+ intervalMs,
36608
+ maxIdleMs,
36609
+ lastPingAt: this.lastPingAt,
36610
+ totalPings: this.totalPings,
36611
+ idleSinceMs
36612
+ },
36613
+ swEviction: {
36614
+ lazyReviveAttempts: this.lazyReviveAttempts,
36615
+ lazyReviveSuccesses: this.lazyReviveSuccesses,
36616
+ lastEvictionDetectedAt: this.lastEvictionDetectedAt
36617
+ }
36390
36618
  };
36391
36619
  }
36392
36620
  recordSuccess() {
36393
36621
  this.lastSuccessAt = Date.now();
36394
36622
  this.consecutiveFailures = 0;
36623
+ this.noteActivityForKeepalive();
36395
36624
  }
36396
36625
  recordFailure(reason) {
36397
36626
  this.lastFailureAt = Date.now();
36398
36627
  this.lastFailureReason = reason;
36399
36628
  this.consecutiveFailures += 1;
36629
+ this.noteActivityForKeepalive();
36630
+ }
36631
+ /**
36632
+ * 0.8.1+ (#67): caller-side hint that work is happening or about to
36633
+ * happen — bumps the keep-alive idle gate so the server keeps pinging
36634
+ * the extension. Useful for MCPs that do a chain of side-effectful
36635
+ * work between bridge calls and don't want the SW to evict in the
36636
+ * gap (e.g. server-side parsing of a previous response that takes
36637
+ * tens of seconds). No-op when `keepAliveIntervalMs` is `0`.
36638
+ */
36639
+ markActive() {
36640
+ this.noteActivityForKeepalive();
36641
+ }
36642
+ noteActivityForKeepalive() {
36643
+ const intervalMs = this.opts.keepAliveIntervalMs;
36644
+ if (intervalMs <= 0)
36645
+ return;
36646
+ this.lastActiveAt = Date.now();
36647
+ this.startKeepaliveIfIdle();
36648
+ }
36649
+ startKeepaliveIfIdle() {
36650
+ if (this.keepAliveTimer !== null)
36651
+ return;
36652
+ const intervalMs = this.opts.keepAliveIntervalMs;
36653
+ if (intervalMs <= 0)
36654
+ return;
36655
+ this.keepAliveTimer = setInterval(() => {
36656
+ const now = Date.now();
36657
+ if (this.lastActiveAt === null || now - this.lastActiveAt > this.opts.keepAliveMaxIdleMs) {
36658
+ this.stopKeepalive();
36659
+ return;
36660
+ }
36661
+ this.totalPings += 1;
36662
+ this.lastPingAt = now;
36663
+ void this.sendKeepalivePing();
36664
+ }, intervalMs);
36665
+ }
36666
+ async sendKeepalivePing() {
36667
+ try {
36668
+ const inner = { type: "ping" };
36669
+ if (this.hostHandle) {
36670
+ await this.hostHandle.sendOwnInner(inner);
36671
+ } else if (this.peerHandle) {
36672
+ await this.peerHandle.sendInner(inner);
36673
+ }
36674
+ } catch (e) {
36675
+ console.error("[fetchproxy] keepalive ping send failed:", e);
36676
+ }
36677
+ }
36678
+ stopKeepalive() {
36679
+ if (this.keepAliveTimer !== null) {
36680
+ clearInterval(this.keepAliveTimer);
36681
+ this.keepAliveTimer = null;
36682
+ }
36400
36683
  }
36401
36684
  /**
36402
36685
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
@@ -36455,7 +36738,8 @@ var FetchproxyServer = class {
36455
36738
  timeoutMs: this.opts.fetchTimeoutMs ?? 0,
36456
36739
  role: this.role,
36457
36740
  port: this.opts.port,
36458
- elapsedMs: result.elapsedMs
36741
+ elapsedMs: result.elapsedMs,
36742
+ retryAttempted
36459
36743
  });
36460
36744
  }
36461
36745
  if (result.kind === "content_script_unreachable") {
@@ -36586,6 +36870,108 @@ var FetchproxyServer = class {
36586
36870
  const response = await this.get(path, this.applyJsonDefaults(opts));
36587
36871
  return response.body;
36588
36872
  }
36873
+ /**
36874
+ * 0.11.0+: method-generic JSON convenience helper. Generalizes the
36875
+ * `fetchJson<T>(path, { method, headers, body })` that
36876
+ * zillow/redfin/compass/homes hand-rolled char-for-char in their
36877
+ * `src/client.ts`:
36878
+ *
36879
+ * - sets `Accept: application/json`;
36880
+ * - adds `Content-Type: application/json` only for a non-GET request
36881
+ * that carries a `body` (and only if the caller didn't set one);
36882
+ * - `JSON.stringify`s the body (GET / no-body sends nothing);
36883
+ * - treats a `204` or an empty body as `data: null` (no parse);
36884
+ * - otherwise `JSON.parse`s the body.
36885
+ *
36886
+ * Scope is serialization + header defaults + 204-handling +
36887
+ * JSON.parse ONLY. It deliberately does NOT assert on the HTTP status
36888
+ * or look for a sign-in interstitial — those guards differ per site
36889
+ * (Zillow's `captcha-delivery`, Redfin's AWS-WAF challenge, …), so it
36890
+ * returns BOTH the parsed `data` and the raw `FetchResult` and leaves
36891
+ * the consumer to run its own `throwIfNotOk` / `throwIfSignInPage`
36892
+ * over `result`.
36893
+ *
36894
+ * Bridge-level failures (no signed-in tab, SW down, timeout) still
36895
+ * throw the typed errors via `request()`, exactly like the verb
36896
+ * helpers — only successful round-trips (any HTTP status) return.
36897
+ */
36898
+ async requestJson(method, path, opts = {}) {
36899
+ const isGet = method.toUpperCase() === "GET";
36900
+ const sendBody = !isGet && opts.body !== void 0;
36901
+ const headers = {
36902
+ Accept: "application/json",
36903
+ ...sendBody && !this.hasContentType(opts.headers ?? {}) ? { "Content-Type": "application/json" } : {},
36904
+ ...opts.headers ?? {}
36905
+ };
36906
+ const response = await this.request(method, path, {
36907
+ headers,
36908
+ body: sendBody ? JSON.stringify(opts.body) : void 0,
36909
+ ...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
36910
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {}
36911
+ });
36912
+ const result = {
36913
+ ok: true,
36914
+ status: response.status,
36915
+ url: response.url,
36916
+ body: response.body
36917
+ };
36918
+ if (response.status === 204 || response.body === "") {
36919
+ return { data: null, result };
36920
+ }
36921
+ let data;
36922
+ try {
36923
+ data = JSON.parse(response.body);
36924
+ } catch (e) {
36925
+ throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
36926
+ }
36927
+ return { data, result };
36928
+ }
36929
+ /**
36930
+ * 0.11.0+: run a single healthcheck probe through `fetchFn`, measure
36931
+ * the elapsed round-trip, classify any thrown error, and project the
36932
+ * post-probe `bridgeHealth()` into a snake-cased `bridge` sub-object.
36933
+ *
36934
+ * This is the transport half of the probe loop zillow/redfin/homes
36935
+ * had duplicated verbatim in `src/tools/healthcheck.ts`. The MCP
36936
+ * supplies its own probe call (`(path) => client.fetchHtml(path)`)
36937
+ * and probe path (e.g. `'/robots.txt'`); the tool registration and
36938
+ * the site-specific plain-English hint text STAY in the consumer.
36939
+ *
36940
+ * `bridgeHealth()` is read AFTER the probe so its freshness counters
36941
+ * (`lastSuccessAt` / `consecutiveFailures` / …) reflect this very
36942
+ * round-trip rather than a stale pre-probe snapshot.
36943
+ */
36944
+ async runProbe(fetchFn, probePath) {
36945
+ const start = Date.now();
36946
+ let ok = false;
36947
+ let error51;
36948
+ try {
36949
+ await fetchFn(probePath);
36950
+ ok = true;
36951
+ } catch (e) {
36952
+ error51 = {
36953
+ kind: classifyBridgeError(e),
36954
+ message: e instanceof Error ? e.message : String(e)
36955
+ };
36956
+ }
36957
+ const elapsed_ms = Date.now() - start;
36958
+ const health = this.bridgeHealth();
36959
+ return {
36960
+ ok,
36961
+ elapsed_ms,
36962
+ bridge: {
36963
+ role: health.role,
36964
+ port: health.port,
36965
+ server_version: health.serverVersion,
36966
+ fetch_timeout_ms: health.fetchTimeoutMs,
36967
+ last_success_at: health.lastSuccessAt,
36968
+ last_failure_at: health.lastFailureAt,
36969
+ last_failure_reason: health.lastFailureReason,
36970
+ consecutive_failures: health.consecutiveFailures
36971
+ },
36972
+ ...error51 ? { error: error51 } : {}
36973
+ };
36974
+ }
36589
36975
  /**
36590
36976
  * Snapshot the user's non-HttpOnly cookies for the chosen domain.
36591
36977
  *
@@ -36754,11 +37140,14 @@ var FetchproxyServer = class {
36754
37140
  this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
36755
37141
  throw err;
36756
37142
  }
37143
+ this.lastEvictionDetectedAt = Date.now();
36757
37144
  const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
36758
37145
  if (reviveMs > 0) {
37146
+ this.lazyReviveAttempts += 1;
36759
37147
  await new Promise((r) => setTimeout(r, reviveMs));
36760
37148
  try {
36761
37149
  const result = await this._captureRequestHeaderOnce(callOpts);
37150
+ this.lazyReviveSuccesses += 1;
36762
37151
  this.recordSuccess();
36763
37152
  return result;
36764
37153
  } catch (retryErr) {
@@ -37051,6 +37440,7 @@ var FetchproxyServer = class {
37051
37440
  * twice in a row.
37052
37441
  */
37053
37442
  async close() {
37443
+ this.stopKeepalive();
37054
37444
  this.rejectAllPending();
37055
37445
  if (this.connectingPromise) {
37056
37446
  await this.connectingPromise.catch(() => void 0);
@@ -37066,19 +37456,6 @@ var FetchproxyServer = class {
37066
37456
  }
37067
37457
  };
37068
37458
 
37069
- // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
37070
- function classifyBridgeError(err) {
37071
- if (err instanceof FetchproxyTimeoutError)
37072
- return "timeout";
37073
- if (err instanceof FetchproxyBridgeDownError)
37074
- return "bridge_down";
37075
- if (err instanceof FetchproxyHttpError)
37076
- return "http";
37077
- if (err instanceof FetchproxyProtocolError)
37078
- return "protocol";
37079
- return "other";
37080
- }
37081
-
37082
37459
  // node_modules/@fetchproxy/bootstrap/dist/index.js
37083
37460
  var defaultFactory = (opts) => new FetchproxyServer(opts);
37084
37461
  async function bootstrap(opts) {
@@ -37110,7 +37487,7 @@ async function bootstrap(opts) {
37110
37487
  const sessionStorageKeys = new Set(opts.declare.sessionStorage);
37111
37488
  for (const p of sessionStoragePointers)
37112
37489
  sessionStorageKeys.add(p.storageKey);
37113
- const server2 = factory({
37490
+ const server = factory({
37114
37491
  serverName: opts.serverName,
37115
37492
  version: opts.version,
37116
37493
  domains: [...opts.domains],
@@ -37145,10 +37522,10 @@ async function bootstrap(opts) {
37145
37522
  if (opts.storageSubdomain !== void 0)
37146
37523
  storageDomainOpts.subdomain = opts.storageSubdomain;
37147
37524
  try {
37148
- await server2.listen();
37525
+ await server.listen();
37149
37526
  const cookies = {};
37150
37527
  if (opts.declare.cookies.length > 0) {
37151
- const joined = await server2.readCookies({
37528
+ const joined = await server.readCookies({
37152
37529
  keys: opts.declare.cookies,
37153
37530
  ...storageDomainOpts
37154
37531
  });
@@ -37168,7 +37545,7 @@ async function bootstrap(opts) {
37168
37545
  for (const p of localStoragePointers) {
37169
37546
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
37170
37547
  }
37171
- localStorage = await server2.readLocalStorage({
37548
+ localStorage = await server.readLocalStorage({
37172
37549
  keys: allKeys,
37173
37550
  ...storageDomainOpts,
37174
37551
  ...localStoragePointers.length > 0 ? { pointers } : {}
@@ -37181,7 +37558,7 @@ async function bootstrap(opts) {
37181
37558
  for (const p of sessionStoragePointers) {
37182
37559
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
37183
37560
  }
37184
- sessionStorage = await server2.readSessionStorage({
37561
+ sessionStorage = await server.readSessionStorage({
37185
37562
  keys: allKeys,
37186
37563
  ...storageDomainOpts,
37187
37564
  ...sessionStoragePointers.length > 0 ? { pointers } : {}
@@ -37197,14 +37574,14 @@ async function bootstrap(opts) {
37197
37574
  opts.onWaiting(`waiting to capture ${h.headerName} \u2014 interact with the page in your browser`);
37198
37575
  }
37199
37576
  }
37200
- capturedHeaders[h.headerName] = await server2.captureRequestHeader({
37577
+ capturedHeaders[h.headerName] = await server.captureRequestHeader({
37201
37578
  urlPattern: h.urlPattern,
37202
37579
  headerName: h.headerName
37203
37580
  });
37204
37581
  }
37205
37582
  const indexedDbBucket = {};
37206
37583
  for (const d of indexedDb) {
37207
- const values = await server2.readIndexedDb({
37584
+ const values = await server.readIndexedDb({
37208
37585
  database: d.database,
37209
37586
  store: d.store,
37210
37587
  keys: [...d.keys],
@@ -37221,7 +37598,7 @@ async function bootstrap(opts) {
37221
37598
  };
37222
37599
  } finally {
37223
37600
  try {
37224
- await server2.close();
37601
+ await server.close();
37225
37602
  } catch {
37226
37603
  }
37227
37604
  }
@@ -37286,8 +37663,8 @@ async function loginWithPassword(username, password) {
37286
37663
 
37287
37664
  // src/config.ts
37288
37665
  import { createHash } from "node:crypto";
37289
- import { homedir as homedir2 } from "node:os";
37290
- import { join as join2 } from "node:path";
37666
+ import { homedir as homedir3 } from "node:os";
37667
+ import { join as join3 } from "node:path";
37291
37668
  function readCacheIdentity() {
37292
37669
  const explicit = process.env.OFW_CACHE_IDENTITY;
37293
37670
  if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
@@ -37298,31 +37675,29 @@ function readCacheIdentity() {
37298
37675
  function getCacheDir() {
37299
37676
  const override = process.env.OFW_CACHE_DIR;
37300
37677
  if (override && override.trim().length > 0) return override.trim();
37301
- return join2(homedir2(), ".cache", "ofw-mcp");
37678
+ return join3(homedir3(), ".cache", "ofw-mcp");
37302
37679
  }
37303
37680
  function getCacheDbPath() {
37304
37681
  const identity = readCacheIdentity();
37305
37682
  const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
37306
- return join2(getCacheDir(), `${hash2}.db`);
37683
+ return join3(getCacheDir(), `${hash2}.db`);
37307
37684
  }
37308
37685
  function getAttachmentsDir() {
37309
37686
  const override = process.env.OFW_ATTACHMENTS_DIR;
37310
37687
  if (override && override.trim().length > 0) return override.trim();
37311
- return join2(homedir2(), "Downloads", "ofw-mcp");
37688
+ return join3(homedir3(), "Downloads", "ofw-mcp");
37312
37689
  }
37313
- function parseBoolEnv(name) {
37314
- const raw = process.env[name];
37315
- if (typeof raw !== "string") return false;
37316
- return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
37690
+ function parseBoolEnv2(name) {
37691
+ return parseBoolEnv(name);
37317
37692
  }
37318
37693
  function getDefaultInlineAttachments() {
37319
- return parseBoolEnv("OFW_INLINE_ATTACHMENTS");
37694
+ return parseBoolEnv2("OFW_INLINE_ATTACHMENTS");
37320
37695
  }
37321
37696
 
37322
37697
  // package.json
37323
37698
  var package_default = {
37324
37699
  name: "ofw-mcp",
37325
- version: "2.2.0",
37700
+ version: "2.3.1",
37326
37701
  mcpName: "io.github.chrischall/ofw-mcp",
37327
37702
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
37328
37703
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -37346,11 +37721,12 @@ var package_default = {
37346
37721
  bundle: `esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js`,
37347
37722
  dev: "node --env-file=.env dist/index.js",
37348
37723
  test: "vitest run",
37724
+ "test:coverage": "vitest run --coverage",
37349
37725
  "test:watch": "vitest"
37350
37726
  },
37351
37727
  dependencies: {
37352
- "@fetchproxy/bootstrap": "^0.8.0",
37353
- "@fetchproxy/server": "^0.8.0",
37728
+ "@chrischall/mcp-utils": "^0.4.0",
37729
+ "@fetchproxy/bootstrap": "^0.11.0",
37354
37730
  "@modelcontextprotocol/sdk": "^1.29.0",
37355
37731
  dotenv: "^17.4.2",
37356
37732
  zod: "^4.4.3"
@@ -37366,16 +37742,10 @@ var package_default = {
37366
37742
 
37367
37743
  // src/auth.ts
37368
37744
  function readEnv(key) {
37369
- const raw = process.env[key];
37370
- if (typeof raw !== "string") return void 0;
37371
- const trimmed = raw.trim();
37372
- if (trimmed.length === 0) return void 0;
37373
- if (trimmed === "undefined" || trimmed === "null") return void 0;
37374
- if (/^\$\{[^}]*\}$/.test(trimmed)) return void 0;
37375
- return trimmed;
37745
+ return readEnvVar(key);
37376
37746
  }
37377
37747
  function fetchproxyDisabled() {
37378
- return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
37748
+ return parseBoolEnv2("OFW_DISABLE_FETCHPROXY");
37379
37749
  }
37380
37750
  async function resolveAuth() {
37381
37751
  const username = readEnv("OFW_USERNAME");
@@ -37435,12 +37805,8 @@ async function resolveAuth() {
37435
37805
  }
37436
37806
 
37437
37807
  // src/client.ts
37438
- try {
37439
- const { config: config2 } = await import("dotenv");
37440
- const __dirname = dirname(fileURLToPath(import.meta.url));
37441
- config2({ path: join3(__dirname, "..", ".env"), override: false, quiet: true });
37442
- } catch {
37443
- }
37808
+ var __dirname = dirname(fileURLToPath(import.meta.url));
37809
+ await loadDotenvSafely({ path: join4(__dirname, "..", ".env") });
37444
37810
  function parseContentDispositionFilename(cd) {
37445
37811
  const extMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(cd);
37446
37812
  if (extMatch) {
@@ -37455,7 +37821,7 @@ function parseContentDispositionFilename(cd) {
37455
37821
  return m ? m[1] : null;
37456
37822
  }
37457
37823
  function debugLogEnabled() {
37458
- return parseBoolEnv("OFW_DEBUG_LOG");
37824
+ return parseBoolEnv2("OFW_DEBUG_LOG");
37459
37825
  }
37460
37826
  function redactHeaders(h) {
37461
37827
  const out = { ...h };
@@ -37584,13 +37950,8 @@ var OFWClient = class {
37584
37950
  var client = new OFWClient();
37585
37951
 
37586
37952
  // src/tools/_shared.ts
37587
- import { isAbsolute, join as join4, resolve } from "node:path";
37588
- function jsonResponse(payload) {
37589
- return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
37590
- }
37591
- function textResponse(text) {
37592
- return { content: [{ type: "text", text }] };
37593
- }
37953
+ var jsonResponse = textResult;
37954
+ var textResponse = rawTextResult;
37594
37955
  function mapRecipients(items) {
37595
37956
  return (items ?? []).map((r) => ({
37596
37957
  userId: r.user?.id ?? 0,
@@ -37598,10 +37959,7 @@ function mapRecipients(items) {
37598
37959
  viewedAt: r.viewed?.dateTime ?? null
37599
37960
  }));
37600
37961
  }
37601
- function expandPath(p) {
37602
- const expanded = p.startsWith("~/") ? join4(process.env.HOME ?? "", p.slice(2)) : p;
37603
- return isAbsolute(expanded) ? expanded : resolve(expanded);
37604
- }
37962
+ var expandPath2 = expandPath;
37605
37963
  async function postMessageAndRefetch(client2, payload) {
37606
37964
  const raw = await client2.request(
37607
37965
  "POST",
@@ -37615,15 +37973,15 @@ async function postMessageAndRefetch(client2, payload) {
37615
37973
  }
37616
37974
 
37617
37975
  // src/tools/user.ts
37618
- function registerUserTools(server2, client2) {
37619
- server2.registerTool("ofw_get_profile", {
37976
+ function registerUserTools(server, client2) {
37977
+ server.registerTool("ofw_get_profile", {
37620
37978
  description: "Get current user and co-parent profile information from OurFamilyWizard",
37621
37979
  annotations: { readOnlyHint: true }
37622
37980
  }, async () => {
37623
37981
  const data = await client2.request("GET", "/pub/v2/profiles");
37624
37982
  return jsonResponse(data);
37625
37983
  });
37626
- server2.registerTool("ofw_get_notifications", {
37984
+ server.registerTool("ofw_get_notifications", {
37627
37985
  description: "Get OurFamilyWizard dashboard summary: unread message count, upcoming events, outstanding expenses. Note: updates your last-seen status.",
37628
37986
  annotations: { readOnlyHint: false }
37629
37987
  }, async () => {
@@ -38164,15 +38522,15 @@ function listDataHintsAtFiles(listData) {
38164
38522
  if (Array.isArray(ld.files)) return ld.files.length > 0;
38165
38523
  return false;
38166
38524
  }
38167
- function registerMessageTools(server2, client2) {
38168
- server2.registerTool("ofw_list_message_folders", {
38525
+ function registerMessageTools(server, client2) {
38526
+ server.registerTool("ofw_list_message_folders", {
38169
38527
  description: "List OurFamilyWizard message folders (inbox, sent, etc.) and their unread counts. Returns folder IDs needed to call ofw_list_messages. Does NOT return message content.",
38170
38528
  annotations: { readOnlyHint: true }
38171
38529
  }, async () => {
38172
38530
  const data = await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true");
38173
38531
  return jsonResponse(data);
38174
38532
  });
38175
- server2.registerTool("ofw_list_messages", {
38533
+ server.registerTool("ofw_list_messages", {
38176
38534
  description: "List messages from the local OurFamilyWizard cache. Supports filtering by folder, date range, and a substring query on subject+body. Pagination is offset-based but if you know what you want (a date range, a topic), prefer the filters over walking pages \u2014 the cache may have 1000+ messages. Call ofw_sync_messages first if the cache is empty or stale.",
38177
38535
  annotations: { readOnlyHint: true },
38178
38536
  inputSchema: {
@@ -38208,7 +38566,7 @@ function registerMessageTools(server2, client2) {
38208
38566
  }
38209
38567
  return jsonResponse(payload);
38210
38568
  });
38211
- server2.registerTool("ofw_get_message", {
38569
+ server.registerTool("ofw_get_message", {
38212
38570
  description: 'Get a single OurFamilyWizard message OR draft by ID. Reads from local cache when available; otherwise fetches from OFW (which will mark unread inbox messages as read on OFW). For ids that match a draft (in the drafts cache), the response carries folder="drafts" and the body/subject/recipients reflect the drafts cache (which ofw_sync_messages keeps fresh) \u2014 drafts have no `fromUser`, and `sentAt`/`fetchedBodyAt` mirror the draft\'s `modifiedAt`. For inbox/sent messages, folder is "inbox" or "sent" as before.',
38213
38571
  annotations: { readOnlyHint: false },
38214
38572
  inputSchema: {
@@ -38273,7 +38631,7 @@ function registerMessageTools(server2, client2) {
38273
38631
  const attachments = listAttachmentsForMessage(detail.id);
38274
38632
  return jsonResponse({ ...row, attachments });
38275
38633
  });
38276
- server2.registerTool("ofw_send_message", {
38634
+ server.registerTool("ofw_send_message", {
38277
38635
  description: "Send a message via OurFamilyWizard. To send an existing draft, pass messageId \u2014 subject/body/recipientIds become optional overrides (missing fields default to the draft's cached values) and the draft is deleted after sending. To send a fresh message, supply subject/body/recipientIds directly. draftId is the legacy spelling of messageId and works the same way. If replyToId is provided, the cache may rewrite it to the latest reply in the same thread (a note is included in the response when this happens). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After sending, the tool re-fetches the message from OFW to populate the local cache and link attachments to the new message id.",
38278
38636
  annotations: { destructiveHint: true },
38279
38637
  inputSchema: {
@@ -38383,7 +38741,7 @@ function registerMessageTools(server2, client2) {
38383
38741
 
38384
38742
  ${text}` : text);
38385
38743
  });
38386
- server2.registerTool("ofw_list_drafts", {
38744
+ server.registerTool("ofw_list_drafts", {
38387
38745
  description: "List draft messages from the local OurFamilyWizard cache. Call ofw_sync_messages first if the cache is empty.",
38388
38746
  annotations: { readOnlyHint: true },
38389
38747
  inputSchema: {
@@ -38397,7 +38755,7 @@ ${text}` : text);
38397
38755
  const payload = drafts.length === 0 ? { drafts: [], note: "Cache empty. Call ofw_sync_messages to populate." } : { drafts };
38398
38756
  return jsonResponse(payload);
38399
38757
  });
38400
- server2.registerTool("ofw_save_draft", {
38758
+ server.registerTool("ofw_save_draft", {
38401
38759
  description: "Save a message as a draft in OurFamilyWizard. Recipients are optional. Pass messageId to replace an existing draft \u2014 note that under the hood this creates a NEW draft and deletes the old one (OFW's update-in-place endpoint silently no-ops while echoing the posted body, so we don't use it); the response.id will be the NEW id, not the messageId you passed, and the change is documented in a transparency NOTE in the response. If replyToId is provided, the cache may rewrite it to the latest reply in the thread (note included in response). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After saving, the tool re-fetches the draft from OFW to populate the local cache from authoritative server state.",
38402
38760
  annotations: { readOnlyHint: false },
38403
38761
  inputSchema: {
@@ -38459,7 +38817,7 @@ ${text}` : text);
38459
38817
 
38460
38818
  ${text}` : text);
38461
38819
  });
38462
- server2.registerTool("ofw_delete_draft", {
38820
+ server.registerTool("ofw_delete_draft", {
38463
38821
  description: "Delete a draft message from OurFamilyWizard. Also removes the draft from the local cache.",
38464
38822
  annotations: { destructiveHint: true },
38465
38823
  inputSchema: {
@@ -38470,7 +38828,7 @@ ${text}` : text);
38470
38828
  deleteDraft(args.messageId);
38471
38829
  return data ? jsonResponse(data) : textResponse("Draft deleted.");
38472
38830
  });
38473
- server2.registerTool("ofw_get_unread_sent", {
38831
+ server.registerTool("ofw_get_unread_sent", {
38474
38832
  description: "List sent messages that have not been read by one or more recipients. Reads from local cache; call ofw_sync_messages first if cache is stale.",
38475
38833
  annotations: { readOnlyHint: true },
38476
38834
  inputSchema: {
@@ -38496,7 +38854,7 @@ ${text}` : text);
38496
38854
  }
38497
38855
  return jsonResponse(unread);
38498
38856
  });
38499
- server2.registerTool("ofw_upload_attachment", {
38857
+ server.registerTool("ofw_upload_attachment", {
38500
38858
  description: `Upload a local file to OurFamilyWizard's "My Files" so it can be attached to a message. Returns the fileId \u2014 pass that to ofw_send_message or ofw_save_draft in myFileIDs to attach it. The file is uploaded as PRIVATE (visible only to you) by default; pass shareClass:"SHARED" to share with co-parents directly via the My Files area.`,
38501
38859
  annotations: { destructiveHint: false },
38502
38860
  inputSchema: {
@@ -38506,14 +38864,13 @@ ${text}` : text);
38506
38864
  description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional()
38507
38865
  }
38508
38866
  }, async (args) => {
38509
- const abs = expandPath(args.path);
38867
+ const abs = expandPath2(args.path);
38510
38868
  const stat = statSync(abs);
38511
38869
  if (!stat.isFile()) throw new Error(`Not a file: ${abs}`);
38512
- const buf = readFileSync(abs);
38513
38870
  const fileName = basename(abs);
38514
38871
  const mime = mimeFromName(fileName);
38515
38872
  const form = new FormData();
38516
- form.append("file", new Blob([new Uint8Array(buf)], { type: mime }), fileName);
38873
+ form.append("file", await fileBlob(abs, { type: mime }), fileName);
38517
38874
  form.append("source", "message");
38518
38875
  form.append("description", args.description ?? fileName);
38519
38876
  form.append("label", args.label ?? fileName);
@@ -38525,7 +38882,7 @@ ${text}` : text);
38525
38882
  fileName: meta3.fileName ?? fileName,
38526
38883
  label: meta3.label ?? args.label ?? fileName,
38527
38884
  mimeType: meta3.fileType ?? mime,
38528
- sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes : buf.length,
38885
+ sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes : stat.size,
38529
38886
  metadata: meta3,
38530
38887
  messageId: 0
38531
38888
  });
@@ -38533,12 +38890,12 @@ ${text}` : text);
38533
38890
  fileId: meta3.fileId,
38534
38891
  fileName: meta3.fileName ?? fileName,
38535
38892
  mimeType: meta3.fileType ?? mime,
38536
- sizeBytes: meta3.sizeInBytes ?? buf.length,
38893
+ sizeBytes: meta3.sizeInBytes ?? stat.size,
38537
38894
  shareClass: meta3.shareClass ?? args.shareClass ?? "PRIVATE",
38538
38895
  note: "Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it."
38539
38896
  });
38540
38897
  });
38541
- server2.registerTool("ofw_download_attachment", {
38898
+ server.registerTool("ofw_download_attachment", {
38542
38899
  description: 'Download an OFW message attachment by fileId. By default, bytes are saved to disk (~/Downloads/ofw-mcp/) and the response carries the absolute path, mime type, and size for the caller to read back. Pass inline:true to skip disk entirely and return the bytes as MCP content blocks \u2014 images come back as ImageContent (the model sees them directly); other files come back as an EmbeddedResource blob. Use inline for small files where you want the model to read content immediately and the host is sandboxed; use disk for large files or when you want a persistent local copy. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var (set to "true" to make inline the default). fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo. Re-downloading to the same path is a no-op (disk mode only).',
38543
38900
  annotations: { readOnlyHint: false },
38544
38901
  inputSchema: {
@@ -38592,7 +38949,7 @@ ${text}` : text);
38592
38949
  let dest;
38593
38950
  if (args.saveTo) {
38594
38951
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
38595
- const abs = expandPath(args.saveTo);
38952
+ const abs = expandPath2(args.saveTo);
38596
38953
  dest = isDirArg ? join5(abs, `${fileId}-${cached2.fileName}`) : abs;
38597
38954
  } else {
38598
38955
  dest = join5(getAttachmentsDir(), `${fileId}-${cached2.fileName}`);
@@ -38619,7 +38976,7 @@ ${text}` : text);
38619
38976
  fileName: response.suggestedFileName ?? cached2.fileName
38620
38977
  });
38621
38978
  });
38622
- server2.registerTool("ofw_sync_messages", {
38979
+ server.registerTool("ofw_sync_messages", {
38623
38980
  description: "Sync messages from OurFamilyWizard into the local cache. Returns counts per folder and a list of unread inbox messages whose bodies were NOT fetched (to avoid mark-as-read on OFW). Call ofw_get_message(id) on those to read them. Pass deep:true to walk all OFW pages instead of stopping at the first all-cached page (use to backfill suspected gaps).",
38624
38981
  annotations: { readOnlyHint: false },
38625
38982
  inputSchema: {
@@ -38643,8 +39000,8 @@ async function deleteOFWMessages(client2, ids) {
38643
39000
  }
38644
39001
 
38645
39002
  // src/tools/calendar.ts
38646
- function registerCalendarTools(server2, client2) {
38647
- server2.registerTool("ofw_list_events", {
39003
+ function registerCalendarTools(server, client2) {
39004
+ server.registerTool("ofw_list_events", {
38648
39005
  description: "List OurFamilyWizard calendar events in a date range",
38649
39006
  annotations: { readOnlyHint: true },
38650
39007
  inputSchema: {
@@ -38660,7 +39017,7 @@ function registerCalendarTools(server2, client2) {
38660
39017
  );
38661
39018
  return jsonResponse(data);
38662
39019
  });
38663
- server2.registerTool("ofw_create_event", {
39020
+ server.registerTool("ofw_create_event", {
38664
39021
  description: "Create a calendar event in OurFamilyWizard",
38665
39022
  annotations: { destructiveHint: false },
38666
39023
  inputSchema: {
@@ -38680,7 +39037,7 @@ function registerCalendarTools(server2, client2) {
38680
39037
  const data = await client2.request("POST", "/pub/v1/calendar/events", args);
38681
39038
  return jsonResponse(data);
38682
39039
  });
38683
- server2.registerTool("ofw_update_event", {
39040
+ server.registerTool("ofw_update_event", {
38684
39041
  description: "Update an existing OurFamilyWizard calendar event",
38685
39042
  annotations: { destructiveHint: true },
38686
39043
  inputSchema: {
@@ -38698,7 +39055,7 @@ function registerCalendarTools(server2, client2) {
38698
39055
  const data = await client2.request("PUT", `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
38699
39056
  return jsonResponse(data);
38700
39057
  });
38701
- server2.registerTool("ofw_delete_event", {
39058
+ server.registerTool("ofw_delete_event", {
38702
39059
  description: "Delete an OurFamilyWizard calendar event",
38703
39060
  annotations: { destructiveHint: true },
38704
39061
  inputSchema: {
@@ -38711,15 +39068,15 @@ function registerCalendarTools(server2, client2) {
38711
39068
  }
38712
39069
 
38713
39070
  // src/tools/expenses.ts
38714
- function registerExpenseTools(server2, client2) {
38715
- server2.registerTool("ofw_get_expense_totals", {
39071
+ function registerExpenseTools(server, client2) {
39072
+ server.registerTool("ofw_get_expense_totals", {
38716
39073
  description: "Get OurFamilyWizard expense summary totals (owed/paid)",
38717
39074
  annotations: { readOnlyHint: true }
38718
39075
  }, async () => {
38719
39076
  const data = await client2.request("GET", "/pub/v2/expense/expenses/totals");
38720
39077
  return jsonResponse(data);
38721
39078
  });
38722
- server2.registerTool("ofw_list_expenses", {
39079
+ server.registerTool("ofw_list_expenses", {
38723
39080
  description: "List OurFamilyWizard expenses with pagination",
38724
39081
  annotations: { readOnlyHint: true },
38725
39082
  inputSchema: {
@@ -38732,7 +39089,7 @@ function registerExpenseTools(server2, client2) {
38732
39089
  const data = await client2.request("GET", `/pub/v2/expense/expenses?start=${start}&max=${max}`);
38733
39090
  return jsonResponse(data);
38734
39091
  });
38735
- server2.registerTool("ofw_create_expense", {
39092
+ server.registerTool("ofw_create_expense", {
38736
39093
  description: "Log a new expense in OurFamilyWizard",
38737
39094
  annotations: { destructiveHint: false },
38738
39095
  inputSchema: {
@@ -38746,8 +39103,8 @@ function registerExpenseTools(server2, client2) {
38746
39103
  }
38747
39104
 
38748
39105
  // src/tools/journal.ts
38749
- function registerJournalTools(server2, client2) {
38750
- server2.registerTool("ofw_list_journal_entries", {
39106
+ function registerJournalTools(server, client2) {
39107
+ server.registerTool("ofw_list_journal_entries", {
38751
39108
  description: "List OurFamilyWizard journal entries",
38752
39109
  annotations: { readOnlyHint: true },
38753
39110
  inputSchema: {
@@ -38760,7 +39117,7 @@ function registerJournalTools(server2, client2) {
38760
39117
  const data = await client2.request("GET", `/pub/v1/journals?start=${start}&max=${max}`);
38761
39118
  return jsonResponse(data);
38762
39119
  });
38763
- server2.registerTool("ofw_create_journal_entry", {
39120
+ server.registerTool("ofw_create_journal_entry", {
38764
39121
  description: "Create a new journal entry in OurFamilyWizard",
38765
39122
  annotations: { destructiveHint: false },
38766
39123
  inputSchema: {
@@ -38784,12 +39141,17 @@ process.emit = function(event, ...args) {
38784
39141
  }
38785
39142
  return originalEmit(event, ...args);
38786
39143
  };
38787
- var server = new McpServer({ name: "ofw", version: "2.2.0" });
38788
- registerUserTools(server, client);
38789
- registerMessageTools(server, client);
38790
- registerCalendarTools(server, client);
38791
- registerExpenseTools(server, client);
38792
- registerJournalTools(server, client);
38793
- console.error("[ofw-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.");
38794
- var transport = new StdioServerTransport();
38795
- await server.connect(transport);
39144
+ await runMcp({
39145
+ name: "ofw",
39146
+ version: "2.3.1",
39147
+ // x-release-please-version
39148
+ deps: client,
39149
+ tools: [
39150
+ registerUserTools,
39151
+ registerMessageTools,
39152
+ registerCalendarTools,
39153
+ registerExpenseTools,
39154
+ registerJournalTools
39155
+ ],
39156
+ banner: "[ofw-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion."
39157
+ });