ofw-mcp 2.4.4 → 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/bundle.js +294 -44
- package/dist/config.js +21 -0
- package/dist/index.js +1 -1
- package/dist/sync.js +18 -2
- package/dist/tools/_shared.js +23 -6
- package/dist/tools/calendar.js +145 -29
- package/dist/tools/messages.js +29 -3
- package/package.json +3 -3
- package/server.json +8 -2
- package/skills/ofw-fpx/SKILL.md +106 -0
- package/skills/ofw-fpx/references/requests.md +252 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "OurFamilyWizard tools for Claude Code",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.5.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"displayName": "OurFamilyWizard",
|
|
15
15
|
"source": "./",
|
|
16
16
|
"description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
|
|
17
|
-
"version": "2.
|
|
17
|
+
"version": "2.5.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/README.md
CHANGED
|
@@ -144,9 +144,9 @@ Read-only tools run automatically. Write tools ask for your confirmation first.
|
|
|
144
144
|
| `ofw_delete_draft` | Delete a draft | Confirm | `drafts` |
|
|
145
145
|
| `ofw_upload_attachment` | Upload a local file to My Files; returns a fileId to attach via `ofw_send_message`/`ofw_save_draft` | Auto | `drafts` |
|
|
146
146
|
| `ofw_list_events` | Calendar events in a date range | Auto | any |
|
|
147
|
-
| `ofw_create_event` | Create a calendar event | Confirm | `all` |
|
|
148
|
-
| `ofw_update_event` | Update a calendar event | Confirm | `all` |
|
|
149
|
-
| `ofw_delete_event` | Delete a calendar event | Confirm | `all` |
|
|
147
|
+
| `ofw_create_event` | Create a calendar event | Confirm | `all` (or `drafts` + `OFW_CALENDAR_WRITES`) |
|
|
148
|
+
| `ofw_update_event` | Update a calendar event | Confirm | `all` (or `drafts` + `OFW_CALENDAR_WRITES`) |
|
|
149
|
+
| `ofw_delete_event` | Delete a calendar event | Confirm | `all` (or `drafts` + `OFW_CALENDAR_WRITES`) |
|
|
150
150
|
| `ofw_get_expense_totals` | Expense summary totals | Auto | any |
|
|
151
151
|
| `ofw_list_expenses` | Expense history | Auto | any |
|
|
152
152
|
| `ofw_create_expense` | Log a new expense | Confirm | `all` |
|
|
@@ -165,6 +165,10 @@ The "Confirm" permission above is a *hint* to the MCP host — a host configured
|
|
|
165
165
|
|
|
166
166
|
Unrecognized values fail closed to `none`, with a warning on stderr — a typo never silently grants write access.
|
|
167
167
|
|
|
168
|
+
#### Calendar opt-in (`OFW_CALENDAR_WRITES`)
|
|
169
|
+
|
|
170
|
+
Calendar events sit between the two message tiers: they have no draft stage (a created event is immediately visible on the shared record), but unlike a sent message they are reversible — an event can be edited or deleted afterward. If you run in `drafts` mode but are comfortable with direct calendar writes, set `OFW_CALENDAR_WRITES=true` to additionally register `ofw_create_event`, `ofw_update_event`, and `ofw_delete_event`. The flag is redundant in `all` mode and never overrides `none`.
|
|
171
|
+
|
|
168
172
|
## Troubleshooting
|
|
169
173
|
|
|
170
174
|
**"0 messages"** — Claude may have read the notification counts rather than the actual messages. Ask explicitly: *"List the messages in my OFW inbox"* or *"Use ofw_list_message_folders then ofw_list_messages"*.
|
package/dist/bundle.js
CHANGED
|
@@ -34931,6 +34931,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34931
34931
|
"capture_request_header",
|
|
34932
34932
|
"capture_redirect",
|
|
34933
34933
|
"read_indexed_db",
|
|
34934
|
+
"read_dom",
|
|
34934
34935
|
"download"
|
|
34935
34936
|
]);
|
|
34936
34937
|
|
|
@@ -35232,6 +35233,44 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35232
35233
|
}
|
|
35233
35234
|
}
|
|
35234
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
|
+
}
|
|
35235
35274
|
function validateFrame(raw) {
|
|
35236
35275
|
assertObject(raw, "frame");
|
|
35237
35276
|
const t = raw.type;
|
|
@@ -35307,6 +35346,9 @@ function validateHello(raw) {
|
|
|
35307
35346
|
if (raw.sessionStoragePointers !== void 0) {
|
|
35308
35347
|
assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
|
|
35309
35348
|
}
|
|
35349
|
+
if (raw.domSelectors !== void 0) {
|
|
35350
|
+
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35351
|
+
}
|
|
35310
35352
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35311
35353
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35312
35354
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35541,6 +35583,21 @@ function validateInnerRequest(raw) {
|
|
|
35541
35583
|
}
|
|
35542
35584
|
return raw;
|
|
35543
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
|
+
}
|
|
35544
35601
|
if (raw.op === "download") {
|
|
35545
35602
|
assertObject(raw.init, "inner.init");
|
|
35546
35603
|
if (raw.init.url === void 0) {
|
|
@@ -35566,7 +35623,7 @@ function validateInnerRequest(raw) {
|
|
|
35566
35623
|
}
|
|
35567
35624
|
return raw;
|
|
35568
35625
|
}
|
|
35569
|
-
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)}`);
|
|
35570
35627
|
}
|
|
35571
35628
|
function assertNonEmptyKeyArray(value, label) {
|
|
35572
35629
|
if (!Array.isArray(value)) {
|
|
@@ -35651,6 +35708,13 @@ function validateInnerResponse(raw) {
|
|
|
35651
35708
|
assertObject(raw.values, "inner.values");
|
|
35652
35709
|
return raw;
|
|
35653
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
|
+
}
|
|
35654
35718
|
if (op === "download") {
|
|
35655
35719
|
assertObject(raw.value, "inner.value");
|
|
35656
35720
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -35990,6 +36054,13 @@ async function buildServerHello(opts) {
|
|
|
35990
36054
|
jsonPointer: d.jsonPointer
|
|
35991
36055
|
}));
|
|
35992
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
|
+
}
|
|
35993
36064
|
return hello;
|
|
35994
36065
|
}
|
|
35995
36066
|
|
|
@@ -36079,7 +36150,8 @@ async function startHost(opts) {
|
|
|
36079
36150
|
captureHeaders: opts.ownCaptureHeaders,
|
|
36080
36151
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
36081
36152
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
36082
|
-
sessionStoragePointers: opts.ownSessionStoragePointers
|
|
36153
|
+
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36154
|
+
domSelectors: opts.ownDomSelectors
|
|
36083
36155
|
});
|
|
36084
36156
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
36085
36157
|
let extensionWs = null;
|
|
@@ -36314,6 +36386,7 @@ async function startPeer(opts) {
|
|
|
36314
36386
|
sessionStorageKeys: opts.sessionStorageKeys,
|
|
36315
36387
|
captureHeaders: opts.captureHeaders,
|
|
36316
36388
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36389
|
+
domSelectors: opts.domSelectors,
|
|
36317
36390
|
localStoragePointers: opts.localStoragePointers,
|
|
36318
36391
|
sessionStoragePointers: opts.sessionStoragePointers
|
|
36319
36392
|
});
|
|
@@ -36721,6 +36794,11 @@ var FetchproxyServer = class {
|
|
|
36721
36794
|
key: d.key,
|
|
36722
36795
|
jsonPointer: d.jsonPointer
|
|
36723
36796
|
})),
|
|
36797
|
+
domSelectors: (opts.domSelectors ?? []).map((d) => ({
|
|
36798
|
+
name: d.name,
|
|
36799
|
+
selector: d.selector,
|
|
36800
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36801
|
+
})),
|
|
36724
36802
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36725
36803
|
// adapter was about to set these to the same numbers anyway; the
|
|
36726
36804
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36841,6 +36919,7 @@ var FetchproxyServer = class {
|
|
|
36841
36919
|
ownIndexedDbScopes: this.opts.indexedDbScopes,
|
|
36842
36920
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36843
36921
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36922
|
+
ownDomSelectors: this.opts.domSelectors,
|
|
36844
36923
|
onPairCode: this.opts.onPairCode
|
|
36845
36924
|
});
|
|
36846
36925
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -36868,7 +36947,8 @@ var FetchproxyServer = class {
|
|
|
36868
36947
|
captureHeaders: this.opts.captureHeaders,
|
|
36869
36948
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
36870
36949
|
localStoragePointers: this.opts.localStoragePointers,
|
|
36871
|
-
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36950
|
+
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36951
|
+
domSelectors: this.opts.domSelectors
|
|
36872
36952
|
});
|
|
36873
36953
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36874
36954
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37843,6 +37923,46 @@ var FetchproxyServer = class {
|
|
|
37843
37923
|
await this.sendInnerFrame(inner);
|
|
37844
37924
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37845
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
|
+
}
|
|
37846
37966
|
assertScopeSubset(requested, declared, label) {
|
|
37847
37967
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37848
37968
|
if (undeclared.length > 0) {
|
|
@@ -37914,7 +38034,7 @@ var FetchproxyServer = class {
|
|
|
37914
38034
|
if (storageCb) {
|
|
37915
38035
|
this.pendingStorage.delete(inner.id);
|
|
37916
38036
|
if (inner.ok) {
|
|
37917
|
-
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) {
|
|
37918
38038
|
storageCb.resolve({ ...inner.values });
|
|
37919
38039
|
} else {
|
|
37920
38040
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
@@ -38285,7 +38405,7 @@ async function loginWithPassword(username, password) {
|
|
|
38285
38405
|
// package.json
|
|
38286
38406
|
var package_default = {
|
|
38287
38407
|
name: "ofw-mcp",
|
|
38288
|
-
version: "2.
|
|
38408
|
+
version: "2.5.0",
|
|
38289
38409
|
license: "MIT",
|
|
38290
38410
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
38291
38411
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -38317,7 +38437,7 @@ var package_default = {
|
|
|
38317
38437
|
"test:watch": "vitest"
|
|
38318
38438
|
},
|
|
38319
38439
|
dependencies: {
|
|
38320
|
-
"@chrischall/mcp-utils": "^0.
|
|
38440
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
38321
38441
|
"@fetchproxy/bootstrap": "^1.3.0",
|
|
38322
38442
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38323
38443
|
dotenv: "^17.4.2",
|
|
@@ -38327,7 +38447,7 @@ var package_default = {
|
|
|
38327
38447
|
"@types/node": "^26.0.0",
|
|
38328
38448
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38329
38449
|
esbuild: "^0.28.0",
|
|
38330
|
-
typescript: "^
|
|
38450
|
+
typescript: "^7.0.2",
|
|
38331
38451
|
vitest: "^4.1.7"
|
|
38332
38452
|
}
|
|
38333
38453
|
};
|
|
@@ -38553,11 +38673,14 @@ var ApiRecipientSchema = external_exports.looseObject({
|
|
|
38553
38673
|
viewed: external_exports.looseObject({ dateTime: external_exports.string() }).nullable().optional()
|
|
38554
38674
|
});
|
|
38555
38675
|
function mapRecipients(items) {
|
|
38556
|
-
return (items ?? []).map((r) =>
|
|
38557
|
-
|
|
38558
|
-
|
|
38559
|
-
|
|
38560
|
-
})
|
|
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"));
|
|
38561
38684
|
}
|
|
38562
38685
|
var expandPath2 = expandPath;
|
|
38563
38686
|
function verifyWriteLanded(kind, sent, persisted) {
|
|
@@ -38646,6 +38769,11 @@ function getWriteMode() {
|
|
|
38646
38769
|
);
|
|
38647
38770
|
return "none";
|
|
38648
38771
|
}
|
|
38772
|
+
function getCalendarWritesAllowed() {
|
|
38773
|
+
const mode = getWriteMode();
|
|
38774
|
+
if (mode === "all") return true;
|
|
38775
|
+
return mode === "drafts" && parseBoolEnv("OFW_CALENDAR_WRITES");
|
|
38776
|
+
}
|
|
38649
38777
|
function getDefaultInlineAttachments() {
|
|
38650
38778
|
return parseBoolEnv("OFW_INLINE_ATTACHMENTS");
|
|
38651
38779
|
}
|
|
@@ -39055,7 +39183,10 @@ var ListItemSchema = external_exports.looseObject({
|
|
|
39055
39183
|
var ListResponseSchema = external_exports.looseObject({ data: external_exports.array(ListItemSchema).optional() });
|
|
39056
39184
|
var DetailResponseSchema = external_exports.looseObject({
|
|
39057
39185
|
body: external_exports.string().optional(),
|
|
39058
|
-
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()
|
|
39059
39190
|
});
|
|
39060
39191
|
async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
39061
39192
|
let page = 1;
|
|
@@ -39075,7 +39206,18 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
39075
39206
|
for (const item of items) {
|
|
39076
39207
|
if (newestId === null || item.id > newestId) newestId = item.id;
|
|
39077
39208
|
const existing = getMessage(item.id);
|
|
39078
|
-
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
|
+
}
|
|
39079
39221
|
pageHadNewItem = true;
|
|
39080
39222
|
const isInboxUnread = folder === "inbox" && item.showNeverViewed === true;
|
|
39081
39223
|
const shouldFetchBody = !isInboxUnread || opts.fetchUnreadBodies;
|
|
@@ -39362,8 +39504,26 @@ function registerMessageTools(server, client2) {
|
|
|
39362
39504
|
}
|
|
39363
39505
|
const cached2 = getMessage(id);
|
|
39364
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
|
+
}
|
|
39365
39525
|
let attachments2 = listAttachmentsForMessage(id);
|
|
39366
|
-
if (attachments2.length === 0 && listDataHintsAtFiles(
|
|
39526
|
+
if (attachments2.length === 0 && listDataHintsAtFiles(row2.listData)) {
|
|
39367
39527
|
try {
|
|
39368
39528
|
const detail2 = parseLenient(
|
|
39369
39529
|
DetailFilesSchema,
|
|
@@ -39377,7 +39537,7 @@ function registerMessageTools(server, client2) {
|
|
|
39377
39537
|
} catch {
|
|
39378
39538
|
}
|
|
39379
39539
|
}
|
|
39380
|
-
return jsonResponse({ ...
|
|
39540
|
+
return jsonResponse({ ...row2, attachments: attachments2 });
|
|
39381
39541
|
}
|
|
39382
39542
|
const detail = parseLenient(
|
|
39383
39543
|
MessageDetailSchema,
|
|
@@ -39793,8 +39953,87 @@ async function deleteOFWMessages(client2, ids) {
|
|
|
39793
39953
|
}
|
|
39794
39954
|
|
|
39795
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
|
+
}
|
|
39796
40035
|
function registerCalendarTools(server, client2) {
|
|
39797
|
-
const allowWrites =
|
|
40036
|
+
const allowWrites = getCalendarWritesAllowed();
|
|
39798
40037
|
server.registerTool("ofw_list_events", {
|
|
39799
40038
|
description: "List OurFamilyWizard calendar events in a date range",
|
|
39800
40039
|
annotations: { readOnlyHint: true },
|
|
@@ -39812,51 +40051,62 @@ function registerCalendarTools(server, client2) {
|
|
|
39812
40051
|
return jsonResponse(data);
|
|
39813
40052
|
});
|
|
39814
40053
|
if (allowWrites) server.registerTool("ofw_create_event", {
|
|
39815
|
-
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.",
|
|
39816
40055
|
annotations: { destructiveHint: false },
|
|
39817
40056
|
inputSchema: {
|
|
39818
40057
|
title: external_exports.string(),
|
|
39819
|
-
|
|
39820
|
-
endDate: external_exports.string().describe("ISO datetime string"),
|
|
39821
|
-
allDay: external_exports.boolean().optional(),
|
|
39822
|
-
location: external_exports.string().optional(),
|
|
39823
|
-
reminder: external_exports.string().describe('Reminder setting (e.g. "1 hour before")').optional(),
|
|
39824
|
-
privateEvent: external_exports.boolean().optional(),
|
|
39825
|
-
eventFor: external_exports.string().describe("neither | parent1 | parent2").optional(),
|
|
39826
|
-
dropOffParent: external_exports.string().optional(),
|
|
39827
|
-
pickUpParent: external_exports.string().optional(),
|
|
39828
|
-
children: external_exports.array(external_exports.number()).describe("Array of child IDs").optional()
|
|
40058
|
+
...eventWriteFields
|
|
39829
40059
|
}
|
|
39830
40060
|
}, async (args) => {
|
|
39831
|
-
const
|
|
39832
|
-
|
|
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
|
+
});
|
|
39833
40067
|
});
|
|
39834
40068
|
if (allowWrites) server.registerTool("ofw_update_event", {
|
|
39835
|
-
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).",
|
|
39836
40070
|
annotations: { destructiveHint: true },
|
|
39837
40071
|
inputSchema: {
|
|
39838
|
-
eventId: external_exports.string(),
|
|
40072
|
+
eventId: external_exports.string().describe("Event id \u2014 the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event"),
|
|
39839
40073
|
title: external_exports.string().optional(),
|
|
39840
|
-
startDate:
|
|
39841
|
-
endDate:
|
|
39842
|
-
|
|
39843
|
-
|
|
39844
|
-
|
|
39845
|
-
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
|
|
39846
40087
|
}
|
|
39847
40088
|
}, async (args) => {
|
|
39848
|
-
const { eventId, ...
|
|
39849
|
-
const
|
|
39850
|
-
|
|
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 });
|
|
39851
40099
|
});
|
|
39852
40100
|
if (allowWrites) server.registerTool("ofw_delete_event", {
|
|
39853
40101
|
description: "Delete an OurFamilyWizard calendar event",
|
|
39854
40102
|
annotations: { destructiveHint: true },
|
|
39855
40103
|
inputSchema: {
|
|
39856
|
-
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()
|
|
39857
40106
|
}
|
|
39858
40107
|
}, async (args) => {
|
|
39859
|
-
|
|
40108
|
+
const includeFuture = args.includeFuture ?? false;
|
|
40109
|
+
await client2.request("DELETE", `/pub/v3/events/${encodeURIComponent(args.eventId)}?includeFuture=${includeFuture}`);
|
|
39860
40110
|
return textResponse(`Event ${args.eventId} deleted`);
|
|
39861
40111
|
});
|
|
39862
40112
|
}
|
|
@@ -39939,7 +40189,7 @@ process.emit = function(event, ...args) {
|
|
|
39939
40189
|
};
|
|
39940
40190
|
await runMcp({
|
|
39941
40191
|
name: "ofw",
|
|
39942
|
-
version: "2.
|
|
40192
|
+
version: "2.5.0",
|
|
39943
40193
|
// x-release-please-version
|
|
39944
40194
|
deps: client,
|
|
39945
40195
|
tools: [
|
package/dist/config.js
CHANGED
|
@@ -61,6 +61,27 @@ export function getWriteMode() {
|
|
|
61
61
|
console.error(`[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" — failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`);
|
|
62
62
|
return 'none';
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Calendar-write opt-in for 'drafts' deployments.
|
|
66
|
+
*
|
|
67
|
+
* Messages have a draft stage (the human sends from the web UI), so 'drafts'
|
|
68
|
+
* mode keeps a human between model output and the court-visible record.
|
|
69
|
+
* Calendar events have no draft stage — but unlike a sent message they are
|
|
70
|
+
* fully reversible (editable and deletable), so a drafts-mode user may accept
|
|
71
|
+
* direct calendar writes without accepting sends. Setting
|
|
72
|
+
* OFW_CALENDAR_WRITES=true registers the calendar write tools
|
|
73
|
+
* (ofw_create_event, ofw_update_event, ofw_delete_event) alongside the
|
|
74
|
+
* draft-level message writes.
|
|
75
|
+
*
|
|
76
|
+
* The flag never overrides 'none': that mode is the hard read-only guarantee,
|
|
77
|
+
* including the fail-closed result of an unrecognized OFW_WRITE_MODE.
|
|
78
|
+
*/
|
|
79
|
+
export function getCalendarWritesAllowed() {
|
|
80
|
+
const mode = getWriteMode();
|
|
81
|
+
if (mode === 'all')
|
|
82
|
+
return true;
|
|
83
|
+
return mode === 'drafts' && parseBoolEnv('OFW_CALENDAR_WRITES');
|
|
84
|
+
}
|
|
64
85
|
// Default for ofw_download_attachment's `inline` arg when the caller doesn't
|
|
65
86
|
// pass one. Set OFW_INLINE_ATTACHMENTS=true to have attachments returned as
|
|
66
87
|
// MCP content blocks by default (skipping disk) — useful on sandboxed MCP
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { registerJournalTools } from './tools/journal.js';
|
|
|
24
24
|
// always succeeds before any credential check runs.
|
|
25
25
|
await runMcp({
|
|
26
26
|
name: 'ofw',
|
|
27
|
-
version: '2.
|
|
27
|
+
version: '2.5.0', // x-release-please-version
|
|
28
28
|
deps: client,
|
|
29
29
|
tools: [
|
|
30
30
|
registerUserTools,
|