ofw-mcp 2.4.1 → 2.4.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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "OurFamilyWizard tools for Claude Code",
9
- "version": "2.4.1"
9
+ "version": "2.4.3"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -14,7 +14,7 @@
14
14
  "displayName": "OurFamilyWizard",
15
15
  "source": "./",
16
16
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
17
- "version": "2.4.1",
17
+ "version": "2.4.3",
18
18
  "author": {
19
19
  "name": "Chris Chall"
20
20
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ofw",
3
3
  "displayName": "OurFamilyWizard",
4
- "version": "2.4.1",
4
+ "version": "2.4.3",
5
5
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall"
@@ -17,40 +17,5 @@
17
17
  "family"
18
18
  ],
19
19
  "skills": "./skills/",
20
- "mcp": "./.mcp.json",
21
- "userConfig": {
22
- "ofw_username": {
23
- "type": "string",
24
- "title": "OFW Email",
25
- "description": "Your OurFamilyWizard login email address. Optional — if omitted, the server falls back to the fetchproxy browser extension (requires being signed in to ourfamilywizard.com in your browser).",
26
- "required": false
27
- },
28
- "ofw_password": {
29
- "type": "string",
30
- "title": "OFW Password",
31
- "description": "Your OurFamilyWizard password. Optional — see ofw_username.",
32
- "required": false,
33
- "sensitive": true
34
- },
35
- "ofw_inline_attachments": {
36
- "type": "boolean",
37
- "title": "Return attachments inline",
38
- "description": "When on, ofw_download_attachment returns bytes inline as MCP content (images render directly; other files come back as embedded resources) instead of writing to disk. Recommended for sandboxed hosts like Claude Desktop where the model cannot read files written to ~/Downloads. Callers can still override per-call via the tool's `inline` argument.",
39
- "required": false,
40
- "default": false
41
- },
42
- "ofw_attachments_dir": {
43
- "type": "directory",
44
- "title": "Attachments download directory",
45
- "description": "Directory where ofw_download_attachment writes files when not returning inline. Defaults to ~/Downloads/ofw-mcp/. Pick a directory that is readable by your MCP host.",
46
- "required": false
47
- },
48
- "ofw_write_mode": {
49
- "type": "string",
50
- "title": "Write mode",
51
- "description": "Structural write gate. \"none\" = no write tools registered (read/sync/search only); \"drafts\" = draft-level writes only (save/delete drafts, upload attachments — never send or calendar/expense/journal writes); \"all\" = everything (default). Unrecognized values fail closed to \"none\".",
52
- "required": false,
53
- "default": "all"
54
- }
55
- }
20
+ "mcp": "./.mcp.json"
56
21
  }
package/.mcp.json CHANGED
@@ -4,11 +4,8 @@
4
4
  "command": "npx",
5
5
  "args": ["-y", "ofw-mcp"],
6
6
  "env": {
7
- "OFW_USERNAME": "${user_config.ofw_username}",
8
- "OFW_PASSWORD": "${user_config.ofw_password}",
9
- "OFW_INLINE_ATTACHMENTS": "${user_config.ofw_inline_attachments}",
10
- "OFW_ATTACHMENTS_DIR": "${user_config.ofw_attachments_dir}",
11
- "OFW_WRITE_MODE": "${user_config.ofw_write_mode}"
7
+ "OFW_USERNAME": "${OFW_USERNAME}",
8
+ "OFW_PASSWORD": "${OFW_PASSWORD}"
12
9
  }
13
10
  }
14
11
  }
