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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "OurFamilyWizard tools for Claude Code",
9
- "version": "2.0.9"
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.9",
17
+ "version": "2.0.10",
18
18
  "author": {
19
19
  "name": "Chris Chall"
20
20
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ofw",
3
3
  "displayName": "OurFamilyWizard",
4
- "version": "2.0.9",
4
+ "version": "2.0.10",
5
5
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall"
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.9" });
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.9' });
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);
@@ -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.9",
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",
9
+ "version": "2.0.10",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ofw-mcp",
14
- "version": "2.0.9",
14
+ "version": "2.0.10",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },