ofw-mcp 2.4.3 → 2.5.0

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
@@ -34686,6 +34686,22 @@ async function runMcp(opts) {
34686
34686
  }
34687
34687
 
34688
34688
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
34689
+ var McpToolError = class extends Error {
34690
+ /** Actionable remediation text, when one applies. */
34691
+ hint;
34692
+ constructor(message, opts) {
34693
+ super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
34694
+ this.name = "McpToolError";
34695
+ if (opts?.hint !== void 0)
34696
+ this.hint = opts.hint;
34697
+ Object.setPrototypeOf(this, new.target.prototype);
34698
+ }
34699
+ };
34700
+ var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
34701
+ var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
34702
+ var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
34703
+ var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
34704
+ var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
34689
34705
  var API_KEY_RE = new RegExp([
34690
34706
  "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
34691
34707
  // OpenAI / Anthropic (incl. sk-ant-…)
@@ -34700,6 +34716,14 @@ var API_KEY_RE = new RegExp([
34700
34716
  "whsec_[A-Za-z0-9]{16,}\\b"
34701
34717
  // webhook signing secret (Stripe-style)
34702
34718
  ].map((p) => `\\b${p}`).join("|"), "g");
34719
+ var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
34720
+ var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
34721
+ var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
34722
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
34723
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
34724
+ function redactSecrets(text) {
34725
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
34726
+ }
34703
34727
 
34704
34728
  // node_modules/@chrischall/mcp-utils/dist/response/index.js
34705
34729
  function textResult(data) {
@@ -34781,6 +34805,21 @@ async function fileBlob(path, opts = {}) {
34781
34805
  return blob;
34782
34806
  }
34783
34807
 
34808
+ // node_modules/@chrischall/mcp-utils/dist/zod/parse-lenient.js
34809
+ function parseLenient(schema, raw, opts) {
34810
+ const result = schema.safeParse(raw);
34811
+ if (result.success)
34812
+ return result.data;
34813
+ const issues = result.error.issues.map((issue2) => `${issue2.path.join(".") || "(root)"}: ${issue2.message}`).join("; ");
34814
+ if (opts.mode === "strict") {
34815
+ throw new McpToolError(`Unexpected ${opts.context} shape from the upstream API. ${issues}`, {
34816
+ hint: "The upstream API may have changed; the schema needs updating."
34817
+ });
34818
+ }
34819
+ console.error(`[${opts.label}] WARNING: unexpected ${opts.context} shape \u2014 proceeding with the raw response. ${issues}`);
34820
+ return raw;
34821
+ }
34822
+
34784
34823
  // node_modules/@chrischall/mcp-utils/dist/zod/index.js
34785
34824
  var PositiveInt = external_exports.number().int().positive();
34786
34825
  var NonNegInt = external_exports.number().int().nonnegative();
@@ -34878,7 +34917,7 @@ var TokenManager = class {
34878
34917
  };
34879
34918
 
34880
34919
  // src/client.ts
34881
- import { dirname, join as join4 } from "path";
34920
+ import { dirname, join as join3 } from "path";
34882
34921
  import { fileURLToPath } from "url";
34883
34922
 
34884
34923
  // node_modules/@fetchproxy/protocol/dist/frames.js
@@ -34892,6 +34931,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
34892
34931
  "capture_request_header",
34893
34932
  "capture_redirect",
34894
34933
  "read_indexed_db",
34934
+ "read_dom",
34895
34935
  "download"
34896
34936
  ]);
34897
34937
 
@@ -35193,6 +35233,44 @@ function assertIndexedDbScopesArray(value, label) {
35193
35233
  }
35194
35234
  }
35195
35235
  }
