ofw-mcp 2.0.1 → 2.0.3
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/dist/bundle.js +12658 -3553
- package/dist/index.js +12 -47
- package/dist/tools/calendar.js +47 -77
- package/dist/tools/expenses.js +23 -44
- package/dist/tools/journal.js +19 -37
- package/dist/tools/messages.js +125 -190
- package/dist/tools/user.js +11 -23
- package/package.json +5 -4
package/dist/tools/messages.js
CHANGED
|
@@ -1,224 +1,159 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerMessageTools(server, client) {
|
|
3
|
+
server.registerTool('ofw_list_message_folders', {
|
|
4
4
|
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.',
|
|
5
5
|
annotations: { readOnlyHint: true },
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
}, async () => {
|
|
7
|
+
const data = await client.request('GET', '/pub/v1/messageFolders?includeFolderCounts=true');
|
|
8
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
9
|
+
});
|
|
10
|
+
server.registerTool('ofw_list_messages', {
|
|
10
11
|
description: 'List messages in an OurFamilyWizard folder. Call ofw_list_message_folders first to get folder IDs. Returns actual message content.',
|
|
11
12
|
annotations: { readOnlyHint: true },
|
|
12
13
|
inputSchema: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
page: { type: 'number', description: 'Page number (default 1)' },
|
|
17
|
-
size: { type: 'number', description: 'Messages per page (default 50)' },
|
|
18
|
-
},
|
|
19
|
-
required: ['folderId'],
|
|
14
|
+
folderId: z.string().describe('Folder ID (get from ofw_list_message_folders)'),
|
|
15
|
+
page: z.number().describe('Page number (default 1)').optional(),
|
|
16
|
+
size: z.number().describe('Messages per page (default 50)').optional(),
|
|
20
17
|
},
|
|
21
|
-
},
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
}, async (args) => {
|
|
19
|
+
const page = args.page ?? 1;
|
|
20
|
+
const size = args.size ?? 50;
|
|
21
|
+
const path = `/pub/v3/messages?folders=${encodeURIComponent(args.folderId)}&page=${page}&size=${size}&sort=date&sortDirection=desc`;
|
|
22
|
+
const data = await client.request('GET', path);
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
24
|
+
});
|
|
25
|
+
server.registerTool('ofw_get_message', {
|
|
24
26
|
description: 'Get a single OurFamilyWizard message by ID. Note: reading an unread message marks it as read.',
|
|
25
27
|
annotations: { readOnlyHint: false },
|
|
26
28
|
inputSchema: {
|
|
27
|
-
|
|
28
|
-
properties: {
|
|
29
|
-
messageId: { type: 'string', description: 'Message ID' },
|
|
30
|
-
},
|
|
31
|
-
required: ['messageId'],
|
|
29
|
+
messageId: z.string().describe('Message ID'),
|
|
32
30
|
},
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
}, async (args) => {
|
|
32
|
+
const data = await client.request('GET', `/pub/v3/messages/${encodeURIComponent(args.messageId)}`);
|
|
33
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
34
|
+
});
|
|
35
|
+
server.registerTool('ofw_send_message', {
|
|
36
36
|
description: 'Send a message via OurFamilyWizard. If sending from a draft, pass draftId to automatically delete the draft after sending.',
|
|
37
37
|
annotations: { destructiveHint: true },
|
|
38
38
|
inputSchema: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
type: 'array',
|
|
45
|
-
items: { type: 'number' },
|
|
46
|
-
description: 'Array of recipient user IDs (get from ofw_get_profile)',
|
|
47
|
-
},
|
|
48
|
-
replyToId: {
|
|
49
|
-
type: 'number',
|
|
50
|
-
description: 'ID of the message being replied to. When provided, the original message thread is included (like a standard email reply).',
|
|
51
|
-
},
|
|
52
|
-
draftId: {
|
|
53
|
-
type: 'number',
|
|
54
|
-
description: 'ID of the draft to delete after sending (omit if not sending from a draft)',
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
required: ['subject', 'body', 'recipientIds'],
|
|
39
|
+
subject: z.string().describe('Message subject'),
|
|
40
|
+
body: z.string().describe('Message body text'),
|
|
41
|
+
recipientIds: z.array(z.number()).describe('Array of recipient user IDs (get from ofw_get_profile)'),
|
|
42
|
+
replyToId: z.number().describe('ID of the message being replied to. When provided, the original message thread is included (like a standard email reply).').optional(),
|
|
43
|
+
draftId: z.number().describe('ID of the draft to delete after sending (omit if not sending from a draft)').optional(),
|
|
58
44
|
},
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
|
|
45
|
+
}, async (args) => {
|
|
46
|
+
const replyToId = args.replyToId ?? null;
|
|
47
|
+
const data = await client.request('POST', '/pub/v3/messages', {
|
|
48
|
+
subject: args.subject,
|
|
49
|
+
body: args.body,
|
|
50
|
+
recipientIds: args.recipientIds,
|
|
51
|
+
attachments: { myFileIDs: [] },
|
|
52
|
+
draft: false,
|
|
53
|
+
includeOriginal: replyToId !== null,
|
|
54
|
+
replyToId,
|
|
55
|
+
});
|
|
56
|
+
if (args.draftId !== undefined) {
|
|
57
|
+
const form = new FormData();
|
|
58
|
+
form.append('messageIds', String(args.draftId));
|
|
59
|
+
await client.request('DELETE', '/pub/v1/messages', form);
|
|
60
|
+
}
|
|
61
|
+
return { content: [{ type: 'text', text: data ? JSON.stringify(data, null, 2) : 'Message sent successfully.' }] };
|
|
62
|
+
});
|
|
63
|
+
server.registerTool('ofw_list_drafts', {
|
|
62
64
|
description: 'List all draft messages in OurFamilyWizard',
|
|
63
65
|
annotations: { readOnlyHint: true },
|
|
64
66
|
inputSchema: {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
page: { type: 'number', description: 'Page number (default 1)' },
|
|
68
|
-
size: { type: 'number', description: 'Drafts per page (default 50)' },
|
|
69
|
-
},
|
|
70
|
-
required: [],
|
|
67
|
+
page: z.number().describe('Page number (default 1)').optional(),
|
|
68
|
+
size: z.number().describe('Drafts per page (default 50)').optional(),
|
|
71
69
|
},
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
}, async (args) => {
|
|
71
|
+
const page = args.page ?? 1;
|
|
72
|
+
const size = args.size ?? 50;
|
|
73
|
+
// 13471259 is the system Drafts folder (folderType: DRAFTS)
|
|
74
|
+
const path = `/pub/v3/messages?folders=13471259&page=${page}&size=${size}&sort=date&sortDirection=desc`;
|
|
75
|
+
const data = await client.request('GET', path);
|
|
76
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
77
|
+
});
|
|
78
|
+
server.registerTool('ofw_save_draft', {
|
|
75
79
|
description: 'Save a message as a draft in OurFamilyWizard. Recipients are optional — a draft can be saved without them. To update an existing draft, provide its messageId.',
|
|
76
80
|
annotations: { readOnlyHint: false },
|
|
77
81
|
inputSchema: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
type: 'array',
|
|
84
|
-
items: { type: 'number' },
|
|
85
|
-
description: 'Array of recipient user IDs (optional for drafts)',
|
|
86
|
-
},
|
|
87
|
-
messageId: {
|
|
88
|
-
type: 'number',
|
|
89
|
-
description: 'ID of an existing draft to update (omit to create a new draft)',
|
|
90
|
-
},
|
|
91
|
-
replyToId: {
|
|
92
|
-
type: 'number',
|
|
93
|
-
description: 'ID of the message this draft is replying to (omit for new messages)',
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
required: ['subject', 'body'],
|
|
82
|
+
subject: z.string().describe('Message subject'),
|
|
83
|
+
body: z.string().describe('Message body text'),
|
|
84
|
+
recipientIds: z.array(z.number()).describe('Array of recipient user IDs (optional for drafts)').optional(),
|
|
85
|
+
messageId: z.number().describe('ID of an existing draft to update (omit to create a new draft)').optional(),
|
|
86
|
+
replyToId: z.number().describe('ID of the message this draft is replying to (omit for new messages)').optional(),
|
|
97
87
|
},
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
|
|
88
|
+
}, async (args) => {
|
|
89
|
+
const replyToId = args.replyToId ?? null;
|
|
90
|
+
const payload = {
|
|
91
|
+
subject: args.subject,
|
|
92
|
+
body: args.body,
|
|
93
|
+
recipientIds: args.recipientIds ?? [],
|
|
94
|
+
attachments: { myFileIDs: [] },
|
|
95
|
+
draft: true,
|
|
96
|
+
includeOriginal: replyToId !== null,
|
|
97
|
+
replyToId,
|
|
98
|
+
};
|
|
99
|
+
if (args.messageId !== undefined)
|
|
100
|
+
payload.messageId = args.messageId;
|
|
101
|
+
const data = await client.request('POST', '/pub/v3/messages', payload);
|
|
102
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
103
|
+
});
|
|
104
|
+
server.registerTool('ofw_delete_draft', {
|
|
101
105
|
description: 'Delete a draft message from OurFamilyWizard',
|
|
102
106
|
annotations: { destructiveHint: true },
|
|
103
107
|
inputSchema: {
|
|
104
|
-
|
|
105
|
-
properties: {
|
|
106
|
-
messageId: { type: 'number', description: 'Draft message ID to delete' },
|
|
107
|
-
},
|
|
108
|
-
required: ['messageId'],
|
|
108
|
+
messageId: z.number().describe('Draft message ID to delete'),
|
|
109
109
|
},
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
}, async (args) => {
|
|
111
|
+
const form = new FormData();
|
|
112
|
+
form.append('messageIds', String(args.messageId));
|
|
113
|
+
const data = await client.request('DELETE', '/pub/v1/messages', form);
|
|
114
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
115
|
+
});
|
|
116
|
+
server.registerTool('ofw_get_unread_sent', {
|
|
113
117
|
description: 'List sent messages that have not been read by one or more recipients. Fetches sent messages page by page and returns only those with unread recipients.',
|
|
114
118
|
annotations: { readOnlyHint: true },
|
|
115
119
|
inputSchema: {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
page: { type: 'number', description: 'Page of sent messages to scan (default 1)' },
|
|
119
|
-
size: { type: 'number', description: 'Number of sent messages per page, max 50 (default 20)' },
|
|
120
|
-
},
|
|
121
|
-
required: [],
|
|
120
|
+
page: z.number().describe('Page of sent messages to scan (default 1)').optional(),
|
|
121
|
+
size: z.number().describe('Number of sent messages per page, max 50 (default 20)').optional(),
|
|
122
122
|
},
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const form = new FormData();
|
|
153
|
-
form.append('messageIds', String(draftId));
|
|
154
|
-
await client.request('DELETE', '/pub/v1/messages', form);
|
|
123
|
+
}, async (args) => {
|
|
124
|
+
const page = args.page ?? 1;
|
|
125
|
+
const size = args.size ?? 20;
|
|
126
|
+
// Step 1: find the sent folder
|
|
127
|
+
const foldersData = await client.request('GET', '/pub/v1/messageFolders?includeFolderCounts=true');
|
|
128
|
+
const sentFolder = (foldersData.systemFolders ?? []).find((f) => f.folderType === 'SENT_MESSAGES');
|
|
129
|
+
if (!sentFolder)
|
|
130
|
+
throw new Error('Sent folder not found');
|
|
131
|
+
// Step 2: list sent messages (the list endpoint already includes per-recipient viewed status)
|
|
132
|
+
const listPath = `/pub/v3/messages?folders=${encodeURIComponent(sentFolder.id)}&page=${page}&size=${size}&sort=date&sortDirection=desc`;
|
|
133
|
+
const listData = await client.request('GET', listPath);
|
|
134
|
+
const messages = listData.data ?? [];
|
|
135
|
+
// Step 3: filter to unread using showNeverViewed (avoids N+1 detail fetches
|
|
136
|
+
// and the detail endpoint's inconsistent viewed field which can return null
|
|
137
|
+
// for read messages instead of the epoch sentinel the list endpoint uses)
|
|
138
|
+
const unread = [];
|
|
139
|
+
for (const msg of messages) {
|
|
140
|
+
if (!msg.showNeverViewed)
|
|
141
|
+
continue;
|
|
142
|
+
const unreadRecipients = (msg.recipients ?? [])
|
|
143
|
+
.filter((r) => !r.viewed)
|
|
144
|
+
.map((r) => r.user.name);
|
|
145
|
+
if (unreadRecipients.length > 0) {
|
|
146
|
+
unread.push({
|
|
147
|
+
id: msg.id,
|
|
148
|
+
subject: msg.subject,
|
|
149
|
+
sentAt: msg.date.dateTime,
|
|
150
|
+
unreadBy: unreadRecipients,
|
|
151
|
+
});
|
|
155
152
|
}
|
|
156
|
-
return { content: [{ type: 'text', text: data ? JSON.stringify(data, null, 2) : 'Message sent successfully.' }] };
|
|
157
|
-
}
|
|
158
|
-
case 'ofw_list_drafts': {
|
|
159
|
-
const { page = 1, size = 50 } = args;
|
|
160
|
-
// 13471259 is the system Drafts folder (folderType: DRAFTS)
|
|
161
|
-
const path = `/pub/v3/messages?folders=13471259&page=${page}&size=${size}&sort=date&sortDirection=desc`;
|
|
162
|
-
const data = await client.request('GET', path);
|
|
163
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
164
153
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const payload = {
|
|
168
|
-
subject, body, recipientIds,
|
|
169
|
-
attachments: { myFileIDs: [] },
|
|
170
|
-
draft: true,
|
|
171
|
-
includeOriginal: replyToId !== null,
|
|
172
|
-
replyToId,
|
|
173
|
-
};
|
|
174
|
-
if (messageId !== undefined)
|
|
175
|
-
payload.messageId = messageId;
|
|
176
|
-
const data = await client.request('POST', '/pub/v3/messages', payload);
|
|
177
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
178
|
-
}
|
|
179
|
-
case 'ofw_delete_draft': {
|
|
180
|
-
const { messageId } = args;
|
|
181
|
-
const form = new FormData();
|
|
182
|
-
form.append('messageIds', String(messageId));
|
|
183
|
-
const data = await client.request('DELETE', '/pub/v1/messages', form);
|
|
184
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
185
|
-
}
|
|
186
|
-
case 'ofw_get_unread_sent': {
|
|
187
|
-
const { page = 1, size = 20 } = args;
|
|
188
|
-
// Step 1: find the sent folder
|
|
189
|
-
const foldersData = await client.request('GET', '/pub/v1/messageFolders?includeFolderCounts=true');
|
|
190
|
-
const sentFolder = (foldersData.systemFolders ?? []).find((f) => f.folderType === 'SENT_MESSAGES');
|
|
191
|
-
if (!sentFolder)
|
|
192
|
-
throw new Error('Sent folder not found');
|
|
193
|
-
// Step 2: list sent messages (the list endpoint already includes per-recipient viewed status)
|
|
194
|
-
const listPath = `/pub/v3/messages?folders=${encodeURIComponent(sentFolder.id)}&page=${page}&size=${size}&sort=date&sortDirection=desc`;
|
|
195
|
-
const listData = await client.request('GET', listPath);
|
|
196
|
-
const messages = listData.data ?? [];
|
|
197
|
-
// Step 3: filter to unread using showNeverViewed (avoids N+1 detail fetches
|
|
198
|
-
// and the detail endpoint's inconsistent viewed field which can return null
|
|
199
|
-
// for read messages instead of the epoch sentinel the list endpoint uses)
|
|
200
|
-
const unread = [];
|
|
201
|
-
for (const msg of messages) {
|
|
202
|
-
if (!msg.showNeverViewed)
|
|
203
|
-
continue;
|
|
204
|
-
const unreadRecipients = (msg.recipients ?? [])
|
|
205
|
-
.filter((r) => !r.viewed)
|
|
206
|
-
.map((r) => r.user.name);
|
|
207
|
-
if (unreadRecipients.length > 0) {
|
|
208
|
-
unread.push({
|
|
209
|
-
id: msg.id,
|
|
210
|
-
subject: msg.subject,
|
|
211
|
-
sentAt: msg.date.dateTime,
|
|
212
|
-
unreadBy: unreadRecipients,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (unread.length === 0) {
|
|
217
|
-
return { content: [{ type: 'text', text: JSON.stringify({ message: 'All scanned sent messages have been read.' }, null, 2) }] };
|
|
218
|
-
}
|
|
219
|
-
return { content: [{ type: 'text', text: JSON.stringify(unread, null, 2) }] };
|
|
154
|
+
if (unread.length === 0) {
|
|
155
|
+
return { content: [{ type: 'text', text: JSON.stringify({ message: 'All scanned sent messages have been read.' }, null, 2) }] };
|
|
220
156
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
157
|
+
return { content: [{ type: 'text', text: JSON.stringify(unread, null, 2) }] };
|
|
158
|
+
});
|
|
224
159
|
}
|
package/dist/tools/user.js
CHANGED
|
@@ -1,28 +1,16 @@
|
|
|
1
|
-
export
|
|
2
|
-
{
|
|
3
|
-
name: 'ofw_get_profile',
|
|
1
|
+
export function registerUserTools(server, client) {
|
|
2
|
+
server.registerTool('ofw_get_profile', {
|
|
4
3
|
description: 'Get current user and co-parent profile information from OurFamilyWizard',
|
|
5
4
|
annotations: { readOnlyHint: true },
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
}, async () => {
|
|
6
|
+
const data = await client.request('GET', '/pub/v2/profiles');
|
|
7
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
8
|
+
});
|
|
9
|
+
server.registerTool('ofw_get_notifications', {
|
|
10
10
|
description: 'Get OurFamilyWizard dashboard summary: unread message count, upcoming events, outstanding expenses. Note: updates your last-seen status.',
|
|
11
11
|
annotations: { readOnlyHint: false },
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
switch (name) {
|
|
17
|
-
case 'ofw_get_profile': {
|
|
18
|
-
const data = await client.request('GET', '/pub/v2/profiles');
|
|
19
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
20
|
-
}
|
|
21
|
-
case 'ofw_get_notifications': {
|
|
22
|
-
const data = await client.request('GET', '/pub/v1/users/useraccountstatus');
|
|
23
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
24
|
-
}
|
|
25
|
-
default:
|
|
26
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
27
|
-
}
|
|
12
|
+
}, async () => {
|
|
13
|
+
const data = await client.request('GET', '/pub/v1/users/useraccountstatus');
|
|
14
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
15
|
+
});
|
|
28
16
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Sonnet 4.6)",
|
|
5
5
|
"author": "Claude Sonnet 4.6 (AI) <https://www.anthropic.com/claude>",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/chrischall/ofw-mcp"
|
|
8
|
+
"url": "git+https://github.com/chrischall/ofw-mcp.git"
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"bin": {
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
-
"dotenv": "^17.4.0"
|
|
29
|
+
"dotenv": "^17.4.0",
|
|
30
|
+
"zod": "^4.3.6"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^25.5.2",
|
|
33
34
|
"@vitest/coverage-v8": "^4.1.2",
|
|
34
|
-
"esbuild": "^0.
|
|
35
|
+
"esbuild": "^0.28.0",
|
|
35
36
|
"typescript": "^6.0.2",
|
|
36
37
|
"vitest": "^4.1.2"
|
|
37
38
|
}
|