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.
@@ -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, isAbsolute, resolve } from 'node:path';
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 { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
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
- content: [{
81
- type: 'text',
82
- text: JSON.stringify({
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 { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify({ ...cached, attachments }, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify({ ...row, attachments }, null, 2) }] };
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
- const form = new FormData();
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
- const finalText = rewriteNote ? `${rewriteNote}\n\n${text}` : text;
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 { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
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 ?? []).map((r) => ({
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
- const finalText = rewriteNote ? `${rewriteNote}\n\n${text}` : text;
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 form = new FormData();
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 { content: [{ type: 'text', text: data ? JSON.stringify(data, null, 2) : 'Draft deleted.' }] };
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 { content: [{ type: 'text', text: JSON.stringify({
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 { content: [{ type: 'text', text: JSON.stringify({
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 { content: [{ type: 'text', text: JSON.stringify(unread, null, 2) }] };
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
- // Resolve and read the local file
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 the metadata so subsequent ofw_get_message calls can surface it
378
- // and ofw_download_attachment short-circuits if asked. messageId is 0
379
- // because no message references this yet — it'll be linked once a
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 { content: [{ type: 'text', text: JSON.stringify({
391
- fileId: meta.fileId,
392
- fileName: meta.fileName ?? fileName,
393
- mimeType: meta.fileType ?? mime,
394
- sizeBytes: meta.sizeInBytes ?? buf.length,
395
- shareClass: meta.shareClass ?? args.shareClass ?? 'PRIVATE',
396
- note: 'Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it.',
397
- }, null, 2) }] };
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. 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.',
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
- saveTo: z.string().describe('Absolute path or directory to write to. If a directory, the OFW filename is used. Default: ~/.cache/ofw-mcp/attachments/<hash>/<fileId>-<filename>').optional(),
405
- force: z.boolean().describe('Re-download even if already on disk. Default false.').optional(),
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
- // Metadata not in cache fetch on the fly.
412
- const meta = await client.request('GET', `/pub/v1/myfiles/${fileId}`);
413
- // Store with a sentinel "metadata-only, no message link" — we don't know which message asked.
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
- // Decide destination path
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
- const expanded = args.saveTo.startsWith('~/')
432
- ? join(process.env.HOME ?? '', args.saveTo.slice(2))
433
- : args.saveTo;
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 { content: [{ type: 'text', text: JSON.stringify({
445
- fileId, path: dest, mimeType: cached.mimeType, sizeBytes: cached.sizeBytes,
446
- fileName: cached.fileName, note: 'already downloaded',
447
- }, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify({
455
- fileId,
456
- path: dest,
457
- mimeType: response.contentType ?? cached.mimeType,
458
- sizeBytes: response.body.length,
459
- fileName: response.suggestedFileName ?? cached.fileName,
460
- }, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
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
+ }
@@ -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 { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
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.11",
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.5.2",
36
- "@vitest/coverage-v8": "^4.1.2",
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.2"
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.11",
9
+ "version": "2.0.13",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ofw-mcp",
14
- "version": "2.0.11",
14
+ "version": "2.0.13",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },