ofw-mcp 2.0.9 → 2.0.10
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 +95 -9
- package/dist/index.js +1 -1
- package/dist/tools/messages.js +104 -6
- package/package.json +1 -1
- 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.10"
|
|
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.10",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -31592,8 +31592,35 @@ async function syncAll(client2, opts) {
|
|
|
31592
31592
|
}
|
|
31593
31593
|
|
|
31594
31594
|
// src/tools/messages.ts
|
|
31595
|
-
import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
|
|
31596
|
-
import { dirname as dirname3, join as join3, isAbsolute, resolve } from "node:path";
|
|
31595
|
+
import { mkdirSync as mkdirSync2, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
31596
|
+
import { basename, dirname as dirname3, extname, join as join3, isAbsolute, resolve } from "node:path";
|
|
31597
|
+
var MIME_BY_EXT = {
|
|
31598
|
+
".pdf": "application/pdf",
|
|
31599
|
+
".png": "image/png",
|
|
31600
|
+
".jpg": "image/jpeg",
|
|
31601
|
+
".jpeg": "image/jpeg",
|
|
31602
|
+
".gif": "image/gif",
|
|
31603
|
+
".webp": "image/webp",
|
|
31604
|
+
".heic": "image/heic",
|
|
31605
|
+
".txt": "text/plain",
|
|
31606
|
+
".md": "text/markdown",
|
|
31607
|
+
".csv": "text/csv",
|
|
31608
|
+
".html": "text/html",
|
|
31609
|
+
".htm": "text/html",
|
|
31610
|
+
".json": "application/json",
|
|
31611
|
+
".xml": "application/xml",
|
|
31612
|
+
".doc": "application/msword",
|
|
31613
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
31614
|
+
".xls": "application/vnd.ms-excel",
|
|
31615
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
31616
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
31617
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
31618
|
+
".zip": "application/zip",
|
|
31619
|
+
".ics": "text/calendar"
|
|
31620
|
+
};
|
|
31621
|
+
function mimeFromName(name) {
|
|
31622
|
+
return MIME_BY_EXT[extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
31623
|
+
}
|
|
31597
31624
|
function registerMessageTools(server2, client2) {
|
|
31598
31625
|
server2.registerTool("ofw_list_message_folders", {
|
|
31599
31626
|
description: "List OurFamilyWizard message folders (inbox, sent, etc.) and their unread counts. Returns folder IDs needed to call ofw_list_messages. Does NOT return message content.",
|
|
@@ -31684,14 +31711,15 @@ function registerMessageTools(server2, client2) {
|
|
|
31684
31711
|
return { content: [{ type: "text", text: JSON.stringify({ ...row, attachments }, null, 2) }] };
|
|
31685
31712
|
});
|
|
31686
31713
|
server2.registerTool("ofw_send_message", {
|
|
31687
|
-
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).",
|
|
31714
|
+
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.",
|
|
31688
31715
|
annotations: { destructiveHint: true },
|
|
31689
31716
|
inputSchema: {
|
|
31690
31717
|
subject: external_exports.string().describe("Message subject"),
|
|
31691
31718
|
body: external_exports.string().describe("Message body text"),
|
|
31692
31719
|
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (get from ofw_get_profile)"),
|
|
31693
31720
|
replyToId: external_exports.number().describe("ID of the message being replied to").optional(),
|
|
31694
|
-
draftId: external_exports.number().describe("ID of the draft to delete after sending (omit if not sending from a draft)").optional()
|
|
31721
|
+
draftId: external_exports.number().describe("ID of the draft to delete after sending (omit if not sending from a draft)").optional(),
|
|
31722
|
+
myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment) to attach to the message").optional()
|
|
31695
31723
|
}
|
|
31696
31724
|
}, async (args) => {
|
|
31697
31725
|
const requestedReplyTo = args.replyToId ?? null;
|
|
@@ -31706,11 +31734,12 @@ function registerMessageTools(server2, client2) {
|
|
|
31706
31734
|
const parent = getMessage(resolvedReplyTo);
|
|
31707
31735
|
chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
|
|
31708
31736
|
}
|
|
31737
|
+
const myFileIDs = args.myFileIDs ?? [];
|
|
31709
31738
|
const data = await client2.request("POST", "/pub/v3/messages", {
|
|
31710
31739
|
subject: args.subject,
|
|
31711
31740
|
body: args.body,
|
|
31712
31741
|
recipientIds: args.recipientIds,
|
|
31713
|
-
attachments: { myFileIDs
|
|
31742
|
+
attachments: { myFileIDs },
|
|
31714
31743
|
draft: false,
|
|
31715
31744
|
includeOriginal: resolvedReplyTo !== null,
|
|
31716
31745
|
replyToId: resolvedReplyTo
|
|
@@ -31735,6 +31764,18 @@ function registerMessageTools(server2, client2) {
|
|
|
31735
31764
|
listData: data
|
|
31736
31765
|
};
|
|
31737
31766
|
upsertMessage(row);
|
|
31767
|
+
for (const fileId of myFileIDs) {
|
|
31768
|
+
const existing = getAttachment(fileId);
|
|
31769
|
+
upsertAttachmentForMessage({
|
|
31770
|
+
fileId,
|
|
31771
|
+
fileName: existing?.fileName ?? `file-${fileId}`,
|
|
31772
|
+
label: existing?.label ?? existing?.fileName ?? `file-${fileId}`,
|
|
31773
|
+
mimeType: existing?.mimeType ?? "application/octet-stream",
|
|
31774
|
+
sizeBytes: existing?.sizeBytes ?? null,
|
|
31775
|
+
metadata: existing?.metadata ?? {},
|
|
31776
|
+
messageId: data.id
|
|
31777
|
+
});
|
|
31778
|
+
}
|
|
31738
31779
|
}
|
|
31739
31780
|
if (args.draftId !== void 0) {
|
|
31740
31781
|
const form = new FormData();
|
|
@@ -31763,14 +31804,15 @@ ${text}` : text;
|
|
|
31763
31804
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
31764
31805
|
});
|
|
31765
31806
|
server2.registerTool("ofw_save_draft", {
|
|
31766
|
-
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).",
|
|
31807
|
+
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.",
|
|
31767
31808
|
annotations: { readOnlyHint: false },
|
|
31768
31809
|
inputSchema: {
|
|
31769
31810
|
subject: external_exports.string().describe("Message subject"),
|
|
31770
31811
|
body: external_exports.string().describe("Message body text"),
|
|
31771
31812
|
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (optional for drafts)").optional(),
|
|
31772
31813
|
messageId: external_exports.number().describe("ID of an existing draft to update (omit to create a new draft)").optional(),
|
|
31773
|
-
replyToId: external_exports.number().describe("ID of the message this draft replies to").optional()
|
|
31814
|
+
replyToId: external_exports.number().describe("ID of the message this draft replies to").optional(),
|
|
31815
|
+
myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment)").optional()
|
|
31774
31816
|
}
|
|
31775
31817
|
}, async (args) => {
|
|
31776
31818
|
const requestedReplyTo = args.replyToId ?? null;
|
|
@@ -31782,11 +31824,12 @@ ${text}` : text;
|
|
|
31782
31824
|
rewriteNote = `replyToId rewritten from ${requestedReplyTo} to ${resolvedReplyTo} (later reply in same thread found in sent cache).`;
|
|
31783
31825
|
}
|
|
31784
31826
|
}
|
|
31827
|
+
const myFileIDs = args.myFileIDs ?? [];
|
|
31785
31828
|
const payload = {
|
|
31786
31829
|
subject: args.subject,
|
|
31787
31830
|
body: args.body,
|
|
31788
31831
|
recipientIds: args.recipientIds ?? [],
|
|
31789
|
-
attachments: { myFileIDs
|
|
31832
|
+
attachments: { myFileIDs },
|
|
31790
31833
|
draft: true,
|
|
31791
31834
|
includeOriginal: resolvedReplyTo !== null,
|
|
31792
31835
|
replyToId: resolvedReplyTo
|
|
@@ -31858,6 +31901,49 @@ ${text}` : text;
|
|
|
31858
31901
|
}
|
|
31859
31902
|
return { content: [{ type: "text", text: JSON.stringify(unread, null, 2) }] };
|
|
31860
31903
|
});
|
|
31904
|
+
server2.registerTool("ofw_upload_attachment", {
|
|
31905
|
+
description: `Upload a local file to OurFamilyWizard's "My Files" so it can be attached to a message. Returns the fileId \u2014 pass that to ofw_send_message or ofw_save_draft in myFileIDs to attach it. The file is uploaded as PRIVATE (visible only to you) by default; pass shareClass:"SHARED" to share with co-parents directly via the My Files area.`,
|
|
31906
|
+
annotations: { destructiveHint: false },
|
|
31907
|
+
inputSchema: {
|
|
31908
|
+
path: external_exports.string().describe("Absolute path to the local file to upload. Tilde (~) is expanded."),
|
|
31909
|
+
shareClass: external_exports.enum(["PRIVATE", "SHARED"]).describe("Share class (default PRIVATE)").optional(),
|
|
31910
|
+
label: external_exports.string().describe("Display label for the file in OFW (default: filename)").optional(),
|
|
31911
|
+
description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional()
|
|
31912
|
+
}
|
|
31913
|
+
}, async (args) => {
|
|
31914
|
+
const expanded = args.path.startsWith("~/") ? join3(process.env.HOME ?? "", args.path.slice(2)) : args.path;
|
|
31915
|
+
const abs = isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
31916
|
+
const stat = statSync(abs);
|
|
31917
|
+
if (!stat.isFile()) throw new Error(`Not a file: ${abs}`);
|
|
31918
|
+
const buf = readFileSync(abs);
|
|
31919
|
+
const fileName = basename(abs);
|
|
31920
|
+
const mime = mimeFromName(fileName);
|
|
31921
|
+
const form = new FormData();
|
|
31922
|
+
form.append("file", new Blob([new Uint8Array(buf)], { type: mime }), fileName);
|
|
31923
|
+
form.append("source", "message");
|
|
31924
|
+
form.append("description", args.description ?? fileName);
|
|
31925
|
+
form.append("label", args.label ?? fileName);
|
|
31926
|
+
form.append("fileName", fileName);
|
|
31927
|
+
form.append("shareClass", args.shareClass ?? "PRIVATE");
|
|
31928
|
+
const meta3 = await client2.request("POST", "/pub/v3/myfiles/multipart", form);
|
|
31929
|
+
upsertAttachmentForMessage({
|
|
31930
|
+
fileId: meta3.fileId,
|
|
31931
|
+
fileName: meta3.fileName ?? fileName,
|
|
31932
|
+
label: meta3.label ?? args.label ?? fileName,
|
|
31933
|
+
mimeType: meta3.fileType ?? mime,
|
|
31934
|
+
sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes : buf.length,
|
|
31935
|
+
metadata: meta3,
|
|
31936
|
+
messageId: 0
|
|
31937
|
+
});
|
|
31938
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
31939
|
+
fileId: meta3.fileId,
|
|
31940
|
+
fileName: meta3.fileName ?? fileName,
|
|
31941
|
+
mimeType: meta3.fileType ?? mime,
|
|
31942
|
+
sizeBytes: meta3.sizeInBytes ?? buf.length,
|
|
31943
|
+
shareClass: meta3.shareClass ?? args.shareClass ?? "PRIVATE",
|
|
31944
|
+
note: "Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it."
|
|
31945
|
+
}, null, 2) }] };
|
|
31946
|
+
});
|
|
31861
31947
|
server2.registerTool("ofw_download_attachment", {
|
|
31862
31948
|
description: "Download an OFW message attachment by fileId. Bytes are saved to disk; the tool returns the absolute path, mime type, and size so the caller can then read/analyze the file. fileId comes from the attachments array on ofw_get_message. Saves under ~/.cache/ofw-mcp/attachments/<hash>/ by default (override via OFW_ATTACHMENTS_DIR or the saveTo argument). Re-downloading is a no-op if the file is already on disk.",
|
|
31863
31949
|
annotations: { readOnlyHint: false },
|
|
@@ -32075,7 +32161,7 @@ process.emit = function(event, ...args) {
|
|
|
32075
32161
|
}
|
|
32076
32162
|
return originalEmit(event, ...args);
|
|
32077
32163
|
};
|
|
32078
|
-
var server = new McpServer({ name: "ofw", version: "2.0.
|
|
32164
|
+
var server = new McpServer({ name: "ofw", version: "2.0.10" });
|
|
32079
32165
|
registerUserTools(server, client);
|
|
32080
32166
|
registerMessageTools(server, client);
|
|
32081
32167
|
registerCalendarTools(server, client);
|
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.10' });
|
|
21
21
|
registerUserTools(server, client);
|
|
22
22
|
registerMessageTools(server, client);
|
|
23
23
|
registerCalendarTools(server, client);
|
package/dist/tools/messages.js
CHANGED
|
@@ -2,8 +2,35 @@ import { z } from 'zod';
|
|
|
2
2
|
import { syncAll, fetchAttachmentMetaForMessage } from '../sync.js';
|
|
3
3
|
import { listMessages, countMessages, listDrafts, getMessage, upsertMessage, upsertDraft, deleteDraft, findLatestReplyTip, listAttachmentsForMessage, getAttachment, upsertAttachmentForMessage, markAttachmentDownloaded, } from '../cache.js';
|
|
4
4
|
import { getAttachmentsDir } from '../config.js';
|
|
5
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import { dirname, join, isAbsolute, resolve } from 'node:path';
|
|
5
|
+
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { basename, dirname, extname, join, isAbsolute, resolve } from 'node:path';
|
|
7
|
+
// Lightweight mime sniff from extension. OFW re-derives mime from the filename
|
|
8
|
+
// server-side anyway, so this is just a polite Content-Type for the Blob.
|
|
9
|
+
const MIME_BY_EXT = {
|
|
10
|
+
'.pdf': 'application/pdf',
|
|
11
|
+
'.png': 'image/png',
|
|
12
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
13
|
+
'.gif': 'image/gif',
|
|
14
|
+
'.webp': 'image/webp',
|
|
15
|
+
'.heic': 'image/heic',
|
|
16
|
+
'.txt': 'text/plain',
|
|
17
|
+
'.md': 'text/markdown',
|
|
18
|
+
'.csv': 'text/csv',
|
|
19
|
+
'.html': 'text/html', '.htm': 'text/html',
|
|
20
|
+
'.json': 'application/json',
|
|
21
|
+
'.xml': 'application/xml',
|
|
22
|
+
'.doc': 'application/msword',
|
|
23
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
24
|
+
'.xls': 'application/vnd.ms-excel',
|
|
25
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
26
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
27
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
28
|
+
'.zip': 'application/zip',
|
|
29
|
+
'.ics': 'text/calendar',
|
|
30
|
+
};
|
|
31
|
+
function mimeFromName(name) {
|
|
32
|
+
return MIME_BY_EXT[extname(name).toLowerCase()] ?? 'application/octet-stream';
|
|
33
|
+
}
|
|
7
34
|
export function registerMessageTools(server, client) {
|
|
8
35
|
server.registerTool('ofw_list_message_folders', {
|
|
9
36
|
description: 'List OurFamilyWizard message folders (inbox, sent, etc.) and their unread counts. Returns folder IDs needed to call ofw_list_messages. Does NOT return message content.',
|
|
@@ -96,7 +123,7 @@ export function registerMessageTools(server, client) {
|
|
|
96
123
|
return { content: [{ type: 'text', text: JSON.stringify({ ...row, attachments }, null, 2) }] };
|
|
97
124
|
});
|
|
98
125
|
server.registerTool('ofw_send_message', {
|
|
99
|
-
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).',
|
|
126
|
+
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.',
|
|
100
127
|
annotations: { destructiveHint: true },
|
|
101
128
|
inputSchema: {
|
|
102
129
|
subject: z.string().describe('Message subject'),
|
|
@@ -104,6 +131,7 @@ export function registerMessageTools(server, client) {
|
|
|
104
131
|
recipientIds: z.array(z.number()).describe('Array of recipient user IDs (get from ofw_get_profile)'),
|
|
105
132
|
replyToId: z.number().describe('ID of the message being replied to').optional(),
|
|
106
133
|
draftId: z.number().describe('ID of the draft to delete after sending (omit if not sending from a draft)').optional(),
|
|
134
|
+
myFileIDs: z.array(z.number()).describe('Attachment file ids (from ofw_upload_attachment) to attach to the message').optional(),
|
|
107
135
|
},
|
|
108
136
|
}, async (args) => {
|
|
109
137
|
const requestedReplyTo = args.replyToId ?? null;
|
|
@@ -118,11 +146,12 @@ export function registerMessageTools(server, client) {
|
|
|
118
146
|
const parent = getMessage(resolvedReplyTo);
|
|
119
147
|
chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
|
|
120
148
|
}
|
|
149
|
+
const myFileIDs = args.myFileIDs ?? [];
|
|
121
150
|
const data = await client.request('POST', '/pub/v3/messages', {
|
|
122
151
|
subject: args.subject,
|
|
123
152
|
body: args.body,
|
|
124
153
|
recipientIds: args.recipientIds,
|
|
125
|
-
attachments: { myFileIDs
|
|
154
|
+
attachments: { myFileIDs },
|
|
126
155
|
draft: false,
|
|
127
156
|
includeOriginal: resolvedReplyTo !== null,
|
|
128
157
|
replyToId: resolvedReplyTo,
|
|
@@ -145,6 +174,21 @@ export function registerMessageTools(server, client) {
|
|
|
145
174
|
listData: data,
|
|
146
175
|
};
|
|
147
176
|
upsertMessage(row);
|
|
177
|
+
// Link attached files to the new message in the attachments cache.
|
|
178
|
+
// We may not have full metadata if the upload happened in a prior
|
|
179
|
+
// session — fall back to what we know.
|
|
180
|
+
for (const fileId of myFileIDs) {
|
|
181
|
+
const existing = getAttachment(fileId);
|
|
182
|
+
upsertAttachmentForMessage({
|
|
183
|
+
fileId,
|
|
184
|
+
fileName: existing?.fileName ?? `file-${fileId}`,
|
|
185
|
+
label: existing?.label ?? existing?.fileName ?? `file-${fileId}`,
|
|
186
|
+
mimeType: existing?.mimeType ?? 'application/octet-stream',
|
|
187
|
+
sizeBytes: existing?.sizeBytes ?? null,
|
|
188
|
+
metadata: existing?.metadata ?? {},
|
|
189
|
+
messageId: data.id,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
148
192
|
}
|
|
149
193
|
if (args.draftId !== undefined) {
|
|
150
194
|
const form = new FormData();
|
|
@@ -173,7 +217,7 @@ export function registerMessageTools(server, client) {
|
|
|
173
217
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
174
218
|
});
|
|
175
219
|
server.registerTool('ofw_save_draft', {
|
|
176
|
-
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).',
|
|
220
|
+
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.',
|
|
177
221
|
annotations: { readOnlyHint: false },
|
|
178
222
|
inputSchema: {
|
|
179
223
|
subject: z.string().describe('Message subject'),
|
|
@@ -181,6 +225,7 @@ export function registerMessageTools(server, client) {
|
|
|
181
225
|
recipientIds: z.array(z.number()).describe('Array of recipient user IDs (optional for drafts)').optional(),
|
|
182
226
|
messageId: z.number().describe('ID of an existing draft to update (omit to create a new draft)').optional(),
|
|
183
227
|
replyToId: z.number().describe('ID of the message this draft replies to').optional(),
|
|
228
|
+
myFileIDs: z.array(z.number()).describe('Attachment file ids (from ofw_upload_attachment)').optional(),
|
|
184
229
|
},
|
|
185
230
|
}, async (args) => {
|
|
186
231
|
const requestedReplyTo = args.replyToId ?? null;
|
|
@@ -192,11 +237,12 @@ export function registerMessageTools(server, client) {
|
|
|
192
237
|
rewriteNote = `replyToId rewritten from ${requestedReplyTo} to ${resolvedReplyTo} (later reply in same thread found in sent cache).`;
|
|
193
238
|
}
|
|
194
239
|
}
|
|
240
|
+
const myFileIDs = args.myFileIDs ?? [];
|
|
195
241
|
const payload = {
|
|
196
242
|
subject: args.subject,
|
|
197
243
|
body: args.body,
|
|
198
244
|
recipientIds: args.recipientIds ?? [],
|
|
199
|
-
attachments: { myFileIDs
|
|
245
|
+
attachments: { myFileIDs },
|
|
200
246
|
draft: true,
|
|
201
247
|
includeOriginal: resolvedReplyTo !== null,
|
|
202
248
|
replyToId: resolvedReplyTo,
|
|
@@ -265,6 +311,58 @@ export function registerMessageTools(server, client) {
|
|
|
265
311
|
}
|
|
266
312
|
return { content: [{ type: 'text', text: JSON.stringify(unread, null, 2) }] };
|
|
267
313
|
});
|
|
314
|
+
server.registerTool('ofw_upload_attachment', {
|
|
315
|
+
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.',
|
|
316
|
+
annotations: { destructiveHint: false },
|
|
317
|
+
inputSchema: {
|
|
318
|
+
path: z.string().describe('Absolute path to the local file to upload. Tilde (~) is expanded.'),
|
|
319
|
+
shareClass: z.enum(['PRIVATE', 'SHARED']).describe('Share class (default PRIVATE)').optional(),
|
|
320
|
+
label: z.string().describe('Display label for the file in OFW (default: filename)').optional(),
|
|
321
|
+
description: z.string().describe('Description shown in OFW My Files (default: filename)').optional(),
|
|
322
|
+
},
|
|
323
|
+
}, async (args) => {
|
|
324
|
+
// Resolve and read the local file
|
|
325
|
+
const expanded = args.path.startsWith('~/')
|
|
326
|
+
? join(process.env.HOME ?? '', args.path.slice(2))
|
|
327
|
+
: args.path;
|
|
328
|
+
const abs = isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
329
|
+
const stat = statSync(abs); // throws if missing
|
|
330
|
+
if (!stat.isFile())
|
|
331
|
+
throw new Error(`Not a file: ${abs}`);
|
|
332
|
+
const buf = readFileSync(abs);
|
|
333
|
+
const fileName = basename(abs);
|
|
334
|
+
const mime = mimeFromName(fileName);
|
|
335
|
+
// Build the multipart payload matching the OFW web UI's request shape
|
|
336
|
+
const form = new FormData();
|
|
337
|
+
form.append('file', new Blob([new Uint8Array(buf)], { type: mime }), fileName);
|
|
338
|
+
form.append('source', 'message');
|
|
339
|
+
form.append('description', args.description ?? fileName);
|
|
340
|
+
form.append('label', args.label ?? fileName);
|
|
341
|
+
form.append('fileName', fileName);
|
|
342
|
+
form.append('shareClass', args.shareClass ?? 'PRIVATE');
|
|
343
|
+
const meta = await client.request('POST', '/pub/v3/myfiles/multipart', form);
|
|
344
|
+
// Cache the metadata so subsequent ofw_get_message calls can surface it
|
|
345
|
+
// and ofw_download_attachment short-circuits if asked. messageId is 0
|
|
346
|
+
// because no message references this yet — it'll be linked once a
|
|
347
|
+
// message is sent with this fileId in its attachments.
|
|
348
|
+
upsertAttachmentForMessage({
|
|
349
|
+
fileId: meta.fileId,
|
|
350
|
+
fileName: meta.fileName ?? fileName,
|
|
351
|
+
label: meta.label ?? args.label ?? fileName,
|
|
352
|
+
mimeType: meta.fileType ?? mime,
|
|
353
|
+
sizeBytes: typeof meta.sizeInBytes === 'number' ? meta.sizeInBytes : buf.length,
|
|
354
|
+
metadata: meta,
|
|
355
|
+
messageId: 0,
|
|
356
|
+
});
|
|
357
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
358
|
+
fileId: meta.fileId,
|
|
359
|
+
fileName: meta.fileName ?? fileName,
|
|
360
|
+
mimeType: meta.fileType ?? mime,
|
|
361
|
+
sizeBytes: meta.sizeInBytes ?? buf.length,
|
|
362
|
+
shareClass: meta.shareClass ?? args.shareClass ?? 'PRIVATE',
|
|
363
|
+
note: 'Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it.',
|
|
364
|
+
}, null, 2) }] };
|
|
365
|
+
});
|
|
268
366
|
server.registerTool('ofw_download_attachment', {
|
|
269
367
|
description: 'Download an OFW message attachment by fileId. Bytes are saved to disk; the tool returns the absolute path, mime type, and size so the caller can then read/analyze the file. fileId comes from the attachments array on ofw_get_message. Saves under ~/.cache/ofw-mcp/attachments/<hash>/ by default (override via OFW_ATTACHMENTS_DIR or the saveTo argument). Re-downloading is a no-op if the file is already on disk.',
|
|
270
368
|
annotations: { readOnlyHint: false },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
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>",
|
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.10",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.0.
|
|
14
|
+
"version": "2.0.10",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|