ofw-mcp 2.0.14 → 2.0.16
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/dist/bundle.js +67 -33
- package/dist/client.js +31 -1
- package/dist/index.js +1 -1
- package/dist/sync.js +9 -4
- package/dist/tools/messages.js +56 -26
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "OurFamilyWizard tools for Claude Code",
|
|
9
|
-
"version": "2.0.
|
|
9
|
+
"version": "2.0.16"
|
|
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.0.
|
|
17
|
+
"version": "2.0.16",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -36616,7 +36616,7 @@ async function loginWithPassword(username, password) {
|
|
|
36616
36616
|
// package.json
|
|
36617
36617
|
var package_default = {
|
|
36618
36618
|
name: "ofw-mcp",
|
|
36619
|
-
version: "2.0.
|
|
36619
|
+
version: "2.0.16",
|
|
36620
36620
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
36621
36621
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
36622
36622
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -36748,6 +36748,15 @@ function parseContentDispositionFilename(cd) {
|
|
|
36748
36748
|
const m = /filename="?([^";]+)"?/i.exec(cd);
|
|
36749
36749
|
return m ? m[1] : null;
|
|
36750
36750
|
}
|
|
36751
|
+
function debugLogEnabled() {
|
|
36752
|
+
const v = process.env.OFW_DEBUG_LOG;
|
|
36753
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
36754
|
+
}
|
|
36755
|
+
function redactHeaders(h) {
|
|
36756
|
+
const out = { ...h };
|
|
36757
|
+
if (out.Authorization) out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}\u2026`;
|
|
36758
|
+
return out;
|
|
36759
|
+
}
|
|
36751
36760
|
var OFWClient = class {
|
|
36752
36761
|
token = null;
|
|
36753
36762
|
tokenExpiry = null;
|
|
@@ -36755,6 +36764,9 @@ var OFWClient = class {
|
|
|
36755
36764
|
await this.ensureAuthenticated();
|
|
36756
36765
|
const response = await this.fetchWithRetry(method, path, body, "application/json", false);
|
|
36757
36766
|
const text = await response.text();
|
|
36767
|
+
if (debugLogEnabled()) {
|
|
36768
|
+
console.error(`[ofw-debug] response body: ${text || "<empty>"}`);
|
|
36769
|
+
}
|
|
36758
36770
|
return text ? JSON.parse(text) : null;
|
|
36759
36771
|
}
|
|
36760
36772
|
/** Like `request`, but returns the raw bytes plus Content-Type/-Disposition metadata. */
|
|
@@ -36778,11 +36790,21 @@ var OFWClient = class {
|
|
|
36778
36790
|
Authorization: `Bearer ${this.token}`
|
|
36779
36791
|
};
|
|
36780
36792
|
if (body !== void 0 && !isFormData) headers["Content-Type"] = "application/json";
|
|
36781
|
-
const
|
|
36793
|
+
const url2 = `${BASE_URL2}${path}`;
|
|
36794
|
+
if (debugLogEnabled()) {
|
|
36795
|
+
const bodyPreview = body === void 0 ? "<none>" : isFormData ? `<FormData entries=${Array.from(body.keys()).join(",")}>` : JSON.stringify(body);
|
|
36796
|
+
console.error(`[ofw-debug] \u2192 ${method} ${url2}${isRetry ? " (retry)" : ""}`);
|
|
36797
|
+
console.error(`[ofw-debug] headers: ${JSON.stringify(redactHeaders(headers))}`);
|
|
36798
|
+
console.error(`[ofw-debug] body: ${bodyPreview}`);
|
|
36799
|
+
}
|
|
36800
|
+
const response = await fetch(url2, {
|
|
36782
36801
|
method,
|
|
36783
36802
|
headers,
|
|
36784
36803
|
...body !== void 0 ? { body: isFormData ? body : JSON.stringify(body) } : {}
|
|
36785
36804
|
});
|
|
36805
|
+
if (debugLogEnabled()) {
|
|
36806
|
+
console.error(`[ofw-debug] \u2190 ${response.status} ${response.statusText}`);
|
|
36807
|
+
}
|
|
36786
36808
|
if (response.status === 401 && !isRetry) {
|
|
36787
36809
|
this.token = null;
|
|
36788
36810
|
this.tokenExpiry = null;
|
|
@@ -37344,9 +37366,6 @@ async function syncDrafts(client2, draftsFolderId) {
|
|
|
37344
37366
|
seenIds.add(item.id);
|
|
37345
37367
|
const modifiedAt = item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
37346
37368
|
const existing = getDraft(item.id);
|
|
37347
|
-
if (existing && existing.modifiedAt === modifiedAt) {
|
|
37348
|
-
continue;
|
|
37349
|
-
}
|
|
37350
37369
|
const detail = await client2.request("GET", `/pub/v3/messages/${item.id}`);
|
|
37351
37370
|
const row = {
|
|
37352
37371
|
id: item.id,
|
|
@@ -37358,7 +37377,9 @@ async function syncDrafts(client2, draftsFolderId) {
|
|
|
37358
37377
|
listData: item
|
|
37359
37378
|
};
|
|
37360
37379
|
upsertDraft(row);
|
|
37361
|
-
|
|
37380
|
+
if (!existing || existing.body !== row.body || existing.subject !== row.subject || existing.replyToId !== row.replyToId) {
|
|
37381
|
+
synced++;
|
|
37382
|
+
}
|
|
37362
37383
|
}
|
|
37363
37384
|
for (const id of listDraftIds()) {
|
|
37364
37385
|
if (!seenIds.has(id)) deleteDraft(id);
|
|
@@ -37520,7 +37541,7 @@ function registerMessageTools(server2, client2) {
|
|
|
37520
37541
|
return jsonResponse({ ...row, attachments });
|
|
37521
37542
|
});
|
|
37522
37543
|
server2.registerTool("ofw_send_message", {
|
|
37523
|
-
description: "Send a message via OurFamilyWizard. If sending from a draft, pass draftId to delete the draft after sending. If replyToId is provided, the cache may rewrite it to the latest reply in the same thread (a note is included in the response when this happens). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs.",
|
|
37544
|
+
description: "Send a message via OurFamilyWizard. If sending from a draft, pass draftId to delete the draft after sending. If replyToId is provided, the cache may rewrite it to the latest reply in the same thread (a note is included in the response when this happens). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After sending, the tool re-fetches the message from OFW to populate the local cache and link attachments to the new message id.",
|
|
37524
37545
|
annotations: { destructiveHint: true },
|
|
37525
37546
|
inputSchema: {
|
|
37526
37547
|
subject: external_exports.string().describe("Message subject"),
|
|
@@ -37553,21 +37574,24 @@ function registerMessageTools(server2, client2) {
|
|
|
37553
37574
|
includeOriginal: resolvedReplyTo !== null,
|
|
37554
37575
|
replyToId: resolvedReplyTo
|
|
37555
37576
|
});
|
|
37556
|
-
|
|
37557
|
-
|
|
37558
|
-
|
|
37577
|
+
const newId = typeof data?.id === "number" ? data.id : typeof data?.entityId === "number" ? data.entityId : null;
|
|
37578
|
+
let persisted = null;
|
|
37579
|
+
if (newId !== null) {
|
|
37580
|
+
const detail = await client2.request("GET", `/pub/v3/messages/${newId}`);
|
|
37581
|
+
persisted = {
|
|
37582
|
+
id: newId,
|
|
37559
37583
|
folder: "sent",
|
|
37560
|
-
subject:
|
|
37561
|
-
fromUser:
|
|
37562
|
-
sentAt:
|
|
37563
|
-
recipients: mapRecipients(
|
|
37564
|
-
body:
|
|
37584
|
+
subject: detail.subject ?? args.subject,
|
|
37585
|
+
fromUser: detail.from?.name ?? "",
|
|
37586
|
+
sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
37587
|
+
recipients: mapRecipients(detail.recipients),
|
|
37588
|
+
body: detail.body ?? args.body,
|
|
37565
37589
|
fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
37566
37590
|
replyToId: resolvedReplyTo,
|
|
37567
37591
|
chainRootId,
|
|
37568
|
-
listData:
|
|
37592
|
+
listData: detail
|
|
37569
37593
|
};
|
|
37570
|
-
upsertMessage(
|
|
37594
|
+
upsertMessage(persisted);
|
|
37571
37595
|
for (const fileId of myFileIDs) {
|
|
37572
37596
|
const existing = getAttachment(fileId);
|
|
37573
37597
|
upsertAttachmentForMessage({
|
|
@@ -37577,7 +37601,7 @@ function registerMessageTools(server2, client2) {
|
|
|
37577
37601
|
mimeType: existing?.mimeType ?? "application/octet-stream",
|
|
37578
37602
|
sizeBytes: existing?.sizeBytes ?? null,
|
|
37579
37603
|
metadata: existing?.metadata ?? {},
|
|
37580
|
-
messageId:
|
|
37604
|
+
messageId: newId
|
|
37581
37605
|
});
|
|
37582
37606
|
}
|
|
37583
37607
|
}
|
|
@@ -37585,7 +37609,8 @@ function registerMessageTools(server2, client2) {
|
|
|
37585
37609
|
await deleteOFWMessages(client2, [args.draftId]);
|
|
37586
37610
|
deleteDraft(args.draftId);
|
|
37587
37611
|
}
|
|
37588
|
-
const
|
|
37612
|
+
const responseObj = persisted ?? data;
|
|
37613
|
+
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
37589
37614
|
return textResponse(rewriteNote ? `${rewriteNote}
|
|
37590
37615
|
|
|
37591
37616
|
${text}` : text);
|
|
@@ -37605,7 +37630,7 @@ ${text}` : text);
|
|
|
37605
37630
|
return jsonResponse(payload);
|
|
37606
37631
|
});
|
|
37607
37632
|
server2.registerTool("ofw_save_draft", {
|
|
37608
|
-
description: "Save a message as a draft in OurFamilyWizard. Recipients are optional. To update an existing draft, provide its messageId. If replyToId is provided, the cache may rewrite it to the latest reply in the thread (note included in response). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs.",
|
|
37633
|
+
description: "Save a message as a draft in OurFamilyWizard. Recipients are optional. To update an existing draft, provide its messageId. If replyToId is provided, the cache may rewrite it to the latest reply in the thread (note included in response). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After saving, the tool re-fetches the draft from OFW to populate the local cache and verify what was actually persisted; if OFW silently no-ops an update (a known issue with repeated updates to the same draft), the response includes a WARNING note with a workaround.",
|
|
37609
37634
|
annotations: { readOnlyHint: false },
|
|
37610
37635
|
inputSchema: {
|
|
37611
37636
|
subject: external_exports.string().describe("Message subject"),
|
|
@@ -37637,20 +37662,29 @@ ${text}` : text);
|
|
|
37637
37662
|
};
|
|
37638
37663
|
if (args.messageId !== void 0) payload.messageId = args.messageId;
|
|
37639
37664
|
const data = await client2.request("POST", "/pub/v3/messages", payload);
|
|
37640
|
-
|
|
37641
|
-
|
|
37642
|
-
|
|
37643
|
-
|
|
37644
|
-
|
|
37645
|
-
|
|
37646
|
-
|
|
37647
|
-
|
|
37648
|
-
|
|
37665
|
+
const newId = typeof data?.id === "number" ? data.id : typeof data?.entityId === "number" ? data.entityId : null;
|
|
37666
|
+
let persisted = null;
|
|
37667
|
+
let noOpWarning = null;
|
|
37668
|
+
if (newId !== null) {
|
|
37669
|
+
const detail = await client2.request("GET", `/pub/v3/messages/${newId}`);
|
|
37670
|
+
persisted = {
|
|
37671
|
+
id: newId,
|
|
37672
|
+
subject: detail.subject ?? args.subject,
|
|
37673
|
+
body: detail.body ?? "",
|
|
37674
|
+
recipients: mapRecipients(detail.recipients),
|
|
37675
|
+
replyToId: detail.replyToId ?? resolvedReplyTo,
|
|
37676
|
+
modifiedAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
37677
|
+
listData: detail
|
|
37649
37678
|
};
|
|
37650
|
-
upsertDraft(
|
|
37679
|
+
upsertDraft(persisted);
|
|
37680
|
+
if (args.messageId !== void 0 && persisted.body !== args.body) {
|
|
37681
|
+
noOpWarning = "WARNING: OFW reported success but the draft body it returned does not match the requested update. The OFW POST /pub/v3/messages endpoint can silently no-op on subsequent updates to the same draft. Workaround: delete this draft (ofw_delete_draft) and create a new one (ofw_save_draft without messageId).";
|
|
37682
|
+
}
|
|
37651
37683
|
}
|
|
37652
|
-
const
|
|
37653
|
-
|
|
37684
|
+
const responseObj = persisted ?? data;
|
|
37685
|
+
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Draft saved.";
|
|
37686
|
+
const notes = [rewriteNote, noOpWarning].filter((n) => n !== null).join("\n\n");
|
|
37687
|
+
return textResponse(notes ? `${notes}
|
|
37654
37688
|
|
|
37655
37689
|
${text}` : text);
|
|
37656
37690
|
});
|
|
@@ -37979,7 +38013,7 @@ process.emit = function(event, ...args) {
|
|
|
37979
38013
|
}
|
|
37980
38014
|
return originalEmit(event, ...args);
|
|
37981
38015
|
};
|
|
37982
|
-
var server = new McpServer({ name: "ofw", version: "2.0.
|
|
38016
|
+
var server = new McpServer({ name: "ofw", version: "2.0.16" });
|
|
37983
38017
|
registerUserTools(server, client);
|
|
37984
38018
|
registerMessageTools(server, client);
|
|
37985
38019
|
registerCalendarTools(server, client);
|
package/dist/client.js
CHANGED
|
@@ -31,6 +31,19 @@ function parseContentDispositionFilename(cd) {
|
|
|
31
31
|
const m = /filename="?([^";]+)"?/i.exec(cd);
|
|
32
32
|
return m ? m[1] : null;
|
|
33
33
|
}
|
|
34
|
+
// Set OFW_DEBUG_LOG=1 (or true/yes/on) to log every OFW request/response to
|
|
35
|
+
// stderr. Authorization is redacted. Bodies are logged in full — set this
|
|
36
|
+
// only when debugging, never in normal use.
|
|
37
|
+
function debugLogEnabled() {
|
|
38
|
+
const v = process.env.OFW_DEBUG_LOG;
|
|
39
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
|
|
40
|
+
}
|
|
41
|
+
function redactHeaders(h) {
|
|
42
|
+
const out = { ...h };
|
|
43
|
+
if (out.Authorization)
|
|
44
|
+
out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}…`;
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
34
47
|
export class OFWClient {
|
|
35
48
|
token = null;
|
|
36
49
|
tokenExpiry = null;
|
|
@@ -38,6 +51,9 @@ export class OFWClient {
|
|
|
38
51
|
await this.ensureAuthenticated();
|
|
39
52
|
const response = await this.fetchWithRetry(method, path, body, 'application/json', false);
|
|
40
53
|
const text = await response.text();
|
|
54
|
+
if (debugLogEnabled()) {
|
|
55
|
+
console.error(`[ofw-debug] response body: ${text || '<empty>'}`);
|
|
56
|
+
}
|
|
41
57
|
return (text ? JSON.parse(text) : null);
|
|
42
58
|
}
|
|
43
59
|
/** Like `request`, but returns the raw bytes plus Content-Type/-Disposition metadata. */
|
|
@@ -62,11 +78,25 @@ export class OFWClient {
|
|
|
62
78
|
};
|
|
63
79
|
if (body !== undefined && !isFormData)
|
|
64
80
|
headers['Content-Type'] = 'application/json';
|
|
65
|
-
const
|
|
81
|
+
const url = `${BASE_URL}${path}`;
|
|
82
|
+
if (debugLogEnabled()) {
|
|
83
|
+
const bodyPreview = body === undefined
|
|
84
|
+
? '<none>'
|
|
85
|
+
: isFormData
|
|
86
|
+
? `<FormData entries=${Array.from(body.keys()).join(',')}>`
|
|
87
|
+
: JSON.stringify(body);
|
|
88
|
+
console.error(`[ofw-debug] → ${method} ${url}${isRetry ? ' (retry)' : ''}`);
|
|
89
|
+
console.error(`[ofw-debug] headers: ${JSON.stringify(redactHeaders(headers))}`);
|
|
90
|
+
console.error(`[ofw-debug] body: ${bodyPreview}`);
|
|
91
|
+
}
|
|
92
|
+
const response = await fetch(url, {
|
|
66
93
|
method,
|
|
67
94
|
headers,
|
|
68
95
|
...(body !== undefined ? { body: isFormData ? body : JSON.stringify(body) } : {}),
|
|
69
96
|
});
|
|
97
|
+
if (debugLogEnabled()) {
|
|
98
|
+
console.error(`[ofw-debug] ← ${response.status} ${response.statusText}`);
|
|
99
|
+
}
|
|
70
100
|
if (response.status === 401 && !isRetry) {
|
|
71
101
|
this.token = null;
|
|
72
102
|
this.tokenExpiry = null;
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { registerMessageTools } from './tools/messages.js';
|
|
|
17
17
|
import { registerCalendarTools } from './tools/calendar.js';
|
|
18
18
|
import { registerExpenseTools } from './tools/expenses.js';
|
|
19
19
|
import { registerJournalTools } from './tools/journal.js';
|
|
20
|
-
const server = new McpServer({ name: 'ofw', version: '2.0.
|
|
20
|
+
const server = new McpServer({ name: 'ofw', version: '2.0.16' });
|
|
21
21
|
registerUserTools(server, client);
|
|
22
22
|
registerMessageTools(server, client);
|
|
23
23
|
registerCalendarTools(server, client);
|
package/dist/sync.js
CHANGED
|
@@ -127,10 +127,10 @@ export async function syncDrafts(client, draftsFolderId) {
|
|
|
127
127
|
for (const item of items) {
|
|
128
128
|
seenIds.add(item.id);
|
|
129
129
|
const modifiedAt = item.date?.dateTime ?? new Date().toISOString();
|
|
130
|
+
// OFW's list endpoint's `date.dateTime` is NOT a reliable modification
|
|
131
|
+
// timestamp for drafts — direct UI edits don't bump it — so we can't
|
|
132
|
+
// use it to skip the detail fetch. Always re-fetch; drafts are few.
|
|
130
133
|
const existing = getDraft(item.id);
|
|
131
|
-
if (existing && existing.modifiedAt === modifiedAt) {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
134
|
const detail = await client.request('GET', `/pub/v3/messages/${item.id}`);
|
|
135
135
|
const row = {
|
|
136
136
|
id: item.id,
|
|
@@ -142,7 +142,12 @@ export async function syncDrafts(client, draftsFolderId) {
|
|
|
142
142
|
listData: item,
|
|
143
143
|
};
|
|
144
144
|
upsertDraft(row);
|
|
145
|
-
|
|
145
|
+
if (!existing
|
|
146
|
+
|| existing.body !== row.body
|
|
147
|
+
|| existing.subject !== row.subject
|
|
148
|
+
|| existing.replyToId !== row.replyToId) {
|
|
149
|
+
synced++;
|
|
150
|
+
}
|
|
146
151
|
}
|
|
147
152
|
for (const id of listDraftIds()) {
|
|
148
153
|
if (!seenIds.has(id))
|
package/dist/tools/messages.js
CHANGED
|
@@ -149,7 +149,7 @@ export function registerMessageTools(server, client) {
|
|
|
149
149
|
return jsonResponse({ ...row, attachments });
|
|
150
150
|
});
|
|
151
151
|
server.registerTool('ofw_send_message', {
|
|
152
|
-
description: 'Send a message via OurFamilyWizard. If sending from a draft, pass draftId to delete the draft after sending. If replyToId is provided, the cache may rewrite it to the latest reply in the same thread (a note is included in the response when this happens). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs.',
|
|
152
|
+
description: 'Send a message via OurFamilyWizard. If sending from a draft, pass draftId to delete the draft after sending. If replyToId is provided, the cache may rewrite it to the latest reply in the same thread (a note is included in the response when this happens). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After sending, the tool re-fetches the message from OFW to populate the local cache and link attachments to the new message id.',
|
|
153
153
|
annotations: { destructiveHint: true },
|
|
154
154
|
inputSchema: {
|
|
155
155
|
subject: z.string().describe('Message subject'),
|
|
@@ -173,6 +173,9 @@ export function registerMessageTools(server, client) {
|
|
|
173
173
|
chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
|
|
174
174
|
}
|
|
175
175
|
const myFileIDs = args.myFileIDs ?? [];
|
|
176
|
+
// OFW's POST /pub/v3/messages response is minimal — typically just
|
|
177
|
+
// `{entityId: <id>}` — so the cache write needs to fetch detail
|
|
178
|
+
// afterwards (same shape as ofw_save_draft).
|
|
176
179
|
const data = await client.request('POST', '/pub/v3/messages', {
|
|
177
180
|
subject: args.subject,
|
|
178
181
|
body: args.body,
|
|
@@ -182,21 +185,26 @@ export function registerMessageTools(server, client) {
|
|
|
182
185
|
includeOriginal: resolvedReplyTo !== null,
|
|
183
186
|
replyToId: resolvedReplyTo,
|
|
184
187
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
const newId = typeof data?.id === 'number' ? data.id
|
|
189
|
+
: typeof data?.entityId === 'number' ? data.entityId
|
|
190
|
+
: null;
|
|
191
|
+
let persisted = null;
|
|
192
|
+
if (newId !== null) {
|
|
193
|
+
const detail = await client.request('GET', `/pub/v3/messages/${newId}`);
|
|
194
|
+
persisted = {
|
|
195
|
+
id: newId,
|
|
188
196
|
folder: 'sent',
|
|
189
|
-
subject:
|
|
190
|
-
fromUser:
|
|
191
|
-
sentAt:
|
|
192
|
-
recipients: mapRecipients(
|
|
193
|
-
body:
|
|
197
|
+
subject: detail.subject ?? args.subject,
|
|
198
|
+
fromUser: detail.from?.name ?? '',
|
|
199
|
+
sentAt: detail.date?.dateTime ?? new Date().toISOString(),
|
|
200
|
+
recipients: mapRecipients(detail.recipients),
|
|
201
|
+
body: detail.body ?? args.body,
|
|
194
202
|
fetchedBodyAt: new Date().toISOString(),
|
|
195
203
|
replyToId: resolvedReplyTo,
|
|
196
204
|
chainRootId,
|
|
197
|
-
listData:
|
|
205
|
+
listData: detail,
|
|
198
206
|
};
|
|
199
|
-
upsertMessage(
|
|
207
|
+
upsertMessage(persisted);
|
|
200
208
|
// Link attached files to the new message in the attachments cache.
|
|
201
209
|
// We may not have full metadata if the upload happened in a prior
|
|
202
210
|
// session — fall back to what we know.
|
|
@@ -209,7 +217,7 @@ export function registerMessageTools(server, client) {
|
|
|
209
217
|
mimeType: existing?.mimeType ?? 'application/octet-stream',
|
|
210
218
|
sizeBytes: existing?.sizeBytes ?? null,
|
|
211
219
|
metadata: existing?.metadata ?? {},
|
|
212
|
-
messageId:
|
|
220
|
+
messageId: newId,
|
|
213
221
|
});
|
|
214
222
|
}
|
|
215
223
|
}
|
|
@@ -217,7 +225,8 @@ export function registerMessageTools(server, client) {
|
|
|
217
225
|
await deleteOFWMessages(client, [args.draftId]);
|
|
218
226
|
deleteDraft(args.draftId);
|
|
219
227
|
}
|
|
220
|
-
const
|
|
228
|
+
const responseObj = persisted ?? data;
|
|
229
|
+
const text = responseObj ? JSON.stringify(responseObj, null, 2) : 'Message sent successfully.';
|
|
221
230
|
return textResponse(rewriteNote ? `${rewriteNote}\n\n${text}` : text);
|
|
222
231
|
});
|
|
223
232
|
server.registerTool('ofw_list_drafts', {
|
|
@@ -237,7 +246,7 @@ export function registerMessageTools(server, client) {
|
|
|
237
246
|
return jsonResponse(payload);
|
|
238
247
|
});
|
|
239
248
|
server.registerTool('ofw_save_draft', {
|
|
240
|
-
description: 'Save a message as a draft in OurFamilyWizard. Recipients are optional. To update an existing draft, provide its messageId. If replyToId is provided, the cache may rewrite it to the latest reply in the thread (note included in response). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs.',
|
|
249
|
+
description: 'Save a message as a draft in OurFamilyWizard. Recipients are optional. To update an existing draft, provide its messageId. If replyToId is provided, the cache may rewrite it to the latest reply in the thread (note included in response). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After saving, the tool re-fetches the draft from OFW to populate the local cache and verify what was actually persisted; if OFW silently no-ops an update (a known issue with repeated updates to the same draft), the response includes a WARNING note with a workaround.',
|
|
241
250
|
annotations: { readOnlyHint: false },
|
|
242
251
|
inputSchema: {
|
|
243
252
|
subject: z.string().describe('Message subject'),
|
|
@@ -269,21 +278,42 @@ export function registerMessageTools(server, client) {
|
|
|
269
278
|
};
|
|
270
279
|
if (args.messageId !== undefined)
|
|
271
280
|
payload.messageId = args.messageId;
|
|
281
|
+
// OFW's POST /pub/v3/messages response for drafts is minimal — typically
|
|
282
|
+
// just `{entityId: <id>}` — and worse, it returns the same success shape
|
|
283
|
+
// even when the server silently no-ops on a subsequent update to the
|
|
284
|
+
// same draft. Don't trust the POST response: extract the id from it,
|
|
285
|
+
// then GET the detail endpoint to repopulate the cache from
|
|
286
|
+
// authoritative server state.
|
|
272
287
|
const data = await client.request('POST', '/pub/v3/messages', payload);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
288
|
+
const newId = typeof data?.id === 'number' ? data.id
|
|
289
|
+
: typeof data?.entityId === 'number' ? data.entityId
|
|
290
|
+
: null;
|
|
291
|
+
let persisted = null;
|
|
292
|
+
let noOpWarning = null;
|
|
293
|
+
if (newId !== null) {
|
|
294
|
+
const detail = await client.request('GET', `/pub/v3/messages/${newId}`);
|
|
295
|
+
persisted = {
|
|
296
|
+
id: newId,
|
|
297
|
+
subject: detail.subject ?? args.subject,
|
|
298
|
+
body: detail.body ?? '',
|
|
299
|
+
recipients: mapRecipients(detail.recipients),
|
|
300
|
+
replyToId: detail.replyToId ?? resolvedReplyTo,
|
|
301
|
+
modifiedAt: detail.date?.dateTime ?? new Date().toISOString(),
|
|
302
|
+
listData: detail,
|
|
282
303
|
};
|
|
283
|
-
upsertDraft(
|
|
304
|
+
upsertDraft(persisted);
|
|
305
|
+
// If this was an update (messageId provided) and OFW's reported body
|
|
306
|
+
// doesn't match what we asked it to save, the server silently
|
|
307
|
+
// dropped the change. Warn the caller so the model can take the
|
|
308
|
+
// create-then-delete fallback.
|
|
309
|
+
if (args.messageId !== undefined && persisted.body !== args.body) {
|
|
310
|
+
noOpWarning = 'WARNING: OFW reported success but the draft body it returned does not match the requested update. The OFW POST /pub/v3/messages endpoint can silently no-op on subsequent updates to the same draft. Workaround: delete this draft (ofw_delete_draft) and create a new one (ofw_save_draft without messageId).';
|
|
311
|
+
}
|
|
284
312
|
}
|
|
285
|
-
const
|
|
286
|
-
|
|
313
|
+
const responseObj = persisted ?? data;
|
|
314
|
+
const text = responseObj ? JSON.stringify(responseObj, null, 2) : 'Draft saved.';
|
|
315
|
+
const notes = [rewriteNote, noOpWarning].filter((n) => n !== null).join('\n\n');
|
|
316
|
+
return textResponse(notes ? `${notes}\n\n${text}` : text);
|
|
287
317
|
});
|
|
288
318
|
server.registerTool('ofw_delete_draft', {
|
|
289
319
|
description: 'Delete a draft message from OurFamilyWizard. Also removes the draft from the local cache.',
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
4
4
|
"mcpName": "io.github.chrischall/ofw-mcp",
|
|
5
|
-
"description": "OurFamilyWizard MCP server for Claude
|
|
5
|
+
"description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/ofw-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.0.
|
|
9
|
+
"version": "2.0.16",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.0.
|
|
14
|
+
"version": "2.0.16",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|