ofw-mcp 2.0.11 → 2.0.13
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 +184 -210
- package/dist/cache.js +14 -26
- package/dist/client.js +38 -60
- package/dist/config.js +17 -4
- package/dist/index.js +1 -1
- package/dist/sync.js +25 -32
- package/dist/tools/_shared.js +22 -0
- package/dist/tools/calendar.js +5 -4
- package/dist/tools/expenses.js +4 -3
- package/dist/tools/journal.js +3 -2
- package/dist/tools/messages.js +98 -101
- package/dist/tools/user.js +3 -2
- package/package.json +4 -4
- package/server.json +2 -2
package/dist/tools/messages.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { syncAll, fetchAttachmentMetaForMessage } from '../sync.js';
|
|
2
|
+
import { syncAll, fetchAttachmentMeta, fetchAttachmentMetaForMessage } from '../sync.js';
|
|
3
3
|
import { listMessages, countMessages, listDrafts, getMessage, upsertMessage, upsertDraft, deleteDraft, findLatestReplyTip, listAttachmentsForMessage, getAttachment, upsertAttachmentForMessage, markAttachmentDownloaded, } from '../cache.js';
|
|
4
|
-
import { getAttachmentsDir } from '../config.js';
|
|
4
|
+
import { getAttachmentsDir, getDefaultInlineAttachments } from '../config.js';
|
|
5
5
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import { basename, dirname, extname, join
|
|
6
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
7
|
+
import { expandPath, jsonResponse, mapRecipients, textResponse } from './_shared.js';
|
|
7
8
|
// Lightweight mime sniff from extension. OFW re-derives mime from the filename
|
|
8
9
|
// server-side anyway, so this is just a polite Content-Type for the Blob.
|
|
9
10
|
const MIME_BY_EXT = {
|
|
@@ -51,7 +52,7 @@ export function registerMessageTools(server, client) {
|
|
|
51
52
|
annotations: { readOnlyHint: true },
|
|
52
53
|
}, async () => {
|
|
53
54
|
const data = await client.request('GET', '/pub/v1/messageFolders?includeFolderCounts=true');
|
|
54
|
-
return
|
|
55
|
+
return jsonResponse(data);
|
|
55
56
|
});
|
|
56
57
|
server.registerTool('ofw_list_messages', {
|
|
57
58
|
description: 'List messages from the local OurFamilyWizard cache. Supports filtering by folder, date range, and a substring query on subject+body. Pagination is offset-based but if you know what you want (a date range, a topic), prefer the filters over walking pages — the cache may have 1000+ messages. Call ofw_sync_messages first if the cache is empty or stale.',
|
|
@@ -76,15 +77,10 @@ export function registerMessageTools(server, client) {
|
|
|
76
77
|
else if (folderArg === 'both')
|
|
77
78
|
folder = undefined;
|
|
78
79
|
else {
|
|
79
|
-
return {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
messages: [],
|
|
84
|
-
note: 'folderId must be "inbox", "sent", or "both". Numeric OFW folder IDs are not supported by the cache.',
|
|
85
|
-
}, null, 2),
|
|
86
|
-
}],
|
|
87
|
-
};
|
|
80
|
+
return jsonResponse({
|
|
81
|
+
messages: [],
|
|
82
|
+
note: 'folderId must be "inbox", "sent", or "both". Numeric OFW folder IDs are not supported by the cache.',
|
|
83
|
+
});
|
|
88
84
|
}
|
|
89
85
|
const filter = { folder, since: args.since, until: args.until, q: args.q };
|
|
90
86
|
const total = countMessages(filter);
|
|
@@ -96,7 +92,7 @@ export function registerMessageTools(server, client) {
|
|
|
96
92
|
else if (page * size < total) {
|
|
97
93
|
payload.note = `Showing ${(page - 1) * size + 1}–${(page - 1) * size + messages.length} of ${total}. Increase 'page' to see more, or narrow with since/until/q.`;
|
|
98
94
|
}
|
|
99
|
-
return
|
|
95
|
+
return jsonResponse(payload);
|
|
100
96
|
});
|
|
101
97
|
server.registerTool('ofw_get_message', {
|
|
102
98
|
description: 'Get a single OurFamilyWizard message by ID. Reads from local cache when available; otherwise fetches from OFW (which will mark unread inbox messages as read on OFW).',
|
|
@@ -128,12 +124,9 @@ export function registerMessageTools(server, client) {
|
|
|
128
124
|
// Backfill is best-effort. Fall through with whatever we have.
|
|
129
125
|
}
|
|
130
126
|
}
|
|
131
|
-
return
|
|
127
|
+
return jsonResponse({ ...cached, attachments });
|
|
132
128
|
}
|
|
133
129
|
const detail = await client.request('GET', `/pub/v3/messages/${encodeURIComponent(args.messageId)}`);
|
|
134
|
-
const recipients = (detail.recipients ?? []).map((r) => ({
|
|
135
|
-
userId: r.user.id, name: r.user.name, viewedAt: r.viewed?.dateTime ?? null,
|
|
136
|
-
}));
|
|
137
130
|
const folder = cached?.folder ?? 'inbox';
|
|
138
131
|
const row = {
|
|
139
132
|
id: detail.id,
|
|
@@ -141,7 +134,7 @@ export function registerMessageTools(server, client) {
|
|
|
141
134
|
subject: detail.subject,
|
|
142
135
|
fromUser: detail.from?.name ?? '',
|
|
143
136
|
sentAt: detail.date?.dateTime ?? new Date().toISOString(),
|
|
144
|
-
recipients,
|
|
137
|
+
recipients: mapRecipients(detail.recipients),
|
|
145
138
|
body: detail.body ?? '',
|
|
146
139
|
fetchedBodyAt: new Date().toISOString(),
|
|
147
140
|
replyToId: cached?.replyToId ?? null,
|
|
@@ -153,7 +146,7 @@ export function registerMessageTools(server, client) {
|
|
|
153
146
|
await fetchAttachmentMetaForMessage(client, detail.id, detail.files);
|
|
154
147
|
}
|
|
155
148
|
const attachments = listAttachmentsForMessage(detail.id);
|
|
156
|
-
return
|
|
149
|
+
return jsonResponse({ ...row, attachments });
|
|
157
150
|
});
|
|
158
151
|
server.registerTool('ofw_send_message', {
|
|
159
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.',
|
|
@@ -190,16 +183,13 @@ export function registerMessageTools(server, client) {
|
|
|
190
183
|
replyToId: resolvedReplyTo,
|
|
191
184
|
});
|
|
192
185
|
if (data && typeof data.id === 'number') {
|
|
193
|
-
const recipients = (data.recipients ?? []).map((r) => ({
|
|
194
|
-
userId: r.user.id, name: r.user.name, viewedAt: r.viewed?.dateTime ?? null,
|
|
195
|
-
}));
|
|
196
186
|
const row = {
|
|
197
187
|
id: data.id,
|
|
198
188
|
folder: 'sent',
|
|
199
189
|
subject: data.subject ?? args.subject,
|
|
200
190
|
fromUser: data.from?.name ?? '',
|
|
201
191
|
sentAt: data.date?.dateTime ?? new Date().toISOString(),
|
|
202
|
-
recipients,
|
|
192
|
+
recipients: mapRecipients(data.recipients),
|
|
203
193
|
body: data.body ?? args.body,
|
|
204
194
|
fetchedBodyAt: new Date().toISOString(),
|
|
205
195
|
replyToId: resolvedReplyTo,
|
|
@@ -224,14 +214,11 @@ export function registerMessageTools(server, client) {
|
|
|
224
214
|
}
|
|
225
215
|
}
|
|
226
216
|
if (args.draftId !== undefined) {
|
|
227
|
-
|
|
228
|
-
form.append('messageIds', String(args.draftId));
|
|
229
|
-
await client.request('DELETE', '/pub/v1/messages', form);
|
|
217
|
+
await deleteOFWMessages(client, [args.draftId]);
|
|
230
218
|
deleteDraft(args.draftId);
|
|
231
219
|
}
|
|
232
220
|
const text = data ? JSON.stringify(data, null, 2) : 'Message sent successfully.';
|
|
233
|
-
|
|
234
|
-
return { content: [{ type: 'text', text: finalText }] };
|
|
221
|
+
return textResponse(rewriteNote ? `${rewriteNote}\n\n${text}` : text);
|
|
235
222
|
});
|
|
236
223
|
server.registerTool('ofw_list_drafts', {
|
|
237
224
|
description: 'List draft messages from the local OurFamilyWizard cache. Call ofw_sync_messages first if the cache is empty.',
|
|
@@ -247,7 +234,7 @@ export function registerMessageTools(server, client) {
|
|
|
247
234
|
const payload = drafts.length === 0
|
|
248
235
|
? { drafts: [], note: 'Cache empty. Call ofw_sync_messages to populate.' }
|
|
249
236
|
: { drafts };
|
|
250
|
-
return
|
|
237
|
+
return jsonResponse(payload);
|
|
251
238
|
});
|
|
252
239
|
server.registerTool('ofw_save_draft', {
|
|
253
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.',
|
|
@@ -288,9 +275,7 @@ export function registerMessageTools(server, client) {
|
|
|
288
275
|
id: data.id,
|
|
289
276
|
subject: data.subject ?? args.subject,
|
|
290
277
|
body: data.body ?? args.body,
|
|
291
|
-
recipients: (data.recipients
|
|
292
|
-
userId: r.user.id, name: r.user.name, viewedAt: r.viewed?.dateTime ?? null,
|
|
293
|
-
})),
|
|
278
|
+
recipients: mapRecipients(data.recipients),
|
|
294
279
|
replyToId: data.replyToId ?? resolvedReplyTo,
|
|
295
280
|
modifiedAt: data.date?.dateTime ?? new Date().toISOString(),
|
|
296
281
|
listData: data,
|
|
@@ -298,8 +283,7 @@ export function registerMessageTools(server, client) {
|
|
|
298
283
|
upsertDraft(draft);
|
|
299
284
|
}
|
|
300
285
|
const text = data ? JSON.stringify(data, null, 2) : 'Draft saved.';
|
|
301
|
-
|
|
302
|
-
return { content: [{ type: 'text', text: finalText }] };
|
|
286
|
+
return textResponse(rewriteNote ? `${rewriteNote}\n\n${text}` : text);
|
|
303
287
|
});
|
|
304
288
|
server.registerTool('ofw_delete_draft', {
|
|
305
289
|
description: 'Delete a draft message from OurFamilyWizard. Also removes the draft from the local cache.',
|
|
@@ -308,11 +292,9 @@ export function registerMessageTools(server, client) {
|
|
|
308
292
|
messageId: z.number().describe('Draft message ID to delete'),
|
|
309
293
|
},
|
|
310
294
|
}, async (args) => {
|
|
311
|
-
const
|
|
312
|
-
form.append('messageIds', String(args.messageId));
|
|
313
|
-
const data = await client.request('DELETE', '/pub/v1/messages', form);
|
|
295
|
+
const data = await deleteOFWMessages(client, [args.messageId]);
|
|
314
296
|
deleteDraft(args.messageId);
|
|
315
|
-
return
|
|
297
|
+
return data ? jsonResponse(data) : textResponse('Draft deleted.');
|
|
316
298
|
});
|
|
317
299
|
server.registerTool('ofw_get_unread_sent', {
|
|
318
300
|
description: 'List sent messages that have not been read by one or more recipients. Reads from local cache; call ofw_sync_messages first if cache is stale.',
|
|
@@ -326,9 +308,7 @@ export function registerMessageTools(server, client) {
|
|
|
326
308
|
const size = args.size ?? 50;
|
|
327
309
|
const sent = listMessages({ folder: 'sent', page, size });
|
|
328
310
|
if (sent.length === 0) {
|
|
329
|
-
return {
|
|
330
|
-
note: 'Sent cache is empty. Call ofw_sync_messages to populate.',
|
|
331
|
-
}, null, 2) }] };
|
|
311
|
+
return jsonResponse({ note: 'Sent cache is empty. Call ofw_sync_messages to populate.' });
|
|
332
312
|
}
|
|
333
313
|
const unread = [];
|
|
334
314
|
for (const msg of sent) {
|
|
@@ -338,11 +318,9 @@ export function registerMessageTools(server, client) {
|
|
|
338
318
|
}
|
|
339
319
|
}
|
|
340
320
|
if (unread.length === 0) {
|
|
341
|
-
return {
|
|
342
|
-
message: 'All scanned sent messages have been read.',
|
|
343
|
-
}, null, 2) }] };
|
|
321
|
+
return jsonResponse({ message: 'All scanned sent messages have been read.' });
|
|
344
322
|
}
|
|
345
|
-
return
|
|
323
|
+
return jsonResponse(unread);
|
|
346
324
|
});
|
|
347
325
|
server.registerTool('ofw_upload_attachment', {
|
|
348
326
|
description: 'Upload a local file to OurFamilyWizard\'s "My Files" so it can be attached to a message. Returns the fileId — pass that to ofw_send_message or ofw_save_draft in myFileIDs to attach it. The file is uploaded as PRIVATE (visible only to you) by default; pass shareClass:"SHARED" to share with co-parents directly via the My Files area.',
|
|
@@ -354,18 +332,14 @@ export function registerMessageTools(server, client) {
|
|
|
354
332
|
description: z.string().describe('Description shown in OFW My Files (default: filename)').optional(),
|
|
355
333
|
},
|
|
356
334
|
}, async (args) => {
|
|
357
|
-
|
|
358
|
-
const expanded = args.path.startsWith('~/')
|
|
359
|
-
? join(process.env.HOME ?? '', args.path.slice(2))
|
|
360
|
-
: args.path;
|
|
361
|
-
const abs = isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
335
|
+
const abs = expandPath(args.path);
|
|
362
336
|
const stat = statSync(abs); // throws if missing
|
|
363
337
|
if (!stat.isFile())
|
|
364
338
|
throw new Error(`Not a file: ${abs}`);
|
|
365
339
|
const buf = readFileSync(abs);
|
|
366
340
|
const fileName = basename(abs);
|
|
367
341
|
const mime = mimeFromName(fileName);
|
|
368
|
-
// Build the multipart payload matching the OFW web UI's request shape
|
|
342
|
+
// Build the multipart payload matching the OFW web UI's request shape.
|
|
369
343
|
const form = new FormData();
|
|
370
344
|
form.append('file', new Blob([new Uint8Array(buf)], { type: mime }), fileName);
|
|
371
345
|
form.append('source', 'message');
|
|
@@ -374,10 +348,9 @@ export function registerMessageTools(server, client) {
|
|
|
374
348
|
form.append('fileName', fileName);
|
|
375
349
|
form.append('shareClass', args.shareClass ?? 'PRIVATE');
|
|
376
350
|
const meta = await client.request('POST', '/pub/v3/myfiles/multipart', form);
|
|
377
|
-
// Cache
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
// message is sent with this fileId in its attachments.
|
|
351
|
+
// Cache metadata so subsequent ofw_get_message calls can surface it and
|
|
352
|
+
// ofw_download_attachment can short-circuit. messageId is 0 (the
|
|
353
|
+
// not-yet-linked sentinel) until a message actually references this file.
|
|
381
354
|
upsertAttachmentForMessage({
|
|
382
355
|
fileId: meta.fileId,
|
|
383
356
|
fileName: meta.fileName ?? fileName,
|
|
@@ -387,77 +360,93 @@ export function registerMessageTools(server, client) {
|
|
|
387
360
|
metadata: meta,
|
|
388
361
|
messageId: 0,
|
|
389
362
|
});
|
|
390
|
-
return
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
363
|
+
return jsonResponse({
|
|
364
|
+
fileId: meta.fileId,
|
|
365
|
+
fileName: meta.fileName ?? fileName,
|
|
366
|
+
mimeType: meta.fileType ?? mime,
|
|
367
|
+
sizeBytes: meta.sizeInBytes ?? buf.length,
|
|
368
|
+
shareClass: meta.shareClass ?? args.shareClass ?? 'PRIVATE',
|
|
369
|
+
note: 'Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it.',
|
|
370
|
+
});
|
|
398
371
|
});
|
|
399
372
|
server.registerTool('ofw_download_attachment', {
|
|
400
|
-
description: 'Download an OFW message attachment by fileId.
|
|
373
|
+
description: 'Download an OFW message attachment by fileId. By default, bytes are saved to disk (~/Downloads/ofw-mcp/) and the response carries the absolute path, mime type, and size for the caller to read back. Pass inline:true to skip disk entirely and return the bytes as MCP content blocks — images come back as ImageContent (the model sees them directly); other files come back as an EmbeddedResource blob. Use inline for small files where you want the model to read content immediately and the host is sandboxed; use disk for large files or when you want a persistent local copy. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var (set to "true" to make inline the default). fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo. Re-downloading to the same path is a no-op (disk mode only).',
|
|
401
374
|
annotations: { readOnlyHint: false },
|
|
402
375
|
inputSchema: {
|
|
403
376
|
fileId: z.number().describe('Attachment file id (from ofw_get_message → attachments[].fileId)'),
|
|
404
|
-
|
|
405
|
-
|
|
377
|
+
inline: z.boolean().describe('If true, return bytes inline as MCP content (image for image/*, embedded resource blob otherwise) and skip the disk write. If false, write to disk and return the path. If omitted, falls back to the OFW_INLINE_ATTACHMENTS env var (default: false = disk).').optional(),
|
|
378
|
+
saveTo: z.string().describe('Absolute path or directory to write to. If a directory, the OFW filename is used. Default: ~/Downloads/ofw-mcp/<fileId>-<filename>. Ignored when inline:true.').optional(),
|
|
379
|
+
force: z.boolean().describe('Re-download even if already on disk. Default false. Ignored when inline:true (inline always fetches fresh bytes, or reuses an on-disk copy if present).').optional(),
|
|
406
380
|
},
|
|
407
381
|
}, async (args) => {
|
|
408
382
|
const fileId = args.fileId;
|
|
383
|
+
const inline = args.inline ?? getDefaultInlineAttachments();
|
|
409
384
|
let cached = getAttachment(fileId);
|
|
410
385
|
if (!cached) {
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
// We'll re-link if a message later references it during sync.
|
|
415
|
-
upsertAttachmentForMessage({
|
|
416
|
-
fileId: meta.fileId ?? fileId,
|
|
417
|
-
fileName: meta.fileName ?? `file-${fileId}`,
|
|
418
|
-
label: meta.label ?? meta.fileName ?? `file-${fileId}`,
|
|
419
|
-
mimeType: meta.fileType ?? 'application/octet-stream',
|
|
420
|
-
sizeBytes: typeof meta.fileSize === 'number' ? meta.fileSize : null,
|
|
421
|
-
metadata: meta,
|
|
422
|
-
messageId: 0, // placeholder; will be cleaned up if a real message references it
|
|
423
|
-
});
|
|
386
|
+
// Not in cache. Fetch metadata and store under the messageId=0
|
|
387
|
+
// sentinel — gets re-linked if a message later references this file.
|
|
388
|
+
await fetchAttachmentMeta(client, fileId, 0);
|
|
424
389
|
cached = getAttachment(fileId);
|
|
425
390
|
if (!cached)
|
|
426
391
|
throw new Error(`failed to fetch metadata for fileId ${fileId}`);
|
|
427
392
|
}
|
|
428
|
-
|
|
393
|
+
if (inline) {
|
|
394
|
+
// Reuse on-disk bytes if we already have them; otherwise fetch fresh.
|
|
395
|
+
let bytes = null;
|
|
396
|
+
let mimeType = cached.mimeType;
|
|
397
|
+
let fileName = cached.fileName;
|
|
398
|
+
if (cached.downloadedPath) {
|
|
399
|
+
try {
|
|
400
|
+
bytes = readFileSync(cached.downloadedPath);
|
|
401
|
+
}
|
|
402
|
+
catch { /* on-disk copy missing; fall through */ }
|
|
403
|
+
}
|
|
404
|
+
if (bytes === null) {
|
|
405
|
+
const response = await client.requestBinary('GET', `/pub/v1/myfiles/${fileId}/data`);
|
|
406
|
+
bytes = response.body;
|
|
407
|
+
mimeType = response.contentType ?? cached.mimeType;
|
|
408
|
+
fileName = response.suggestedFileName ?? cached.fileName;
|
|
409
|
+
}
|
|
410
|
+
const base64 = bytes.toString('base64');
|
|
411
|
+
const metaBlock = { type: 'text', text: JSON.stringify({
|
|
412
|
+
fileId, fileName, mimeType, sizeBytes: bytes.length, mode: 'inline',
|
|
413
|
+
}, null, 2) };
|
|
414
|
+
if (mimeType.startsWith('image/')) {
|
|
415
|
+
return { content: [metaBlock, { type: 'image', data: base64, mimeType }] };
|
|
416
|
+
}
|
|
417
|
+
return { content: [metaBlock, { type: 'resource', resource: {
|
|
418
|
+
uri: `ofw://attachment/${fileId}/${encodeURIComponent(fileName)}`,
|
|
419
|
+
mimeType,
|
|
420
|
+
blob: base64,
|
|
421
|
+
} }] };
|
|
422
|
+
}
|
|
429
423
|
let dest;
|
|
430
424
|
if (args.saveTo) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const abs = isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
435
|
-
// If it looks like a directory (ends with /) OR is an existing directory, treat as dir.
|
|
436
|
-
const isDirArg = expanded.endsWith('/') || expanded.endsWith('\\');
|
|
425
|
+
// Treat saveTo as a directory if it ends with a separator; otherwise as a full path.
|
|
426
|
+
const isDirArg = args.saveTo.endsWith('/') || args.saveTo.endsWith('\\');
|
|
427
|
+
const abs = expandPath(args.saveTo);
|
|
437
428
|
dest = isDirArg ? join(abs, `${fileId}-${cached.fileName}`) : abs;
|
|
438
429
|
}
|
|
439
430
|
else {
|
|
440
431
|
dest = join(getAttachmentsDir(), `${fileId}-${cached.fileName}`);
|
|
441
432
|
}
|
|
442
|
-
// Short-circuit if already downloaded to this path
|
|
443
433
|
if (!args.force && cached.downloadedPath === dest) {
|
|
444
|
-
return
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
434
|
+
return jsonResponse({
|
|
435
|
+
fileId, path: dest, mimeType: cached.mimeType, sizeBytes: cached.sizeBytes,
|
|
436
|
+
fileName: cached.fileName, note: 'already downloaded',
|
|
437
|
+
});
|
|
448
438
|
}
|
|
449
|
-
// Fetch bytes
|
|
450
439
|
const response = await client.requestBinary('GET', `/pub/v1/myfiles/${fileId}/data`);
|
|
451
440
|
mkdirSync(dirname(dest), { recursive: true });
|
|
452
441
|
writeFileSync(dest, response.body);
|
|
453
442
|
markAttachmentDownloaded(fileId, dest);
|
|
454
|
-
return
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
443
|
+
return jsonResponse({
|
|
444
|
+
fileId,
|
|
445
|
+
path: dest,
|
|
446
|
+
mimeType: response.contentType ?? cached.mimeType,
|
|
447
|
+
sizeBytes: response.body.length,
|
|
448
|
+
fileName: response.suggestedFileName ?? cached.fileName,
|
|
449
|
+
});
|
|
461
450
|
});
|
|
462
451
|
server.registerTool('ofw_sync_messages', {
|
|
463
452
|
description: 'Sync messages from OurFamilyWizard into the local cache. Returns counts per folder and a list of unread inbox messages whose bodies were NOT fetched (to avoid mark-as-read on OFW). Call ofw_get_message(id) on those to read them. Pass deep:true to walk all OFW pages instead of stopping at the first all-cached page (use to backfill suspected gaps).',
|
|
@@ -473,6 +462,14 @@ export function registerMessageTools(server, client) {
|
|
|
473
462
|
fetchUnreadBodies: args.fetchUnreadBodies,
|
|
474
463
|
deep: args.deep,
|
|
475
464
|
});
|
|
476
|
-
return
|
|
465
|
+
return jsonResponse(result);
|
|
477
466
|
});
|
|
478
467
|
}
|
|
468
|
+
// OFW's bulk-delete endpoint takes a multipart form with `messageIds`.
|
|
469
|
+
// Used by both ofw_delete_draft and ofw_send_message (draft cleanup).
|
|
470
|
+
async function deleteOFWMessages(client, ids) {
|
|
471
|
+
const form = new FormData();
|
|
472
|
+
for (const id of ids)
|
|
473
|
+
form.append('messageIds', String(id));
|
|
474
|
+
return client.request('DELETE', '/pub/v1/messages', form);
|
|
475
|
+
}
|
package/dist/tools/user.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { jsonResponse } from './_shared.js';
|
|
1
2
|
export function registerUserTools(server, client) {
|
|
2
3
|
server.registerTool('ofw_get_profile', {
|
|
3
4
|
description: 'Get current user and co-parent profile information from OurFamilyWizard',
|
|
4
5
|
annotations: { readOnlyHint: true },
|
|
5
6
|
}, async () => {
|
|
6
7
|
const data = await client.request('GET', '/pub/v2/profiles');
|
|
7
|
-
return
|
|
8
|
+
return jsonResponse(data);
|
|
8
9
|
});
|
|
9
10
|
server.registerTool('ofw_get_notifications', {
|
|
10
11
|
description: 'Get OurFamilyWizard dashboard summary: unread message count, upcoming events, outstanding expenses. Note: updates your last-seen status.',
|
|
11
12
|
annotations: { readOnlyHint: false },
|
|
12
13
|
}, async () => {
|
|
13
14
|
const data = await client.request('GET', '/pub/v1/users/useraccountstatus');
|
|
14
|
-
return
|
|
15
|
+
return jsonResponse(data);
|
|
15
16
|
});
|
|
16
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
4
4
|
"mcpName": "io.github.chrischall/ofw-mcp",
|
|
5
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>",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"zod": "^4.4.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@types/node": "^25.
|
|
36
|
-
"@vitest/coverage-v8": "^4.1.
|
|
35
|
+
"@types/node": "^25.8.0",
|
|
36
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
37
37
|
"esbuild": "^0.28.0",
|
|
38
38
|
"typescript": "^6.0.2",
|
|
39
|
-
"vitest": "^4.1.
|
|
39
|
+
"vitest": "^4.1.6"
|
|
40
40
|
}
|
|
41
41
|
}
|
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.13",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.0.
|
|
14
|
+
"version": "2.0.13",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|