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