package/dist/bundle.js CHANGED
@@ -35837,10 +35837,20 @@ async function openEncryptedFrame(sessionKey, frame) {
35837
35837
 
35838
35838
  // node_modules/@fetchproxy/server/dist/election.js
35839
35839
  import { createServer, Server as HttpServer } from "node:http";
35840
+ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
35840
35841
  async function electRole(opts) {
35841
35842
  const server = createServer();
35843
+ const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
35842
35844
  return new Promise((resolve2, reject) => {
35845
+ let timer;
35846
+ const clearBindTimer = () => {
35847
+ if (timer) {
35848
+ clearTimeout(timer);
35849
+ timer = void 0;
35850
+ }
35851
+ };
35843
35852
  const onError = (e) => {
35853
+ clearBindTimer();
35844
35854
  server.removeListener("listening", onListening);
35845
35855
  if (e.code === "EADDRINUSE") {
35846
35856
  try {
@@ -35853,11 +35863,25 @@ async function electRole(opts) {
35853
35863
  }
35854
35864
  };
35855
35865
  const onListening = () => {
35866
+ clearBindTimer();
35856
35867
  server.removeListener("error", onError);
35857
35868
  resolve2({ role: "host", server });
35858
35869
  };
35859
35870
  server.once("error", onError);
35860
35871
  server.once("listening", onListening);
35872
+ if (bindTimeoutMs > 0) {
35873
+ timer = setTimeout(() => {
35874
+ server.removeListener("error", onError);
35875
+ server.removeListener("listening", onListening);
35876
+ try {
35877
+ server.close();
35878
+ } catch {
35879
+ }
35880
+ reject(new Error(`fetchproxy: bind to ${opts.host}:${opts.port} timed out after ${bindTimeoutMs}ms (port may be in a bad state)`));
35881
+ }, bindTimeoutMs);
35882
+ if (typeof timer.unref === "function")
35883
+ timer.unref();
35884
+ }
35861
35885
  server.listen(opts.port, opts.host);
35862
35886
  });
35863
35887
  }
@@ -35950,6 +35974,44 @@ var SessionState = class {
35950
35974
  }
35951
35975
  };
35952
35976
 
35977
+ // node_modules/@fetchproxy/server/dist/session-ready.js
35978
+ var SESSION_READY_TIMEOUT_MS = 3e4;
35979
+ var FetchproxySessionNotReadyError = class extends Error {
35980
+ reason;
35981
+ pairCode;
35982
+ mcpId;
35983
+ hint;
35984
+ constructor(info) {
35985
+ const pairing = info.pairCode !== null && info.pairCode !== "";
35986
+ const hint = pairing ? `Open the Transporter extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
35987
+ super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
35988
+ this.name = "FetchproxySessionNotReadyError";
35989
+ this.reason = pairing ? "pair-required" : "not-ready";
35990
+ this.pairCode = pairing ? info.pairCode : null;
35991
+ this.mcpId = info.mcpId;
35992
+ this.hint = hint;
35993
+ Object.setPrototypeOf(this, new.target.prototype);
35994
+ }
35995
+ };
35996
+ async function awaitSessionReady(ready, opts) {
35997
+ const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
35998
+ if (ms <= 0)
35999
+ return ready;
36000
+ let timer;
36001
+ const timeout = new Promise((_, reject) => {
36002
+ timer = setTimeout(() => {
36003
+ reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
36004
+ }, ms);
36005
+ timer.unref?.();
36006
+ });
36007
+ try {
36008
+ return await Promise.race([ready, timeout]);
36009
+ } finally {
36010
+ if (timer)
36011
+ clearTimeout(timer);
36012
+ }
36013
+ }
36014
+
35953
36015
  // node_modules/@fetchproxy/server/dist/host.js
35954
36016
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
35955
36017
  var enc2 = new TextEncoder();
@@ -36037,6 +36099,29 @@ async function startHost(opts) {
36037
36099
  return;
36038
36100
  }
36039
36101
  if (frame.type === "hello" && frame.role === "server") {
36102
+ const peerEdPub = fromB64(frame.identityEd25519Pub);
36103
+ const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
36104
+ const peerSig = fromB64(frame.sessionSig);
36105
+ let peerSigOk = false;
36106
+ try {
36107
+ peerSigOk = await ed25519Verify(peerEdPub, peerSigMsg, peerSig);
36108
+ } catch {
36109
+ peerSigOk = false;
36110
+ }
36111
+ if (!peerSigOk) {
36112
+ console.warn("[fetchproxy] peer hello signature invalid \u2014 refusing registration (possible squatter)");
36113
+ ws.close(1008, "peer hello signature invalid");
36114
+ return;
36115
+ }
36116
+ const existing = peers.get(frame.mcpId);
36117
+ if (existing && existing.ws !== ws) {
36118
+ const existingEdPub = existing.helloFrame.identityEd25519Pub;
36119
+ if (existingEdPub !== frame.identityEd25519Pub) {
36120
+ console.warn("[fetchproxy] peer mcpId already mapped to a different identity \u2014 refusing (mcpId squatting)");
36121
+ ws.close(1008, "mcpId already registered to another identity");
36122
+ return;
36123
+ }
36124
+ }
36040
36125
  identified = "peer";
36041
36126
  peerMcpId = frame.mcpId;
36042
36127
  peers.set(frame.mcpId, { ws, helloFrame: frame });
@@ -36129,8 +36214,10 @@ async function startHost(opts) {
36129
36214
  resetSessionPromise();
36130
36215
  disconnectListeners.forEach((cb) => cb());
36131
36216
  }
36132
- if (identified === "peer" && peerMcpId)
36133
- peers.delete(peerMcpId);
36217
+ if (identified === "peer" && peerMcpId) {
36218
+ if (peers.get(peerMcpId)?.ws === ws)
36219
+ peers.delete(peerMcpId);
36220
+ }
36134
36221
  });
36135
36222
  });
36136
36223
  return {
@@ -36146,7 +36233,10 @@ async function startHost(opts) {
36146
36233
  });
36147
36234
  }),
36148
36235
  sendOwnInner: async (inner) => {
36149
- const session = await ownSessionReady;
36236
+ const session = await awaitSessionReady(ownSessionReady, {
36237
+ mcpId: opts.ownMcpId,
36238
+ pendingPairCode: () => ownPendingPairCode
36239
+ });
36150
36240
  if (!extensionWs)
36151
36241
  throw new Error("host: no extension connected");
36152
36242
  const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
@@ -36251,7 +36341,10 @@ async function startPeer(opts) {
36251
36341
  ws,
36252
36342
  session: sessionPromise,
36253
36343
  sendInner: async (inner) => {
36254
- await sessionPromise;
36344
+ await awaitSessionReady(sessionPromise, {
36345
+ mcpId: opts.mcpId,
36346
+ pendingPairCode: () => pendingPairCode
36347
+ });
36255
36348
  const s = session;
36256
36349
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
36257
36350
  ws.send(JSON.stringify(sealed));
@@ -36917,6 +37010,35 @@ var FetchproxyServer = class {
36917
37010
  this.keepAliveTimer = null;
36918
37011
  }
36919
37012
  }
37013
+ /**
37014
+ * Send an inner request frame via whichever bridge handle is active. If the
37015
+ * send throws (e.g. `FetchproxySessionNotReadyError` — the session never
37016
+ * confirmed), the frame never reached the bridge, so no reply will arrive:
37017
+ * drop the just-registered pending resolver for this id (it lives in exactly
37018
+ * one of the op maps — request ids are unique) so it doesn't leak until the
37019
+ * server closes, then rethrow.
37020
+ */
37021
+ async sendInnerFrame(inner) {
37022
+ try {
37023
+ if (this.hostHandle) {
37024
+ await this.hostHandle.sendOwnInner(inner);
37025
+ } else if (this.peerHandle) {
37026
+ await this.peerHandle.sendInner(inner);
37027
+ }
37028
+ } catch (err) {
37029
+ if ("id" in inner && typeof inner.id === "number") {
37030
+ const { id } = inner;
37031
+ this.pending.delete(id);
37032
+ this.pendingReadCookies.delete(id);
37033
+ this.pendingStorage.delete(id);
37034
+ this.pendingCapture.delete(id);
37035
+ this.pendingRedirect.delete(id);
37036
+ this.pendingDownload.delete(id);
37037
+ this.pendingIdb.delete(id);
37038
+ }
37039
+ throw err;
37040
+ }
37041
+ }
36920
37042
  /**
36921
37043
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
36922
37044
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
@@ -36928,11 +37050,7 @@ var FetchproxyServer = class {
36928
37050
  const pending = new Promise((resolve2) => {
36929
37051
  this.pending.set(id, resolve2);
36930
37052
  });
36931
- if (this.hostHandle) {
36932
- await this.hostHandle.sendOwnInner(inner);
36933
- } else if (this.peerHandle) {
36934
- await this.peerHandle.sendInner(inner);
36935
- }
37053
+ await this.sendInnerFrame(inner);
36936
37054
  const timeoutMs = this.opts.fetchTimeoutMs;
36937
37055
  if (timeoutMs === void 0 || timeoutMs <= 0)
36938
37056
  return pending;
@@ -36961,6 +37079,51 @@ var FetchproxyServer = class {
36961
37079
  clearTimeout(timer);
36962
37080
  }
36963
37081
  }
37082
+ /**
37083
+ * FP-B2: bound a non-`fetch` verb's reply wait by `fetchTimeoutMs`.
37084
+ *
37085
+ * Before this, only `fetch()` raced its `pending` reply against a timer
37086
+ * (`_fetchOnceWithTimeout`); `readCookies`, the storage reads,
37087
+ * `capture_request_header`, `capture_redirect`, `download`, and
37088
+ * `read_indexed_db` awaited their `pending` promise with no race, so a
37089
+ * wedged extension hung the tool call indefinitely.
37090
+ *
37091
+ * `pending` is the already-registered reply promise. `pendingMap`/`id`
37092
+ * point at the op-specific map entry so we can drop it on expiry exactly
37093
+ * as `_fetchOnceWithTimeout` does — otherwise a late bridge reply would
37094
+ * resolve into a stale resolver / leak. On timeout we reject with a
37095
+ * `FetchproxyTimeoutError` (the same throwable the convenience methods
37096
+ * already surface for fetch timeouts). `0`/unset opts out (unbounded),
37097
+ * matching the fetch path.
37098
+ */
37099
+ async _withVerbTimeout(pending, pendingMap, id, url2) {
37100
+ const timeoutMs = this.opts.fetchTimeoutMs;
37101
+ if (timeoutMs === void 0 || timeoutMs <= 0)
37102
+ return pending;
37103
+ let timer;
37104
+ const start = Date.now();
37105
+ try {
37106
+ return await Promise.race([
37107
+ pending,
37108
+ new Promise((_resolve, reject) => {
37109
+ timer = setTimeout(() => {
37110
+ pendingMap.delete(id);
37111
+ reject(new FetchproxyTimeoutError({
37112
+ url: url2,
37113
+ timeoutMs,
37114
+ role: this.role,
37115
+ port: this.opts.port,
37116
+ elapsedMs: Date.now() - start,
37117
+ retryAttempted: false
37118
+ }));
37119
+ }, timeoutMs);
37120
+ })
37121
+ ]);
37122
+ } finally {
37123
+ if (timer)
37124
+ clearTimeout(timer);
37125
+ }
37126
+ }
36964
37127
  /**
36965
37128
  * Map an `ok:false` fetch result to its typed throwable. Centralizes
36966
37129
  * the kind-to-error-class switch so `request()` and (via the same
@@ -37251,12 +37414,8 @@ var FetchproxyServer = class {
37251
37414
  const pending = new Promise((resolve2) => {
37252
37415
  this.pendingReadCookies.set(id, resolve2);
37253
37416
  });
37254
- if (this.hostHandle) {
37255
- await this.hostHandle.sendOwnInner(inner);
37256
- } else if (this.peerHandle) {
37257
- await this.peerHandle.sendInner(inner);
37258
- }
37259
- const result = await pending;
37417
+ await this.sendInnerFrame(inner);
37418
+ const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37260
37419
  if (!result.ok) {
37261
37420
  throw new FetchproxyProtocolError(result.error);
37262
37421
  }
@@ -37322,12 +37481,8 @@ var FetchproxyServer = class {
37322
37481
  const pending = new Promise((resolve2, reject) => {
37323
37482
  this.pendingStorage.set(id, { resolve: resolve2, reject });
37324
37483
  });
37325
- if (this.hostHandle) {
37326
- await this.hostHandle.sendOwnInner(inner);
37327
- } else if (this.peerHandle) {
37328
- await this.peerHandle.sendInner(inner);
37329
- }
37330
- return pending;
37484
+ await this.sendInnerFrame(inner);
37485
+ return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
37331
37486
  }
37332
37487
  /**
37333
37488
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
@@ -37431,12 +37586,8 @@ var FetchproxyServer = class {
37431
37586
  const pending = new Promise((resolve2, reject) => {
37432
37587
  this.pendingCapture.set(id, { resolve: resolve2, reject });
37433
37588
  });
37434
- if (this.hostHandle) {
37435
- await this.hostHandle.sendOwnInner(inner);
37436
- } else if (this.peerHandle) {
37437
- await this.peerHandle.sendInner(inner);
37438
- }
37439
- return pending;
37589
+ await this.sendInnerFrame(inner);
37590
+ return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
37440
37591
  }
37441
37592
  /**
37442
37593
  * Snapshot the redirect target URL of the next request the browser
@@ -37520,12 +37671,8 @@ var FetchproxyServer = class {
37520
37671
  const pending = new Promise((resolve2, reject) => {
37521
37672
  this.pendingRedirect.set(id, { resolve: resolve2, reject });
37522
37673
  });
37523
- if (this.hostHandle) {
37524
- await this.hostHandle.sendOwnInner(inner);
37525
- } else if (this.peerHandle) {
37526
- await this.peerHandle.sendInner(inner);
37527
- }
37528
- return pending;
37674
+ await this.sendInnerFrame(inner);
37675
+ return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
37529
37676
  }
37530
37677
  /**
37531
37678
  * Download `url` through the BROWSER's own network stack via
@@ -37606,12 +37753,8 @@ var FetchproxyServer = class {
37606
37753
  const pending = new Promise((resolve2, reject) => {
37607
37754
  this.pendingDownload.set(id, { resolve: resolve2, reject });
37608
37755
  });
37609
- if (this.hostHandle) {
37610
- await this.hostHandle.sendOwnInner(inner);
37611
- } else if (this.peerHandle) {
37612
- await this.peerHandle.sendInner(inner);
37613
- }
37614
- return pending;
37756
+ await this.sendInnerFrame(inner);
37757
+ return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
37615
37758
  }
37616
37759
  /**
37617
37760
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
@@ -37658,12 +37801,8 @@ var FetchproxyServer = class {
37658
37801
  const pending = new Promise((resolve2, reject) => {
37659
37802
  this.pendingIdb.set(id, { resolve: resolve2, reject });
37660
37803
  });
37661
- if (this.hostHandle) {
37662
- await this.hostHandle.sendOwnInner(inner);
37663
- } else if (this.peerHandle) {
37664
- await this.peerHandle.sendInner(inner);
37665
- }
37666
- return pending;
37804
+ await this.sendInnerFrame(inner);
37805
+ return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37667
37806
  }
37668
37807
  assertScopeSubset(requested, declared, label) {
37669
37808
  const undeclared = undeclaredKeys(requested, declared);
@@ -38150,7 +38289,7 @@ function getDefaultInlineAttachments() {
38150
38289
  // package.json
38151
38290
  var package_default = {
38152
38291
  name: "ofw-mcp",
38153
- version: "2.4.1",
38292
+ version: "2.4.3",
38154
38293
  license: "MIT",
38155
38294
  mcpName: "io.github.chrischall/ofw-mcp",
38156
38295
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -38182,7 +38321,7 @@ var package_default = {
38182
38321
  "test:watch": "vitest"
38183
38322
  },
38184
38323
  dependencies: {
38185
- "@chrischall/mcp-utils": "^0.9.0",
38324
+ "@chrischall/mcp-utils": "^0.10.3",
38186
38325
  "@fetchproxy/bootstrap": "^1.3.0",
38187
38326
  "@modelcontextprotocol/sdk": "^1.29.0",
38188
38327
  dotenv: "^17.4.2",
@@ -38261,26 +38400,7 @@ async function resolveAuth() {
38261
38400
  );
38262
38401
  }
38263
38402
 
38264
- // src/env-bootstrap.ts
38265
- var USER_CONFIG_KEYS = [
38266
- "OFW_USERNAME",
38267
- "OFW_PASSWORD",
38268
- "OFW_INLINE_ATTACHMENTS",
38269
- "OFW_ATTACHMENTS_DIR",
38270
- "OFW_WRITE_MODE"
38271
- ];
38272
- function clearBlankInjectedEnv(env = process.env, keys = USER_CONFIG_KEYS) {
38273
- for (const key of keys) {
38274
- const value = env[key];
38275
- if (value === void 0) continue;
38276
- if (value.trim() === "" || value.includes("${")) {
38277
- delete env[key];
38278
- }
38279
- }
38280
- }
38281
-
38282
38403
  // src/client.ts
38283
- clearBlankInjectedEnv();
38284
38404
  var __dirname = dirname(fileURLToPath(import.meta.url));
38285
38405
  await loadDotenvSafely({ path: join4(__dirname, "..", ".env") });
38286
38406
  function parseContentDispositionFilename(cd) {
@@ -39806,7 +39926,7 @@ process.emit = function(event, ...args) {
39806
39926
  };
39807
39927
  await runMcp({
39808
39928
  name: "ofw",
39809
- version: "2.4.1",
39929
+ version: "2.4.3",
39810
39930
  // x-release-please-version
39811
39931
  deps: client,
39812
39932
  tools: [
package/dist/client.js CHANGED
@@ -4,13 +4,7 @@ import { dirname, join } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { resolveAuth } from './auth.js';
6
6
  import { parseBoolEnv } from './config.js';
7
- import { clearBlankInjectedEnv } from './env-bootstrap.js';
8
7
  import { BASE_URL, OFW_PROTOCOL_HEADERS, OFW_TOKEN_TTL_MS, OFW_TOKEN_EXPIRY_SKEW_MS } from './protocol.js';
9
- // When the plugin runs via a host that maps creds from optional
10
- // `${user_config.*}` fields, an unset field can be injected as a blank /
11
- // placeholder env value. Clear those first so the next step's .env (and the
12
- // shell env) can still populate them — a filled field keeps its real value.
13
- clearBlankInjectedEnv();
14
8
  // Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
15
9
  // bundle). loadDotenvSafely applies override:false + quiet:true and swallows a
16
10
  // missing dotenv module, matching the prior inline try/catch exactly.
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import { registerJournalTools } from './tools/journal.js';
24
24
  // always succeeds before any credential check runs.
25
25
  await runMcp({
26
26
  name: 'ofw',
27
- version: '2.4.1', // x-release-please-version
27
+ version: '2.4.3', // x-release-please-version
28
28
  deps: client,
29
29
  tools: [
30
30
  registerUserTools,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofw-mcp",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "license": "MIT",
5
5
  "mcpName": "io.github.chrischall/ofw-mcp",
6
6
  "description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Code)",
@@ -32,7 +32,7 @@
32
32
  "test:watch": "vitest"
33
33
  },
34
34
  "dependencies": {
35
- "@chrischall/mcp-utils": "^0.9.0",
35
+ "@chrischall/mcp-utils": "^0.10.3",
36
36
  "@fetchproxy/bootstrap": "^1.3.0",
37
37
  "@modelcontextprotocol/sdk": "^1.29.0",
38
38
  "dotenv": "^17.4.2",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/ofw-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.4.1",
9
+ "version": "2.4.3",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ofw-mcp",
14
- "version": "2.4.1",
14
+ "version": "2.4.3",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -1,28 +0,0 @@
1
- // Credentials + config the plugin's .mcp.json maps from `${user_config.*}`.
2
- // These are the keys whose host-injected values we sanity-check before
3
- // loading .env.
4
- export const USER_CONFIG_KEYS = [
5
- 'OFW_USERNAME',
6
- 'OFW_PASSWORD',
7
- 'OFW_INLINE_ATTACHMENTS',
8
- 'OFW_ATTACHMENTS_DIR',
9
- 'OFW_WRITE_MODE',
10
- ];
11
- // A host that maps an UNSET optional `${user_config.x}` into the server env
12
- // may inject the key as an empty string or the literal, unexpanded
13
- // "${user_config.x}". Either one SHADOWS the user's `.env`/shell value,
14
- // because the server loads .env with dotenv `override:false` (it won't
15
- // replace a key that's already present). Clearing those blanks first lets the
16
- // `.env`/shell credential path keep working when the Connectors field is left
17
- // empty — while a real, user-provided value is left untouched (so the desktop
18
- // userConfig path still wins when set).
19
- export function clearBlankInjectedEnv(env = process.env, keys = USER_CONFIG_KEYS) {
20
- for (const key of keys) {
21
- const value = env[key];
22
- if (value === undefined)
23
- continue;
24
- if (value.trim() === '' || value.includes('${')) {
25
- delete env[key];
26
- }
27
- }
28
- }