ofw-mcp 2.0.19 → 2.2.0
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/auth.js +6 -0
- package/dist/bundle.js +766 -135
- package/dist/client.js +46 -6
- package/dist/index.js +1 -1
- package/dist/tools/messages.js +55 -14
- package/package.json +9 -8
- package/server.json +2 -2
package/dist/client.js
CHANGED
|
@@ -40,6 +40,19 @@ function redactHeaders(h) {
|
|
|
40
40
|
out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}…`;
|
|
41
41
|
return out;
|
|
42
42
|
}
|
|
43
|
+
// Per-request timeout. Overridable via OFW_REQUEST_TIMEOUT_MS. The default
|
|
44
|
+
// (30s) is comfortably above OFW's typical p99 but low enough that a stuck
|
|
45
|
+
// upstream fails fast instead of burning the MCP client-side budget — which
|
|
46
|
+
// is what produced the multi-minute hangs we've seen on ofw_list_messages
|
|
47
|
+
// and ofw_save_draft. Each retry (401/429 replay) gets its own fresh window.
|
|
48
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
49
|
+
function getRequestTimeoutMs() {
|
|
50
|
+
const raw = process.env.OFW_REQUEST_TIMEOUT_MS;
|
|
51
|
+
if (typeof raw !== 'string' || raw.trim().length === 0)
|
|
52
|
+
return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
53
|
+
const n = Number(raw.trim());
|
|
54
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
55
|
+
}
|
|
43
56
|
export class OFWClient {
|
|
44
57
|
token = null;
|
|
45
58
|
tokenExpiry = null;
|
|
@@ -85,13 +98,40 @@ export class OFWClient {
|
|
|
85
98
|
console.error(`[ofw-debug] headers: ${JSON.stringify(redactHeaders(headers))}`);
|
|
86
99
|
console.error(`[ofw-debug] body: ${bodyPreview}`);
|
|
87
100
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
// AbortController + setTimeout (not AbortSignal.timeout) so vitest fake
|
|
102
|
+
// timers can drive the timeout in tests, and so we can attach a clear
|
|
103
|
+
// error message instead of a bare DOMException on the abort path.
|
|
104
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
105
|
+
const ac = new AbortController();
|
|
106
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
107
|
+
const startedAt = Date.now();
|
|
108
|
+
let response;
|
|
109
|
+
try {
|
|
110
|
+
response = await fetch(url, {
|
|
111
|
+
method,
|
|
112
|
+
headers,
|
|
113
|
+
signal: ac.signal,
|
|
114
|
+
...(body !== undefined ? { body: isFormData ? body : JSON.stringify(body) } : {}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const elapsed = Date.now() - startedAt;
|
|
119
|
+
if (ac.signal.aborted) {
|
|
120
|
+
if (debugLogEnabled()) {
|
|
121
|
+
console.error(`[ofw-debug] ⏱ TIMEOUT after ${elapsed}ms: ${method} ${url}`);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`OFW API request timed out after ${timeoutMs}ms: ${method} ${path}`);
|
|
124
|
+
}
|
|
125
|
+
if (debugLogEnabled()) {
|
|
126
|
+
console.error(`[ofw-debug] ✗ ${err.message} after ${elapsed}ms: ${method} ${url}`);
|
|
127
|
+
}
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
}
|
|
93
133
|
if (debugLogEnabled()) {
|
|
94
|
-
console.error(`[ofw-debug] ← ${response.status} ${response.statusText}`);
|
|
134
|
+
console.error(`[ofw-debug] ← ${response.status} ${response.statusText} (${Date.now() - startedAt}ms)`);
|
|
95
135
|
}
|
|
96
136
|
if (response.status === 401 && !isRetry) {
|
|
97
137
|
this.token = null;
|
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.2.0' }); // x-release-please-version
|
|
21
21
|
registerUserTools(server, client);
|
|
22
22
|
registerMessageTools(server, client);
|
|
23
23
|
registerCalendarTools(server, client);
|
package/dist/tools/messages.js
CHANGED
|
@@ -175,18 +175,59 @@ export function registerMessageTools(server, client) {
|
|
|
175
175
|
return jsonResponse({ ...row, attachments });
|
|
176
176
|
});
|
|
177
177
|
server.registerTool('ofw_send_message', {
|
|
178
|
-
description: 'Send a message via OurFamilyWizard.
|
|
178
|
+
description: 'Send a message via OurFamilyWizard. To send an existing draft, pass messageId — subject/body/recipientIds become optional overrides (missing fields default to the draft\'s cached values) and the draft is deleted after sending. To send a fresh message, supply subject/body/recipientIds directly. draftId is the legacy spelling of messageId and works the same way. 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. After sending, the tool re-fetches the message from OFW to populate the local cache and link attachments to the new message id.',
|
|
179
179
|
annotations: { destructiveHint: true },
|
|
180
180
|
inputSchema: {
|
|
181
|
-
subject: z.string().describe('Message subject'),
|
|
182
|
-
body: z.string().describe('Message body text'),
|
|
183
|
-
recipientIds: z.array(z.number()).describe('Array of recipient user IDs (get from ofw_get_profile)'),
|
|
181
|
+
subject: z.string().describe('Message subject. Required unless messageId/draftId references a cached draft.').optional(),
|
|
182
|
+
body: z.string().describe('Message body text. Required unless messageId/draftId references a cached draft.').optional(),
|
|
183
|
+
recipientIds: z.array(z.number()).describe('Array of recipient user IDs (get from ofw_get_profile). Required unless messageId/draftId references a cached draft.').optional(),
|
|
184
184
|
replyToId: z.number().describe('ID of the message being replied to').optional(),
|
|
185
|
-
|
|
185
|
+
messageId: z.number().describe('ID of an existing draft to send. When set, missing subject/body/recipientIds default to the draft\'s cached values, and the draft is deleted after sending.').optional(),
|
|
186
|
+
draftId: z.number().describe('Legacy synonym for messageId. If both are passed they must be equal.').optional(),
|
|
186
187
|
myFileIDs: z.array(z.number()).describe('Attachment file ids (from ofw_upload_attachment) to attach to the message').optional(),
|
|
187
188
|
},
|
|
188
189
|
}, async (args) => {
|
|
189
|
-
|
|
190
|
+
if (args.messageId !== undefined && args.draftId !== undefined && args.messageId !== args.draftId) {
|
|
191
|
+
throw new Error(`messageId (${args.messageId}) and draftId (${args.draftId}) refer to different drafts; pass only one.`);
|
|
192
|
+
}
|
|
193
|
+
const draftRef = args.messageId ?? args.draftId;
|
|
194
|
+
// Best-effort draft lookup: when draftRef points at a cached draft, use
|
|
195
|
+
// its stored fields (including replyToId) as defaults for anything the
|
|
196
|
+
// caller didn't supply. The "missing draft" case only matters when we
|
|
197
|
+
// actually NEED the defaults — a caller passing all fields explicitly
|
|
198
|
+
// can use draftId as a pure delete-target even on an empty cache.
|
|
199
|
+
let subject = args.subject;
|
|
200
|
+
let body = args.body;
|
|
201
|
+
let recipientIds = args.recipientIds;
|
|
202
|
+
let draftReplyToId = null;
|
|
203
|
+
let draftLookupAttempted = false;
|
|
204
|
+
let draftFound = false;
|
|
205
|
+
if (draftRef !== undefined) {
|
|
206
|
+
draftLookupAttempted = true;
|
|
207
|
+
const draft = getDraft(draftRef);
|
|
208
|
+
if (draft !== null) {
|
|
209
|
+
draftFound = true;
|
|
210
|
+
subject = subject ?? draft.subject;
|
|
211
|
+
body = body ?? draft.body;
|
|
212
|
+
recipientIds = recipientIds ?? draft.recipients.map((r) => r.userId);
|
|
213
|
+
draftReplyToId = draft.replyToId;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (subject === undefined || body === undefined || recipientIds === undefined) {
|
|
217
|
+
if (draftLookupAttempted && !draftFound) {
|
|
218
|
+
throw new Error(`draft ${draftRef} not found in local cache. Call ofw_sync_messages first, or supply subject/body/recipientIds explicitly.`);
|
|
219
|
+
}
|
|
220
|
+
const missing = [
|
|
221
|
+
subject === undefined ? 'subject' : null,
|
|
222
|
+
body === undefined ? 'body' : null,
|
|
223
|
+
recipientIds === undefined ? 'recipientIds' : null,
|
|
224
|
+
].filter((n) => n !== null).join(', ');
|
|
225
|
+
throw new Error(`ofw_send_message requires ${missing}. Pass it directly, or pass messageId to default missing fields from a cached draft.`);
|
|
226
|
+
}
|
|
227
|
+
// Inherit the draft's replyToId when the caller didn't supply one. A
|
|
228
|
+
// reply-draft saved with replyToId would otherwise be sent as a
|
|
229
|
+
// top-level message — silently losing the thread.
|
|
230
|
+
const requestedReplyTo = args.replyToId ?? draftReplyToId ?? null;
|
|
190
231
|
let resolvedReplyTo = requestedReplyTo;
|
|
191
232
|
let chainRootId = null;
|
|
192
233
|
let rewriteNote = null;
|
|
@@ -200,9 +241,9 @@ export function registerMessageTools(server, client) {
|
|
|
200
241
|
}
|
|
201
242
|
const myFileIDs = args.myFileIDs ?? [];
|
|
202
243
|
const { id: newId, detail, raw } = await postMessageAndRefetch(client, {
|
|
203
|
-
subject
|
|
204
|
-
body
|
|
205
|
-
recipientIds
|
|
244
|
+
subject,
|
|
245
|
+
body,
|
|
246
|
+
recipientIds,
|
|
206
247
|
attachments: { myFileIDs },
|
|
207
248
|
draft: false,
|
|
208
249
|
includeOriginal: resolvedReplyTo !== null,
|
|
@@ -213,11 +254,11 @@ export function registerMessageTools(server, client) {
|
|
|
213
254
|
persisted = {
|
|
214
255
|
id: newId,
|
|
215
256
|
folder: 'sent',
|
|
216
|
-
subject: detail.subject ??
|
|
257
|
+
subject: detail.subject ?? subject,
|
|
217
258
|
fromUser: detail.from?.name ?? '',
|
|
218
259
|
sentAt: detail.date?.dateTime ?? new Date().toISOString(),
|
|
219
260
|
recipients: mapRecipients(detail.recipients),
|
|
220
|
-
body: detail.body ??
|
|
261
|
+
body: detail.body ?? body,
|
|
221
262
|
fetchedBodyAt: new Date().toISOString(),
|
|
222
263
|
replyToId: resolvedReplyTo,
|
|
223
264
|
chainRootId,
|
|
@@ -240,9 +281,9 @@ export function registerMessageTools(server, client) {
|
|
|
240
281
|
});
|
|
241
282
|
}
|
|
242
283
|
}
|
|
243
|
-
if (
|
|
244
|
-
await deleteOFWMessages(client, [
|
|
245
|
-
deleteDraft(
|
|
284
|
+
if (draftRef !== undefined) {
|
|
285
|
+
await deleteOFWMessages(client, [draftRef]);
|
|
286
|
+
deleteDraft(draftRef);
|
|
246
287
|
}
|
|
247
288
|
const responseObj = persisted ?? raw;
|
|
248
289
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : 'Message sent successfully.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
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>",
|
|
@@ -27,16 +27,17 @@
|
|
|
27
27
|
"test:watch": "vitest"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@fetchproxy/bootstrap": "^0.
|
|
30
|
+
"@fetchproxy/bootstrap": "^0.8.0",
|
|
31
|
+
"@fetchproxy/server": "^0.8.0",
|
|
31
32
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
-
"dotenv": "^17.4.
|
|
33
|
-
"zod": "^4.4.
|
|
33
|
+
"dotenv": "^17.4.2",
|
|
34
|
+
"zod": "^4.4.3"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@types/node": "^25.
|
|
37
|
-
"@vitest/coverage-v8": "^4.1.
|
|
37
|
+
"@types/node": "^25.9.1",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
38
39
|
"esbuild": "^0.28.0",
|
|
39
|
-
"typescript": "^6.0.
|
|
40
|
-
"vitest": "^4.1.
|
|
40
|
+
"typescript": "^6.0.3",
|
|
41
|
+
"vitest": "^4.1.7"
|
|
41
42
|
}
|
|
42
43
|
}
|
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.2.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.0
|
|
14
|
+
"version": "2.2.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|