ofw-mcp 2.19.2 → 2.19.4

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
@@ -2279,7 +2279,7 @@ var require_websocket = __commonJS({
2279
2279
  var http = __require("http");
2280
2280
  var net = __require("net");
2281
2281
  var tls = __require("tls");
2282
- var { randomBytes: randomBytes4, createHash: createHash2 } = __require("crypto");
2282
+ var { randomBytes: randomBytes6, createHash: createHash6 } = __require("crypto");
2283
2283
  var { Duplex, Readable } = __require("stream");
2284
2284
  var { URL: URL2 } = __require("url");
2285
2285
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2817,7 +2817,7 @@ var require_websocket = __commonJS({
2817
2817
  }
2818
2818
  }
2819
2819
  const defaultPort = isSecure ? 443 : 80;
2820
- const key = randomBytes4(16).toString("base64");
2820
+ const key = randomBytes6(16).toString("base64");
2821
2821
  const request = isSecure ? https.request : http.request;
2822
2822
  const protocolSet = /* @__PURE__ */ new Set();
2823
2823
  let perMessageDeflate;
@@ -2947,7 +2947,7 @@ var require_websocket = __commonJS({
2947
2947
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2948
2948
  return;
2949
2949
  }
2950
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
2950
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
2951
2951
  if (res.headers["sec-websocket-accept"] !== digest) {
2952
2952
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2953
2953
  return;
@@ -3316,7 +3316,7 @@ var require_websocket_server = __commonJS({
3316
3316
  var EventEmitter = __require("events");
3317
3317
  var http = __require("http");
3318
3318
  var { Duplex } = __require("stream");
3319
- var { createHash: createHash2 } = __require("crypto");
3319
+ var { createHash: createHash6 } = __require("crypto");
3320
3320
  var extension2 = require_extension();
3321
3321
  var PerMessageDeflate2 = require_permessage_deflate();
3322
3322
  var subprotocol2 = require_subprotocol();
@@ -3623,7 +3623,7 @@ var require_websocket_server = __commonJS({
3623
3623
  );
3624
3624
  }
3625
3625
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3626
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
3626
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
3627
3627
  const headers = [
3628
3628
  "HTTP/1.1 101 Switching Protocols",
3629
3629
  "Upgrade: websocket",
@@ -3734,6 +3734,33 @@ var storage2 = new AsyncLocalStorage2();
3734
3734
  function withCallerCapabilities(capabilities, fn) {
3735
3735
  return capabilities ? storage2.run(capabilities, fn) : fn();
3736
3736
  }
3737
+ function currentCallerCapabilities() {
3738
+ return storage2.getStore();
3739
+ }
3740
+ var ENVELOPE_CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
3741
+ var ELICITATION_MODES = ["form", "url"];
3742
+ function isRecord(value) {
3743
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3744
+ }
3745
+ function callerCapabilities(ctx) {
3746
+ const envelope = isRecord(ctx) && isRecord(ctx.mcpReq) ? ctx.mcpReq.envelope : void 0;
3747
+ if (isRecord(envelope)) {
3748
+ const declared = envelope[ENVELOPE_CAPABILITIES_KEY];
3749
+ if (isRecord(declared))
3750
+ return declared;
3751
+ }
3752
+ return currentCallerCapabilities();
3753
+ }
3754
+ function callerAcceptsFormElicitation(ctx) {
3755
+ const capabilities = callerCapabilities(ctx);
3756
+ if (!capabilities)
3757
+ return void 0;
3758
+ const elicitation = capabilities.elicitation;
3759
+ if (!isRecord(elicitation))
3760
+ return false;
3761
+ const namedModes = ELICITATION_MODES.filter((mode) => mode in elicitation);
3762
+ return namedModes.length === 0 || namedModes.includes("form");
3763
+ }
3737
3764
 
3738
3765
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
3739
3766
  var __create2 = Object.create;
@@ -8818,7 +8845,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
8818
8845
  var NONE = 0;
8819
8846
  var ASSUMED = 1;
8820
8847
  var PROVEN = 2;
8821
- function isRecursive(inst, stack, resolve4) {
8848
+ function isRecursive(inst, stack, resolve6) {
8822
8849
  const cached2 = recursive.get(inst);
8823
8850
  if (cached2 !== void 0)
8824
8851
  return cached2 ? PROVEN : NONE;
@@ -8828,7 +8855,7 @@ function isRecursive(inst, stack, resolve4) {
8828
8855
  let result = NONE;
8829
8856
  const check2 = (child) => {
8830
8857
  if (result !== PROVEN && child?._zod) {
8831
- const answer = isRecursive(child, stack, resolve4);
8858
+ const answer = isRecursive(child, stack, resolve6);
8832
8859
  if (answer > result)
8833
8860
  result = answer;
8834
8861
  }
@@ -8839,7 +8866,7 @@ function isRecursive(inst, stack, resolve4) {
8839
8866
  const desc = Object.getOwnPropertyDescriptor(sh, key);
8840
8867
  if (spread && !desc.enumerable)
8841
8868
  continue;
8842
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve4) : NONE;
8869
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
8843
8870
  if (child > answer)
8844
8871
  answer = child;
8845
8872
  }
@@ -8903,7 +8930,7 @@ function isRecursive(inst, stack, resolve4) {
8903
8930
  break;
8904
8931
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
8905
8932
  case "lazy": {
8906
- const inner = def._cachedInner ?? (resolve4 ? inst._zod.innerType : void 0);
8933
+ const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
8907
8934
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
8908
8935
  break;
8909
8936
  }
@@ -27798,19 +27825,50 @@ var inputRequired = Object.assign(buildInputRequired, {
27798
27825
  return { method: "roots/list" };
27799
27826
  }
27800
27827
  });
27828
+ function acceptedContent(responses, key, schema) {
27829
+ const view = inputResponse(responses, key);
27830
+ if (view.kind !== "elicit" || view.action !== "accept" || view.content === void 0) return void 0;
27831
+ if (schema === void 0) return view.content;
27832
+ const outcome = schema["~standard"].validate(view.content);
27833
+ if (outcome instanceof Promise) throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
27834
+ return outcome.issues === void 0 ? outcome.value : void 0;
27835
+ }
27836
+ function inputResponse(responses, key) {
27837
+ if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
27838
+ const entry = responses[key];
27839
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
27840
+ const candidate = entry;
27841
+ if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
27842
+ const content = candidate["content"];
27843
+ return {
27844
+ kind: "elicit",
27845
+ action: candidate["action"],
27846
+ ...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
27847
+ };
27848
+ }
27849
+ if (Array.isArray(candidate["roots"])) return {
27850
+ kind: "roots",
27851
+ roots: candidate["roots"]
27852
+ };
27853
+ if (typeof candidate["role"] === "string" && candidate["content"] !== void 0) return {
27854
+ kind: "sampling",
27855
+ result: candidate
27856
+ };
27857
+ return { kind: "missing" };
27858
+ }
27801
27859
  var REQUEST_STATE_ONLY_LEG_PACING_MS = 250;
27802
27860
  function inputRequiredRoundsExceededMessage(method, maxRounds) {
27803
27861
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
27804
27862
  }
27805
27863
  function sleep(ms, signal) {
27806
- return new Promise((resolve4, reject) => {
27864
+ return new Promise((resolve6, reject) => {
27807
27865
  if (signal?.aborted) {
27808
27866
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
27809
27867
  return;
27810
27868
  }
27811
27869
  const timer = setTimeout(() => {
27812
27870
  signal?.removeEventListener("abort", onAbort);
27813
- resolve4();
27871
+ resolve6();
27814
27872
  }, ms);
27815
27873
  const onAbort = () => {
27816
27874
  clearTimeout(timer);
@@ -28604,7 +28662,7 @@ var Protocol = class {
28604
28662
  const flowStartedAt = Date.now();
28605
28663
  let onAbort;
28606
28664
  let cleanupMessageId;
28607
- return new Promise((resolve4, reject) => {
28665
+ return new Promise((resolve6, reject) => {
28608
28666
  const earlyReject = (error62) => {
28609
28667
  reject(error62);
28610
28668
  };
@@ -28672,7 +28730,7 @@ var Protocol = class {
28672
28730
  }
28673
28731
  if (decoded.kind === "invalid") return reject(decoded.error);
28674
28732
  if (decoded.kind === "input_required") {
28675
- if (options?.allowInputRequired === true) return resolve4(manualInputRequiredValue(decoded));
28733
+ if (options?.allowInputRequired === true) return resolve6(manualInputRequiredValue(decoded));
28676
28734
  const flow = {
28677
28735
  codec: codec2,
28678
28736
  request,
@@ -28684,11 +28742,11 @@ var Protocol = class {
28684
28742
  params
28685
28743
  }, resultSchema, legOptions)
28686
28744
  };
28687
- return resolve4(this._resolveNonCompleteResult(decoded, flow));
28745
+ return resolve6(this._resolveNonCompleteResult(decoded, flow));
28688
28746
  }
28689
28747
  const result = decoded.result;
28690
28748
  validateStandardSchema(resultSchema, result).then((parseResult) => {
28691
- if (parseResult.success) resolve4(parseResult.data);
28749
+ if (parseResult.success) resolve6(parseResult.data);
28692
28750
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
28693
28751
  }, reject);
28694
28752
  });
@@ -31600,7 +31658,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
31600
31658
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
31601
31659
  const schOrFunc = root.refs[ref];
31602
31660
  if (schOrFunc) return schOrFunc;
31603
- let _sch = resolve4.call(this, root, ref);
31661
+ let _sch = resolve6.call(this, root, ref);
31604
31662
  if (_sch === void 0) {
31605
31663
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
31606
31664
  const { schemaId } = this.opts;
@@ -31626,7 +31684,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
31626
31684
  function sameSchemaEnv(s1, s2) {
31627
31685
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
31628
31686
  }
31629
- function resolve4(root, ref) {
31687
+ function resolve6(root, ref) {
31630
31688
  let sch;
31631
31689
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
31632
31690
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -32076,47 +32134,47 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32076
32134
  else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
32077
32135
  return uri;
32078
32136
  }
32079
- function resolve4(baseURI, relativeURI, options) {
32137
+ function resolve6(baseURI, relativeURI, options) {
32080
32138
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
32081
32139
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
32082
32140
  schemelessOptions.skipEscape = true;
32083
32141
  return serialize(resolved, schemelessOptions);
32084
32142
  }
32085
- function resolveComponent(base, relative2, options, skipNormalization) {
32143
+ function resolveComponent(base, relative3, options, skipNormalization) {
32086
32144
  const target = {};
32087
32145
  if (!skipNormalization) {
32088
32146
  base = parse3(serialize(base, options), options);
32089
- relative2 = parse3(serialize(relative2, options), options);
32147
+ relative3 = parse3(serialize(relative3, options), options);
32090
32148
  }
32091
32149
  options = options || {};
32092
- if (!options.tolerant && relative2.scheme) {
32093
- target.scheme = relative2.scheme;
32094
- target.userinfo = relative2.userinfo;
32095
- target.host = relative2.host;
32096
- target.port = relative2.port;
32097
- target.path = removeDotSegments(relative2.path || "");
32098
- target.query = relative2.query;
32150
+ if (!options.tolerant && relative3.scheme) {
32151
+ target.scheme = relative3.scheme;
32152
+ target.userinfo = relative3.userinfo;
32153
+ target.host = relative3.host;
32154
+ target.port = relative3.port;
32155
+ target.path = removeDotSegments(relative3.path || "");
32156
+ target.query = relative3.query;
32099
32157
  } else {
32100
- if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
32101
- target.userinfo = relative2.userinfo;
32102
- target.host = relative2.host;
32103
- target.port = relative2.port;
32104
- target.path = removeDotSegments(relative2.path || "");
32105
- target.query = relative2.query;
32158
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
32159
+ target.userinfo = relative3.userinfo;
32160
+ target.host = relative3.host;
32161
+ target.port = relative3.port;
32162
+ target.path = removeDotSegments(relative3.path || "");
32163
+ target.query = relative3.query;
32106
32164
  } else {
32107
- if (!relative2.path) {
32165
+ if (!relative3.path) {
32108
32166
  target.path = base.path;
32109
- if (relative2.query !== void 0) target.query = relative2.query;
32167
+ if (relative3.query !== void 0) target.query = relative3.query;
32110
32168
  else target.query = base.query;
32111
32169
  } else {
32112
- if (relative2.path[0] === "/") target.path = removeDotSegments(relative2.path);
32170
+ if (relative3.path[0] === "/") target.path = removeDotSegments(relative3.path);
32113
32171
  else {
32114
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative2.path;
32115
- else if (!base.path) target.path = relative2.path;
32116
- else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
32172
+ if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative3.path;
32173
+ else if (!base.path) target.path = relative3.path;
32174
+ else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
32117
32175
  target.path = removeDotSegments(target.path);
32118
32176
  }
32119
- target.query = relative2.query;
32177
+ target.query = relative3.query;
32120
32178
  }
32121
32179
  target.userinfo = base.userinfo;
32122
32180
  target.host = base.host;
@@ -32124,7 +32182,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32124
32182
  }
32125
32183
  target.scheme = base.scheme;
32126
32184
  }
32127
- target.fragment = relative2.fragment;
32185
+ target.fragment = relative3.fragment;
32128
32186
  return target;
32129
32187
  }
32130
32188
  function equal(uriA, uriB, options) {
@@ -32250,7 +32308,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32250
32308
  const fastUri = {
32251
32309
  SCHEMES,
32252
32310
  normalize,
32253
- resolve: resolve4,
32311
+ resolve: resolve6,
32254
32312
  resolveComponent,
32255
32313
  equal,
32256
32314
  serialize,
@@ -37623,7 +37681,7 @@ var StdioServerTransport = class {
37623
37681
  }
37624
37682
  send(message) {
37625
37683
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
37626
- return new Promise((resolve4, reject) => {
37684
+ return new Promise((resolve6, reject) => {
37627
37685
  const json2 = serializeMessage(message);
37628
37686
  let settled = false;
37629
37687
  const onError = (error62) => {
@@ -37638,14 +37696,14 @@ var StdioServerTransport = class {
37638
37696
  settled = true;
37639
37697
  this._stdout.off("error", onError);
37640
37698
  this._stdout.off("drain", onDrain);
37641
- resolve4();
37699
+ resolve6();
37642
37700
  };
37643
37701
  this._stdout.once("error", onError);
37644
37702
  if (this._stdout.write(json2)) {
37645
37703
  if (settled) return;
37646
37704
  settled = true;
37647
37705
  this._stdout.off("error", onError);
37648
- resolve4();
37706
+ resolve6();
37649
37707
  } else if (!settled) this._stdout.once("drain", onDrain);
37650
37708
  });
37651
37709
  }
@@ -37699,14 +37757,14 @@ var StdioConnectionChannel = class {
37699
37757
  */
37700
37758
  async whenRequestsAnswered(timeoutMs) {
37701
37759
  if (this._closed || this._pendingRequests.size === 0) return true;
37702
- return await new Promise((resolve4) => {
37760
+ return await new Promise((resolve6) => {
37703
37761
  const waiter = () => {
37704
37762
  clearTimeout(timer);
37705
- resolve4(true);
37763
+ resolve6(true);
37706
37764
  };
37707
37765
  const timer = setTimeout(() => {
37708
37766
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
37709
- resolve4(false);
37767
+ resolve6(false);
37710
37768
  }, timeoutMs);
37711
37769
  this._drainWaiters.push(waiter);
37712
37770
  });
@@ -37845,14 +37903,14 @@ function serveStdio(factory, options = {}) {
37845
37903
  return false;
37846
37904
  };
37847
37905
  const answerLegacyRejection = (request, reason, requestedVersion) => {
37848
- const rejection2 = modernOnlyStrictRejection({
37906
+ const rejection3 = modernOnlyStrictRejection({
37849
37907
  kind: "legacy",
37850
37908
  reason,
37851
37909
  ...requestedVersion !== void 0 && { requestedVersion }
37852
37910
  }, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
37853
- if (rejection2 === void 0) return Promise.resolve();
37854
- reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection2.cell}): ${rejection2.message}`));
37855
- return writeErrorResponse(request.id, rejection2.code, rejection2.message, rejection2.data);
37911
+ if (rejection3 === void 0) return Promise.resolve();
37912
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
37913
+ return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
37856
37914
  };
37857
37915
  const onInstanceClosed = (channel) => {
37858
37916
  if (closing || channel === discarding) return;
@@ -38164,6 +38222,11 @@ var MEDIA_QUALIFIER = "(?:primary|secondary|main|default|cover|hero|profile|mast
38164
38222
  var MEDIA_KEY = new RegExp(`^(?:(?:${MEDIA_QUALIFIER}|${MEDIA_NOUN})[_-]?)?${MEDIA_NOUN}s?(?:[_-]?(?:link|uri|url|src)s?)?$`, "i");
38165
38223
 
38166
38224
  // node_modules/@chrischall/mcp-utils/dist/response/index.js
38225
+ function textResult(data) {
38226
+ return {
38227
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
38228
+ };
38229
+ }
38167
38230
  function rawTextResult(text) {
38168
38231
  return { content: [{ type: "text", text }] };
38169
38232
  }
@@ -38182,6 +38245,432 @@ function pruneUndefined(obj) {
38182
38245
  return out;
38183
38246
  }
38184
38247
 
38248
+ // node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
38249
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
38250
+
38251
+ // node_modules/@chrischall/mcp-utils/dist/server/canonical.js
38252
+ function canonicalJson(value) {
38253
+ if (value === null || typeof value === "string" || typeof value === "boolean")
38254
+ return JSON.stringify(value);
38255
+ if (value === void 0)
38256
+ return "null";
38257
+ if (typeof value === "number")
38258
+ return Number.isFinite(value) ? JSON.stringify(value) : `{"$num":"${String(value)}"}`;
38259
+ if (typeof value === "bigint")
38260
+ return `{"$bigint":"${value.toString()}"}`;
38261
+ if (typeof value === "function" || typeof value === "symbol") {
38262
+ throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
38263
+ }
38264
+ if (Array.isArray(value))
38265
+ return `[${value.map(canonicalJson).join(",")}]`;
38266
+ if (value instanceof Date) {
38267
+ return `{"$date":${JSON.stringify(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString())}}`;
38268
+ }
38269
+ if (ArrayBuffer.isView(value)) {
38270
+ const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
38271
+ return `{"$bytes":${JSON.stringify(bytes.toString("base64"))}}`;
38272
+ }
38273
+ if (value instanceof ArrayBuffer)
38274
+ return `{"$bytes":${JSON.stringify(Buffer.from(value).toString("base64"))}}`;
38275
+ if (value instanceof Map) {
38276
+ const entries2 = [...value.entries()].map(([k, v]) => `[${canonicalJson(k)},${canonicalJson(v)}]`).sort();
38277
+ return `{"$map":[${entries2.join(",")}]}`;
38278
+ }
38279
+ if (value instanceof Set) {
38280
+ return `{"$set":[${[...value].map(canonicalJson).sort().join(",")}]}`;
38281
+ }
38282
+ const proto = Object.getPrototypeOf(value);
38283
+ if (proto !== Object.prototype && proto !== null) {
38284
+ throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
38285
+ }
38286
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
38287
+ return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
38288
+ }
38289
+ function escapeKey(key) {
38290
+ return key.startsWith("$") ? `$${key}` : key;
38291
+ }
38292
+
38293
+ // node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
38294
+ var DEFAULT_REQUEST_KEY = "confirmation";
38295
+ var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
38296
+ var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
38297
+ var STATE_PREFIX = "mcpu.confirm.v1.";
38298
+ function bindingKey(binding) {
38299
+ if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
38300
+ throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
38301
+ }
38302
+ const key = typeof binding.key === "string" ? Buffer.from(binding.key, "utf8") : Buffer.from(binding.key);
38303
+ if (key.length < 32) {
38304
+ throw new RangeError("requireConfirmation: binding.key must be at least 32 bytes.");
38305
+ }
38306
+ return key;
38307
+ }
38308
+ function commitment(action, args) {
38309
+ return createHash("sha256").update(`${action}\0${canonicalJson(args)}`).digest("base64url");
38310
+ }
38311
+ function mintState(key, action, binding) {
38312
+ const exp = Math.floor(Date.now() / 1e3) + (binding.ttlSeconds ?? 600);
38313
+ const body = Buffer.from(JSON.stringify({ c: commitment(action, binding.args), exp })).toString("base64url");
38314
+ const mac3 = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest("base64url");
38315
+ return `${STATE_PREFIX}${body}.${mac3}`;
38316
+ }
38317
+ function verifyState(key, action, binding, state) {
38318
+ if (typeof state !== "string" || !state.startsWith(STATE_PREFIX))
38319
+ return false;
38320
+ const rest = state.slice(STATE_PREFIX.length);
38321
+ const dot = rest.indexOf(".");
38322
+ if (dot < 0)
38323
+ return false;
38324
+ const body = rest.slice(0, dot);
38325
+ const mac3 = Buffer.from(rest.slice(dot + 1), "base64url");
38326
+ const expected = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest();
38327
+ if (mac3.length !== expected.length || !timingSafeEqual(mac3, expected))
38328
+ return false;
38329
+ let payload;
38330
+ try {
38331
+ payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
38332
+ } catch {
38333
+ return false;
38334
+ }
38335
+ if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now())
38336
+ return false;
38337
+ const want = Buffer.from(commitment(action, binding.args));
38338
+ const got = Buffer.from(typeof payload.c === "string" ? payload.c : "");
38339
+ return got.length === want.length && timingSafeEqual(got, want);
38340
+ }
38341
+ function echoedState(ctx) {
38342
+ const accessor = ctx.mcpReq.requestState;
38343
+ return typeof accessor === "function" ? accessor() : void 0;
38344
+ }
38345
+ function requireConfirmation(ctx, options) {
38346
+ const requestKey = options.requestKey ?? DEFAULT_REQUEST_KEY;
38347
+ const key = options.binding ? bindingKey(options.binding) : void 0;
38348
+ const confirmationSchema = external_exports.object({
38349
+ confirmed: external_exports.boolean().describe(options.confirmationLabel ?? DEFAULT_CONFIRMATION_LABEL)
38350
+ });
38351
+ const ask = () => {
38352
+ const preview = {
38353
+ action: options.action,
38354
+ ...options.details === void 0 ? {} : { details: options.details }
38355
+ };
38356
+ return inputRequired({
38357
+ inputRequests: {
38358
+ [requestKey]: inputRequired.elicit({
38359
+ message: `${options.message}
38360
+ ${JSON.stringify(preview, null, 2)}`,
38361
+ requestedSchema: confirmationSchema
38362
+ })
38363
+ },
38364
+ ...key && options.binding ? { requestState: mintState(key, options.action, options.binding) } : {}
38365
+ });
38366
+ };
38367
+ const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
38368
+ const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
38369
+ if (response.kind === "missing") {
38370
+ if (callerAcceptsFormElicitation(ctx) === false) {
38371
+ return textResult({
38372
+ confirmed: false,
38373
+ dispatched: false,
38374
+ action: options.action,
38375
+ reason: "confirmation-unsupported",
38376
+ note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
38377
+ });
38378
+ }
38379
+ return ask();
38380
+ }
38381
+ if (response.kind === "elicit" && response.action === "accept" && accepted?.confirmed === true) {
38382
+ if (key && options.binding) {
38383
+ const state = echoedState(ctx);
38384
+ if (state === void 0 || state === null) {
38385
+ return errorResult(`Confirmation for ${options.action} was accepted, but the retry carried no requestState, so it cannot be checked against the prompt that was shown. Nothing was done. The likely cause is that the MCP client or host does not round-trip requestState (it must echo the requestState from the input_required result back on the retry); asking again would loop.`);
38386
+ }
38387
+ if (!verifyState(key, options.action, options.binding, state))
38388
+ return ask();
38389
+ }
38390
+ return void 0;
38391
+ }
38392
+ return textResult({
38393
+ confirmed: false,
38394
+ cancelled: true,
38395
+ action: options.action,
38396
+ note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
38397
+ });
38398
+ }
38399
+
38400
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
38401
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
38402
+ function createSpentTokenStore() {
38403
+ const spent = /* @__PURE__ */ new Map();
38404
+ return {
38405
+ has: (nonce) => spent.has(nonce),
38406
+ add: (nonce, exp) => {
38407
+ spent.set(nonce, exp);
38408
+ },
38409
+ prune: (now) => {
38410
+ for (const [nonce, exp] of spent)
38411
+ if (exp < now)
38412
+ spent.delete(nonce);
38413
+ },
38414
+ clear: () => spent.clear(),
38415
+ get size() {
38416
+ return spent.size;
38417
+ }
38418
+ };
38419
+ }
38420
+ var PROCESS_SPENT = createSpentTokenStore();
38421
+ var PREFIX = "mcpu.token.v1.";
38422
+ var DEFAULT_TTL_SECONDS = 600;
38423
+ function tokenKey(key) {
38424
+ const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
38425
+ if (bytes.length < 32)
38426
+ throw new RangeError("confirm token: key must be at least 32 bytes.");
38427
+ return bytes;
38428
+ }
38429
+ function ttlOf(ttlSeconds) {
38430
+ if (ttlSeconds === void 0)
38431
+ return DEFAULT_TTL_SECONDS;
38432
+ if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
38433
+ throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
38434
+ }
38435
+ return ttlSeconds;
38436
+ }
38437
+ function sign(key, body) {
38438
+ return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
38439
+ }
38440
+ function hashConfirmPayload(payload) {
38441
+ return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
38442
+ }
38443
+ function issueConfirmToken(key, binding, options = {}) {
38444
+ const k = tokenKey(key);
38445
+ const now = options.now ?? Date.now();
38446
+ const exp = now + ttlOf(options.ttlSeconds) * 1e3;
38447
+ const claims = {
38448
+ t: binding.tool,
38449
+ ...binding.account === void 0 ? {} : { a: binding.account },
38450
+ g: binding.target,
38451
+ ...binding.revision === void 0 ? {} : { r: binding.revision },
38452
+ h: binding.payloadHash,
38453
+ exp,
38454
+ n: randomBytes(16).toString("base64url")
38455
+ };
38456
+ const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
38457
+ return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
38458
+ }
38459
+ function parseClaims(body) {
38460
+ try {
38461
+ const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
38462
+ return claims && typeof claims === "object" ? claims : void 0;
38463
+ } catch {
38464
+ return void 0;
38465
+ }
38466
+ }
38467
+ function verifyConfirmToken(key, token, binding, options = {}) {
38468
+ const k = tokenKey(key);
38469
+ const now = options.now ?? Date.now();
38470
+ const spent = options.spent ?? PROCESS_SPENT;
38471
+ spent.prune(now);
38472
+ if (!token.startsWith(PREFIX))
38473
+ return { ok: false, error: "TOKEN_INVALID" };
38474
+ const rest = token.slice(PREFIX.length);
38475
+ const dot = rest.indexOf(".");
38476
+ const body = dot < 0 ? "" : rest.slice(0, dot);
38477
+ const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
38478
+ const expected = sign(k, body);
38479
+ if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
38480
+ return { ok: false, error: "TOKEN_INVALID" };
38481
+ const claims = parseClaims(body);
38482
+ if (!claims)
38483
+ return { ok: false, error: "TOKEN_INVALID" };
38484
+ if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
38485
+ return { ok: false, error: "TOKEN_INVALID" };
38486
+ }
38487
+ if (spent.has(claims.n))
38488
+ return { ok: false, error: "TOKEN_REUSED" };
38489
+ if (now > claims.exp)
38490
+ return { ok: false, error: "TOKEN_EXPIRED" };
38491
+ if (claims.r !== binding.revision)
38492
+ return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
38493
+ if (claims.h !== binding.payloadHash)
38494
+ return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
38495
+ spent.add(claims.n, claims.exp);
38496
+ return { ok: true };
38497
+ }
38498
+ var CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
38499
+ var confirmTokenParam = external_exports.string().optional().describe(`ONLY for the two-step confirmation fallback (a client without MCP elicitation). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`);
38500
+ var TOKEN_ERROR_NOTE = {
38501
+ TOKEN_EXPIRED: "Nothing was sent or changed: the confirmToken expired. Call again WITHOUT confirmToken for a fresh preview, and ask the user to approve it again.",
38502
+ TOKEN_REUSED: "Nothing was sent or changed by this call: this confirmToken was already used, and one approval acts once. If doing it again is really intended, call again WITHOUT confirmToken and get a new approval.",
38503
+ TOKEN_INVALID: "Nothing was sent or changed: this confirmToken was not issued by this server for this tool, account and target (or the server has restarted since). Call again WITHOUT confirmToken for a fresh preview and approval."
38504
+ };
38505
+ var CHANGED_NOTE = {
38506
+ "revision-changed": "Nothing was sent or changed: the target was edited since the user approved it (its version rotated), so what would happen is not what they saw.",
38507
+ "payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
38508
+ };
38509
+ function isToolResult(value) {
38510
+ return Array.isArray(value.content);
38511
+ }
38512
+ function rejection2(data) {
38513
+ return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
38514
+ }
38515
+ async function tokenConfirmation(action, fb) {
38516
+ const subject = await fb.subject();
38517
+ if (isToolResult(subject))
38518
+ return subject;
38519
+ const binding = {
38520
+ tool: fb.tool,
38521
+ ...fb.account === void 0 ? {} : { account: fb.account },
38522
+ target: subject.target,
38523
+ ...subject.revision === void 0 ? {} : { revision: subject.revision },
38524
+ payloadHash: hashConfirmPayload(subject.payload)
38525
+ };
38526
+ const phaseOne = () => {
38527
+ const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
38528
+ return {
38529
+ action,
38530
+ preview: subject.preview,
38531
+ confirmToken: token,
38532
+ expiresAt,
38533
+ ttlSeconds: ttlOf(fb.ttlSeconds),
38534
+ instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
38535
+ };
38536
+ };
38537
+ if (!fb.confirmToken) {
38538
+ return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
38539
+ }
38540
+ const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
38541
+ if (verdict.ok)
38542
+ return void 0;
38543
+ if (verdict.error === "DRAFT_CHANGED") {
38544
+ return rejection2({
38545
+ error: "DRAFT_CHANGED",
38546
+ reason: verdict.reason,
38547
+ note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
38548
+ ...phaseOne()
38549
+ });
38550
+ }
38551
+ return rejection2({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
38552
+ }
38553
+ async function requireConfirmationWithFallback(ctx, options) {
38554
+ const { tokenFallback, ...confirmation } = options;
38555
+ if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
38556
+ return tokenConfirmation(options.action, tokenFallback);
38557
+ }
38558
+ return requireConfirmation(ctx, confirmation);
38559
+ }
38560
+
38561
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
38562
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
38563
+
38564
+ // node_modules/@chrischall/mcp-utils/dist/config/index.js
38565
+ import { homedir } from "node:os";
38566
+ import { isAbsolute, join, resolve } from "node:path";
38567
+ var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
38568
+ function readEnvVar(key, opts = {}) {
38569
+ const env = opts.env ?? process.env;
38570
+ const raw = env[key];
38571
+ if (typeof raw === "string") {
38572
+ const trimmed = raw.trim();
38573
+ if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
38574
+ return trimmed;
38575
+ }
38576
+ }
38577
+ return opts.default;
38578
+ }
38579
+ var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
38580
+ var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
38581
+ function parseBoolEnv(key, opts = {}) {
38582
+ const fallback = opts.default ?? false;
38583
+ const raw = readEnvVar(key, { env: opts.env });
38584
+ if (raw === void 0)
38585
+ return fallback;
38586
+ const token = raw.toLowerCase();
38587
+ if (TRUE_TOKENS.has(token))
38588
+ return true;
38589
+ if (FALSE_TOKENS.has(token))
38590
+ return false;
38591
+ return fallback;
38592
+ }
38593
+ function expandPath(p) {
38594
+ let expanded = p;
38595
+ if (p === "~") {
38596
+ expanded = homedir();
38597
+ } else if (p.startsWith("~/")) {
38598
+ expanded = join(homedir(), p.slice(2));
38599
+ }
38600
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
38601
+ }
38602
+ async function loadDotenvSafely(opts = {}) {
38603
+ try {
38604
+ const mod = await import(
38605
+ /* @vite-ignore */
38606
+ "dotenv"
38607
+ );
38608
+ const result = mod.config({
38609
+ ...opts.path !== void 0 ? { path: opts.path } : {},
38610
+ override: opts.override ?? false,
38611
+ quiet: true
38612
+ });
38613
+ return result.error === void 0;
38614
+ } catch {
38615
+ return false;
38616
+ }
38617
+ }
38618
+
38619
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
38620
+ var MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
38621
+ var DEFAULT_TTL_SECONDS2 = 600;
38622
+ var warned = /* @__PURE__ */ new Set();
38623
+ function readConfirmMode(env = process.env) {
38624
+ const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
38625
+ if (!raw)
38626
+ return "ask-user";
38627
+ if (MODES.has(raw))
38628
+ return raw;
38629
+ if (!warned.has(raw)) {
38630
+ warned.add(raw);
38631
+ process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
38632
+ `);
38633
+ }
38634
+ return "refuse";
38635
+ }
38636
+ function confirmTtlFromEnv(env = process.env) {
38637
+ const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
38638
+ return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
38639
+ }
38640
+ var processKey;
38641
+ function confirmKeyFromEnv(env = process.env) {
38642
+ const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
38643
+ if (secret)
38644
+ return createHash3("sha256").update(secret, "utf8").digest();
38645
+ processKey ??= randomBytes2(32);
38646
+ return processKey;
38647
+ }
38648
+ var CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
38649
+ var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
38650
+ function confirmationFromEnv(options) {
38651
+ const { tool, account, confirmToken, subject, instruction, spent, env = process.env, ...confirmation } = options;
38652
+ const mode = readConfirmMode(env);
38653
+ if (mode === "refuse") {
38654
+ return {
38655
+ ...confirmation,
38656
+ unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
38657
+ };
38658
+ }
38659
+ return {
38660
+ ...confirmation,
38661
+ tokenFallback: {
38662
+ key: confirmKeyFromEnv(env),
38663
+ tool,
38664
+ ...account === void 0 ? {} : { account },
38665
+ ...confirmToken === void 0 ? {} : { confirmToken },
38666
+ subject,
38667
+ ttlSeconds: confirmTtlFromEnv(env),
38668
+ instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
38669
+ ...spent === void 0 ? {} : { spent }
38670
+ }
38671
+ };
38672
+ }
38673
+
38185
38674
  // node_modules/@chrischall/mcp-utils/dist/server/index.js
38186
38675
  var SERVER_PROTOCOL_VERSIONS = Object.freeze([
38187
38676
  "2026-07-28",
@@ -38357,61 +38846,6 @@ function runMcp(opts) {
38357
38846
  return handle;
38358
38847
  }
38359
38848
 
38360
- // node_modules/@chrischall/mcp-utils/dist/config/index.js
38361
- import { homedir } from "node:os";
38362
- import { isAbsolute, join, resolve } from "node:path";
38363
- var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
38364
- function readEnvVar(key, opts = {}) {
38365
- const env = opts.env ?? process.env;
38366
- const raw = env[key];
38367
- if (typeof raw === "string") {
38368
- const trimmed = raw.trim();
38369
- if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
38370
- return trimmed;
38371
- }
38372
- }
38373
- return opts.default;
38374
- }
38375
- var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
38376
- var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
38377
- function parseBoolEnv(key, opts = {}) {
38378
- const fallback = opts.default ?? false;
38379
- const raw = readEnvVar(key, { env: opts.env });
38380
- if (raw === void 0)
38381
- return fallback;
38382
- const token = raw.toLowerCase();
38383
- if (TRUE_TOKENS.has(token))
38384
- return true;
38385
- if (FALSE_TOKENS.has(token))
38386
- return false;
38387
- return fallback;
38388
- }
38389
- function expandPath(p) {
38390
- let expanded = p;
38391
- if (p === "~") {
38392
- expanded = homedir();
38393
- } else if (p.startsWith("~/")) {
38394
- expanded = join(homedir(), p.slice(2));
38395
- }
38396
- return isAbsolute(expanded) ? expanded : resolve(expanded);
38397
- }
38398
- async function loadDotenvSafely(opts = {}) {
38399
- try {
38400
- const mod = await import(
38401
- /* @vite-ignore */
38402
- "dotenv"
38403
- );
38404
- const result = mod.config({
38405
- ...opts.path !== void 0 ? { path: opts.path } : {},
38406
- override: opts.override ?? false,
38407
- quiet: true
38408
- });
38409
- return result.error === void 0;
38410
- } catch {
38411
- return false;
38412
- }
38413
- }
38414
-
38415
38849
  // node_modules/@chrischall/mcp-utils/dist/fs/index.js
38416
38850
  import { openAsBlob } from "node:fs";
38417
38851
 
@@ -38546,9 +38980,9 @@ async function resolveAuthPattern(pattern) {
38546
38980
  import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
38547
38981
  import { dirname as dirname2, join as join2, resolve as resolve3 } from "node:path";
38548
38982
  import { homedir as homedir2 } from "node:os";
38549
- import { randomBytes, createHmac } from "node:crypto";
38983
+ import { randomBytes as randomBytes3, createHmac as createHmac3 } from "node:crypto";
38550
38984
  function bindingDigest(secret, salt) {
38551
- return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
38985
+ return createHmac3("sha256", salt).update(secret).digest("hex").slice(0, 32);
38552
38986
  }
38553
38987
  function isEnvelope(raw) {
38554
38988
  return raw !== null && typeof raw === "object" && raw.v === 1 && "state" in raw;
@@ -38598,7 +39032,7 @@ function createFileStatePersistence(opts) {
38598
39032
  },
38599
39033
  save(state) {
38600
39034
  const dir = dirname2(filePath);
38601
- const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
39035
+ const tmp = `${filePath}.tmp-${randomBytes3(6).toString("hex")}`;
38602
39036
  const dirExisted = existsSync2(dir);
38603
39037
  try {
38604
39038
  mkdirSync(dir, { recursive: true, mode: 448 });
@@ -38606,7 +39040,7 @@ function createFileStatePersistence(opts) {
38606
39040
  chmodSync(dir, 448);
38607
39041
  let boundTo;
38608
39042
  if (secret !== void 0) {
38609
- const salt = randomBytes(16).toString("hex");
39043
+ const salt = randomBytes3(16).toString("hex");
38610
39044
  boundTo = { salt, digest: bindingDigest(secret, salt) };
38611
39045
  }
38612
39046
  const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
@@ -40480,7 +40914,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
40480
40914
  async function electRole(opts) {
40481
40915
  const server = createServer();
40482
40916
  const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
40483
- return new Promise((resolve4, reject) => {
40917
+ return new Promise((resolve6, reject) => {
40484
40918
  let timer;
40485
40919
  const clearBindTimer = () => {
40486
40920
  if (timer) {
@@ -40496,7 +40930,7 @@ async function electRole(opts) {
40496
40930
  server.close();
40497
40931
  } catch {
40498
40932
  }
40499
- resolve4({ role: "peer" });
40933
+ resolve6({ role: "peer" });
40500
40934
  } else {
40501
40935
  reject(e);
40502
40936
  }
@@ -40504,7 +40938,7 @@ async function electRole(opts) {
40504
40938
  const onListening = () => {
40505
40939
  clearBindTimer();
40506
40940
  server.removeListener("error", onError);
40507
- resolve4({ role: "host", server });
40941
+ resolve6({ role: "host", server });
40508
40942
  };
40509
40943
  server.once("error", onError);
40510
40944
  server.once("listening", onListening);
@@ -40782,12 +41216,12 @@ async function awaitSessionReady(ready, opts) {
40782
41216
 
40783
41217
  // node_modules/@fetchproxy/server/dist/extension-trust.js
40784
41218
  import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2, rm as rm2 } from "node:fs/promises";
40785
- import { randomBytes as randomBytes3 } from "node:crypto";
41219
+ import { randomBytes as randomBytes5 } from "node:crypto";
40786
41220
  import { isAbsolute as isAbsolute4, join as join4 } from "node:path";
40787
41221
 
40788
41222
  // node_modules/@fetchproxy/server/dist/identity.js
40789
41223
  import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
40790
- import { randomBytes as randomBytes2 } from "node:crypto";
41224
+ import { randomBytes as randomBytes4 } from "node:crypto";
40791
41225
  import { isAbsolute as isAbsolute3, join as join3 } from "node:path";
40792
41226
  import { homedir as homedir3 } from "node:os";
40793
41227
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
@@ -40908,7 +41342,7 @@ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "
40908
41342
  async function publishIdentityIfAbsent(dir, serverName, id) {
40909
41343
  const path = identityFilePath(dir, serverName);
40910
41344
  await sweepStagingFiles(dir, serverName, { olderThanMs: 6e4 });
40911
- const tmp = `${path}.${randomBytes2(STAGING_BYTES).toString("hex")}.tmp`;
41345
+ const tmp = `${path}.${randomBytes4(STAGING_BYTES).toString("hex")}.tmp`;
40912
41346
  try {
40913
41347
  await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
40914
41348
  await chmod(tmp, 384);
@@ -41037,7 +41471,7 @@ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
41037
41471
  async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
41038
41472
  const path = extensionTrustPath(serverName, dir);
41039
41473
  await mkdir2(dir, { recursive: true, mode: 448 });
41040
- const tmp = `${path}.${randomBytes3(6).toString("hex")}.tmp`;
41474
+ const tmp = `${path}.${randomBytes5(6).toString("hex")}.tmp`;
41041
41475
  try {
41042
41476
  await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384, flag: "wx" });
41043
41477
  await chmod2(tmp, 384);
@@ -41153,8 +41587,8 @@ async function startHost(opts) {
41153
41587
  let rejectOwnSession;
41154
41588
  let ownSessionReady;
41155
41589
  function resetSessionPromise() {
41156
- ownSessionReady = new Promise((resolve4, reject) => {
41157
- resolveOwnSession = resolve4;
41590
+ ownSessionReady = new Promise((resolve6, reject) => {
41591
+ resolveOwnSession = resolve6;
41158
41592
  rejectOwnSession = reject;
41159
41593
  });
41160
41594
  ownSessionReady.catch(() => {
@@ -41556,7 +41990,7 @@ async function startHost(opts) {
41556
41990
  });
41557
41991
  });
41558
41992
  return {
41559
- close: () => new Promise((resolve4) => {
41993
+ close: () => new Promise((resolve6) => {
41560
41994
  for (const client2 of wss.clients) {
41561
41995
  try {
41562
41996
  client2.terminate();
@@ -41564,7 +41998,7 @@ async function startHost(opts) {
41564
41998
  }
41565
41999
  }
41566
42000
  wss.close(() => {
41567
- opts.httpServer.close(() => resolve4());
42001
+ opts.httpServer.close(() => resolve6());
41568
42002
  });
41569
42003
  }),
41570
42004
  sendOwnInner: async (inner) => {
@@ -41604,11 +42038,11 @@ async function startPeer(opts) {
41604
42038
  maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
41605
42039
  });
41606
42040
  const dialTimeoutMs = opts.dialTimeoutMs ?? PEER_DIAL_TIMEOUT_MS;
41607
- await new Promise((resolve4, reject) => {
42041
+ await new Promise((resolve6, reject) => {
41608
42042
  let timer;
41609
42043
  const onOpen = () => {
41610
42044
  cleanup();
41611
- resolve4();
42045
+ resolve6();
41612
42046
  };
41613
42047
  const onError = (e) => {
41614
42048
  cleanup();
@@ -41696,10 +42130,10 @@ async function startPeer(opts) {
41696
42130
  let sessionPromiseSettled = false;
41697
42131
  function resetSessionPromise() {
41698
42132
  sessionPromiseSettled = false;
41699
- sessionPromise = new Promise((resolve4, reject) => {
42133
+ sessionPromise = new Promise((resolve6, reject) => {
41700
42134
  resolveFirstReady = (st) => {
41701
42135
  sessionPromiseSettled = true;
41702
- resolve4(st);
42136
+ resolve6(st);
41703
42137
  };
41704
42138
  rejectFirstReady = (e) => {
41705
42139
  sessionPromiseSettled = true;
@@ -42849,8 +43283,8 @@ var FetchproxyServer = class {
42849
43283
  async _fetchOnceWithTimeout(init, fetchOpts = {}) {
42850
43284
  const id = this.nextRequestId++;
42851
43285
  const inner = { type: "request", id, op: "fetch", init };
42852
- const pending = this.guardPending(new Promise((resolve4) => {
42853
- this.pending.set(id, resolve4);
43286
+ const pending = this.guardPending(new Promise((resolve6) => {
43287
+ this.pending.set(id, resolve6);
42854
43288
  }));
42855
43289
  try {
42856
43290
  await this.sendInnerFrame(inner, {
@@ -42873,12 +43307,12 @@ var FetchproxyServer = class {
42873
43307
  try {
42874
43308
  return await Promise.race([
42875
43309
  pending,
42876
- new Promise((resolve4) => {
43310
+ new Promise((resolve6) => {
42877
43311
  timer = setTimeout(() => {
42878
43312
  this.pending.delete(id);
42879
43313
  const elapsedMs = Date.now() - start;
42880
43314
  const error62 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
42881
- resolve4({
43315
+ resolve6({
42882
43316
  ok: false,
42883
43317
  error: error62,
42884
43318
  kind: "timeout",
@@ -43022,9 +43456,9 @@ var FetchproxyServer = class {
43022
43456
  if (opts.subdomain !== void 0)
43023
43457
  assertSubdomainLabel(opts.subdomain);
43024
43458
  const baseDomain = this.resolveBaseDomain(opts.domain);
43025
- const isAbsolute5 = path.startsWith("http://") || path.startsWith("https://");
43459
+ const isAbsolute6 = path.startsWith("http://") || path.startsWith("https://");
43026
43460
  let host;
43027
- if (isAbsolute5) {
43461
+ if (isAbsolute6) {
43028
43462
  try {
43029
43463
  host = new URL(path).host;
43030
43464
  } catch {
@@ -43033,7 +43467,7 @@ var FetchproxyServer = class {
43033
43467
  } else {
43034
43468
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
43035
43469
  }
43036
- const url2 = isAbsolute5 ? path : `https://${host}${path}`;
43470
+ const url2 = isAbsolute6 ? path : `https://${host}${path}`;
43037
43471
  assertUrlInDomains("request url", url2, this.opts.domains);
43038
43472
  let tabUrl = `https://${host}/`;
43039
43473
  if (opts.viaTab !== void 0) {
@@ -43298,8 +43732,8 @@ var FetchproxyServer = class {
43298
43732
  const tabUrl = `https://${host}/`;
43299
43733
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
43300
43734
  }
43301
- const pending = this.guardPending(new Promise((resolve4) => {
43302
- this.pendingReadCookies.set(id, resolve4);
43735
+ const pending = this.guardPending(new Promise((resolve6) => {
43736
+ this.pendingReadCookies.set(id, resolve6);
43303
43737
  }));
43304
43738
  await this.sendInnerFrame(inner);
43305
43739
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
@@ -43357,8 +43791,8 @@ var FetchproxyServer = class {
43357
43791
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
43358
43792
  }
43359
43793
  };
43360
- const pending = this.guardPending(new Promise((resolve4, reject) => {
43361
- this.pendingWriteCookies.set(id, { resolve: resolve4, reject });
43794
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43795
+ this.pendingWriteCookies.set(id, { resolve: resolve6, reject });
43362
43796
  }));
43363
43797
  await this.sendInnerFrame(inner);
43364
43798
  return this._withVerbTimeout(
@@ -43428,8 +43862,8 @@ var FetchproxyServer = class {
43428
43862
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
43429
43863
  }
43430
43864
  };
43431
- const pending = this.guardPending(new Promise((resolve4, reject) => {
43432
- this.pendingStorage.set(id, { resolve: resolve4, reject });
43865
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43866
+ this.pendingStorage.set(id, { resolve: resolve6, reject });
43433
43867
  }));
43434
43868
  await this.sendInnerFrame(inner);
43435
43869
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
@@ -43487,8 +43921,8 @@ var FetchproxyServer = class {
43487
43921
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
43488
43922
  }
43489
43923
  };
43490
- const pending = this.guardPending(new Promise((resolve4, reject) => {
43491
- this.pendingCapture.set(id, { resolve: resolve4, reject });
43924
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43925
+ this.pendingCapture.set(id, { resolve: resolve6, reject });
43492
43926
  }));
43493
43927
  await this.sendInnerFrame(inner);
43494
43928
  return this._withVerbTimeout(
@@ -43535,8 +43969,8 @@ var FetchproxyServer = class {
43535
43969
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
43536
43970
  }
43537
43971
  };
43538
- const pending = this.guardPending(new Promise((resolve4, reject) => {
43539
- this.pendingRedirect.set(id, { resolve: resolve4, reject });
43972
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
43973
+ this.pendingRedirect.set(id, { resolve: resolve6, reject });
43540
43974
  }));
43541
43975
  await this.sendInnerFrame(inner);
43542
43976
  return this._withVerbTimeout(
@@ -43631,8 +44065,8 @@ var FetchproxyServer = class {
43631
44065
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
43632
44066
  }
43633
44067
  };
43634
- const pending = this.guardPending(new Promise((resolve4, reject) => {
43635
- this.pendingDownload.set(id, { resolve: resolve4, reject });
44068
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
44069
+ this.pendingDownload.set(id, { resolve: resolve6, reject });
43636
44070
  }));
43637
44071
  await this.sendInnerFrame(inner);
43638
44072
  return this._withVerbTimeout(
@@ -43692,8 +44126,8 @@ var FetchproxyServer = class {
43692
44126
  keys: [...opts.keys]
43693
44127
  }
43694
44128
  };
43695
- const pending = this.guardPending(new Promise((resolve4, reject) => {
43696
- this.pendingIdb.set(id, { resolve: resolve4, reject });
44129
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
44130
+ this.pendingIdb.set(id, { resolve: resolve6, reject });
43697
44131
  }));
43698
44132
  await this.sendInnerFrame(inner);
43699
44133
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
@@ -43732,8 +44166,8 @@ var FetchproxyServer = class {
43732
44166
  op: "read_dom",
43733
44167
  init: { origin, names: [...opts.names] }
43734
44168
  };
43735
- const pending = this.guardPending(new Promise((resolve4, reject) => {
43736
- this.pendingStorage.set(id, { resolve: resolve4, reject });
44169
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
44170
+ this.pendingStorage.set(id, { resolve: resolve6, reject });
43737
44171
  }));
43738
44172
  await this.sendInnerFrame(inner);
43739
44173
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
@@ -43778,8 +44212,8 @@ var FetchproxyServer = class {
43778
44212
  op: "read_dom_list",
43779
44213
  init: { origin, name: opts.name }
43780
44214
  };
43781
- const pending = this.guardPending(new Promise((resolve4, reject) => {
43782
- this.pendingDomList.set(id, { resolve: resolve4, reject });
44215
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
44216
+ this.pendingDomList.set(id, { resolve: resolve6, reject });
43783
44217
  }));
43784
44218
  await this.sendInnerFrame(inner);
43785
44219
  return this._withVerbTimeout(pending, this.pendingDomList, id, origin);
@@ -43824,8 +44258,8 @@ var FetchproxyServer = class {
43824
44258
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
43825
44259
  }
43826
44260
  };
43827
- const pending = this.guardPending(new Promise((resolve4, reject) => {
43828
- this.pendingGraphql.set(id, { resolve: resolve4, reject });
44261
+ const pending = this.guardPending(new Promise((resolve6, reject) => {
44262
+ this.pendingGraphql.set(id, { resolve: resolve6, reject });
43829
44263
  }));
43830
44264
  const retrySafe = opts.retryOnTimeout === true;
43831
44265
  await this.sendInnerFrame(inner, { resendable: retrySafe });
@@ -44574,6 +45008,9 @@ function registerCredentialHealthcheckTool(args) {
44574
45008
  }, async () => runCredentialHealthcheck(args));
44575
45009
  }
44576
45010
 
45011
+ // src/auth-password.ts
45012
+ import { createHash as createHash4 } from "node:crypto";
45013
+
44577
45014
  // src/protocol.ts
44578
45015
  var BASE_URL = "https://ofw.ourfamilywizard.com";
44579
45016
  var OFW_PROTOCOL_HEADERS = {
@@ -44598,7 +45035,23 @@ function assertOfwUrl(rawUrl) {
44598
45035
  }
44599
45036
 
44600
45037
  // src/auth-password.ts
45038
+ var CredentialsRejectedError = class extends Error {
45039
+ constructor(message) {
45040
+ super(message);
45041
+ this.name = "CredentialsRejectedError";
45042
+ }
45043
+ };
45044
+ var rejectedPairs = /* @__PURE__ */ new Set();
45045
+ function pairDigest(username, password) {
45046
+ return createHash4("sha256").update(`${username.length}:${username}\0${password}`).digest("hex");
45047
+ }
44601
45048
  async function loginWithPassword(username, password) {
45049
+ const digest = pairDigest(username, password);
45050
+ if (rejectedPairs.has(digest)) {
45051
+ throw new CredentialsRejectedError(
45052
+ "OFW login not attempted \u2014 this OurFamilyWizard email and password were already rejected by OFW earlier in this session, and OFW counts failed sign-ins against the account, so they are not re-sent. Update OFW_USERNAME / OFW_PASSWORD to the current values (or restart the server) and try again."
45053
+ );
45054
+ }
44602
45055
  const initUrl = `${BASE_URL}/ofw/login.form`;
44603
45056
  assertOfwUrl(initUrl);
44604
45057
  const initResponse = await fetch(initUrl, {
@@ -44634,8 +45087,9 @@ async function loginWithPassword(username, password) {
44634
45087
  const contentType = response.headers.get("content-type") ?? "";
44635
45088
  if (!contentType.includes("application/json")) {
44636
45089
  if (contentType.includes("text/html")) {
44637
- throw new Error(
44638
- "OFW login failed \u2014 your OurFamilyWizard email or password was not accepted. Check them and try again."
45090
+ rejectedPairs.add(digest);
45091
+ throw new CredentialsRejectedError(
45092
+ "OFW login failed \u2014 your OurFamilyWizard email or password was not accepted. Check them and try again. They will not be re-sent until OFW_USERNAME / OFW_PASSWORD change or the server restarts."
44639
45093
  );
44640
45094
  }
44641
45095
  const body = await response.text();
@@ -44651,7 +45105,7 @@ async function loginWithPassword(username, password) {
44651
45105
  // package.json
44652
45106
  var package_default = {
44653
45107
  name: "ofw-mcp",
44654
- version: "2.19.2",
45108
+ version: "2.19.4",
44655
45109
  license: "MIT",
44656
45110
  mcpName: "io.github.chrischall/ofw-mcp",
44657
45111
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -44685,7 +45139,7 @@ var package_default = {
44685
45139
  typecheck: "tsc -p tsconfig.json --noEmit"
44686
45140
  },
44687
45141
  dependencies: {
44688
- "@chrischall/mcp-utils": "^2.4.0",
45142
+ "@chrischall/mcp-utils": "^2.6.0",
44689
45143
  "@fetchproxy/bootstrap": "^3.2.0",
44690
45144
  "@modelcontextprotocol/server": "^2.0.0",
44691
45145
  dotenv: "^18.0.0",
@@ -45019,9 +45473,9 @@ function offsetAt(instant, tz) {
45019
45473
  const w = wallPartsIn(instant, tz);
45020
45474
  const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
45021
45475
  const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
45022
- const sign = minutes < 0 ? "-" : "+";
45476
+ const sign2 = minutes < 0 ? "-" : "+";
45023
45477
  const abs = Math.abs(minutes);
45024
- return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
45478
+ return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
45025
45479
  }
45026
45480
  var wallPartsFormatters = /* @__PURE__ */ new Map();
45027
45481
  var displayFormatters = /* @__PURE__ */ new Map();
@@ -45168,6 +45622,34 @@ function parseTimestampValue(key, value, assumeNaiveIn) {
45168
45622
  }
45169
45623
  return null;
45170
45624
  }
45625
+ function naiveWallClockOf(instant, tz) {
45626
+ return isoWithOffset(instant, tz, "");
45627
+ }
45628
+ function nowNaiveWallClock(tz = displayTimeZone()) {
45629
+ return naiveWallClockOf(/* @__PURE__ */ new Date(), tz);
45630
+ }
45631
+ function toNaiveWallClock(value, tz = displayTimeZone()) {
45632
+ const raw = value.trim();
45633
+ if (DATE_ONLY.test(raw)) return raw;
45634
+ if (RFC3339_WITH_OFFSET.test(raw)) {
45635
+ const instant = parseTimestampValue("", raw, tz);
45636
+ return instant ? naiveWallClockOf(instant, tz) : null;
45637
+ }
45638
+ const naive = NAIVE_DATE_TIME.exec(raw);
45639
+ if (!naive) return null;
45640
+ const [, y, mo, d, h, mi, s, frac] = naive;
45641
+ const parts = {
45642
+ year: Number(y),
45643
+ month: Number(mo),
45644
+ day: Number(d),
45645
+ hour: Number(h),
45646
+ minute: Number(mi),
45647
+ second: Number(s ?? "0")
45648
+ };
45649
+ if (!isRealCalendarDate(parts)) return null;
45650
+ const ms = frac ? `.${frac.padEnd(3, "0").slice(0, 3)}` : "";
45651
+ return `${y}-${mo}-${d}T${h}:${mi}:${pad(parts.second)}${ms}`;
45652
+ }
45171
45653
  function walk(node2, tz) {
45172
45654
  if (Array.isArray(node2)) {
45173
45655
  for (const item of node2) walk(item, tz);
@@ -45270,17 +45752,58 @@ var PostMessagesResponseSchema = external_exports.looseObject({
45270
45752
  id: external_exports.number().optional(),
45271
45753
  entityId: external_exports.number().optional()
45272
45754
  }).nullable();
45755
+ var UnconfirmedWriteError = class extends Error {
45756
+ constructor(postedId, cause) {
45757
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
45758
+ this.postedId = postedId;
45759
+ this.name = "UnconfirmedWriteError";
45760
+ }
45761
+ postedId;
45762
+ };
45763
+ function isDefinitiveRejection(e) {
45764
+ if (!(e instanceof Error)) return false;
45765
+ if (/^Rate limited by OFW API/.test(e.message)) return true;
45766
+ const m = /OFW API error: (4\d\d)\b/.exec(e.message);
45767
+ return m !== null && m[1] !== "408";
45768
+ }
45769
+ async function requestWrite(client2, method, path, body) {
45770
+ try {
45771
+ return await client2.request(method, path, body);
45772
+ } catch (e) {
45773
+ throw isDefinitiveRejection(e) ? e : new UnconfirmedWriteError(null, e);
45774
+ }
45775
+ }
45776
+ function unconfirmedWriteResponse(e, opts) {
45777
+ return jsonErrorResponse({
45778
+ result: opts.result,
45779
+ mayHaveLanded: true,
45780
+ reason: `The request to ${opts.what} failed without a definitive answer from OFW: ${e.message}. It MAY HAVE BEEN APPLIED on OurFamilyWizard.`,
45781
+ remedy: `Do NOT retry blindly \u2014 a second attempt can put a duplicate on the co-parent-visible record. First check with ${opts.checkWith} (or on ourfamilywizard.com), and retry only once you have confirmed it did not land.`
45782
+ });
45783
+ }
45273
45784
  async function postMessageAndRefetch(client2, payload, detailSchema, ctx) {
45785
+ let posted;
45786
+ try {
45787
+ posted = await client2.request("POST", "/pub/v3/messages", payload);
45788
+ } catch (e) {
45789
+ throw isDefinitiveRejection(e) ? e : new UnconfirmedWriteError(null, e);
45790
+ }
45274
45791
  const raw = parseLenient(
45275
45792
  PostMessagesResponseSchema,
45276
- await client2.request("POST", "/pub/v3/messages", payload),
45793
+ posted,
45277
45794
  { label: "ofw-mcp", context: `POST /pub/v3/messages (${ctx})`, mode: "strict" }
45278
45795
  );
45279
45796
  const id = typeof raw?.id === "number" ? raw.id : typeof raw?.entityId === "number" ? raw.entityId : null;
45280
45797
  if (id === null) return { id: null, detail: null, raw };
45798
+ let fetched;
45799
+ try {
45800
+ fetched = await client2.request("GET", `/pub/v3/messages/${id}`);
45801
+ } catch (e) {
45802
+ throw new UnconfirmedWriteError(id, e);
45803
+ }
45281
45804
  const detail = parseLenient(
45282
45805
  detailSchema,
45283
- await client2.request("GET", `/pub/v3/messages/${id}`),
45806
+ fetched,
45284
45807
  { label: "ofw-mcp", context: `GET /pub/v3/messages/{id} (${ctx})`, mode: "strict" }
45285
45808
  );
45286
45809
  return { id, detail, raw };
@@ -45305,7 +45828,7 @@ function registerUserTools(server, client2) {
45305
45828
  }
45306
45829
 
45307
45830
  // src/tools/healthcheck.ts
45308
- function registerHealthcheckTools(server, client2, resolve4 = resolveAuth) {
45831
+ function registerHealthcheckTools(server, client2, resolve6 = resolveAuth) {
45309
45832
  registerCredentialHealthcheckTool({
45310
45833
  server,
45311
45834
  prefix: "ofw",
@@ -45316,7 +45839,7 @@ function registerHealthcheckTools(server, client2, resolve4 = resolveAuth) {
45316
45839
  probePath: "/pub/v2/profiles",
45317
45840
  resolveCredential: async () => {
45318
45841
  try {
45319
- const auth = await resolve4();
45842
+ const auth = await resolve6();
45320
45843
  return {
45321
45844
  source: auth.source,
45322
45845
  // Never the token. Expiry is the fact that explains a connector
@@ -45522,7 +46045,7 @@ async function walkPages(client2, folder, folderId, opts, store) {
45522
46045
  folder,
45523
46046
  subject: item.subject ?? "(no subject)",
45524
46047
  fromUser: item.from?.name ?? "",
45525
- sentAt: item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
46048
+ sentAt: item.date?.dateTime ?? nowNaiveWallClock(),
45526
46049
  recipients: mapRecipients(detailRecipients ?? item.recipients),
45527
46050
  body,
45528
46051
  fetchedBodyAt,
@@ -45665,7 +46188,7 @@ async function syncDrafts(client2, draftsFolderId, store, budget) {
45665
46188
  body: detail.body ?? "",
45666
46189
  recipients: mapRecipients(item.recipients),
45667
46190
  replyToId: threadedReplyTo(item),
45668
- modifiedAt: item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
46191
+ modifiedAt: item.date?.dateTime ?? nowNaiveWallClock(),
45669
46192
  listData: item
45670
46193
  });
45671
46194
  }
@@ -45768,7 +46291,7 @@ async function syncAll(client2, opts, store) {
45768
46291
  }
45769
46292
 
45770
46293
  // src/config.ts
45771
- import { createHash } from "node:crypto";
46294
+ import { createHash as createHash5 } from "node:crypto";
45772
46295
  import { homedir as homedir4 } from "node:os";
45773
46296
  import { join as join6 } from "node:path";
45774
46297
  function readCacheIdentity() {
@@ -45781,14 +46304,22 @@ function getCacheDir() {
45781
46304
  }
45782
46305
  function getCacheDbPath() {
45783
46306
  const identity = readCacheIdentity();
45784
- const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
46307
+ const hash2 = createHash5("sha256").update(identity).digest("hex").slice(0, 16);
45785
46308
  return join6(getCacheDir(), `${hash2}.db`);
45786
46309
  }
45787
46310
  function getAttachmentsDir() {
45788
46311
  const override = process.env.OFW_ATTACHMENTS_DIR;
45789
46312
  if (override && override.trim().length > 0) return override.trim();
46313
+ return getDefaultAttachmentsDir();
46314
+ }
46315
+ function getDefaultAttachmentsDir() {
45790
46316
  return join6(homedir4(), "Downloads", "ofw-mcp");
45791
46317
  }
46318
+ function getUploadDir() {
46319
+ const override = process.env.OFW_UPLOAD_DIR;
46320
+ if (override && override.trim().length > 0) return override.trim();
46321
+ return getAttachmentsDir();
46322
+ }
45792
46323
  function getWriteMode() {
45793
46324
  const raw = process.env.OFW_WRITE_MODE;
45794
46325
  if (typeof raw !== "string" || raw.trim().length === 0) return "all";
@@ -46290,6 +46821,31 @@ function newDraftKey() {
46290
46821
  return `dk_${crypto.randomUUID()}`;
46291
46822
  }
46292
46823
 
46824
+ // src/tools/_confirm.ts
46825
+ var CONFIRM_NOTE = "Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call performs NO write and returns a preview of exactly what would happen plus a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).";
46826
+ function confirmWrite(ctx, opts) {
46827
+ return requireConfirmationWithFallback(
46828
+ ctx,
46829
+ confirmationFromEnv({
46830
+ action: opts.action,
46831
+ message: opts.message,
46832
+ details: opts.preview,
46833
+ unsupportedNote: "Complete this action on ourfamilywizard.com instead.",
46834
+ tool: opts.tool,
46835
+ confirmToken: opts.confirmToken,
46836
+ subject: () => ({
46837
+ target: opts.target,
46838
+ ...opts.revision !== void 0 ? { revision: opts.revision } : {},
46839
+ payload: opts.payload,
46840
+ preview: opts.preview
46841
+ })
46842
+ })
46843
+ );
46844
+ }
46845
+ function stateRevision(value) {
46846
+ return `s1:${fnv1a64(JSON.stringify(value))}`;
46847
+ }
46848
+
46293
46849
  // src/extract/inflate.ts
46294
46850
  var MAX_DECOMPRESSED_BYTES = 32 * 1024 * 1024;
46295
46851
  var DecompressionLimitError = class extends Error {
@@ -46465,6 +47021,8 @@ function findPart(zip, conventional, fileName) {
46465
47021
 
46466
47022
  // src/extract/spreadsheet.ts
46467
47023
  var DEFAULT_MAX_CELLS = 2e5;
47024
+ var DEFAULT_MAX_GRID_CELLS = 2e6;
47025
+ var MAX_COLUMNS = 16384;
46468
47026
  var BUILTIN_DATE_FORMATS = /* @__PURE__ */ new Set([14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47]);
46469
47027
  var SERIAL_EPOCH_OFFSET = 25569;
46470
47028
  function excelSerialToIso(serial, date1904) {
@@ -46483,11 +47041,11 @@ function isDateFormat(numFmtId, formatCode) {
46483
47041
  return /[ymdhs]/i.test(bare);
46484
47042
  }
46485
47043
  function columnIndex(ref) {
46486
- const m = /^([A-Z]+)/.exec(ref);
47044
+ const m = /^([A-Z]{1,3})(?![A-Z])/.exec(ref);
46487
47045
  if (!m) return null;
46488
47046
  let index = 0;
46489
47047
  for (const ch of m[1]) index = index * 26 + (ch.charCodeAt(0) - 64);
46490
- return index - 1;
47048
+ return index > MAX_COLUMNS ? null : index - 1;
46491
47049
  }
46492
47050
  function csvField(value) {
46493
47051
  return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
@@ -46542,7 +47100,7 @@ function cellValue(attrs, inner, ctx) {
46542
47100
  }
46543
47101
  }
46544
47102
  }
46545
- function parseSheet(xml, name, ctx, maxCells) {
47103
+ function parseSheet(xml, name, ctx, maxCells, maxGridCells) {
46546
47104
  const rows = [];
46547
47105
  let cols = 0;
46548
47106
  let cells = 0;
@@ -46553,14 +47111,20 @@ function parseSheet(xml, name, ctx, maxCells) {
46553
47111
  break;
46554
47112
  }
46555
47113
  const values = [];
47114
+ let rowCols = cols;
46556
47115
  for (const cell of elements(row.inner, "c")) {
46557
47116
  const ref = attr(cell.attrs, "r");
46558
47117
  const index = ref === null ? null : columnIndex(ref);
46559
47118
  if (index === null) continue;
46560
47119
  values[index] = cellValue(cell.attrs, cell.inner, ctx);
46561
47120
  cells++;
46562
- if (index + 1 > cols) cols = index + 1;
47121
+ if (index + 1 > rowCols) rowCols = index + 1;
47122
+ }
47123
+ if ((rows.length + 1) * rowCols > maxGridCells) {
47124
+ truncated = true;
47125
+ break;
46563
47126
  }
47127
+ cols = rowCols;
46564
47128
  rows.push(values);
46565
47129
  }
46566
47130
  return { name, rows: rows.length, cols, csv: toCsv(rows, cols), ...truncated ? { truncated } : {} };
@@ -46579,6 +47143,7 @@ async function extractXlsx(bytes, opts = {}) {
46579
47143
  date1904: /<workbookPr[^>]*date1904="(1|true)"/i.test(workbookXml)
46580
47144
  };
46581
47145
  const maxCells = opts.maxCells ?? DEFAULT_MAX_CELLS;
47146
+ const maxGridCells = opts.maxGridCells ?? DEFAULT_MAX_GRID_CELLS;
46582
47147
  const sheets = [];
46583
47148
  const omitted = [];
46584
47149
  let index = 0;
@@ -46596,7 +47161,7 @@ async function extractXlsx(bytes, opts = {}) {
46596
47161
  omitted.push(`${name} (sheet part not found in the workbook)`);
46597
47162
  continue;
46598
47163
  }
46599
- sheets.push(parseSheet(xml, name, ctx, maxCells));
47164
+ sheets.push(parseSheet(xml, name, ctx, maxCells, maxGridCells));
46600
47165
  }
46601
47166
  const truncated = sheets.some((s) => s.truncated);
46602
47167
  return {
@@ -46656,12 +47221,16 @@ function parseDelimitedRows(text, delimiter) {
46656
47221
  }
46657
47222
  return rows;
46658
47223
  }
46659
- function extractDelimited(text, name, delimiter) {
46660
- const rows = parseDelimitedRows(text, delimiter);
47224
+ function extractDelimited(text, name, delimiter, maxGridCells = DEFAULT_MAX_GRID_CELLS) {
47225
+ let rows = parseDelimitedRows(text, delimiter);
46661
47226
  const cols = rows.reduce((max, r) => Math.max(max, r.length), 0);
47227
+ const keep = cols === 0 ? rows.length : Math.floor(maxGridCells / cols);
47228
+ const truncated = rows.length > keep;
47229
+ if (truncated) rows = rows.slice(0, keep);
46662
47230
  return {
46663
47231
  kind: "spreadsheet",
46664
- sheets: [{ name, rows: rows.length, cols, csv: toCsv(rows, cols) }]
47232
+ sheets: [{ name, rows: rows.length, cols, csv: toCsv(rows, cols), ...truncated ? { truncated } : {} }],
47233
+ ...truncated ? { truncated } : {}
46665
47234
  };
46666
47235
  }
46667
47236
 
@@ -47157,8 +47726,17 @@ async function extractAttachment(bytes, mimeType, fileName, opts = {}) {
47157
47726
  }
47158
47727
 
47159
47728
  // src/tools/attachments.ts
47160
- import { readFileSync as readFileSync2, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
47161
- import { basename, dirname as dirname4, extname } from "node:path";
47729
+ import { chmodSync as chmodSync2, existsSync as existsSync3, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
47730
+ import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute5, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
47731
+ function isWithin(root, candidate) {
47732
+ const rel = relative2(root, candidate);
47733
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute5(rel);
47734
+ }
47735
+ function deepestExisting(path) {
47736
+ let current = path;
47737
+ while (!existsSync3(current)) current = dirname4(current);
47738
+ return current;
47739
+ }
47162
47740
  var MIME_BY_EXT = {
47163
47741
  ".pdf": "application/pdf",
47164
47742
  ".png": "image/png",
@@ -47219,15 +47797,28 @@ function resolveDownloadMime(bytes, headerMime, fileName) {
47219
47797
  function isHostRenderableImage(mime) {
47220
47798
  return HOST_RENDERABLE_IMAGE_MIMES.has(mime);
47221
47799
  }
47800
+ var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
47222
47801
  var NodeAttachmentIO = class {
47223
47802
  supportsDisk = true;
47224
47803
  async resolveUpload(path) {
47225
- const abs = expandPath(path);
47226
- const stat = statSync(abs);
47804
+ const root = resolve4(getUploadDir());
47805
+ const abs = resolve4(root, path.startsWith("~") ? expandPath(path) : path);
47806
+ const outside = new Error(`Refusing to upload ${abs}: it is outside the upload directory (${root}). Only files placed in that directory can be uploaded \u2014 ask the user to copy the file there, or set OFW_UPLOAD_DIR.`);
47807
+ if (!isWithin(root, abs)) throw outside;
47808
+ const real = realpathSync2(abs);
47809
+ const realRoot = realpathSync2(root);
47810
+ if (!isWithin(realRoot, real)) throw outside;
47811
+ if (relative2(realRoot, real).split(sep2).some((segment) => segment.startsWith("."))) {
47812
+ throw new Error(`Refusing to upload ${abs}: hidden files and files in hidden directories (dotfiles, credential stores) are never uploaded.`);
47813
+ }
47814
+ const stat = statSync(real);
47227
47815
  if (!stat.isFile()) throw new Error(`Not a file: ${abs}`);
47816
+ if (stat.size > MAX_UPLOAD_BYTES) {
47817
+ throw new Error(`Refusing to upload ${abs}: it is too large (${stat.size} bytes; the limit is ${MAX_UPLOAD_BYTES}).`);
47818
+ }
47228
47819
  const fileName = basename(abs);
47229
47820
  const mimeType = mimeFromName(fileName);
47230
- const blob = await fileBlob(abs, { type: mimeType });
47821
+ const blob = await fileBlob(real, { type: mimeType, allowedRoots: [realRoot] });
47231
47822
  return { blob, fileName, mimeType, sizeBytes: stat.size };
47232
47823
  }
47233
47824
  readDownloaded(path) {
@@ -47237,9 +47828,30 @@ var NodeAttachmentIO = class {
47237
47828
  return null;
47238
47829
  }
47239
47830
  }
47240
- writeDownload(dest, bytes) {
47241
- mkdirSync2(dirname4(dest), { recursive: true });
47242
- writeFileSync2(dest, bytes);
47831
+ writeDownload(dest, bytes, { root, overwrite }) {
47832
+ mkdirSync2(root, { recursive: true, mode: 448 });
47833
+ if (resolve4(root) === resolve4(getDefaultAttachmentsDir())) chmodSync2(root, 448);
47834
+ const realRoot = realpathSync2(root);
47835
+ const parent = dirname4(dest);
47836
+ const anchor2 = realpathSync2(deepestExisting(parent));
47837
+ if (anchor2 !== realRoot && !isWithin(realRoot, anchor2)) {
47838
+ throw new Error(`Refusing to write ${dest}: it resolves outside the attachments directory (${root}).`);
47839
+ }
47840
+ mkdirSync2(parent, { recursive: true, mode: 448 });
47841
+ if (overwrite) {
47842
+ try {
47843
+ unlinkSync2(dest);
47844
+ } catch {
47845
+ }
47846
+ }
47847
+ try {
47848
+ writeFileSync2(dest, bytes, { flag: "wx", mode: 384 });
47849
+ } catch (e) {
47850
+ if (e.code === "EEXIST") {
47851
+ throw new Error(`Refusing to overwrite ${dest}: a file already exists there. Pass force:true to replace it, or choose another saveTo.`);
47852
+ }
47853
+ throw e;
47854
+ }
47243
47855
  }
47244
47856
  };
47245
47857
 
@@ -47303,7 +47915,7 @@ async function buildInlineDelivery(input2) {
47303
47915
  }
47304
47916
 
47305
47917
  // src/tools/messages.ts
47306
- import { basename as basename2, join as join7 } from "node:path";
47918
+ import { basename as basename2, join as join7, resolve as resolve5 } from "node:path";
47307
47919
 
47308
47920
  // src/tools/pagination.ts
47309
47921
  function pageState(input2) {
@@ -47568,6 +48180,19 @@ function markReadVerdict(cached2, requested) {
47568
48180
  ...cached2 === null ? { hint: "Run ofw_sync_messages first: it walks list pages, not bodies, so it can tell you what this id is without stamping anything." } : { subject: cached2.subject, fromUser: cached2.fromUser, sentAt: cached2.sentAt }
47569
48181
  });
47570
48182
  }
48183
+ async function describeRecipients(cache, recipientIds, known) {
48184
+ const names = /* @__PURE__ */ new Map();
48185
+ const learn = (rs) => {
48186
+ for (const r of rs ?? []) {
48187
+ if (r.userId !== 0 && r.name && !names.has(r.userId)) names.set(r.userId, r.name);
48188
+ }
48189
+ };
48190
+ known.forEach(learn);
48191
+ if (recipientIds.some((id) => !names.has(id))) {
48192
+ for (const row of await cache.listMessages({ page: 1, size: 200 })) learn(row.recipients);
48193
+ }
48194
+ return recipientIds.map((userId) => ({ userId, name: names.get(userId) ?? null }));
48195
+ }
47571
48196
  function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47572
48197
  const writeMode = getWriteMode();
47573
48198
  const allowSend = writeMode === "all";
@@ -47587,8 +48212,8 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47587
48212
  folderId: external_exports.string().describe('Folder name: "inbox", "sent", or "both" (default "both")').optional(),
47588
48213
  page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
47589
48214
  size: external_exports.number().int().min(1).describe("Messages per page (default 50)").optional(),
47590
- since: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at >= since (inclusive)").optional(),
47591
- until: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at < until (exclusive)").optional(),
48215
+ since: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at >= since (inclusive). A value with an offset or Z is compared as that instant; a naive value is read as the account's local time (DISPLAY_TZ)").optional(),
48216
+ until: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at < until (exclusive). A value with an offset or Z is compared as that instant; a naive value is read as the account's local time (DISPLAY_TZ)").optional(),
47592
48217
  q: external_exports.string().describe("Substring match on subject AND body (case-insensitive). Use to find messages on a specific topic.").optional(),
47593
48218
  sort: external_exports.enum(["newest", "oldest"]).describe('Result order: "newest" (default, newest first) or "oldest" (oldest first). This decides which end a truncated page keeps \u2014 with "newest" page 1 of a wide date range holds its most RECENT slice, with "oldest" its earliest. Use "oldest" to start at the old end of a range instead of paging to it.').optional(),
47594
48219
  autoRefresh: external_exports.boolean().describe(AUTO_REFRESH_DESC).optional(),
@@ -47615,9 +48240,25 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47615
48240
  note: 'No lookup was performed. This says NOTHING about what is in the cache \u2014 do not read it as "no messages".'
47616
48241
  });
47617
48242
  }
48243
+ const bounds = {};
48244
+ for (const key of ["since", "until"]) {
48245
+ const value2 = args[key];
48246
+ if (value2 === void 0) continue;
48247
+ const converted = toNaiveWallClock(value2);
48248
+ if (converted === null) {
48249
+ return jsonErrorResponse({
48250
+ result: "INVALID_DATE",
48251
+ reason: `${key} must be an ISO date (YYYY-MM-DD) or datetime, optionally with an offset or Z (got ${JSON.stringify(value2)}).`,
48252
+ remedy: `Re-call with ${key} as e.g. "2026-07-27" or "2026-07-27T22:00:00-04:00".`,
48253
+ complete: false,
48254
+ note: 'No lookup was performed. This says NOTHING about what is in the cache \u2014 do not read it as "no messages".'
48255
+ });
48256
+ }
48257
+ bounds[key] = converted;
48258
+ }
47618
48259
  const cache = cacheProvider();
47619
48260
  const folders = folder === void 0 ? ["inbox", "sent"] : [folder];
47620
- const filter = { folder, since: args.since, until: args.until, q: args.q };
48261
+ const filter = { folder, since: bounds.since, until: bounds.until, q: args.q };
47621
48262
  const { value, refreshed, unverifiedEmpty } = await guardedCacheRead({
47622
48263
  client: client2,
47623
48264
  cache,
@@ -47791,7 +48432,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47791
48432
  folder,
47792
48433
  subject: detail.subject,
47793
48434
  fromUser: detail.from?.name ?? "",
47794
- sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
48435
+ sentAt: detail.date?.dateTime ?? nowNaiveWallClock(),
47795
48436
  recipients: mapRecipients(detail.recipients),
47796
48437
  body: detail.body ?? "",
47797
48438
  fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -47808,8 +48449,8 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47808
48449
  return jsonResponse({ ...viewOne(view, withReadState(row)), attachments, freshness });
47809
48450
  });
47810
48451
  if (allowSend) server.registerTool("ofw_send_message", {
47811
- description: "Send a message via OurFamilyWizard \u2014 the ONE irreversible operation here, so it carries the strongest guard. TO SEND AN EXISTING DRAFT (the safe default): pass draftId (or messageId \u2014 same thing). The tool re-reads the draft from OFW and sends the SERVER'S version, so what goes out is what is on OurFamilyWizard, not what this session remembers \u2014 subject/body act only as explicit overrides. It is guarded exactly like ofw_save_draft: pass expectedRevision to assert which version you are sending; if the draft changed on OFW since you read it \u2014 or no longer exists (it may already have been SENT) \u2014 the send is REFUSED with the current server content echoed back, and nothing goes out. RECIPIENTS: OurFamilyWizard does not persist recipients on drafts, so recipientIds is usually still required at send time (ids from ofw_get_profile). After the send is CONFIRMED (OFW returned the new message id and the re-fetched sent record matches what was posted), the source draft is deleted automatically; pass deleteDraftOnSuccess:false to keep it. On ANY failure or ambiguity the draft is never deleted \u2014 the response carries draftRetained:true with the reason. TO COMPOSE FROM SCRATCH: supply subject/body/recipientIds with no draftId. If replyToId is provided (or inherited from the draft), the cache may rewrite it to the latest reply in the same thread (a note is included when this happens). ATTACHMENTS: when sending by draftId, the server draft's own attachments carry over automatically; myFileIDs (from ofw_upload_attachment) overrides or attaches files on a fresh compose. The response leads with sentMessageId and the stable draftKey, and reports threaded (whether OFW actually linked the reply) and draftDeleted.",
47812
- annotations: { destructiveHint: true },
48452
+ description: "Send a message via OurFamilyWizard \u2014 the ONE irreversible operation here, so it carries the strongest guard. TO SEND AN EXISTING DRAFT (the safe default): pass draftId (or messageId \u2014 same thing). The tool re-reads the draft from OFW and sends the SERVER'S version, so what goes out is what is on OurFamilyWizard, not what this session remembers \u2014 subject/body act only as explicit overrides. It is guarded exactly like ofw_save_draft: pass expectedRevision to assert which version you are sending; if the draft changed on OFW since you read it \u2014 or no longer exists (it may already have been SENT) \u2014 the send is REFUSED with the current server content echoed back, and nothing goes out. RECIPIENTS: OurFamilyWizard does not persist recipients on drafts, so recipientIds is usually still required at send time (ids from ofw_get_profile). After the send is CONFIRMED (OFW returned the new message id and the re-fetched sent record matches what was posted), the source draft is deleted automatically; pass deleteDraftOnSuccess:false to keep it. On ANY failure or ambiguity the draft is never deleted \u2014 the response carries draftRetained:true with the reason. If the send request times out or drops without a definitive answer, the result is SEND_UNCONFIRMED: the message may already have been delivered, so do NOT retry until a sent-folder sync (or ourfamilywizard.com) shows it did not go out. TO COMPOSE FROM SCRATCH: supply subject/body/recipientIds with no draftId. If replyToId is provided (or inherited from the draft), the cache may rewrite it to the latest reply in the same thread (a note is included when this happens). ATTACHMENTS: when sending by draftId, the server draft's own attachments carry over automatically; myFileIDs (from ofw_upload_attachment) overrides or attaches files on a fresh compose. The response leads with sentMessageId and the stable draftKey, and reports threaded (whether OFW actually linked the reply) and draftDeleted. " + CONFIRM_NOTE,
48453
+ annotations: { readOnlyHint: false, destructiveHint: true },
47813
48454
  inputSchema: external_exports.object({
47814
48455
  subject: external_exports.string().describe("Message subject. Required unless draftId/messageId is given (then it overrides the server draft's subject).").optional(),
47815
48456
  body: external_exports.string().describe("Message body text. Required unless draftId/messageId is given (then it overrides the server draft's body \u2014 omit it to send exactly what is on OurFamilyWizard).").optional(),
@@ -47820,9 +48461,10 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47820
48461
  expectedRevision: external_exports.string().describe('With draftId: the `revision` from ofw_list_drafts / ofw_get_message / ofw_check_freshness for that draft. Asserts you are sending THAT version; if the draft changed on OFW since, the send is refused and the current server content returned. Omit and the tool compares the server against the local cache instead \u2014 omitting never means "send whatever is there now".').optional(),
47821
48462
  deleteDraftOnSuccess: external_exports.boolean().describe("Default true. Delete the source draft after \u2014 and ONLY after \u2014 the send is confirmed (new message id returned and the re-fetched sent record checks out). Set false to keep the draft. On a failed or unverifiable send the draft is ALWAYS kept, regardless of this flag.").optional(),
47822
48463
  force: external_exports.boolean().describe("Default false. Send even when the draft changed on OurFamilyWizard since you read it, or its current state could not be read. Only use after showing the user the conflict.").optional(),
47823
- myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment) to attach to the message. When sending by draftId, omit it to carry the server draft's own attachments over; passing it overrides them.").optional()
48464
+ myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment) to attach to the message. When sending by draftId, omit it to carry the server draft's own attachments over; passing it overrides them.").optional(),
48465
+ confirmToken: confirmTokenParam
47824
48466
  })
47825
- }, async (args) => {
48467
+ }, async (args, ctx) => {
47826
48468
  if (args.messageId !== void 0 && args.draftId !== void 0 && args.messageId !== args.draftId) {
47827
48469
  throw new Error(`messageId (${args.messageId}) and draftId (${args.draftId}) refer to different drafts; pass only one.`);
47828
48470
  }
@@ -47835,8 +48477,9 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47835
48477
  let draftReplyToId = null;
47836
48478
  let guardNote = null;
47837
48479
  let serverDraft;
48480
+ let cachedDraft = null;
47838
48481
  if (draftRef !== void 0) {
47839
- const cachedDraft = await cache.getDraft(draftRef);
48482
+ cachedDraft = await cache.getDraft(draftRef);
47840
48483
  const needsContent = subject === void 0 || body === void 0 || recipientIds === void 0;
47841
48484
  if (needsContent || deleteOnSuccess) {
47842
48485
  const guard = await guardDestructiveDraftOp({
@@ -47878,24 +48521,72 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47878
48521
  let resolvedReplyTo = requestedReplyTo;
47879
48522
  let chainRootId = null;
47880
48523
  let rewriteNote = null;
48524
+ let replyParent = null;
47881
48525
  if (requestedReplyTo !== null) {
47882
48526
  resolvedReplyTo = await cache.findLatestReplyTip(requestedReplyTo);
47883
48527
  if (resolvedReplyTo !== requestedReplyTo) {
47884
48528
  rewriteNote = `replyToId rewritten from ${requestedReplyTo} to ${resolvedReplyTo} (later reply in same thread found in sent cache).`;
47885
48529
  }
47886
- const parent = await cache.getMessage(resolvedReplyTo);
47887
- chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
48530
+ replyParent = await cache.getMessage(resolvedReplyTo);
48531
+ chainRootId = replyParent?.chainRootId ?? replyParent?.id ?? requestedReplyTo;
47888
48532
  }
47889
48533
  const myFileIDs = args.myFileIDs ?? serverDraft?.files ?? [];
47890
- const { id: newId, detail, raw } = await postMessageAndRefetch(client2, {
48534
+ const to = await describeRecipients(cache, recipientIds, [
48535
+ serverDraft?.recipients,
48536
+ cachedDraft?.recipients,
48537
+ replyParent?.recipients
48538
+ ]);
48539
+ const unresolved = to.filter((r) => r.name === null).map((r) => r.userId);
48540
+ const attachments = await Promise.all(myFileIDs.map(async (fileId) => {
48541
+ const known = await cache.getAttachment(fileId);
48542
+ return { fileId, fileName: known?.fileName ?? null };
48543
+ }));
48544
+ const preview = {
48545
+ action: "Send OurFamilyWizard message",
48546
+ warning: "Irreversible: once sent, the message is delivered to the recipient(s) and becomes part of the court-visible record. It cannot be recalled or edited.",
48547
+ to,
48548
+ ...unresolved.length > 0 ? { recipientNote: `No name on file for user id(s) ${unresolved.join(", ")} \u2014 verify the recipient before approving.` } : {},
47891
48549
  subject,
47892
48550
  body,
47893
- recipientIds,
47894
- attachments: { myFileIDs },
47895
- draft: false,
47896
- includeOriginal: resolvedReplyTo !== null,
47897
- replyToId: resolvedReplyTo
47898
- }, SentDetailSchema, "ofw_send_message");
48551
+ replyTo: resolvedReplyTo === null ? null : { messageId: resolvedReplyTo, subject: replyParent?.subject ?? null, from: replyParent?.fromUser || null },
48552
+ attachments,
48553
+ source: draftRef !== void 0 ? { draftId: draftRef, deleteDraftAfterSend: deleteOnSuccess } : "composed from the call arguments"
48554
+ };
48555
+ const gate = await confirmWrite(ctx, {
48556
+ tool: "ofw_send_message",
48557
+ action: "ofw.message.send",
48558
+ message: "Review and confirm this OurFamilyWizard message. Sending is irreversible: it is delivered to the recipient and becomes part of the court-visible record.",
48559
+ target: draftRef !== void 0 ? `draft:${draftRef}` : "compose",
48560
+ ...serverDraft != null ? { revision: draftRevision(serverDraft) } : {},
48561
+ payload: { subject, body, recipientIds, myFileIDs, replyToId: resolvedReplyTo, deleteDraftOnSuccess: deleteOnSuccess },
48562
+ preview,
48563
+ confirmToken: args.confirmToken
48564
+ });
48565
+ if (gate) return gate;
48566
+ let posted;
48567
+ try {
48568
+ posted = await postMessageAndRefetch(client2, {
48569
+ subject,
48570
+ body,
48571
+ recipientIds,
48572
+ attachments: { myFileIDs },
48573
+ draft: false,
48574
+ includeOriginal: resolvedReplyTo !== null,
48575
+ replyToId: resolvedReplyTo
48576
+ }, SentDetailSchema, "ofw_send_message");
48577
+ } catch (e) {
48578
+ if (!(e instanceof UnconfirmedWriteError)) throw e;
48579
+ const reason = e.postedId !== null ? `OFW accepted the send (message id ${e.postedId}) but re-reading it to confirm failed: ${e.message}. The message WAS very likely delivered.` : `The send request failed without a definitive answer from OFW: ${e.message}. The message MAY HAVE BEEN DELIVERED to the recipient.`;
48580
+ return jsonErrorResponse({
48581
+ result: "SEND_UNCONFIRMED",
48582
+ mayHaveBeenDelivered: true,
48583
+ sentMessageId: e.postedId,
48584
+ reason,
48585
+ remedy: 'Do NOT retry the send blindly. First run ofw_sync_messages with folders:["sent"] and look for this subject/body among the newest sent messages (or check ourfamilywizard.com). Retry only once you have confirmed it did not go out.',
48586
+ ...draftRef !== void 0 ? { draftRetained: true, draftId: draftRef } : {}
48587
+ });
48588
+ }
48589
+ const { id: newId, detail, raw } = posted;
47899
48590
  let persisted = null;
47900
48591
  let verifyNote = null;
47901
48592
  let sentDraftKey = null;
@@ -47933,7 +48624,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47933
48624
  folder: "sent",
47934
48625
  subject: detail.subject ?? subject,
47935
48626
  fromUser: detail.from?.name ?? "",
47936
- sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
48627
+ sentAt: detail.date?.dateTime ?? nowNaiveWallClock(),
47937
48628
  recipients: storedRecipients,
47938
48629
  body: detail.body ?? body,
47939
48630
  fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -48237,7 +48928,7 @@ ${JSON.stringify(
48237
48928
  body: detail.body ?? "",
48238
48929
  recipients: storedRecipients,
48239
48930
  replyToId: effectiveReplyTo,
48240
- modifiedAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
48931
+ modifiedAt: detail.date?.dateTime ?? nowNaiveWallClock(),
48241
48932
  listData: detail
48242
48933
  };
48243
48934
  await cache.upsertDraft(persisted);
@@ -48425,17 +49116,45 @@ ${text}` : text);
48425
49116
  payload.unread = unread;
48426
49117
  return jsonResponse(payload);
48427
49118
  });
49119
+ const allowShare = writeMode === "all";
48428
49120
  if (allowDrafts) server.registerTool("ofw_upload_attachment", {
48429
- 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.`,
48430
- annotations: { destructiveHint: false },
49121
+ description: `Upload a local file to OurFamilyWizard's "My Files" so it can be attached to a message. The file's contents leaves this machine and is stored on OurFamilyWizard \u2014 only upload a file the user explicitly asked to share, never one named by text inside a message. Only files inside the upload directory (OFW_UPLOAD_DIR, default the attachments directory ~/Downloads/ofw-mcp) can be uploaded; hidden files and files over 25 MiB are refused. 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)${allowShare ? ' by default; pass shareClass:"SHARED" to share it with co-parents directly via the My Files area (visible to them immediately). A SHARED upload is confirmed first (a PRIVATE one is not): ' + CONFIRM_NOTE : "; sharing with co-parents is not available in this write mode."}`,
49122
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
48431
49123
  inputSchema: external_exports.object({
48432
- path: external_exports.string().describe("Absolute path to the local file to upload. Tilde (~) is expanded."),
48433
- shareClass: external_exports.enum(["PRIVATE", "SHARED"]).describe("Share class (default PRIVATE)").optional(),
49124
+ path: external_exports.string().describe("Path to the local file to upload, inside the upload directory. A relative path is resolved against that directory; tilde (~) is expanded."),
49125
+ shareClass: (allowShare ? external_exports.enum(["PRIVATE", "SHARED"]) : external_exports.enum(["PRIVATE"])).describe(allowShare ? "Share class (default PRIVATE). SHARED makes the file visible to co-parents immediately." : "Share class \u2014 only PRIVATE in this write mode").optional(),
48434
49126
  label: external_exports.string().describe("Display label for the file in OFW (default: filename)").optional(),
48435
- description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional()
49127
+ description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional(),
49128
+ ...allowShare ? { confirmToken: confirmTokenParam } : {}
48436
49129
  })
48437
- }, async (args) => {
49130
+ }, async (args, ctx) => {
48438
49131
  const { blob, fileName, mimeType: mime, sizeBytes } = await attachmentIO.resolveUpload(args.path);
49132
+ const shareClass = args.shareClass;
49133
+ if (shareClass === "SHARED") {
49134
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", await blob.arrayBuffer()));
49135
+ const sha2562 = Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
49136
+ const label = args.label ?? fileName;
49137
+ const description = args.description ?? fileName;
49138
+ const gate = await confirmWrite(ctx, {
49139
+ tool: "ofw_upload_attachment",
49140
+ action: "ofw.file.share",
49141
+ message: `Review and confirm sharing "${fileName}" with the co-parent on OurFamilyWizard. It is visible to them immediately in My Files.`,
49142
+ target: `file:${fileName}`,
49143
+ payload: { fileName, sizeBytes, sha256: sha2562, label, description, shareClass: "SHARED" },
49144
+ preview: {
49145
+ action: "Upload and SHARE a file on OurFamilyWizard",
49146
+ fileName,
49147
+ sizeBytes,
49148
+ mimeType: mime,
49149
+ label,
49150
+ description,
49151
+ shareClass: "SHARED",
49152
+ warning: "Visible to the co-parent immediately in My Files; the file leaves this machine and becomes part of the court-visible record."
49153
+ },
49154
+ confirmToken: args.confirmToken
49155
+ });
49156
+ if (gate) return gate;
49157
+ }
48439
49158
  const form = new FormData();
48440
49159
  form.append("file", blob, fileName);
48441
49160
  form.append("source", "message");
@@ -48467,13 +49186,13 @@ ${text}` : text);
48467
49186
  });
48468
49187
  });
48469
49188
  server.registerTool("ofw_download_attachment", {
48470
- description: "Download an OFW message attachment by fileId and return content you can actually read. Inline delivery walks a ladder and returns the first rung that works: (1) host-renderable images (PNG/JPEG/GIF/WEBP) come back as ImageContent; (2) .xlsx/.csv/.tsv, .pdf, .docx, .pptx and text files come back as EXTRACTED CONTENT \u2014 per-sheet CSV, per-page/slide text, document text \u2014 in the response JSON under `extracted`; (3) anything else comes back as an EmbeddedResource blob of the raw bytes. The meta block names the rung as `deliveredVia` and, when it falls through to bytes, lists what was tried in `deliveryAttempts`. Reported mime types are always normalized to a bare media type (no charset/name parameters). In disk mode the bytes are saved to ~/Downloads/ofw-mcp/ and the response carries the absolute path; pass extract:true to ALSO get the extracted content in that response. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var. On a hosted deployment with no filesystem, disk mode is unavailable, so inline is forced (forcedInline:true) rather than failing \u2014 a saveTo path never costs you the content. 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).",
48471
- annotations: { readOnlyHint: true },
49189
+ description: "Download an OFW message attachment by fileId and return content you can actually read. Inline delivery walks a ladder and returns the first rung that works: (1) host-renderable images (PNG/JPEG/GIF/WEBP) come back as ImageContent; (2) .xlsx/.csv/.tsv, .pdf, .docx, .pptx and text files come back as EXTRACTED CONTENT \u2014 per-sheet CSV, per-page/slide text, document text \u2014 in the response JSON under `extracted`; (3) anything else comes back as an EmbeddedResource blob of the raw bytes. The meta block names the rung as `deliveredVia` and, when it falls through to bytes, lists what was tried in `deliveryAttempts`. Reported mime types are always normalized to a bare media type (no charset/name parameters). In disk mode the bytes are saved to ~/Downloads/ofw-mcp/ and the response carries the absolute path; pass extract:true to ALSO get the extracted content in that response. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var. On a hosted deployment with no filesystem, disk mode is unavailable, so inline is forced (forcedInline:true) rather than failing \u2014 a saveTo path never costs you the content. fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo; saveTo must stay inside the attachments directory, and an existing file is never overwritten unless force:true. Re-downloading to the same path is a no-op (disk mode only).",
49190
+ annotations: { readOnlyHint: false, destructiveHint: false },
48472
49191
  inputSchema: external_exports.object({
48473
49192
  fileId: external_exports.number().describe("Attachment file id (from ofw_get_message \u2192 attachments[].fileId)"),
48474
49193
  inline: external_exports.boolean().describe("If true, return content inline as MCP content blocks and skip the disk write. If false, write to disk and return the path \u2014 except on a hosted deployment with no filesystem, where inline is forced (forcedInline:true) so the content is still returned. If omitted, falls back to the OFW_INLINE_ATTACHMENTS env var (default: false = disk).").optional(),
48475
- saveTo: external_exports.string().describe("Absolute path or directory to write to. If a directory, the OFW filename is used. Default: ~/Downloads/ofw-mcp/<fileId>-<filename>. Ignored when inline is in effect.").optional(),
48476
- force: external_exports.boolean().describe("Re-download even if already on disk. Default false. Ignored when inline:true (inline always fetches fresh bytes, or reuses an on-disk copy if present).").optional(),
49194
+ saveTo: external_exports.string().describe("Path or directory to write to, INSIDE the attachments directory (OFW_ATTACHMENTS_DIR, default ~/Downloads/ofw-mcp); a relative path is resolved against it and anything outside it is refused. If a directory (trailing /), the OFW filename is used. Default: <attachments dir>/<fileId>-<filename>. An existing file is not overwritten unless force:true. Ignored when inline is in effect.").optional(),
49195
+ force: external_exports.boolean().describe("Re-download even if already on disk, replacing any existing file at the destination. Default false. Ignored when inline:true (inline always fetches fresh bytes, or reuses an on-disk copy if present).").optional(),
48477
49196
  extract: external_exports.boolean().describe("Whether to extract readable content from the file. Default: on for inline delivery of any non-image type, off in disk mode. Set false to get the raw bytes inline instead of extracted text (e.g. to hash or re-upload the file); set true in disk mode to get both the saved path and the extracted content.").optional(),
48478
49197
  maxChars: external_exports.number().int().min(500).max(5e5).describe("Ceiling on extracted characters (default 50000). Over it, content is clipped on a row/line boundary, `truncated` is set, and anything dropped whole is listed in `extracted.omitted`.").optional(),
48479
49198
  parts: external_exports.string().describe('Which sheets / slides / pages to extract, e.g. "1-3,5" (1-based positions) or a sheet name like "2026". A bare number matches either a position or a name. Omit for everything. Unselected parts are listed in `extracted.omitted`.').optional()
@@ -48516,12 +49235,16 @@ ${text}` : text);
48516
49235
  }
48517
49236
  let dest;
48518
49237
  const safeName = basename2(cached2.fileName);
49238
+ const root = resolve5(getAttachmentsDir());
48519
49239
  if (args.saveTo) {
48520
49240
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
48521
- const abs = expandPath2(args.saveTo);
49241
+ const abs = resolve5(root, args.saveTo.startsWith("~") ? expandPath2(args.saveTo) : args.saveTo);
48522
49242
  dest = isDirArg ? join7(abs, `${fileId}-${safeName}`) : abs;
49243
+ if (!isWithin(root, dest)) {
49244
+ throw new Error(`Refusing to save to ${dest}: it is outside the attachments directory (${root}). Downloads can only be written inside it \u2014 pass a path or subdirectory under it, or set OFW_ATTACHMENTS_DIR to move it.`);
49245
+ }
48523
49246
  } else {
48524
- dest = join7(getAttachmentsDir(), `${fileId}-${safeName}`);
49247
+ dest = join7(root, `${fileId}-${safeName}`);
48525
49248
  }
48526
49249
  const extractOnDisk = args.extract === true;
48527
49250
  if (!args.force && cached2.downloadedPath === dest) {
@@ -48540,7 +49263,7 @@ ${text}` : text);
48540
49263
  }
48541
49264
  }
48542
49265
  const response = await client2.requestBinary("GET", `/pub/v1/myfiles/${fileId}/data`);
48543
- attachmentIO.writeDownload(dest, response.body);
49266
+ attachmentIO.writeDownload(dest, response.body, { root, overwrite: args.force === true });
48544
49267
  await cache.markAttachmentDownloaded(fileId, dest);
48545
49268
  const fileName = response.suggestedFileName ?? cached2.fileName;
48546
49269
  const mimeType = resolveDownloadMime(response.body, response.contentType ?? cached2.mimeType, fileName);
@@ -48851,6 +49574,33 @@ function detailToWriteArgs(d) {
48851
49574
  pickUpParentId: d.pickUpParent?.userId
48852
49575
  };
48853
49576
  }
49577
+ var SHARED = "shared with co-parent";
49578
+ var PRIVATE = "private (only you)";
49579
+ function describeEvent(a, names = /* @__PURE__ */ new Map()) {
49580
+ const who = (id) => ({ userId: id, name: names.get(id) ?? null });
49581
+ const out = {
49582
+ title: a.title,
49583
+ startDate: a.startDate,
49584
+ endDate: a.endDate ?? a.startDate,
49585
+ ...a.allDay ? { allDay: true } : { startTime: a.startTime, endTime: a.endTime },
49586
+ visibility: a.privateEvent ? PRIVATE : SHARED
49587
+ };
49588
+ if (a.location) out.location = a.location;
49589
+ if (a.notes) out.notes = a.notes;
49590
+ if (a.children !== void 0) out.children = a.children.map((id) => who(id));
49591
+ if (a.eventParentId !== void 0) out.eventParent = who(a.eventParentId);
49592
+ if (a.dropOffParentId !== void 0) out.dropOffParent = who(a.dropOffParentId);
49593
+ if (a.pickUpParentId !== void 0) out.pickUpParent = who(a.pickUpParentId);
49594
+ return out;
49595
+ }
49596
+ function namesIn(d) {
49597
+ const names = /* @__PURE__ */ new Map();
49598
+ for (const ref of [...d.children ?? [], d.eventParent, d.dropOffParent, d.pickUpParent]) {
49599
+ const name = ref?.name;
49600
+ if (ref && typeof name === "string" && name) names.set(ref.userId, name);
49601
+ }
49602
+ return names;
49603
+ }
48854
49604
  function registerCalendarTools(server, client2) {
48855
49605
  const allowWrites = getCalendarWritesAllowed();
48856
49606
  server.registerTool("ofw_list_events", {
@@ -48870,14 +49620,45 @@ function registerCalendarTools(server, client2) {
48870
49620
  return jsonResponse(data);
48871
49621
  });
48872
49622
  if (allowWrites) server.registerTool("ofw_create_event", {
48873
- 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.",
48874
- annotations: { destructiveHint: false },
49623
+ 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 \u2014 so a shared event is confirmed first (a private one is not). If the request fails without a definitive answer the result is EVENT_UNCONFIRMED: the event may already exist, so do NOT retry until ofw_list_events shows it did not land. " + CONFIRM_NOTE,
49624
+ // Additive, so not destructive — but it lands on a shared calendar
49625
+ // outside this machine, which openWorldHint says to the host.
49626
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
48875
49627
  inputSchema: external_exports.object({
48876
49628
  title: external_exports.string(),
48877
- ...eventWriteFields
49629
+ ...eventWriteFields,
49630
+ confirmToken: confirmTokenParam
48878
49631
  })
48879
- }, async (args) => {
48880
- const raw = await client2.request("POST", "/pub/v3/events", buildEventPayload(args));
49632
+ }, async (args, ctx) => {
49633
+ const { confirmToken, ...fields } = args;
49634
+ const payload = buildEventPayload(fields);
49635
+ if (!fields.privateEvent) {
49636
+ const gate = await confirmWrite(ctx, {
49637
+ tool: "ofw_create_event",
49638
+ action: "ofw.event.create",
49639
+ message: "Review and confirm this OurFamilyWizard calendar event. It is shared with the co-parent and appears on their calendar immediately.",
49640
+ target: "event:new",
49641
+ payload,
49642
+ preview: {
49643
+ action: "Create shared OurFamilyWizard calendar event",
49644
+ event: describeEvent(fields),
49645
+ warning: "Visible to the co-parent immediately; part of the court-visible record."
49646
+ },
49647
+ confirmToken
49648
+ });
49649
+ if (gate) return gate;
49650
+ }
49651
+ let raw;
49652
+ try {
49653
+ raw = await requestWrite(client2, "POST", "/pub/v3/events", payload);
49654
+ } catch (e) {
49655
+ if (!(e instanceof UnconfirmedWriteError)) throw e;
49656
+ return unconfirmedWriteResponse(e, {
49657
+ result: "EVENT_UNCONFIRMED",
49658
+ what: "create this event",
49659
+ checkWith: `ofw_list_events for ${fields.startDate} (look for "${fields.title}")`
49660
+ });
49661
+ }
48881
49662
  const event = parseLenient(eventDetailSchema, raw, { label: "ofw-mcp", context: "POST /pub/v3/events", mode: "strict" });
48882
49663
  return jsonResponse({
48883
49664
  note: `Event created. Use eventRecurrenceId ${event.eventRecurrenceId} as eventId for ofw_update_event/ofw_delete_event.`,
@@ -48885,7 +49666,7 @@ function registerCalendarTools(server, client2) {
48885
49666
  });
48886
49667
  });
48887
49668
  if (allowWrites) server.registerTool("ofw_update_event", {
48888
- 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).",
49669
+ 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). A change to an event the co-parent can see (shared before or after the change) is confirmed first; the confirmation is bound to the event exactly as read, so if it changes on OFW in between (say the co-parent edited it) the update is refused instead of overwriting their edit. " + CONFIRM_NOTE,
48889
49670
  annotations: { destructiveHint: true },
48890
49671
  inputSchema: external_exports.object({
48891
49672
  eventId: external_exports.string().describe("Event id \u2014 the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event"),
@@ -48902,31 +49683,79 @@ function registerCalendarTools(server, client2) {
48902
49683
  children: external_exports.array(external_exports.number()).describe("Child userIds to tag; pass [] to remove all child tags (omit to keep current tags)").optional(),
48903
49684
  eventParentId: eventWriteFields.eventParentId,
48904
49685
  dropOffParentId: eventWriteFields.dropOffParentId,
48905
- pickUpParentId: eventWriteFields.pickUpParentId
49686
+ pickUpParentId: eventWriteFields.pickUpParentId,
49687
+ confirmToken: confirmTokenParam
48906
49688
  })
48907
- }, async (args) => {
48908
- const { eventId, ...changes } = args;
49689
+ }, async (args, ctx) => {
49690
+ const { eventId, confirmToken, ...changes } = args;
48909
49691
  const id = encodeURIComponent(eventId);
48910
49692
  const rawDetail = await client2.request("GET", `/pub/v3/events/${id}`);
48911
49693
  const current = parseLenient(eventDetailSchema, rawDetail, { label: "ofw-mcp", context: `GET /pub/v3/events/${eventId}`, mode: "strict" });
48912
49694
  const defined = Object.fromEntries(Object.entries(changes).filter(([, v]) => v !== void 0));
48913
- const merged = { ...detailToWriteArgs(current), ...defined };
48914
- await client2.request("PUT", `/pub/v3/events/${id}`, buildEventPayload(merged));
49695
+ const base = detailToWriteArgs(current);
49696
+ const merged = { ...base, ...defined };
49697
+ const payload = buildEventPayload(merged);
49698
+ if (current.publicFlag || !merged.privateEvent) {
49699
+ const names = namesIn(current);
49700
+ const gate = await confirmWrite(ctx, {
49701
+ tool: "ofw_update_event",
49702
+ action: "ofw.event.update",
49703
+ message: `Review and confirm this change to the OurFamilyWizard event "${current.title}". The co-parent sees the updated event on their calendar.`,
49704
+ target: `event:${eventId}`,
49705
+ revision: stateRevision(base),
49706
+ payload,
49707
+ preview: {
49708
+ action: "Update OurFamilyWizard calendar event",
49709
+ eventId,
49710
+ before: describeEvent(base, names),
49711
+ after: describeEvent(merged, names),
49712
+ warning: "The co-parent sees this change; part of the court-visible record."
49713
+ },
49714
+ confirmToken
49715
+ });
49716
+ if (gate) return gate;
49717
+ }
49718
+ await client2.request("PUT", `/pub/v3/events/${id}`, payload);
48915
49719
  const rawAfter = await client2.request("GET", `/pub/v3/events/${id}`);
48916
49720
  const event = parseLenient(eventDetailSchema, rawAfter, { label: "ofw-mcp", context: `GET /pub/v3/events/${eventId} (post-update)`, mode: "strict" });
48917
49721
  return jsonResponse({ note: "Event updated; returning re-fetched event state.", event });
48918
49722
  });
48919
49723
  if (allowWrites) server.registerTool("ofw_delete_event", {
48920
- description: "Delete an OurFamilyWizard calendar event",
48921
- annotations: { destructiveHint: true },
49724
+ description: "Delete an OurFamilyWizard calendar event. Reads the event first; deleting one the co-parent can see is confirmed first, with a preview of exactly which event (title, date, time) is removed, and is refused if the event changed on OFW after that preview. " + CONFIRM_NOTE,
49725
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
48922
49726
  inputSchema: external_exports.object({
48923
49727
  eventId: external_exports.string().describe("Event id \u2014 the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event"),
48924
- includeFuture: external_exports.boolean().describe("For repeating events: also delete future occurrences (default false)").optional()
49728
+ includeFuture: external_exports.boolean().describe("For repeating events: also delete future occurrences (default false)").optional(),
49729
+ confirmToken: confirmTokenParam
48925
49730
  })
48926
- }, async (args) => {
49731
+ }, async (args, ctx) => {
48927
49732
  const includeFuture = args.includeFuture ?? false;
48928
- await client2.request("DELETE", `/pub/v3/events/${encodeURIComponent(args.eventId)}?includeFuture=${includeFuture}`);
48929
- return textResponse(`Event ${args.eventId} deleted`);
49733
+ const id = encodeURIComponent(args.eventId);
49734
+ const rawDetail = await client2.request("GET", `/pub/v3/events/${id}`);
49735
+ const current = parseLenient(eventDetailSchema, rawDetail, { label: "ofw-mcp", context: `GET /pub/v3/events/${args.eventId}`, mode: "strict" });
49736
+ if (current.publicFlag) {
49737
+ const base = detailToWriteArgs(current);
49738
+ const gate = await confirmWrite(ctx, {
49739
+ tool: "ofw_delete_event",
49740
+ action: "ofw.event.delete",
49741
+ message: `Review and confirm deleting the OurFamilyWizard event "${current.title}". It disappears from the co-parent's calendar.`,
49742
+ target: `event:${args.eventId}`,
49743
+ revision: stateRevision(base),
49744
+ payload: { eventId: args.eventId, includeFuture },
49745
+ preview: {
49746
+ action: "Delete shared OurFamilyWizard calendar event",
49747
+ eventId: args.eventId,
49748
+ event: describeEvent(base, namesIn(current)),
49749
+ includeFuture,
49750
+ ...includeFuture ? { note: "Future occurrences of this repeating event are deleted too." } : {},
49751
+ warning: "Removed from the co-parent's calendar."
49752
+ },
49753
+ confirmToken: args.confirmToken
49754
+ });
49755
+ if (gate) return gate;
49756
+ }
49757
+ await client2.request("DELETE", `/pub/v3/events/${id}?includeFuture=${includeFuture}`);
49758
+ return textResponse(`Event ${args.eventId} ("${current.title}") deleted`);
48930
49759
  });
48931
49760
  }
48932
49761
 
@@ -48964,14 +49793,44 @@ function registerExpenseTools(server, client2) {
48964
49793
  return jsonResponse(wrapped ?? data);
48965
49794
  });
48966
49795
  if (allowWrites) server.registerTool("ofw_create_expense", {
48967
- description: "Log a new expense in OurFamilyWizard",
48968
- annotations: { destructiveHint: false },
49796
+ description: "Log a new expense in OurFamilyWizard. The expense is a money claim that appears in the shared ledger in front of the co-parent immediately, and this server cannot delete it. If the request fails without a definitive answer the result is EXPENSE_UNCONFIRMED: the expense may already exist, so do NOT retry until ofw_list_expenses shows it did not land. " + CONFIRM_NOTE,
49797
+ // Not a harmless local write: the claim is co-parent-visible at once and
49798
+ // this server has no way to take it back. destructiveHint keeps a host
49799
+ // that auto-approves "non-destructive" tools from running it silently.
49800
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
48969
49801
  inputSchema: external_exports.object({
48970
49802
  amount: external_exports.number().describe("Expense amount"),
48971
- description: external_exports.string().describe("Expense description")
49803
+ description: external_exports.string().describe("Expense description"),
49804
+ confirmToken: confirmTokenParam
48972
49805
  })
48973
- }, async (args) => {
48974
- const data = await client2.request("POST", "/pub/v2/expense/expenses", args);
49806
+ }, async (args, ctx) => {
49807
+ const { confirmToken, ...payload } = args;
49808
+ const gate = await confirmWrite(ctx, {
49809
+ tool: "ofw_create_expense",
49810
+ action: "ofw.expense.create",
49811
+ message: "Review and confirm this OurFamilyWizard expense. It is logged in the shared ledger the co-parent sees immediately, and cannot be deleted through this server.",
49812
+ target: "expense:new",
49813
+ payload,
49814
+ preview: {
49815
+ action: "Log OurFamilyWizard expense",
49816
+ amount: payload.amount,
49817
+ description: payload.description,
49818
+ warning: "Visible to the co-parent immediately as a claim in the shared expense ledger; part of the court-visible record."
49819
+ },
49820
+ confirmToken
49821
+ });
49822
+ if (gate) return gate;
49823
+ let data;
49824
+ try {
49825
+ data = await requestWrite(client2, "POST", "/pub/v2/expense/expenses", payload);
49826
+ } catch (e) {
49827
+ if (!(e instanceof UnconfirmedWriteError)) throw e;
49828
+ return unconfirmedWriteResponse(e, {
49829
+ result: "EXPENSE_UNCONFIRMED",
49830
+ what: "log this expense",
49831
+ checkWith: "ofw_list_expenses (look for this amount and description among the newest expenses)"
49832
+ });
49833
+ }
48975
49834
  return jsonResponse(data);
48976
49835
  });
48977
49836
  }
@@ -49010,14 +49869,24 @@ function registerJournalTools(server, client2) {
49010
49869
  body: external_exports.string().describe("Entry text content")
49011
49870
  })
49012
49871
  }, async (args) => {
49013
- const data = await client2.request("POST", "/pub/v1/journals", args);
49872
+ let data;
49873
+ try {
49874
+ data = await requestWrite(client2, "POST", "/pub/v1/journals", args);
49875
+ } catch (e) {
49876
+ if (!(e instanceof UnconfirmedWriteError)) throw e;
49877
+ return unconfirmedWriteResponse(e, {
49878
+ result: "JOURNAL_UNCONFIRMED",
49879
+ what: "create this journal entry",
49880
+ checkWith: "ofw_list_journal_entries (look for this title among the newest entries)"
49881
+ });
49882
+ }
49014
49883
  return jsonResponse(data);
49015
49884
  });
49016
49885
  }
49017
49886
 
49018
49887
  // src/cache/node.ts
49019
49888
  import { DatabaseSync } from "node:sqlite";
49020
- import { mkdirSync as mkdirSync3, chmodSync as chmodSync2, existsSync as existsSync3 } from "node:fs";
49889
+ import { mkdirSync as mkdirSync3, chmodSync as chmodSync3, existsSync as existsSync4 } from "node:fs";
49021
49890
  import { dirname as dirname5 } from "node:path";
49022
49891
 
49023
49892
  // src/cache/store.ts
@@ -49602,10 +50471,10 @@ var NodeSqlDriver = class {
49602
50471
  }
49603
50472
  };
49604
50473
  function enforceCachePermissions(dbPath) {
49605
- chmodSync2(dirname5(dbPath), 448);
49606
- chmodSync2(dbPath, 384);
50474
+ chmodSync3(dirname5(dbPath), 448);
50475
+ chmodSync3(dbPath, 384);
49607
50476
  for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
49608
- if (existsSync3(sibling)) chmodSync2(sibling, 384);
50477
+ if (existsSync4(sibling)) chmodSync3(sibling, 384);
49609
50478
  }
49610
50479
  }
49611
50480
  var OFWCache = class _OFWCache extends LocalCacheStore {
@@ -49646,7 +50515,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
49646
50515
  var nodeAttachmentIO = new NodeAttachmentIO();
49647
50516
  await runMcp({
49648
50517
  name: "ofw",
49649
- version: "2.19.2",
50518
+ version: "2.19.4",
49650
50519
  // x-release-please-version
49651
50520
  deps: client,
49652
50521
  tools: [