ofw-mcp 2.0.10 → 2.0.12
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 +208 -215
- package/dist/cache.js +24 -30
- package/dist/client.js +38 -60
- package/dist/config.js +17 -4
- package/dist/index.js +1 -1
- package/dist/sync.js +25 -32
- package/dist/tools/_shared.js +22 -0
- package/dist/tools/calendar.js +5 -4
- package/dist/tools/expenses.js +4 -3
- package/dist/tools/journal.js +3 -2
- package/dist/tools/messages.js +132 -102
- package/dist/tools/user.js +3 -2
- package/package.json +4 -4
- package/server.json +2 -2
package/dist/cache.js
CHANGED
|
@@ -131,9 +131,9 @@ export function getMessage(id) {
|
|
|
131
131
|
const r = db.prepare('SELECT * FROM messages WHERE id = ?').get(id);
|
|
132
132
|
return r ? rowFromDb(r) : null;
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
// Build the WHERE clause + bound params for message queries. listMessages and
|
|
135
|
+
// countMessages share this so the filter semantics can't drift.
|
|
136
|
+
function buildMessageFilter(opts) {
|
|
137
137
|
const wheres = [];
|
|
138
138
|
const params = [];
|
|
139
139
|
if (opts.folder !== undefined) {
|
|
@@ -153,35 +153,23 @@ export function listMessages(opts) {
|
|
|
153
153
|
wheres.push('(subject LIKE ? OR body LIKE ?)');
|
|
154
154
|
params.push(pattern, pattern);
|
|
155
155
|
}
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
return {
|
|
157
|
+
where: wheres.length > 0 ? `WHERE ${wheres.join(' AND ')}` : '',
|
|
158
|
+
params,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export function listMessages(opts) {
|
|
162
|
+
const { db } = openCache();
|
|
163
|
+
const { where, params } = buildMessageFilter(opts);
|
|
164
|
+
const offset = (opts.page - 1) * opts.size;
|
|
158
165
|
const rows = db.prepare(`SELECT * FROM messages ${where}
|
|
159
166
|
ORDER BY sent_at DESC, id DESC
|
|
160
|
-
LIMIT ? OFFSET ?`).all(...params);
|
|
167
|
+
LIMIT ? OFFSET ?`).all(...params, opts.size, offset);
|
|
161
168
|
return rows.map(rowFromDb);
|
|
162
169
|
}
|
|
163
170
|
export function countMessages(opts) {
|
|
164
171
|
const { db } = openCache();
|
|
165
|
-
const
|
|
166
|
-
const params = [];
|
|
167
|
-
if (opts.folder !== undefined) {
|
|
168
|
-
wheres.push('folder = ?');
|
|
169
|
-
params.push(opts.folder);
|
|
170
|
-
}
|
|
171
|
-
if (opts.since !== undefined) {
|
|
172
|
-
wheres.push('sent_at >= ?');
|
|
173
|
-
params.push(opts.since);
|
|
174
|
-
}
|
|
175
|
-
if (opts.until !== undefined) {
|
|
176
|
-
wheres.push('sent_at < ?');
|
|
177
|
-
params.push(opts.until);
|
|
178
|
-
}
|
|
179
|
-
if (opts.q !== undefined && opts.q.length > 0) {
|
|
180
|
-
const pattern = `%${opts.q}%`;
|
|
181
|
-
wheres.push('(subject LIKE ? OR body LIKE ?)');
|
|
182
|
-
params.push(pattern, pattern);
|
|
183
|
-
}
|
|
184
|
-
const where = wheres.length > 0 ? `WHERE ${wheres.join(' AND ')}` : '';
|
|
172
|
+
const { where, params } = buildMessageFilter(opts);
|
|
185
173
|
const r = db.prepare(`SELECT COUNT(*) as n FROM messages ${where}`)
|
|
186
174
|
.get(...params);
|
|
187
175
|
return r?.n ?? 0;
|
|
@@ -295,13 +283,19 @@ export function upsertAttachmentForMessage(input) {
|
|
|
295
283
|
const { db } = openCache();
|
|
296
284
|
const existing = db.prepare('SELECT message_ids_json FROM attachments WHERE file_id = ?')
|
|
297
285
|
.get(input.fileId);
|
|
286
|
+
// messageId === 0 is the "metadata-only, not yet linked to a message"
|
|
287
|
+
// sentinel used by upload-without-send and download-by-id. Don't
|
|
288
|
+
// pollute the array with it — leave the list empty / unchanged.
|
|
289
|
+
const prior = existing ? JSON.parse(existing.message_ids_json) : [];
|
|
298
290
|
let messageIds;
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
291
|
+
if (input.messageId === 0) {
|
|
292
|
+
messageIds = prior;
|
|
293
|
+
}
|
|
294
|
+
else if (prior.includes(input.messageId)) {
|
|
295
|
+
messageIds = prior;
|
|
302
296
|
}
|
|
303
297
|
else {
|
|
304
|
-
messageIds = [input.messageId];
|
|
298
|
+
messageIds = [...prior, input.messageId];
|
|
305
299
|
}
|
|
306
300
|
db.prepare(`INSERT INTO attachments (
|
|
307
301
|
file_id, file_name, label, mime_type, size_bytes,
|
package/dist/client.js
CHANGED
|
@@ -28,78 +28,56 @@ function readVar(key) {
|
|
|
28
28
|
return trimmed;
|
|
29
29
|
}
|
|
30
30
|
const BASE_URL = 'https://ofw.ourfamilywizard.com';
|
|
31
|
-
const
|
|
31
|
+
const OFW_PROTOCOL_HEADERS = {
|
|
32
32
|
'ofw-client': 'WebApplication',
|
|
33
33
|
'ofw-version': '1.0.0',
|
|
34
|
-
Accept: 'application/json',
|
|
35
|
-
'Content-Type': 'application/json',
|
|
36
34
|
};
|
|
35
|
+
// Parse a Content-Disposition header for a filename. Prefers RFC 6266
|
|
36
|
+
// `filename*=UTF-8''…` (percent-decoded) and falls back to `filename="…"`.
|
|
37
|
+
function parseContentDispositionFilename(cd) {
|
|
38
|
+
const extMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(cd);
|
|
39
|
+
if (extMatch) {
|
|
40
|
+
const raw = extMatch[1].trim().replace(/^"|"$/g, '');
|
|
41
|
+
try {
|
|
42
|
+
return decodeURIComponent(raw);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return raw;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const m = /filename="?([^";]+)"?/i.exec(cd);
|
|
49
|
+
return m ? m[1] : null;
|
|
50
|
+
}
|
|
37
51
|
export class OFWClient {
|
|
38
52
|
token = null;
|
|
39
53
|
tokenExpiry = null;
|
|
40
54
|
async request(method, path, body) {
|
|
41
55
|
await this.ensureAuthenticated();
|
|
42
|
-
|
|
56
|
+
const response = await this.fetchWithRetry(method, path, body, 'application/json', false);
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
return (text ? JSON.parse(text) : null);
|
|
43
59
|
}
|
|
44
60
|
/** Like `request`, but returns the raw bytes plus Content-Type/-Disposition metadata. */
|
|
45
61
|
async requestBinary(method, path) {
|
|
46
62
|
await this.ensureAuthenticated();
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
async doRequestBinary(method, path, isRetry) {
|
|
50
|
-
const headers = {
|
|
51
|
-
'ofw-client': 'WebApplication',
|
|
52
|
-
'ofw-version': '1.0.0',
|
|
53
|
-
Accept: 'application/octet-stream',
|
|
54
|
-
Authorization: `Bearer ${this.token}`,
|
|
55
|
-
};
|
|
56
|
-
const response = await fetch(`${BASE_URL}${path}`, { method, headers });
|
|
57
|
-
if (response.status === 401 && !isRetry) {
|
|
58
|
-
this.token = null;
|
|
59
|
-
this.tokenExpiry = null;
|
|
60
|
-
await this.ensureAuthenticated();
|
|
61
|
-
return this.doRequestBinary(method, path, true);
|
|
62
|
-
}
|
|
63
|
-
if (response.status === 429 && !isRetry) {
|
|
64
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
65
|
-
return this.doRequestBinary(method, path, true);
|
|
66
|
-
}
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
|
|
69
|
-
}
|
|
70
|
-
const buf = Buffer.from(await response.arrayBuffer());
|
|
71
|
-
const cd = response.headers.get('content-disposition') ?? '';
|
|
72
|
-
// RFC 6266: filename*=UTF-8''… takes priority; fall back to filename="…"
|
|
73
|
-
let suggestedFileName = null;
|
|
74
|
-
const extMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(cd);
|
|
75
|
-
if (extMatch) {
|
|
76
|
-
try {
|
|
77
|
-
suggestedFileName = decodeURIComponent(extMatch[1].trim().replace(/^"|"$/g, ''));
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
suggestedFileName = extMatch[1];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
const m = /filename="?([^";]+)"?/i.exec(cd);
|
|
85
|
-
if (m)
|
|
86
|
-
suggestedFileName = m[1];
|
|
87
|
-
}
|
|
63
|
+
const response = await this.fetchWithRetry(method, path, undefined, 'application/octet-stream', false);
|
|
88
64
|
return {
|
|
89
|
-
body:
|
|
65
|
+
body: Buffer.from(await response.arrayBuffer()),
|
|
90
66
|
contentType: response.headers.get('content-type'),
|
|
91
|
-
suggestedFileName,
|
|
67
|
+
suggestedFileName: parseContentDispositionFilename(response.headers.get('content-disposition') ?? ''),
|
|
92
68
|
};
|
|
93
69
|
}
|
|
94
|
-
|
|
70
|
+
// Single fetch+retry scaffold for both JSON and binary callers. Handles
|
|
71
|
+
// 401 (re-auth and replay once), 429 (wait 2s and replay once), and
|
|
72
|
+
// turns any other non-2xx into a thrown Error.
|
|
73
|
+
async fetchWithRetry(method, path, body, accept, isRetry) {
|
|
95
74
|
const isFormData = body instanceof FormData;
|
|
96
75
|
const headers = {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Accept: 'application/json',
|
|
76
|
+
...OFW_PROTOCOL_HEADERS,
|
|
77
|
+
Accept: accept,
|
|
100
78
|
Authorization: `Bearer ${this.token}`,
|
|
101
79
|
};
|
|
102
|
-
if (!isFormData)
|
|
80
|
+
if (body !== undefined && !isFormData)
|
|
103
81
|
headers['Content-Type'] = 'application/json';
|
|
104
82
|
const response = await fetch(`${BASE_URL}${path}`, {
|
|
105
83
|
method,
|
|
@@ -110,20 +88,19 @@ export class OFWClient {
|
|
|
110
88
|
this.token = null;
|
|
111
89
|
this.tokenExpiry = null;
|
|
112
90
|
await this.ensureAuthenticated();
|
|
113
|
-
return this.
|
|
91
|
+
return this.fetchWithRetry(method, path, body, accept, true);
|
|
114
92
|
}
|
|
115
93
|
if (response.status === 429) {
|
|
116
94
|
if (!isRetry) {
|
|
117
95
|
await new Promise((r) => setTimeout(r, 2000));
|
|
118
|
-
return this.
|
|
96
|
+
return this.fetchWithRetry(method, path, body, accept, true);
|
|
119
97
|
}
|
|
120
98
|
throw new Error('Rate limited by OFW API');
|
|
121
99
|
}
|
|
122
100
|
if (!response.ok) {
|
|
123
101
|
throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
|
|
124
102
|
}
|
|
125
|
-
|
|
126
|
-
return (text ? JSON.parse(text) : null);
|
|
103
|
+
return response;
|
|
127
104
|
}
|
|
128
105
|
async ensureAuthenticated() {
|
|
129
106
|
if (!this.isTokenExpiredSoon())
|
|
@@ -139,23 +116,24 @@ export class OFWClient {
|
|
|
139
116
|
// Spring Security requires a SESSION cookie before accepting the login POST.
|
|
140
117
|
// GET /ofw/login.form with redirect:manual to capture the Set-Cookie from the 303 response.
|
|
141
118
|
const initResponse = await fetch(`${BASE_URL}/ofw/login.form`, {
|
|
142
|
-
headers: {
|
|
119
|
+
headers: { ...OFW_PROTOCOL_HEADERS },
|
|
143
120
|
redirect: 'manual',
|
|
144
121
|
});
|
|
145
122
|
// Extract just the SESSION=value part (strip attributes like Path, Secure, etc.)
|
|
146
123
|
const setCookie = initResponse.headers.get('set-cookie') ?? '';
|
|
147
|
-
const sessionCookie = setCookie.split(';')[0];
|
|
124
|
+
const sessionCookie = setCookie.split(';')[0];
|
|
148
125
|
const response = await fetch(`${BASE_URL}/ofw/login`, {
|
|
149
126
|
method: 'POST',
|
|
150
127
|
headers: {
|
|
151
|
-
...
|
|
128
|
+
...OFW_PROTOCOL_HEADERS,
|
|
129
|
+
Accept: 'application/json',
|
|
152
130
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
153
131
|
...(sessionCookie ? { Cookie: sessionCookie } : {}),
|
|
154
132
|
},
|
|
155
133
|
body: new URLSearchParams({
|
|
156
134
|
submit: 'Sign In',
|
|
157
135
|
_eventId: 'submit',
|
|
158
|
-
username
|
|
136
|
+
username,
|
|
159
137
|
password,
|
|
160
138
|
}).toString(),
|
|
161
139
|
});
|
package/dist/config.js
CHANGED
|
@@ -23,8 +23,21 @@ export function getAttachmentsDir() {
|
|
|
23
23
|
const override = process.env.OFW_ATTACHMENTS_DIR;
|
|
24
24
|
if (override && override.trim().length > 0)
|
|
25
25
|
return override.trim();
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// Default to ~/Downloads/ofw-mcp/ — the cache dir (~/.cache/...) is hidden and
|
|
27
|
+
// typically outside the filesystem allowlist of sandboxed MCP hosts like
|
|
28
|
+
// Claude Desktop, so files written there are unreadable to the model that
|
|
29
|
+
// just downloaded them. Downloads is the standard "user-accessible files"
|
|
30
|
+
// location across macOS/Linux/Windows.
|
|
31
|
+
return join(homedir(), 'Downloads', 'ofw-mcp');
|
|
32
|
+
}
|
|
33
|
+
// Default for ofw_download_attachment's `inline` arg when the caller doesn't
|
|
34
|
+
// pass one. Set OFW_INLINE_ATTACHMENTS=true to have attachments returned as
|
|
35
|
+
// MCP content blocks by default (skipping disk) — useful on sandboxed MCP
|
|
36
|
+
// hosts where filesystem reads back to the model aren't available.
|
|
37
|
+
// Accepts: "1", "true", "yes", "on" (case-insensitive) → true; anything else → false.
|
|
38
|
+
export function getDefaultInlineAttachments() {
|
|
39
|
+
const raw = process.env.OFW_INLINE_ATTACHMENTS;
|
|
40
|
+
if (typeof raw !== 'string')
|
|
41
|
+
return false;
|
|
42
|
+
return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase());
|
|
30
43
|
}
|
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.12' });
|
|
21
21
|
registerUserTools(server, client);
|
|
22
22
|
registerMessageTools(server, client);
|
|
23
23
|
registerCalendarTools(server, client);
|
package/dist/sync.js
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import { setMeta, upsertMessage, getMessage, setSyncState, upsertDraft, getDraft, deleteDraft, listDraftIds, upsertAttachmentForMessage, } from './cache.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// via ofw_download_attachment, which will surface the actual error.
|
|
19
|
-
}
|
|
2
|
+
import { mapRecipients } from './tools/_shared.js';
|
|
3
|
+
// Fetches OFW attachment metadata for one file id and writes it to the cache.
|
|
4
|
+
// Throws on network/HTTP errors — callers in bulk-sync paths wrap this in the
|
|
5
|
+
// best-effort helper below; callers that need the result (download tool) let
|
|
6
|
+
// the throw propagate.
|
|
7
|
+
export async function fetchAttachmentMeta(client, fileId, messageId) {
|
|
8
|
+
const meta = await client.request('GET', `/pub/v1/myfiles/${fileId}`);
|
|
9
|
+
upsertAttachmentForMessage({
|
|
10
|
+
fileId: meta.fileId ?? fileId,
|
|
11
|
+
fileName: meta.fileName ?? `file-${fileId}`,
|
|
12
|
+
label: meta.label ?? meta.fileName ?? `file-${fileId}`,
|
|
13
|
+
mimeType: meta.fileType ?? 'application/octet-stream',
|
|
14
|
+
sizeBytes: typeof meta.fileSize === 'number' ? meta.fileSize : null,
|
|
15
|
+
metadata: meta,
|
|
16
|
+
messageId,
|
|
17
|
+
});
|
|
20
18
|
}
|
|
21
19
|
export async function fetchAttachmentMetaForMessage(client, messageId, fileIds) {
|
|
22
20
|
for (const fid of fileIds) {
|
|
23
|
-
|
|
21
|
+
// Best-effort: a single bad attachment shouldn't break the surrounding
|
|
22
|
+
// sync. The file id stays in the message's listData; the model can
|
|
23
|
+
// retry later via ofw_download_attachment, which surfaces the real error.
|
|
24
|
+
try {
|
|
25
|
+
await fetchAttachmentMeta(client, fid, messageId);
|
|
26
|
+
}
|
|
27
|
+
catch { /* swallow */ }
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
export async function resolveFolderIds(client) {
|
|
@@ -40,13 +44,6 @@ export async function resolveFolderIds(client) {
|
|
|
40
44
|
setMeta('drafts_folder_id', ids.drafts);
|
|
41
45
|
return ids;
|
|
42
46
|
}
|
|
43
|
-
function recipientsFromList(item) {
|
|
44
|
-
return (item.recipients ?? []).map((r) => ({
|
|
45
|
-
userId: r.user.id,
|
|
46
|
-
name: r.user.name,
|
|
47
|
-
viewedAt: r.viewed?.dateTime ?? null,
|
|
48
|
-
}));
|
|
49
|
-
}
|
|
50
47
|
export async function syncMessageFolder(client, folder, folderId, opts) {
|
|
51
48
|
let page = 1;
|
|
52
49
|
let synced = 0;
|
|
@@ -93,7 +90,7 @@ export async function syncMessageFolder(client, folder, folderId, opts) {
|
|
|
93
90
|
subject: item.subject ?? '(no subject)',
|
|
94
91
|
fromUser: item.from?.name ?? '',
|
|
95
92
|
sentAt: item.date?.dateTime ?? new Date().toISOString(),
|
|
96
|
-
recipients:
|
|
93
|
+
recipients: mapRecipients(item.recipients),
|
|
97
94
|
body,
|
|
98
95
|
fetchedBodyAt,
|
|
99
96
|
replyToId: null,
|
|
@@ -139,11 +136,7 @@ export async function syncDrafts(client, draftsFolderId) {
|
|
|
139
136
|
id: item.id,
|
|
140
137
|
subject: detail.subject ?? item.subject ?? '(no subject)',
|
|
141
138
|
body: detail.body ?? '',
|
|
142
|
-
recipients: (item.recipients
|
|
143
|
-
userId: r.user?.id ?? 0,
|
|
144
|
-
name: r.user?.name ?? '',
|
|
145
|
-
viewedAt: r.viewed?.dateTime ?? null,
|
|
146
|
-
})),
|
|
139
|
+
recipients: mapRecipients(item.recipients),
|
|
147
140
|
replyToId: item.replyToId ?? null,
|
|
148
141
|
modifiedAt,
|
|
149
142
|
listData: item,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
2
|
+
export function jsonResponse(payload) {
|
|
3
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
4
|
+
}
|
|
5
|
+
export function textResponse(text) {
|
|
6
|
+
return { content: [{ type: 'text', text }] };
|
|
7
|
+
}
|
|
8
|
+
// Translates OFW API recipient shape into the cache's normalized Recipient.
|
|
9
|
+
// Used wherever we surface or persist recipients (sync, get_message, send,
|
|
10
|
+
// save_draft) — all five call sites had near-identical inline mappings.
|
|
11
|
+
export function mapRecipients(items) {
|
|
12
|
+
return (items ?? []).map((r) => ({
|
|
13
|
+
userId: r.user?.id ?? 0,
|
|
14
|
+
name: r.user?.name ?? '',
|
|
15
|
+
viewedAt: r.viewed?.dateTime ?? null,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
// Expand a user-provided path: ~ → $HOME, relative → absolute.
|
|
19
|
+
export function expandPath(p) {
|
|
20
|
+
const expanded = p.startsWith('~/') ? join(process.env.HOME ?? '', p.slice(2)) : p;
|
|
21
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
22
|
+
}
|
package/dist/tools/calendar.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { jsonResponse, textResponse } from './_shared.js';
|
|
2
3
|
export function registerCalendarTools(server, client) {
|
|
3
4
|
server.registerTool('ofw_list_events', {
|
|
4
5
|
description: 'List OurFamilyWizard calendar events in a date range',
|
|
@@ -11,7 +12,7 @@ export function registerCalendarTools(server, client) {
|
|
|
11
12
|
}, async (args) => {
|
|
12
13
|
const variant = args.detailed ? 'detailed' : 'basic';
|
|
13
14
|
const data = await client.request('GET', `/pub/v1/calendar/${variant}?startDate=${encodeURIComponent(args.startDate)}&endDate=${encodeURIComponent(args.endDate)}`);
|
|
14
|
-
return
|
|
15
|
+
return jsonResponse(data);
|
|
15
16
|
});
|
|
16
17
|
server.registerTool('ofw_create_event', {
|
|
17
18
|
description: 'Create a calendar event in OurFamilyWizard',
|
|
@@ -31,7 +32,7 @@ export function registerCalendarTools(server, client) {
|
|
|
31
32
|
},
|
|
32
33
|
}, async (args) => {
|
|
33
34
|
const data = await client.request('POST', '/pub/v1/calendar/events', args);
|
|
34
|
-
return
|
|
35
|
+
return jsonResponse(data);
|
|
35
36
|
});
|
|
36
37
|
server.registerTool('ofw_update_event', {
|
|
37
38
|
description: 'Update an existing OurFamilyWizard calendar event',
|
|
@@ -49,7 +50,7 @@ export function registerCalendarTools(server, client) {
|
|
|
49
50
|
}, async (args) => {
|
|
50
51
|
const { eventId, ...updateData } = args;
|
|
51
52
|
const data = await client.request('PUT', `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
|
|
52
|
-
return
|
|
53
|
+
return jsonResponse(data);
|
|
53
54
|
});
|
|
54
55
|
server.registerTool('ofw_delete_event', {
|
|
55
56
|
description: 'Delete an OurFamilyWizard calendar event',
|
|
@@ -59,6 +60,6 @@ export function registerCalendarTools(server, client) {
|
|
|
59
60
|
},
|
|
60
61
|
}, async (args) => {
|
|
61
62
|
await client.request('DELETE', `/pub/v1/calendar/events/${encodeURIComponent(args.eventId)}`);
|
|
62
|
-
return
|
|
63
|
+
return textResponse(`Event ${args.eventId} deleted`);
|
|
63
64
|
});
|
|
64
65
|
}
|
package/dist/tools/expenses.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { jsonResponse } from './_shared.js';
|
|
2
3
|
export function registerExpenseTools(server, client) {
|
|
3
4
|
server.registerTool('ofw_get_expense_totals', {
|
|
4
5
|
description: 'Get OurFamilyWizard expense summary totals (owed/paid)',
|
|
5
6
|
annotations: { readOnlyHint: true },
|
|
6
7
|
}, async () => {
|
|
7
8
|
const data = await client.request('GET', '/pub/v2/expense/expenses/totals');
|
|
8
|
-
return
|
|
9
|
+
return jsonResponse(data);
|
|
9
10
|
});
|
|
10
11
|
server.registerTool('ofw_list_expenses', {
|
|
11
12
|
description: 'List OurFamilyWizard expenses with pagination',
|
|
@@ -18,7 +19,7 @@ export function registerExpenseTools(server, client) {
|
|
|
18
19
|
const start = args.start ?? 0;
|
|
19
20
|
const max = args.max ?? 20;
|
|
20
21
|
const data = await client.request('GET', `/pub/v2/expense/expenses?start=${start}&max=${max}`);
|
|
21
|
-
return
|
|
22
|
+
return jsonResponse(data);
|
|
22
23
|
});
|
|
23
24
|
server.registerTool('ofw_create_expense', {
|
|
24
25
|
description: 'Log a new expense in OurFamilyWizard',
|
|
@@ -29,6 +30,6 @@ export function registerExpenseTools(server, client) {
|
|
|
29
30
|
},
|
|
30
31
|
}, async (args) => {
|
|
31
32
|
const data = await client.request('POST', '/pub/v2/expense/expenses', args);
|
|
32
|
-
return
|
|
33
|
+
return jsonResponse(data);
|
|
33
34
|
});
|
|
34
35
|
}
|
package/dist/tools/journal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { jsonResponse } from './_shared.js';
|
|
2
3
|
export function registerJournalTools(server, client) {
|
|
3
4
|
server.registerTool('ofw_list_journal_entries', {
|
|
4
5
|
description: 'List OurFamilyWizard journal entries',
|
|
@@ -12,7 +13,7 @@ export function registerJournalTools(server, client) {
|
|
|
12
13
|
const start = args.start ?? 1;
|
|
13
14
|
const max = args.max ?? 10;
|
|
14
15
|
const data = await client.request('GET', `/pub/v1/journals?start=${start}&max=${max}`);
|
|
15
|
-
return
|
|
16
|
+
return jsonResponse(data);
|
|
16
17
|
});
|
|
17
18
|
server.registerTool('ofw_create_journal_entry', {
|
|
18
19
|
description: 'Create a new journal entry in OurFamilyWizard',
|
|
@@ -23,6 +24,6 @@ export function registerJournalTools(server, client) {
|
|
|
23
24
|
},
|
|
24
25
|
}, async (args) => {
|
|
25
26
|
const data = await client.request('POST', '/pub/v1/journals', args);
|
|
26
|
-
return
|
|
27
|
+
return jsonResponse(data);
|
|
27
28
|
});
|
|
28
29
|
}
|