35236
+ var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
35237
+ var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
35238
+ function assertDomSelectorsArray(value, label) {
35239
+ if (!Array.isArray(value)) {
35240
+ throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
35241
+ }
35242
+ const seen = /* @__PURE__ */ new Set();
35243
+ for (let i = 0; i < value.length; i++) {
35244
+ const entry = value[i];
35245
+ assertObject(entry, `${label}[${i}]`);
35246
+ if (entry.name === void 0) {
35247
+ throw new ProtocolError(`${label}[${i}].name: missing`);
35248
+ }
35249
+ if (entry.selector === void 0) {
35250
+ throw new ProtocolError(`${label}[${i}].selector: missing`);
35251
+ }
35252
+ if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
35253
+ throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
35254
+ }
35255
+ if (typeof entry.selector !== "string" || !DOM_SELECTOR_RE.test(entry.selector)) {
35256
+ throw new ProtocolError(`${label}[${i}].selector: invalid ${JSON.stringify(entry.selector)}`);
35257
+ }
35258
+ if (entry.attribute !== void 0) {
35259
+ if (typeof entry.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(entry.attribute)) {
35260
+ throw new ProtocolError(`${label}[${i}].attribute: invalid ${JSON.stringify(entry.attribute)}`);
35261
+ }
35262
+ }
35263
+ if (seen.has(entry.name)) {
35264
+ throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
35265
+ }
35266
+ seen.add(entry.name);
35267
+ for (const k of Object.keys(entry)) {
35268
+ if (k !== "name" && k !== "selector" && k !== "attribute") {
35269
+ throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
35270
+ }
35271
+ }
35272
+ }
35273
+ }
35196
35274
  function validateFrame(raw) {
35197
35275
  assertObject(raw, "frame");
35198
35276
  const t = raw.type;
@@ -35268,6 +35346,9 @@ function validateHello(raw) {
35268
35346
  if (raw.sessionStoragePointers !== void 0) {
35269
35347
  assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
35270
35348
  }
35349
+ if (raw.domSelectors !== void 0) {
35350
+ assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
35351
+ }
35271
35352
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
35272
35353
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
35273
35354
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
@@ -35502,6 +35583,21 @@ function validateInnerRequest(raw) {
35502
35583
  }
35503
35584
  return raw;
35504
35585
  }
35586
+ if (raw.op === "read_dom") {
35587
+ assertObject(raw.init, "inner.init");
35588
+ if (raw.init.origin === void 0)
35589
+ throw new ProtocolError("inner.init.origin: missing");
35590
+ if (raw.init.names === void 0)
35591
+ throw new ProtocolError("inner.init.names: missing");
35592
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35593
+ assertNonEmptyKeyArray(raw.init.names, "inner.init.names");
35594
+ for (const k of Object.keys(raw.init)) {
35595
+ if (k !== "origin" && k !== "names") {
35596
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom`);
35597
+ }
35598
+ }
35599
+ return raw;
35600
+ }
35505
35601
  if (raw.op === "download") {
35506
35602
  assertObject(raw.init, "inner.init");
35507
35603
  if (raw.init.url === void 0) {
@@ -35527,7 +35623,7 @@ function validateInnerRequest(raw) {
35527
35623
  }
35528
35624
  return raw;
35529
35625
  }
35530
- throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
35626
+ throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download"; got ${JSON.stringify(raw.op)}`);
35531
35627
  }
35532
35628
  function assertNonEmptyKeyArray(value, label) {
35533
35629
  if (!Array.isArray(value)) {
@@ -35612,6 +35708,13 @@ function validateInnerResponse(raw) {
35612
35708
  assertObject(raw.values, "inner.values");
35613
35709
  return raw;
35614
35710
  }
35711
+ if (op === "read_dom") {
35712
+ if (raw.values === void 0) {
35713
+ throw new ProtocolError("inner.values: missing on read_dom response");
35714
+ }
35715
+ assertStringMap(raw.values, "inner.values");
35716
+ return raw;
35717
+ }
35615
35718
  if (op === "download") {
35616
35719
  assertObject(raw.value, "inner.value");
35617
35720
  assertString(raw.value.path, "inner.value.path");
@@ -35951,6 +36054,13 @@ async function buildServerHello(opts) {
35951
36054
  jsonPointer: d.jsonPointer
35952
36055
  }));
35953
36056
  }
36057
+ if (opts.domSelectors && opts.domSelectors.length > 0) {
36058
+ hello.domSelectors = opts.domSelectors.map((d) => ({
36059
+ name: d.name,
36060
+ selector: d.selector,
36061
+ ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
36062
+ }));
36063
+ }
35954
36064
  return hello;
35955
36065
  }
35956
36066
 
@@ -36040,7 +36150,8 @@ async function startHost(opts) {
36040
36150
  captureHeaders: opts.ownCaptureHeaders,
36041
36151
  indexedDbScopes: opts.ownIndexedDbScopes,
36042
36152
  localStoragePointers: opts.ownLocalStoragePointers,
36043
- sessionStoragePointers: opts.ownSessionStoragePointers
36153
+ sessionStoragePointers: opts.ownSessionStoragePointers,
36154
+ domSelectors: opts.ownDomSelectors
36044
36155
  });
36045
36156
  const ownSessionNonce = fromB64(ownHello.sessionNonce);
36046
36157
  let extensionWs = null;
@@ -36275,6 +36386,7 @@ async function startPeer(opts) {
36275
36386
  sessionStorageKeys: opts.sessionStorageKeys,
36276
36387
  captureHeaders: opts.captureHeaders,
36277
36388
  indexedDbScopes: opts.indexedDbScopes,
36389
+ domSelectors: opts.domSelectors,
36278
36390
  localStoragePointers: opts.localStoragePointers,
36279
36391
  sessionStoragePointers: opts.sessionStoragePointers
36280
36392
  });
@@ -36682,6 +36794,11 @@ var FetchproxyServer = class {
36682
36794
  key: d.key,
36683
36795
  jsonPointer: d.jsonPointer
36684
36796
  })),
36797
+ domSelectors: (opts.domSelectors ?? []).map((d) => ({
36798
+ name: d.name,
36799
+ selector: d.selector,
36800
+ ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
36801
+ })),
36685
36802
  // 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
36686
36803
  // adapter was about to set these to the same numbers anyway; the
36687
36804
  // back-door is `0` (explicit opt-out) if a caller genuinely wants
@@ -36802,6 +36919,7 @@ var FetchproxyServer = class {
36802
36919
  ownIndexedDbScopes: this.opts.indexedDbScopes,
36803
36920
  ownLocalStoragePointers: this.opts.localStoragePointers,
36804
36921
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
36922
+ ownDomSelectors: this.opts.domSelectors,
36805
36923
  onPairCode: this.opts.onPairCode
36806
36924
  });
36807
36925
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
@@ -36829,7 +36947,8 @@ var FetchproxyServer = class {
36829
36947
  captureHeaders: this.opts.captureHeaders,
36830
36948
  indexedDbScopes: this.opts.indexedDbScopes,
36831
36949
  localStoragePointers: this.opts.localStoragePointers,
36832
- sessionStoragePointers: this.opts.sessionStoragePointers
36950
+ sessionStoragePointers: this.opts.sessionStoragePointers,
36951
+ domSelectors: this.opts.domSelectors
36833
36952
  });
36834
36953
  this.peerHandle.onInner((inner) => this.onInner(inner));
36835
36954
  this.peerHandle.onRenegotiate(() => {
@@ -37804,6 +37923,46 @@ var FetchproxyServer = class {
37804
37923
  await this.sendInnerFrame(inner);
37805
37924
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37806
37925
  }
37926
+ /**
37927
+ * 1.4.0+: read declared DOM values from the user's signed-in tab.
37928
+ * Requires `'read_dom'` in capabilities AND every requested `name` to
37929
+ * match a declared `domSelectors` entry. The extension reads each
37930
+ * declared selector from the matched tab's DOM (isolated-world
37931
+ * `querySelector`, value or attribute) — no page-JS execution.
37932
+ *
37933
+ * Returns a `Record<string, string>` of `name → value`, with names
37934
+ * whose element (or attribute) was absent omitted. Throws
37935
+ * `FetchproxyProtocolError` on bridge failures and a plain `Error` on
37936
+ * developer mistakes (undeclared capability, undeclared name).
37937
+ */
37938
+ async readDom(opts) {
37939
+ if (!this.opts.capabilities.includes("read_dom")) {
37940
+ throw new Error('FetchproxyServer.readDom(): MCP did not declare "read_dom" in capabilities');
37941
+ }
37942
+ await this.ensureConnected();
37943
+ this.throwIfPendingPair();
37944
+ if (!Array.isArray(opts.names) || opts.names.length === 0) {
37945
+ throw new Error("FetchproxyServer.readDom: opts.names must be a non-empty array");
37946
+ }
37947
+ this.assertScopeSubset(opts.names, this.opts.domSelectors.map((d) => d.name), "domSelectors");
37948
+ if (opts.subdomain !== void 0)
37949
+ assertSubdomainLabel(opts.subdomain);
37950
+ const baseDomain = this.resolveBaseDomain(opts.domain);
37951
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
37952
+ const origin = `https://${host}`;
37953
+ const id = this.nextRequestId++;
37954
+ const inner = {
37955
+ type: "request",
37956
+ id,
37957
+ op: "read_dom",
37958
+ init: { origin, names: [...opts.names] }
37959
+ };
37960
+ const pending = new Promise((resolve2, reject) => {
37961
+ this.pendingStorage.set(id, { resolve: resolve2, reject });
37962
+ });
37963
+ await this.sendInnerFrame(inner);
37964
+ return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
37965
+ }
37807
37966
  assertScopeSubset(requested, declared, label) {
37808
37967
  const undeclared = undeclaredKeys(requested, declared);
37809
37968
  if (undeclared.length > 0) {
@@ -37875,7 +38034,7 @@ var FetchproxyServer = class {
37875
38034
  if (storageCb) {
37876
38035
  this.pendingStorage.delete(inner.id);
37877
38036
  if (inner.ok) {
37878
- if ((inner.op === "read_local_storage" || inner.op === "read_session_storage") && inner.values) {
38037
+ if ((inner.op === "read_local_storage" || inner.op === "read_session_storage" || inner.op === "read_dom") && inner.values) {
37879
38038
  storageCb.resolve({ ...inner.values });
37880
38039
  } else {
37881
38040
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
@@ -38243,53 +38402,10 @@ async function loginWithPassword(username, password) {
38243
38402
  };
38244
38403
  }
38245
38404
 
38246
- // src/config.ts
38247
- import { createHash } from "node:crypto";
38248
- import { homedir as homedir3 } from "node:os";
38249
- import { join as join3 } from "node:path";
38250
- function readCacheIdentity() {
38251
- const explicit = process.env.OFW_CACHE_IDENTITY;
38252
- if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
38253
- const username = process.env.OFW_USERNAME;
38254
- if (typeof username === "string" && username.trim().length > 0) return username.trim();
38255
- return "_default";
38256
- }
38257
- function getCacheDir() {
38258
- const override = process.env.OFW_CACHE_DIR;
38259
- if (override && override.trim().length > 0) return override.trim();
38260
- return join3(homedir3(), ".cache", "ofw-mcp");
38261
- }
38262
- function getCacheDbPath() {
38263
- const identity = readCacheIdentity();
38264
- const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
38265
- return join3(getCacheDir(), `${hash2}.db`);
38266
- }
38267
- function getAttachmentsDir() {
38268
- const override = process.env.OFW_ATTACHMENTS_DIR;
38269
- if (override && override.trim().length > 0) return override.trim();
38270
- return join3(homedir3(), "Downloads", "ofw-mcp");
38271
- }
38272
- function parseBoolEnv2(name) {
38273
- return parseBoolEnv(name);
38274
- }
38275
- function getWriteMode() {
38276
- const raw = process.env.OFW_WRITE_MODE;
38277
- if (typeof raw !== "string" || raw.trim().length === 0) return "all";
38278
- const mode = raw.trim().toLowerCase();
38279
- if (mode === "none" || mode === "drafts" || mode === "all") return mode;
38280
- console.error(
38281
- `[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" \u2014 failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`
38282
- );
38283
- return "none";
38284
- }
38285
- function getDefaultInlineAttachments() {
38286
- return parseBoolEnv2("OFW_INLINE_ATTACHMENTS");
38287
- }
38288
-
38289
38405
  // package.json
38290
38406
  var package_default = {
38291
38407
  name: "ofw-mcp",
38292
- version: "2.4.3",
38408
+ version: "2.5.0",
38293
38409
  license: "MIT",
38294
38410
  mcpName: "io.github.chrischall/ofw-mcp",
38295
38411
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -38321,31 +38437,28 @@ var package_default = {
38321
38437
  "test:watch": "vitest"
38322
38438
  },
38323
38439
  dependencies: {
38324
- "@chrischall/mcp-utils": "^0.10.3",
38440
+ "@chrischall/mcp-utils": "^0.13.0",
38325
38441
  "@fetchproxy/bootstrap": "^1.3.0",
38326
38442
  "@modelcontextprotocol/sdk": "^1.29.0",
38327
38443
  dotenv: "^17.4.2",
38328
38444
  zod: "^4.4.3"
38329
38445
  },
38330
38446
  devDependencies: {
38331
- "@types/node": "^25.9.1",
38447
+ "@types/node": "^26.0.0",
38332
38448
  "@vitest/coverage-v8": "^4.1.7",
38333
38449
  esbuild: "^0.28.0",
38334
- typescript: "^6.0.3",
38450
+ typescript: "^7.0.2",
38335
38451
  vitest: "^4.1.7"
38336
38452
  }
38337
38453
  };
38338
38454
 
38339
38455
  // src/auth.ts
38340
- function readEnv(key) {
38341
- return readEnvVar(key);
38342
- }
38343
38456
  function fetchproxyDisabled() {
38344
- return parseBoolEnv2("OFW_DISABLE_FETCHPROXY");
38457
+ return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
38345
38458
  }
38346
38459
  async function resolveAuth() {
38347
- const username = readEnv("OFW_USERNAME");
38348
- const password = readEnv("OFW_PASSWORD");
38460
+ const username = readEnvVar("OFW_USERNAME");
38461
+ const password = readEnvVar("OFW_PASSWORD");
38349
38462
  if (username && password) {
38350
38463
  const { token, expiresAt } = await loginWithPassword(username, password);
38351
38464
  return { token, expiresAt, source: "env" };
@@ -38402,7 +38515,7 @@ async function resolveAuth() {
38402
38515
 
38403
38516
  // src/client.ts
38404
38517
  var __dirname = dirname(fileURLToPath(import.meta.url));
38405
- await loadDotenvSafely({ path: join4(__dirname, "..", ".env") });
38518
+ await loadDotenvSafely({ path: join3(__dirname, "..", ".env") });
38406
38519
  function parseContentDispositionFilename(cd) {
38407
38520
  const extMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(cd);
38408
38521
  if (extMatch) {
@@ -38417,12 +38530,7 @@ function parseContentDispositionFilename(cd) {
38417
38530
  return m ? m[1] : null;
38418
38531
  }
38419
38532
  function debugLogEnabled() {
38420
- return parseBoolEnv2("OFW_DEBUG_LOG");
38421
- }
38422
- function redactHeaders(h) {
38423
- const out = { ...h };
38424
- if (out.Authorization) out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}\u2026`;
38425
- return out;
38533
+ return parseBoolEnv("OFW_DEBUG_LOG");
38426
38534
  }
38427
38535
  var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
38428
38536
  function getRequestTimeoutMs() {
@@ -38517,7 +38625,7 @@ var OFWClient = class {
38517
38625
  if (debugLogEnabled()) {
38518
38626
  const bodyPreview = body === void 0 ? "<none>" : isFormData ? `<FormData entries=${Array.from(body.keys()).join(",")}>` : JSON.stringify(body);
38519
38627
  console.error(`[ofw-debug] \u2192 ${method} ${url2}${isRetry ? " (retry)" : ""}`);
38520
- console.error(`[ofw-debug] headers: ${JSON.stringify(redactHeaders(headers))}`);
38628
+ console.error(`[ofw-debug] headers: ${redactSecrets(JSON.stringify(headers))}`);
38521
38629
  console.error(`[ofw-debug] body: ${bodyPreview}`);
38522
38630
  }
38523
38631
  const timeoutMs = getRequestTimeoutMs();
@@ -38557,17 +38665,6 @@ var OFWClient = class {
38557
38665
  };
38558
38666
  var client = new OFWClient();
38559
38667
 
38560
- // src/validate.ts
38561
- function parseOFW(schema, raw, ctx, mode = "lenient") {
38562
- const result = schema.safeParse(raw);
38563
- if (result.success) return result.data;
38564
- const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
38565
- const message = `OFW response for ${ctx} failed validation: ${issues}`;
38566
- if (mode === "strict") throw new Error(message);
38567
- console.error(`[ofw-mcp] WARNING: ${message} \u2014 continuing with the raw response; fields derived from it may be missing or wrong.`);
38568
- return raw;
38569
- }
38570
-
38571
38668
  // src/tools/_shared.ts
38572
38669
  var jsonResponse = textResult;
38573
38670
  var textResponse = rawTextResult;
@@ -38576,11 +38673,14 @@ var ApiRecipientSchema = external_exports.looseObject({
38576
38673
  viewed: external_exports.looseObject({ dateTime: external_exports.string() }).nullable().optional()
38577
38674
  });
38578
38675
  function mapRecipients(items) {
38579
- return (items ?? []).map((r) => ({
38580
- userId: r.user?.id ?? 0,
38581
- name: r.user?.name ?? "",
38582
- viewedAt: r.viewed?.dateTime ?? null
38583
- }));
38676
+ return (items ?? []).map((r) => {
38677
+ const dt = r.viewed?.dateTime;
38678
+ const viewedAt = typeof dt === "string" && !dt.startsWith("1970-01-01") ? dt : null;
38679
+ return { userId: r.user?.id ?? 0, name: r.user?.name ?? "", viewedAt };
38680
+ });
38681
+ }
38682
+ function hasRealView(recipients) {
38683
+ return recipients.some((r) => r.viewedAt !== null && !r.viewedAt.startsWith("1970-01-01"));
38584
38684
  }
38585
38685
  var expandPath2 = expandPath;
38586
38686
  function verifyWriteLanded(kind, sent, persisted) {
@@ -38599,19 +38699,17 @@ var PostMessagesResponseSchema = external_exports.looseObject({
38599
38699
  entityId: external_exports.number().optional()
38600
38700
  }).nullable();
38601
38701
  async function postMessageAndRefetch(client2, payload, detailSchema, ctx) {
38602
- const raw = parseOFW(
38702
+ const raw = parseLenient(
38603
38703
  PostMessagesResponseSchema,
38604
38704
  await client2.request("POST", "/pub/v3/messages", payload),
38605
- `POST /pub/v3/messages (${ctx})`,
38606
- "strict"
38705
+ { label: "ofw-mcp", context: `POST /pub/v3/messages (${ctx})`, mode: "strict" }
38607
38706
  );
38608
38707
  const id = typeof raw?.id === "number" ? raw.id : typeof raw?.entityId === "number" ? raw.entityId : null;
38609
38708
  if (id === null) return { id: null, detail: null, raw };
38610
- const detail = parseOFW(
38709
+ const detail = parseLenient(
38611
38710
  detailSchema,
38612
38711
  await client2.request("GET", `/pub/v3/messages/${id}`),
38613
- `GET /pub/v3/messages/{id} (${ctx})`,
38614
- "strict"
38712
+ { label: "ofw-mcp", context: `GET /pub/v3/messages/{id} (${ctx})`, mode: "strict" }
38615
38713
  );
38616
38714
  return { id, detail, raw };
38617
38715
  }
@@ -38638,6 +38736,49 @@ function registerUserTools(server, client2) {
38638
38736
  import { DatabaseSync } from "node:sqlite";
38639
38737
  import { mkdirSync, chmodSync, existsSync } from "node:fs";
38640
38738
  import { dirname as dirname2 } from "node:path";
38739
+
38740
+ // src/config.ts
38741
+ import { createHash } from "node:crypto";
38742
+ import { homedir as homedir3 } from "node:os";
38743
+ import { join as join4 } from "node:path";
38744
+ function readCacheIdentity() {
38745
+ return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
38746
+ }
38747
+ function getCacheDir() {
38748
+ const override = process.env.OFW_CACHE_DIR;
38749
+ if (override && override.trim().length > 0) return override.trim();
38750
+ return join4(homedir3(), ".cache", "ofw-mcp");
38751
+ }
38752
+ function getCacheDbPath() {
38753
+ const identity = readCacheIdentity();
38754
+ const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
38755
+ return join4(getCacheDir(), `${hash2}.db`);
38756
+ }
38757
+ function getAttachmentsDir() {
38758
+ const override = process.env.OFW_ATTACHMENTS_DIR;
38759
+ if (override && override.trim().length > 0) return override.trim();
38760
+ return join4(homedir3(), "Downloads", "ofw-mcp");
38761
+ }
38762
+ function getWriteMode() {
38763
+ const raw = process.env.OFW_WRITE_MODE;
38764
+ if (typeof raw !== "string" || raw.trim().length === 0) return "all";
38765
+ const mode = raw.trim().toLowerCase();
38766
+ if (mode === "none" || mode === "drafts" || mode === "all") return mode;
38767
+ console.error(
38768
+ `[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" \u2014 failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`
38769
+ );
38770
+ return "none";
38771
+ }
38772
+ function getCalendarWritesAllowed() {
38773
+ const mode = getWriteMode();
38774
+ if (mode === "all") return true;
38775
+ return mode === "drafts" && parseBoolEnv("OFW_CALENDAR_WRITES");
38776
+ }
38777
+ function getDefaultInlineAttachments() {
38778
+ return parseBoolEnv("OFW_INLINE_ATTACHMENTS");
38779
+ }
38780
+
38781
+ // src/cache.ts
38641
38782
  var instance = null;
38642
38783
  var SCHEMA_V1 = `
38643
38784
  CREATE TABLE IF NOT EXISTS messages (
@@ -38990,10 +39131,10 @@ var FileMetaSchema = external_exports.looseObject({
38990
39131
  fileSize: external_exports.number().optional()
38991
39132
  });
38992
39133
  async function fetchAttachmentMeta(client2, fileId, messageId) {
38993
- const meta3 = parseOFW(
39134
+ const meta3 = parseLenient(
38994
39135
  FileMetaSchema,
38995
39136
  await client2.request("GET", `/pub/v1/myfiles/${fileId}`),
38996
- "GET /pub/v1/myfiles/{fileId}"
39137
+ { label: "ofw-mcp", context: "GET /pub/v1/myfiles/{fileId}" }
38997
39138
  );
38998
39139
  upsertAttachmentForMessage({
38999
39140
  fileId: meta3.fileId ?? fileId,
@@ -39012,10 +39153,10 @@ var FoldersSchema = external_exports.looseObject({
39012
39153
  systemFolders: external_exports.array(external_exports.looseObject({ id: external_exports.string(), folderType: external_exports.string() })).optional()
39013
39154
  });
39014
39155
  async function resolveFolderIds(client2) {
39015
- const data = parseOFW(
39156
+ const data = parseLenient(
39016
39157
  FoldersSchema,
39017
39158
  await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true"),
39018
- "GET /pub/v1/messageFolders"
39159
+ { label: "ofw-mcp", context: "GET /pub/v1/messageFolders" }
39019
39160
  );
39020
39161
  const sys = data.systemFolders ?? [];
39021
39162
  const find = (type) => {
@@ -39042,7 +39183,10 @@ var ListItemSchema = external_exports.looseObject({
39042
39183
  var ListResponseSchema = external_exports.looseObject({ data: external_exports.array(ListItemSchema).optional() });
39043
39184
  var DetailResponseSchema = external_exports.looseObject({
39044
39185
  body: external_exports.string().optional(),
39045
- files: external_exports.array(external_exports.number()).optional()
39186
+ files: external_exports.array(external_exports.number()).optional(),
39187
+ // The detail endpoint carries the REAL recipient view timestamps (the list
39188
+ // endpoint only has an epoch placeholder) — used by the view-status refresh.
39189
+ recipients: external_exports.array(ApiRecipientSchema).optional()
39046
39190
  });
39047
39191
  async function syncMessageFolder(client2, folder, folderId, opts) {
39048
39192
  let page = 1;
@@ -39051,10 +39195,10 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
39051
39195
  const unread = [];
39052
39196
  while (true) {
39053
39197
  const path = `/pub/v3/messages?folders=${encodeURIComponent(folderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
39054
- const list = parseOFW(
39198
+ const list = parseLenient(
39055
39199
  ListResponseSchema,
39056
39200
  await client2.request("GET", path),
39057
- `GET /pub/v3/messages?folders={${folder}}`
39201
+ { label: "ofw-mcp", context: `GET /pub/v3/messages?folders={${folder}}` }
39058
39202
  );
39059
39203
  const items = list.data ?? [];
39060
39204
  if (items.length === 0) break;
@@ -39062,7 +39206,18 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
39062
39206
  for (const item of items) {
39063
39207
  if (newestId === null || item.id > newestId) newestId = item.id;
39064
39208
  const existing = getMessage(item.id);
39065
- if (existing) continue;
39209
+ if (existing) {
39210
+ if (folder === "sent" && item.showNeverViewed === false && !hasRealView(existing.recipients)) {
39211
+ const detail = parseLenient(
39212
+ DetailResponseSchema,
39213
+ await client2.request("GET", `/pub/v3/messages/${item.id}`),
39214
+ { label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (view-status refresh)" }
39215
+ );
39216
+ upsertMessage({ ...existing, recipients: mapRecipients(detail.recipients), listData: item });
39217
+ synced++;
39218
+ }
39219
+ continue;
39220
+ }
39066
39221
  pageHadNewItem = true;
39067
39222
  const isInboxUnread = folder === "inbox" && item.showNeverViewed === true;
39068
39223
  const shouldFetchBody = !isInboxUnread || opts.fetchUnreadBodies;
@@ -39070,10 +39225,10 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
39070
39225
  let fetchedBodyAt = null;
39071
39226
  let detailFileIds = [];
39072
39227
  if (shouldFetchBody) {
39073
- const detail = parseOFW(
39228
+ const detail = parseLenient(
39074
39229
  DetailResponseSchema,
39075
39230
  await client2.request("GET", `/pub/v3/messages/${item.id}`),
39076
- "GET /pub/v3/messages/{id} (sync)"
39231
+ { label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (sync)" }
39077
39232
  );
39078
39233
  body = detail.body ?? "";
39079
39234
  fetchedBodyAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -39133,10 +39288,10 @@ async function syncDrafts(client2, draftsFolderId) {
39133
39288
  let page = 1;
39134
39289
  while (true) {
39135
39290
  const path = `/pub/v3/messages?folders=${encodeURIComponent(draftsFolderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
39136
- const list = parseOFW(
39291
+ const list = parseLenient(
39137
39292
  DraftListResponseSchema,
39138
39293
  await client2.request("GET", path),
39139
- "GET /pub/v3/messages?folders={drafts}"
39294
+ { label: "ofw-mcp", context: "GET /pub/v3/messages?folders={drafts}" }
39140
39295
  );
39141
39296
  const pageItems = list.data ?? [];
39142
39297
  items.push(...pageItems);
@@ -39149,10 +39304,10 @@ async function syncDrafts(client2, draftsFolderId) {
39149
39304
  seenIds.add(item.id);
39150
39305
  const modifiedAt = item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString();
39151
39306
  const existing = getDraft(item.id);
39152
- const detail = parseOFW(
39307
+ const detail = parseLenient(
39153
39308
  DraftDetailSchema,
39154
39309
  await client2.request("GET", `/pub/v3/messages/${item.id}`),
39155
- "GET /pub/v3/messages/{id} (drafts sync)"
39310
+ { label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (drafts sync)" }
39156
39311
  );
39157
39312
  const row = {
39158
39313
  id: item.id,
@@ -39349,13 +39504,31 @@ function registerMessageTools(server, client2) {
39349
39504
  }
39350
39505
  const cached2 = getMessage(id);
39351
39506
  if (cached2 && cached2.body !== null) {
39507
+ let row2 = cached2;
39508
+ if (cached2.folder === "sent" && !hasRealView(cached2.recipients)) {
39509
+ try {
39510
+ const detail2 = parseLenient(
39511
+ MessageDetailSchema,
39512
+ await client2.request("GET", `/pub/v3/messages/${id}`),
39513
+ { label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (view-status refresh)" }
39514
+ );
39515
+ const recipients = mapRecipients(detail2.recipients);
39516
+ row2 = {
39517
+ ...cached2,
39518
+ recipients,
39519
+ listData: { ...cached2.listData, showNeverViewed: !hasRealView(recipients) }
39520
+ };
39521
+ upsertMessage(row2);
39522
+ } catch {
39523
+ }
39524
+ }
39352
39525
  let attachments2 = listAttachmentsForMessage(id);
39353
- if (attachments2.length === 0 && listDataHintsAtFiles(cached2.listData)) {
39526
+ if (attachments2.length === 0 && listDataHintsAtFiles(row2.listData)) {
39354
39527
  try {
39355
- const detail2 = parseOFW(
39528
+ const detail2 = parseLenient(
39356
39529
  DetailFilesSchema,
39357
39530
  await client2.request("GET", `/pub/v3/messages/${id}`),
39358
- "GET /pub/v3/messages/{id} (attachment backfill)"
39531
+ { label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (attachment backfill)" }
39359
39532
  );
39360
39533
  if (Array.isArray(detail2.files) && detail2.files.length > 0) {
39361
39534
  await fetchAttachmentMetaForMessage(client2, id, detail2.files);
@@ -39364,12 +39537,12 @@ function registerMessageTools(server, client2) {
39364
39537
  } catch {
39365
39538
  }
39366
39539
  }
39367
- return jsonResponse({ ...cached2, attachments: attachments2 });
39540
+ return jsonResponse({ ...row2, attachments: attachments2 });
39368
39541
  }
39369
- const detail = parseOFW(
39542
+ const detail = parseLenient(
39370
39543
  MessageDetailSchema,
39371
39544
  await client2.request("GET", `/pub/v3/messages/${encodeURIComponent(args.messageId)}`),
39372
- "GET /pub/v3/messages/{id} (ofw_get_message)"
39545
+ { label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (ofw_get_message)" }
39373
39546
  );
39374
39547
  const folder = cached2?.folder ?? "inbox";
39375
39548
  const row = {
@@ -39651,11 +39824,10 @@ ${text}` : text);
39651
39824
  form.append("label", args.label ?? fileName);
39652
39825
  form.append("fileName", fileName);
39653
39826
  form.append("shareClass", args.shareClass ?? "PRIVATE");
39654
- const meta3 = parseOFW(
39827
+ const meta3 = parseLenient(
39655
39828
  UploadedFileSchema,
39656
39829
  await client2.request("POST", "/pub/v3/myfiles/multipart", form),
39657
- "POST /pub/v3/myfiles/multipart (ofw_upload_attachment)",
39658
- "strict"
39830
+ { label: "ofw-mcp", context: "POST /pub/v3/myfiles/multipart (ofw_upload_attachment)", mode: "strict" }
39659
39831
  );
39660
39832
  upsertAttachmentForMessage({
39661
39833
  fileId: meta3.fileId,
@@ -39727,12 +39899,13 @@ ${text}` : text);
39727
39899
  } }] };
39728
39900
  }
39729
39901
  let dest;
39902
+ const safeName = basename(cached2.fileName);
39730
39903
  if (args.saveTo) {
39731
39904
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
39732
39905
  const abs = expandPath2(args.saveTo);
39733
- dest = isDirArg ? join5(abs, `${fileId}-${cached2.fileName}`) : abs;
39906
+ dest = isDirArg ? join5(abs, `${fileId}-${safeName}`) : abs;
39734
39907
  } else {
39735
- dest = join5(getAttachmentsDir(), `${fileId}-${cached2.fileName}`);
39908
+ dest = join5(getAttachmentsDir(), `${fileId}-${safeName}`);
39736
39909
  }
39737
39910
  if (!args.force && cached2.downloadedPath === dest) {
39738
39911
  return jsonResponse({
@@ -39780,8 +39953,87 @@ async function deleteOFWMessages(client2, ids) {
39780
39953
  }
39781
39954
 
39782
39955
  // src/tools/calendar.ts
39956
+ var ofwDate = external_exports.looseObject({ dateTime: external_exports.string() });
39957
+ var userRef = external_exports.looseObject({ userId: external_exports.number() });
39958
+ var eventDetailSchema = external_exports.looseObject({
39959
+ eventRecurrenceId: external_exports.number(),
39960
+ title: external_exports.string(),
39961
+ allDay: external_exports.boolean(),
39962
+ publicFlag: external_exports.boolean(),
39963
+ startDate: ofwDate,
39964
+ endDate: ofwDate,
39965
+ location: external_exports.string().nullish(),
39966
+ notes: external_exports.string().nullish(),
39967
+ reminderMinutes: external_exports.number().nullish(),
39968
+ children: external_exports.array(userRef).nullish(),
39969
+ eventParent: userRef.nullish(),
39970
+ dropOffParent: userRef.nullish(),
39971
+ pickUpParent: userRef.nullish()
39972
+ });
39973
+ var eventWriteFields = {
39974
+ startDate: external_exports.string().describe("Start date YYYY-MM-DD"),
39975
+ endDate: external_exports.string().describe("End date YYYY-MM-DD (default: startDate)").optional(),
39976
+ startTime: external_exports.string().describe("Start time HH:mm, 24-hour (required unless allDay)").optional(),
39977
+ endTime: external_exports.string().describe("End time HH:mm, 24-hour (required unless allDay)").optional(),
39978
+ allDay: external_exports.boolean().optional(),
39979
+ privateEvent: external_exports.boolean().describe("true = visible only to you; default false = shared with co-parent").optional(),
39980
+ location: external_exports.string().optional(),
39981
+ notes: external_exports.string().optional(),
39982
+ reminderMinutes: external_exports.number().int().min(0).optional(),
39983
+ children: external_exports.array(external_exports.number()).describe("Child userIds to tag (see ofw_get_profile)").optional(),
39984
+ eventParentId: external_exports.number().describe("userId of the parent the event is 'for'").optional(),
39985
+ dropOffParentId: external_exports.number().describe("userId of the drop-off parent").optional(),
39986
+ pickUpParentId: external_exports.number().describe("userId of the pick-up parent").optional()
39987
+ };
39988
+ function buildEventPayload(a) {
39989
+ const allDay = a.allDay ?? false;
39990
+ if (!allDay && (!a.startTime || !a.endTime)) {
39991
+ throw new Error("startTime and endTime (HH:mm) are required unless allDay is true");
39992
+ }
39993
+ const payload = {
39994
+ title: a.title,
39995
+ startDate: a.startDate,
39996
+ endDate: a.endDate ?? a.startDate,
39997
+ // The web form always sends times; for all-day events it uses 01:00/02:00
39998
+ // placeholders that OFW ignores.
39999
+ startTime: a.startTime ?? "01:00",
40000
+ endTime: a.endTime ?? "02:00",
40001
+ allDay,
40002
+ publicFlag: !(a.privateEvent ?? false)
40003
+ };
40004
+ if (a.location) payload.location = a.location;
40005
+ if (a.notes) payload.notes = a.notes;
40006
+ if (a.reminderMinutes !== void 0) payload.reminderMinutes = String(a.reminderMinutes);
40007
+ if (a.children !== void 0) payload.children = a.children;
40008
+ if (a.eventParentId !== void 0) payload.eventParentId = String(a.eventParentId);
40009
+ if (a.dropOffParentId !== void 0) payload.dropOffParentId = String(a.dropOffParentId);
40010
+ if (a.pickUpParentId !== void 0) payload.pickUpParentId = String(a.pickUpParentId);
40011
+ return payload;
40012
+ }
40013
+ function detailToWriteArgs(d) {
40014
+ const [startDate, startClock] = d.startDate.dateTime.split("T");
40015
+ const [endDate, endClock] = d.endDate.dateTime.split("T");
40016
+ return {
40017
+ title: d.title,
40018
+ startDate,
40019
+ endDate,
40020
+ startTime: (startClock ?? "01:00:00").slice(0, 5),
40021
+ endTime: (endClock ?? "02:00:00").slice(0, 5),
40022
+ allDay: d.allDay,
40023
+ privateEvent: !d.publicFlag,
40024
+ location: d.location ?? void 0,
40025
+ notes: d.notes ?? void 0,
40026
+ reminderMinutes: d.reminderMinutes ?? void 0,
40027
+ // Untagged (nullish or empty) → undefined, so the merged PUT omits the
40028
+ // field (omission preserves; only a CALLER-supplied [] should clear).
40029
+ children: d.children?.length ? d.children.map((c) => c.userId) : void 0,
40030
+ eventParentId: d.eventParent?.userId,
40031
+ dropOffParentId: d.dropOffParent?.userId,
40032
+ pickUpParentId: d.pickUpParent?.userId
40033
+ };
40034
+ }
39783
40035
  function registerCalendarTools(server, client2) {
39784
- const allowWrites = getWriteMode() === "all";
40036
+ const allowWrites = getCalendarWritesAllowed();
39785
40037
  server.registerTool("ofw_list_events", {
39786
40038
  description: "List OurFamilyWizard calendar events in a date range",
39787
40039
  annotations: { readOnlyHint: true },
@@ -39799,51 +40051,62 @@ function registerCalendarTools(server, client2) {
39799
40051
  return jsonResponse(data);
39800
40052
  });
39801
40053
  if (allowWrites) server.registerTool("ofw_create_event", {
39802
- description: "Create a calendar event in OurFamilyWizard",
40054
+ description: "Create a calendar event in OurFamilyWizard. Unless privateEvent is true, the event is immediately visible to the co-parent \u2014 there is no draft stage.",
39803
40055
  annotations: { destructiveHint: false },
39804
40056
  inputSchema: {
39805
40057
  title: external_exports.string(),
39806
- startDate: external_exports.string().describe("ISO datetime string"),
39807
- endDate: external_exports.string().describe("ISO datetime string"),
39808
- allDay: external_exports.boolean().optional(),
39809
- location: external_exports.string().optional(),
39810
- reminder: external_exports.string().describe('Reminder setting (e.g. "1 hour before")').optional(),
39811
- privateEvent: external_exports.boolean().optional(),
39812
- eventFor: external_exports.string().describe("neither | parent1 | parent2").optional(),
39813
- dropOffParent: external_exports.string().optional(),
39814
- pickUpParent: external_exports.string().optional(),
39815
- children: external_exports.array(external_exports.number()).describe("Array of child IDs").optional()
40058
+ ...eventWriteFields
39816
40059
  }
39817
40060
  }, async (args) => {
39818
- const data = await client2.request("POST", "/pub/v1/calendar/events", args);
39819
- return jsonResponse(data);
40061
+ const raw = await client2.request("POST", "/pub/v3/events", buildEventPayload(args));
40062
+ const event = parseLenient(eventDetailSchema, raw, { label: "ofw-mcp", context: "POST /pub/v3/events", mode: "strict" });
40063
+ return jsonResponse({
40064
+ note: `Event created. Use eventRecurrenceId ${event.eventRecurrenceId} as eventId for ofw_update_event/ofw_delete_event.`,
40065
+ event
40066
+ });
39820
40067
  });
39821
40068
  if (allowWrites) server.registerTool("ofw_update_event", {
39822
- description: "Update an existing OurFamilyWizard calendar event",
40069
+ description: "Update an existing OurFamilyWizard calendar event. Fetches the event, applies the given changes, and writes the merged result back (OFW has no partial update).",
39823
40070
  annotations: { destructiveHint: true },
39824
40071
  inputSchema: {
39825
- eventId: external_exports.string(),
40072
+ eventId: external_exports.string().describe("Event id \u2014 the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event"),
39826
40073
  title: external_exports.string().optional(),
39827
- startDate: external_exports.string().optional(),
39828
- endDate: external_exports.string().optional(),
39829
- allDay: external_exports.boolean().optional(),
39830
- location: external_exports.string().optional(),
39831
- reminder: external_exports.string().optional(),
39832
- privateEvent: external_exports.boolean().optional()
40074
+ startDate: eventWriteFields.startDate.optional(),
40075
+ endDate: eventWriteFields.endDate,
40076
+ startTime: eventWriteFields.startTime,
40077
+ endTime: eventWriteFields.endTime,
40078
+ allDay: eventWriteFields.allDay,
40079
+ privateEvent: eventWriteFields.privateEvent,
40080
+ location: eventWriteFields.location,
40081
+ notes: eventWriteFields.notes,
40082
+ reminderMinutes: eventWriteFields.reminderMinutes,
40083
+ children: external_exports.array(external_exports.number()).describe("Child userIds to tag; pass [] to remove all child tags (omit to keep current tags)").optional(),
40084
+ eventParentId: eventWriteFields.eventParentId,
40085
+ dropOffParentId: eventWriteFields.dropOffParentId,
40086
+ pickUpParentId: eventWriteFields.pickUpParentId
39833
40087
  }
39834
40088
  }, async (args) => {
39835
- const { eventId, ...updateData } = args;
39836
- const data = await client2.request("PUT", `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
39837
- return jsonResponse(data);
40089
+ const { eventId, ...changes } = args;
40090
+ const id = encodeURIComponent(eventId);
40091
+ const rawDetail = await client2.request("GET", `/pub/v3/events/${id}`);
40092
+ const current = parseLenient(eventDetailSchema, rawDetail, { label: "ofw-mcp", context: `GET /pub/v3/events/${eventId}`, mode: "strict" });
40093
+ const defined = Object.fromEntries(Object.entries(changes).filter(([, v]) => v !== void 0));
40094
+ const merged = { ...detailToWriteArgs(current), ...defined };
40095
+ await client2.request("PUT", `/pub/v3/events/${id}`, buildEventPayload(merged));
40096
+ const rawAfter = await client2.request("GET", `/pub/v3/events/${id}`);
40097
+ const event = parseLenient(eventDetailSchema, rawAfter, { label: "ofw-mcp", context: `GET /pub/v3/events/${eventId} (post-update)`, mode: "strict" });
40098
+ return jsonResponse({ note: "Event updated; returning re-fetched event state.", event });
39838
40099
  });
39839
40100
  if (allowWrites) server.registerTool("ofw_delete_event", {
39840
40101
  description: "Delete an OurFamilyWizard calendar event",
39841
40102
  annotations: { destructiveHint: true },
39842
40103
  inputSchema: {
39843
- eventId: external_exports.string().describe("Event ID to delete")
40104
+ eventId: external_exports.string().describe("Event id \u2014 the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event"),
40105
+ includeFuture: external_exports.boolean().describe("For repeating events: also delete future occurrences (default false)").optional()
39844
40106
  }
39845
40107
  }, async (args) => {
39846
- await client2.request("DELETE", `/pub/v1/calendar/events/${encodeURIComponent(args.eventId)}`);
40108
+ const includeFuture = args.includeFuture ?? false;
40109
+ await client2.request("DELETE", `/pub/v3/events/${encodeURIComponent(args.eventId)}?includeFuture=${includeFuture}`);
39847
40110
  return textResponse(`Event ${args.eventId} deleted`);
39848
40111
  });
39849
40112
  }
@@ -39926,7 +40189,7 @@ process.emit = function(event, ...args) {
39926
40189
  };
39927
40190
  await runMcp({
39928
40191
  name: "ofw",
39929
- version: "2.4.3",
40192
+ version: "2.5.0",
39930
40193
  // x-release-please-version
39931
40194
  deps: client,
39932
40195
  tools: [