ofw-mcp 2.3.2 → 2.4.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/LICENSE +21 -0
- package/README.md +42 -22
- package/dist/auth-password.js +7 -2
- package/dist/bundle.js +226 -47
- package/dist/config.js +26 -0
- package/dist/index.js +1 -1
- package/dist/sync.js +68 -9
- package/dist/tools/_shared.js +47 -3
- package/dist/tools/calendar.js +54 -48
- package/dist/tools/expenses.js +17 -13
- package/dist/tools/journal.js +17 -13
- package/dist/tools/messages.js +313 -242
- package/dist/validate.js +35 -0
- package/package.json +5 -1
- package/server.json +8 -2
package/dist/bundle.js
CHANGED
|
@@ -38069,8 +38069,7 @@ async function loginWithPassword(username, password) {
|
|
|
38069
38069
|
headers: { ...OFW_PROTOCOL_HEADERS },
|
|
38070
38070
|
redirect: "manual"
|
|
38071
38071
|
});
|
|
38072
|
-
const
|
|
38073
|
-
const sessionCookie = setCookie.split(";")[0];
|
|
38072
|
+
const sessionCookie = initResponse.headers.getSetCookie().map((c) => c.split(";")[0]).join("; ");
|
|
38074
38073
|
const response = await fetch(`${BASE_URL}/ofw/login`, {
|
|
38075
38074
|
method: "POST",
|
|
38076
38075
|
headers: {
|
|
@@ -38130,6 +38129,16 @@ function getAttachmentsDir() {
|
|
|
38130
38129
|
function parseBoolEnv2(name) {
|
|
38131
38130
|
return parseBoolEnv(name);
|
|
38132
38131
|
}
|
|
38132
|
+
function getWriteMode() {
|
|
38133
|
+
const raw = process.env.OFW_WRITE_MODE;
|
|
38134
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return "all";
|
|
38135
|
+
const mode = raw.trim().toLowerCase();
|
|
38136
|
+
if (mode === "none" || mode === "drafts" || mode === "all") return mode;
|
|
38137
|
+
console.error(
|
|
38138
|
+
`[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" \u2014 failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`
|
|
38139
|
+
);
|
|
38140
|
+
return "none";
|
|
38141
|
+
}
|
|
38133
38142
|
function getDefaultInlineAttachments() {
|
|
38134
38143
|
return parseBoolEnv2("OFW_INLINE_ATTACHMENTS");
|
|
38135
38144
|
}
|
|
@@ -38137,7 +38146,8 @@ function getDefaultInlineAttachments() {
|
|
|
38137
38146
|
// package.json
|
|
38138
38147
|
var package_default = {
|
|
38139
38148
|
name: "ofw-mcp",
|
|
38140
|
-
version: "2.
|
|
38149
|
+
version: "2.4.0",
|
|
38150
|
+
license: "MIT",
|
|
38141
38151
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
38142
38152
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
38143
38153
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38146,6 +38156,9 @@ var package_default = {
|
|
|
38146
38156
|
url: "git+https://github.com/chrischall/ofw-mcp.git"
|
|
38147
38157
|
},
|
|
38148
38158
|
type: "module",
|
|
38159
|
+
engines: {
|
|
38160
|
+
node: ">=22.5.0"
|
|
38161
|
+
},
|
|
38149
38162
|
bin: {
|
|
38150
38163
|
"ofw-mcp": "dist/index.js"
|
|
38151
38164
|
},
|
|
@@ -38401,9 +38414,24 @@ var OFWClient = class {
|
|
|
38401
38414
|
};
|
|
38402
38415
|
var client = new OFWClient();
|
|
38403
38416
|
|
|
38417
|
+
// src/validate.ts
|
|
38418
|
+
function parseOFW(schema, raw, ctx, mode = "lenient") {
|
|
38419
|
+
const result = schema.safeParse(raw);
|
|
38420
|
+
if (result.success) return result.data;
|
|
38421
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
38422
|
+
const message = `OFW response for ${ctx} failed validation: ${issues}`;
|
|
38423
|
+
if (mode === "strict") throw new Error(message);
|
|
38424
|
+
console.error(`[ofw-mcp] WARNING: ${message} \u2014 continuing with the raw response; fields derived from it may be missing or wrong.`);
|
|
38425
|
+
return raw;
|
|
38426
|
+
}
|
|
38427
|
+
|
|
38404
38428
|
// src/tools/_shared.ts
|
|
38405
38429
|
var jsonResponse = textResult;
|
|
38406
38430
|
var textResponse = rawTextResult;
|
|
38431
|
+
var ApiRecipientSchema = external_exports.looseObject({
|
|
38432
|
+
user: external_exports.looseObject({ id: external_exports.number().optional(), name: external_exports.string().optional() }).optional(),
|
|
38433
|
+
viewed: external_exports.looseObject({ dateTime: external_exports.string() }).nullable().optional()
|
|
38434
|
+
});
|
|
38407
38435
|
function mapRecipients(items) {
|
|
38408
38436
|
return (items ?? []).map((r) => ({
|
|
38409
38437
|
userId: r.user?.id ?? 0,
|
|
@@ -38412,15 +38440,36 @@ function mapRecipients(items) {
|
|
|
38412
38440
|
}));
|
|
38413
38441
|
}
|
|
38414
38442
|
var expandPath2 = expandPath;
|
|
38415
|
-
|
|
38416
|
-
const
|
|
38417
|
-
|
|
38418
|
-
"
|
|
38419
|
-
|
|
38443
|
+
function verifyWriteLanded(kind, sent, persisted) {
|
|
38444
|
+
const mismatches = [];
|
|
38445
|
+
if (typeof persisted.subject !== "string" || !persisted.subject.includes(sent.subject)) {
|
|
38446
|
+
mismatches.push("subject");
|
|
38447
|
+
}
|
|
38448
|
+
if (typeof persisted.body !== "string" || !persisted.body.includes(sent.body)) {
|
|
38449
|
+
mismatches.push("body");
|
|
38450
|
+
}
|
|
38451
|
+
if (mismatches.length === 0) return null;
|
|
38452
|
+
return `WARNING: the ${kind} re-fetched from OFW does not contain the ${mismatches.join(" and ")} that was posted \u2014 OFW may have silently dropped or altered the write. Verify the ${kind} on ourfamilywizard.com before relying on it.`;
|
|
38453
|
+
}
|
|
38454
|
+
var PostMessagesResponseSchema = external_exports.looseObject({
|
|
38455
|
+
id: external_exports.number().optional(),
|
|
38456
|
+
entityId: external_exports.number().optional()
|
|
38457
|
+
}).nullable();
|
|
38458
|
+
async function postMessageAndRefetch(client2, payload, detailSchema, ctx) {
|
|
38459
|
+
const raw = parseOFW(
|
|
38460
|
+
PostMessagesResponseSchema,
|
|
38461
|
+
await client2.request("POST", "/pub/v3/messages", payload),
|
|
38462
|
+
`POST /pub/v3/messages (${ctx})`,
|
|
38463
|
+
"strict"
|
|
38420
38464
|
);
|
|
38421
38465
|
const id = typeof raw?.id === "number" ? raw.id : typeof raw?.entityId === "number" ? raw.entityId : null;
|
|
38422
38466
|
if (id === null) return { id: null, detail: null, raw };
|
|
38423
|
-
const detail =
|
|
38467
|
+
const detail = parseOFW(
|
|
38468
|
+
detailSchema,
|
|
38469
|
+
await client2.request("GET", `/pub/v3/messages/${id}`),
|
|
38470
|
+
`GET /pub/v3/messages/{id} (${ctx})`,
|
|
38471
|
+
"strict"
|
|
38472
|
+
);
|
|
38424
38473
|
return { id, detail, raw };
|
|
38425
38474
|
}
|
|
38426
38475
|
|
|
@@ -38789,8 +38838,20 @@ function markAttachmentDownloaded(fileId, path) {
|
|
|
38789
38838
|
}
|
|
38790
38839
|
|
|
38791
38840
|
// src/sync.ts
|
|
38841
|
+
var FileMetaSchema = external_exports.looseObject({
|
|
38842
|
+
fileId: external_exports.number(),
|
|
38843
|
+
label: external_exports.string().optional(),
|
|
38844
|
+
fileName: external_exports.string().optional(),
|
|
38845
|
+
fileType: external_exports.string().optional(),
|
|
38846
|
+
// MIME
|
|
38847
|
+
fileSize: external_exports.number().optional()
|
|
38848
|
+
});
|
|
38792
38849
|
async function fetchAttachmentMeta(client2, fileId, messageId) {
|
|
38793
|
-
const meta3 =
|
|
38850
|
+
const meta3 = parseOFW(
|
|
38851
|
+
FileMetaSchema,
|
|
38852
|
+
await client2.request("GET", `/pub/v1/myfiles/${fileId}`),
|
|
38853
|
+
"GET /pub/v1/myfiles/{fileId}"
|
|
38854
|
+
);
|
|
38794
38855
|
upsertAttachmentForMessage({
|
|
38795
38856
|
fileId: meta3.fileId ?? fileId,
|
|
38796
38857
|
fileName: meta3.fileName ?? `file-${fileId}`,
|
|
@@ -38804,10 +38865,14 @@ async function fetchAttachmentMeta(client2, fileId, messageId) {
|
|
|
38804
38865
|
async function fetchAttachmentMetaForMessage(client2, messageId, fileIds) {
|
|
38805
38866
|
await Promise.allSettled(fileIds.map((fid) => fetchAttachmentMeta(client2, fid, messageId)));
|
|
38806
38867
|
}
|
|
38868
|
+
var FoldersSchema = external_exports.looseObject({
|
|
38869
|
+
systemFolders: external_exports.array(external_exports.looseObject({ id: external_exports.string(), folderType: external_exports.string() })).optional()
|
|
38870
|
+
});
|
|
38807
38871
|
async function resolveFolderIds(client2) {
|
|
38808
|
-
const data =
|
|
38809
|
-
|
|
38810
|
-
"/pub/v1/messageFolders?includeFolderCounts=true"
|
|
38872
|
+
const data = parseOFW(
|
|
38873
|
+
FoldersSchema,
|
|
38874
|
+
await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true"),
|
|
38875
|
+
"GET /pub/v1/messageFolders"
|
|
38811
38876
|
);
|
|
38812
38877
|
const sys = data.systemFolders ?? [];
|
|
38813
38878
|
const find = (type) => {
|
|
@@ -38823,6 +38888,19 @@ async function resolveFolderIds(client2) {
|
|
|
38823
38888
|
setMeta("drafts_folder_id", ids.drafts);
|
|
38824
38889
|
return ids;
|
|
38825
38890
|
}
|
|
38891
|
+
var ListItemSchema = external_exports.looseObject({
|
|
38892
|
+
id: external_exports.number(),
|
|
38893
|
+
subject: external_exports.string(),
|
|
38894
|
+
date: external_exports.looseObject({ dateTime: external_exports.string() }),
|
|
38895
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
38896
|
+
showNeverViewed: external_exports.boolean(),
|
|
38897
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
38898
|
+
});
|
|
38899
|
+
var ListResponseSchema = external_exports.looseObject({ data: external_exports.array(ListItemSchema).optional() });
|
|
38900
|
+
var DetailResponseSchema = external_exports.looseObject({
|
|
38901
|
+
body: external_exports.string().optional(),
|
|
38902
|
+
files: external_exports.array(external_exports.number()).optional()
|
|
38903
|
+
});
|
|
38826
38904
|
async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
38827
38905
|
let page = 1;
|
|
38828
38906
|
let synced = 0;
|
|
@@ -38830,7 +38908,11 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
38830
38908
|
const unread = [];
|
|
38831
38909
|
while (true) {
|
|
38832
38910
|
const path = `/pub/v3/messages?folders=${encodeURIComponent(folderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
38833
|
-
const list =
|
|
38911
|
+
const list = parseOFW(
|
|
38912
|
+
ListResponseSchema,
|
|
38913
|
+
await client2.request("GET", path),
|
|
38914
|
+
`GET /pub/v3/messages?folders={${folder}}`
|
|
38915
|
+
);
|
|
38834
38916
|
const items = list.data ?? [];
|
|
38835
38917
|
if (items.length === 0) break;
|
|
38836
38918
|
let pageHadNewItem = false;
|
|
@@ -38845,7 +38927,11 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
38845
38927
|
let fetchedBodyAt = null;
|
|
38846
38928
|
let detailFileIds = [];
|
|
38847
38929
|
if (shouldFetchBody) {
|
|
38848
|
-
const detail =
|
|
38930
|
+
const detail = parseOFW(
|
|
38931
|
+
DetailResponseSchema,
|
|
38932
|
+
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
38933
|
+
"GET /pub/v3/messages/{id} (sync)"
|
|
38934
|
+
);
|
|
38849
38935
|
body = detail.body ?? "";
|
|
38850
38936
|
fetchedBodyAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38851
38937
|
if (Array.isArray(detail.files) && detail.files.length > 0) {
|
|
@@ -38887,17 +38973,44 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
38887
38973
|
});
|
|
38888
38974
|
return { synced, unread };
|
|
38889
38975
|
}
|
|
38976
|
+
var DraftListItemSchema = external_exports.looseObject({
|
|
38977
|
+
id: external_exports.number(),
|
|
38978
|
+
subject: external_exports.string(),
|
|
38979
|
+
date: external_exports.looseObject({ dateTime: external_exports.string() }),
|
|
38980
|
+
replyToId: external_exports.number().nullable().optional(),
|
|
38981
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
38982
|
+
});
|
|
38983
|
+
var DraftListResponseSchema = external_exports.looseObject({ data: external_exports.array(DraftListItemSchema).optional() });
|
|
38984
|
+
var DraftDetailSchema = external_exports.looseObject({
|
|
38985
|
+
body: external_exports.string().optional(),
|
|
38986
|
+
subject: external_exports.string().optional()
|
|
38987
|
+
});
|
|
38890
38988
|
async function syncDrafts(client2, draftsFolderId) {
|
|
38891
|
-
const
|
|
38892
|
-
|
|
38893
|
-
|
|
38989
|
+
const items = [];
|
|
38990
|
+
let page = 1;
|
|
38991
|
+
while (true) {
|
|
38992
|
+
const path = `/pub/v3/messages?folders=${encodeURIComponent(draftsFolderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
38993
|
+
const list = parseOFW(
|
|
38994
|
+
DraftListResponseSchema,
|
|
38995
|
+
await client2.request("GET", path),
|
|
38996
|
+
"GET /pub/v3/messages?folders={drafts}"
|
|
38997
|
+
);
|
|
38998
|
+
const pageItems = list.data ?? [];
|
|
38999
|
+
items.push(...pageItems);
|
|
39000
|
+
if (pageItems.length < 50) break;
|
|
39001
|
+
page++;
|
|
39002
|
+
}
|
|
38894
39003
|
const seenIds = /* @__PURE__ */ new Set();
|
|
38895
39004
|
let synced = 0;
|
|
38896
39005
|
for (const item of items) {
|
|
38897
39006
|
seenIds.add(item.id);
|
|
38898
39007
|
const modifiedAt = item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
38899
39008
|
const existing = getDraft(item.id);
|
|
38900
|
-
const detail =
|
|
39009
|
+
const detail = parseOFW(
|
|
39010
|
+
DraftDetailSchema,
|
|
39011
|
+
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
39012
|
+
"GET /pub/v3/messages/{id} (drafts sync)"
|
|
39013
|
+
);
|
|
38901
39014
|
const row = {
|
|
38902
39015
|
id: item.id,
|
|
38903
39016
|
subject: detail.subject ?? item.subject ?? "(no subject)",
|
|
@@ -38949,6 +39062,39 @@ async function syncAll(client2, opts) {
|
|
|
38949
39062
|
// src/tools/messages.ts
|
|
38950
39063
|
import { mkdirSync as mkdirSync2, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
38951
39064
|
import { basename, dirname as dirname3, extname, join as join5 } from "node:path";
|
|
39065
|
+
var DateSchema = external_exports.looseObject({ dateTime: external_exports.string() });
|
|
39066
|
+
var SentDetailSchema = external_exports.looseObject({
|
|
39067
|
+
subject: external_exports.string().optional(),
|
|
39068
|
+
body: external_exports.string().optional(),
|
|
39069
|
+
date: DateSchema.optional(),
|
|
39070
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
39071
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39072
|
+
});
|
|
39073
|
+
var SavedDraftDetailSchema = external_exports.looseObject({
|
|
39074
|
+
subject: external_exports.string().optional(),
|
|
39075
|
+
body: external_exports.string().optional(),
|
|
39076
|
+
date: DateSchema.optional(),
|
|
39077
|
+
replyToId: external_exports.number().nullable().optional(),
|
|
39078
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39079
|
+
});
|
|
39080
|
+
var MessageDetailSchema = external_exports.looseObject({
|
|
39081
|
+
id: external_exports.number(),
|
|
39082
|
+
subject: external_exports.string(),
|
|
39083
|
+
body: external_exports.string().optional(),
|
|
39084
|
+
date: DateSchema,
|
|
39085
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
39086
|
+
files: external_exports.array(external_exports.number()).optional(),
|
|
39087
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39088
|
+
});
|
|
39089
|
+
var DetailFilesSchema = external_exports.looseObject({ files: external_exports.array(external_exports.number()).optional() });
|
|
39090
|
+
var UploadedFileSchema = external_exports.looseObject({
|
|
39091
|
+
fileId: external_exports.number(),
|
|
39092
|
+
fileName: external_exports.string().optional(),
|
|
39093
|
+
label: external_exports.string().optional(),
|
|
39094
|
+
fileType: external_exports.string().optional(),
|
|
39095
|
+
sizeInBytes: external_exports.number().optional(),
|
|
39096
|
+
shareClass: external_exports.string().optional()
|
|
39097
|
+
});
|
|
38952
39098
|
var MIME_BY_EXT = {
|
|
38953
39099
|
".pdf": "application/pdf",
|
|
38954
39100
|
".png": "image/png",
|
|
@@ -38984,6 +39130,9 @@ function listDataHintsAtFiles(listData) {
|
|
|
38984
39130
|
return false;
|
|
38985
39131
|
}
|
|
38986
39132
|
function registerMessageTools(server, client2) {
|
|
39133
|
+
const writeMode = getWriteMode();
|
|
39134
|
+
const allowSend = writeMode === "all";
|
|
39135
|
+
const allowDrafts = writeMode !== "none";
|
|
38987
39136
|
server.registerTool("ofw_list_message_folders", {
|
|
38988
39137
|
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.",
|
|
38989
39138
|
annotations: { readOnlyHint: true }
|
|
@@ -38996,8 +39145,8 @@ function registerMessageTools(server, client2) {
|
|
|
38996
39145
|
annotations: { readOnlyHint: true },
|
|
38997
39146
|
inputSchema: {
|
|
38998
39147
|
folderId: external_exports.string().describe('Folder name: "inbox", "sent", or "both" (default "both")').optional(),
|
|
38999
|
-
page: external_exports.number().describe("Page number (default 1)").optional(),
|
|
39000
|
-
size: external_exports.number().describe("Messages per page (default 50)").optional(),
|
|
39148
|
+
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
39149
|
+
size: external_exports.number().int().min(1).describe("Messages per page (default 50)").optional(),
|
|
39001
39150
|
since: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at >= since (inclusive)").optional(),
|
|
39002
39151
|
until: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at < until (exclusive)").optional(),
|
|
39003
39152
|
q: external_exports.string().describe("Substring match on subject AND body (case-insensitive). Use to find messages on a specific topic.").optional()
|
|
@@ -39060,7 +39209,11 @@ function registerMessageTools(server, client2) {
|
|
|
39060
39209
|
let attachments2 = listAttachmentsForMessage(id);
|
|
39061
39210
|
if (attachments2.length === 0 && listDataHintsAtFiles(cached2.listData)) {
|
|
39062
39211
|
try {
|
|
39063
|
-
const detail2 =
|
|
39212
|
+
const detail2 = parseOFW(
|
|
39213
|
+
DetailFilesSchema,
|
|
39214
|
+
await client2.request("GET", `/pub/v3/messages/${id}`),
|
|
39215
|
+
"GET /pub/v3/messages/{id} (attachment backfill)"
|
|
39216
|
+
);
|
|
39064
39217
|
if (Array.isArray(detail2.files) && detail2.files.length > 0) {
|
|
39065
39218
|
await fetchAttachmentMetaForMessage(client2, id, detail2.files);
|
|
39066
39219
|
attachments2 = listAttachmentsForMessage(id);
|
|
@@ -39070,7 +39223,11 @@ function registerMessageTools(server, client2) {
|
|
|
39070
39223
|
}
|
|
39071
39224
|
return jsonResponse({ ...cached2, attachments: attachments2 });
|
|
39072
39225
|
}
|
|
39073
|
-
const detail =
|
|
39226
|
+
const detail = parseOFW(
|
|
39227
|
+
MessageDetailSchema,
|
|
39228
|
+
await client2.request("GET", `/pub/v3/messages/${encodeURIComponent(args.messageId)}`),
|
|
39229
|
+
"GET /pub/v3/messages/{id} (ofw_get_message)"
|
|
39230
|
+
);
|
|
39074
39231
|
const folder = cached2?.folder ?? "inbox";
|
|
39075
39232
|
const row = {
|
|
39076
39233
|
id: detail.id,
|
|
@@ -39092,7 +39249,7 @@ function registerMessageTools(server, client2) {
|
|
|
39092
39249
|
const attachments = listAttachmentsForMessage(detail.id);
|
|
39093
39250
|
return jsonResponse({ ...row, attachments });
|
|
39094
39251
|
});
|
|
39095
|
-
server.registerTool("ofw_send_message", {
|
|
39252
|
+
if (allowSend) server.registerTool("ofw_send_message", {
|
|
39096
39253
|
description: "Send a message via OurFamilyWizard. To send an existing draft, pass messageId \u2014 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.",
|
|
39097
39254
|
annotations: { destructiveHint: true },
|
|
39098
39255
|
inputSchema: {
|
|
@@ -39162,9 +39319,11 @@ function registerMessageTools(server, client2) {
|
|
|
39162
39319
|
draft: false,
|
|
39163
39320
|
includeOriginal: resolvedReplyTo !== null,
|
|
39164
39321
|
replyToId: resolvedReplyTo
|
|
39165
|
-
});
|
|
39322
|
+
}, SentDetailSchema, "ofw_send_message");
|
|
39166
39323
|
let persisted = null;
|
|
39324
|
+
let verifyNote = null;
|
|
39167
39325
|
if (newId !== null) {
|
|
39326
|
+
verifyNote = verifyWriteLanded("message", { subject, body }, detail);
|
|
39168
39327
|
persisted = {
|
|
39169
39328
|
id: newId,
|
|
39170
39329
|
folder: "sent",
|
|
@@ -39192,13 +39351,18 @@ function registerMessageTools(server, client2) {
|
|
|
39192
39351
|
});
|
|
39193
39352
|
}
|
|
39194
39353
|
}
|
|
39195
|
-
|
|
39354
|
+
let unconfirmedNote = null;
|
|
39355
|
+
if (newId === null) {
|
|
39356
|
+
const draftClause = draftRef !== void 0 ? `Draft ${draftRef} was NOT deleted \u2014 check` : "Check";
|
|
39357
|
+
unconfirmedNote = `WARNING: OFW's send response did not include a message id, so the send could not be confirmed. ${draftClause} ourfamilywizard.com to see whether the message went out before retrying.`;
|
|
39358
|
+
} else if (draftRef !== void 0) {
|
|
39196
39359
|
await deleteOFWMessages(client2, [draftRef]);
|
|
39197
39360
|
deleteDraft(draftRef);
|
|
39198
39361
|
}
|
|
39199
39362
|
const responseObj = persisted ?? raw;
|
|
39200
39363
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
39201
|
-
|
|
39364
|
+
const notes = [rewriteNote, verifyNote, unconfirmedNote].filter((n) => n !== null).join("\n\n");
|
|
39365
|
+
return textResponse(notes ? `${notes}
|
|
39202
39366
|
|
|
39203
39367
|
${text}` : text);
|
|
39204
39368
|
});
|
|
@@ -39206,8 +39370,8 @@ ${text}` : text);
|
|
|
39206
39370
|
description: "List draft messages from the local OurFamilyWizard cache. Call ofw_sync_messages first if the cache is empty.",
|
|
39207
39371
|
annotations: { readOnlyHint: true },
|
|
39208
39372
|
inputSchema: {
|
|
39209
|
-
page: external_exports.number().describe("Page number (default 1)").optional(),
|
|
39210
|
-
size: external_exports.number().describe("Drafts per page (default 50)").optional()
|
|
39373
|
+
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
39374
|
+
size: external_exports.number().int().min(1).describe("Drafts per page (default 50)").optional()
|
|
39211
39375
|
}
|
|
39212
39376
|
}, async (args) => {
|
|
39213
39377
|
const page = args.page ?? 1;
|
|
@@ -39216,7 +39380,7 @@ ${text}` : text);
|
|
|
39216
39380
|
const payload = drafts.length === 0 ? { drafts: [], note: "Cache empty. Call ofw_sync_messages to populate." } : { drafts };
|
|
39217
39381
|
return jsonResponse(payload);
|
|
39218
39382
|
});
|
|
39219
|
-
server.registerTool("ofw_save_draft", {
|
|
39383
|
+
if (allowDrafts) server.registerTool("ofw_save_draft", {
|
|
39220
39384
|
description: "Save a message as a draft in OurFamilyWizard. Recipients are optional. Pass messageId to replace an existing draft \u2014 note that under the hood this creates a NEW draft and deletes the old one (OFW's update-in-place endpoint silently no-ops while echoing the posted body, so we don't use it); the response.id will be the NEW id, not the messageId you passed, and the change is documented in a transparency NOTE in the response. 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. After saving, the tool re-fetches the draft from OFW to populate the local cache from authoritative server state.",
|
|
39221
39385
|
annotations: { readOnlyHint: false },
|
|
39222
39386
|
inputSchema: {
|
|
@@ -39247,10 +39411,17 @@ ${text}` : text);
|
|
|
39247
39411
|
includeOriginal: resolvedReplyTo !== null,
|
|
39248
39412
|
replyToId: resolvedReplyTo
|
|
39249
39413
|
};
|
|
39250
|
-
const { id: newId, detail, raw } = await postMessageAndRefetch(
|
|
39414
|
+
const { id: newId, detail, raw } = await postMessageAndRefetch(
|
|
39415
|
+
client2,
|
|
39416
|
+
payload,
|
|
39417
|
+
SavedDraftDetailSchema,
|
|
39418
|
+
"ofw_save_draft"
|
|
39419
|
+
);
|
|
39251
39420
|
let persisted = null;
|
|
39252
39421
|
let replaceNote = null;
|
|
39422
|
+
let verifyNote = null;
|
|
39253
39423
|
if (newId !== null) {
|
|
39424
|
+
verifyNote = verifyWriteLanded("draft", { subject: args.subject, body: args.body }, detail);
|
|
39254
39425
|
persisted = {
|
|
39255
39426
|
id: newId,
|
|
39256
39427
|
subject: detail.subject ?? args.subject,
|
|
@@ -39273,12 +39444,12 @@ ${text}` : text);
|
|
|
39273
39444
|
}
|
|
39274
39445
|
const responseObj = persisted ?? raw;
|
|
39275
39446
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Draft saved.";
|
|
39276
|
-
const notes = [rewriteNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
39447
|
+
const notes = [rewriteNote, verifyNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
39277
39448
|
return textResponse(notes ? `${notes}
|
|
39278
39449
|
|
|
39279
39450
|
${text}` : text);
|
|
39280
39451
|
});
|
|
39281
|
-
server.registerTool("ofw_delete_draft", {
|
|
39452
|
+
if (allowDrafts) server.registerTool("ofw_delete_draft", {
|
|
39282
39453
|
description: "Delete a draft message from OurFamilyWizard. Also removes the draft from the local cache.",
|
|
39283
39454
|
annotations: { destructiveHint: true },
|
|
39284
39455
|
inputSchema: {
|
|
@@ -39293,8 +39464,8 @@ ${text}` : text);
|
|
|
39293
39464
|
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.",
|
|
39294
39465
|
annotations: { readOnlyHint: true },
|
|
39295
39466
|
inputSchema: {
|
|
39296
|
-
page: external_exports.number().describe("Page (default 1)").optional(),
|
|
39297
|
-
size: external_exports.number().describe("Per page (default 50)").optional()
|
|
39467
|
+
page: external_exports.number().int().min(1).describe("Page (default 1)").optional(),
|
|
39468
|
+
size: external_exports.number().int().min(1).describe("Per page (default 50)").optional()
|
|
39298
39469
|
}
|
|
39299
39470
|
}, async (args) => {
|
|
39300
39471
|
const page = args.page ?? 1;
|
|
@@ -39315,7 +39486,7 @@ ${text}` : text);
|
|
|
39315
39486
|
}
|
|
39316
39487
|
return jsonResponse(unread);
|
|
39317
39488
|
});
|
|
39318
|
-
server.registerTool("ofw_upload_attachment", {
|
|
39489
|
+
if (allowDrafts) server.registerTool("ofw_upload_attachment", {
|
|
39319
39490
|
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.`,
|
|
39320
39491
|
annotations: { destructiveHint: false },
|
|
39321
39492
|
inputSchema: {
|
|
@@ -39337,7 +39508,12 @@ ${text}` : text);
|
|
|
39337
39508
|
form.append("label", args.label ?? fileName);
|
|
39338
39509
|
form.append("fileName", fileName);
|
|
39339
39510
|
form.append("shareClass", args.shareClass ?? "PRIVATE");
|
|
39340
|
-
const meta3 =
|
|
39511
|
+
const meta3 = parseOFW(
|
|
39512
|
+
UploadedFileSchema,
|
|
39513
|
+
await client2.request("POST", "/pub/v3/myfiles/multipart", form),
|
|
39514
|
+
"POST /pub/v3/myfiles/multipart (ofw_upload_attachment)",
|
|
39515
|
+
"strict"
|
|
39516
|
+
);
|
|
39341
39517
|
upsertAttachmentForMessage({
|
|
39342
39518
|
fileId: meta3.fileId,
|
|
39343
39519
|
fileName: meta3.fileName ?? fileName,
|
|
@@ -39462,6 +39638,7 @@ async function deleteOFWMessages(client2, ids) {
|
|
|
39462
39638
|
|
|
39463
39639
|
// src/tools/calendar.ts
|
|
39464
39640
|
function registerCalendarTools(server, client2) {
|
|
39641
|
+
const allowWrites = getWriteMode() === "all";
|
|
39465
39642
|
server.registerTool("ofw_list_events", {
|
|
39466
39643
|
description: "List OurFamilyWizard calendar events in a date range",
|
|
39467
39644
|
annotations: { readOnlyHint: true },
|
|
@@ -39478,7 +39655,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39478
39655
|
);
|
|
39479
39656
|
return jsonResponse(data);
|
|
39480
39657
|
});
|
|
39481
|
-
server.registerTool("ofw_create_event", {
|
|
39658
|
+
if (allowWrites) server.registerTool("ofw_create_event", {
|
|
39482
39659
|
description: "Create a calendar event in OurFamilyWizard",
|
|
39483
39660
|
annotations: { destructiveHint: false },
|
|
39484
39661
|
inputSchema: {
|
|
@@ -39498,7 +39675,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39498
39675
|
const data = await client2.request("POST", "/pub/v1/calendar/events", args);
|
|
39499
39676
|
return jsonResponse(data);
|
|
39500
39677
|
});
|
|
39501
|
-
server.registerTool("ofw_update_event", {
|
|
39678
|
+
if (allowWrites) server.registerTool("ofw_update_event", {
|
|
39502
39679
|
description: "Update an existing OurFamilyWizard calendar event",
|
|
39503
39680
|
annotations: { destructiveHint: true },
|
|
39504
39681
|
inputSchema: {
|
|
@@ -39516,7 +39693,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39516
39693
|
const data = await client2.request("PUT", `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
|
|
39517
39694
|
return jsonResponse(data);
|
|
39518
39695
|
});
|
|
39519
|
-
server.registerTool("ofw_delete_event", {
|
|
39696
|
+
if (allowWrites) server.registerTool("ofw_delete_event", {
|
|
39520
39697
|
description: "Delete an OurFamilyWizard calendar event",
|
|
39521
39698
|
annotations: { destructiveHint: true },
|
|
39522
39699
|
inputSchema: {
|
|
@@ -39530,6 +39707,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39530
39707
|
|
|
39531
39708
|
// src/tools/expenses.ts
|
|
39532
39709
|
function registerExpenseTools(server, client2) {
|
|
39710
|
+
const allowWrites = getWriteMode() === "all";
|
|
39533
39711
|
server.registerTool("ofw_get_expense_totals", {
|
|
39534
39712
|
description: "Get OurFamilyWizard expense summary totals (owed/paid)",
|
|
39535
39713
|
annotations: { readOnlyHint: true }
|
|
@@ -39541,8 +39719,8 @@ function registerExpenseTools(server, client2) {
|
|
|
39541
39719
|
description: "List OurFamilyWizard expenses with pagination",
|
|
39542
39720
|
annotations: { readOnlyHint: true },
|
|
39543
39721
|
inputSchema: {
|
|
39544
|
-
start: external_exports.number().describe("Start offset (default 0)").optional(),
|
|
39545
|
-
max: external_exports.number().describe("Max results (default 20)").optional()
|
|
39722
|
+
start: external_exports.number().int().min(0).describe("Start offset (default 0)").optional(),
|
|
39723
|
+
max: external_exports.number().int().min(1).describe("Max results (default 20)").optional()
|
|
39546
39724
|
}
|
|
39547
39725
|
}, async (args) => {
|
|
39548
39726
|
const start = args.start ?? 0;
|
|
@@ -39550,7 +39728,7 @@ function registerExpenseTools(server, client2) {
|
|
|
39550
39728
|
const data = await client2.request("GET", `/pub/v2/expense/expenses?start=${start}&max=${max}`);
|
|
39551
39729
|
return jsonResponse(data);
|
|
39552
39730
|
});
|
|
39553
|
-
server.registerTool("ofw_create_expense", {
|
|
39731
|
+
if (allowWrites) server.registerTool("ofw_create_expense", {
|
|
39554
39732
|
description: "Log a new expense in OurFamilyWizard",
|
|
39555
39733
|
annotations: { destructiveHint: false },
|
|
39556
39734
|
inputSchema: {
|
|
@@ -39565,12 +39743,13 @@ function registerExpenseTools(server, client2) {
|
|
|
39565
39743
|
|
|
39566
39744
|
// src/tools/journal.ts
|
|
39567
39745
|
function registerJournalTools(server, client2) {
|
|
39746
|
+
const allowWrites = getWriteMode() === "all";
|
|
39568
39747
|
server.registerTool("ofw_list_journal_entries", {
|
|
39569
39748
|
description: "List OurFamilyWizard journal entries",
|
|
39570
39749
|
annotations: { readOnlyHint: true },
|
|
39571
39750
|
inputSchema: {
|
|
39572
|
-
start: external_exports.number().describe("Start offset (default 1)").optional(),
|
|
39573
|
-
max: external_exports.number().describe("Max results (default 10)").optional()
|
|
39751
|
+
start: external_exports.number().int().min(1).describe("Start offset (default 1)").optional(),
|
|
39752
|
+
max: external_exports.number().int().min(1).describe("Max results (default 10)").optional()
|
|
39574
39753
|
}
|
|
39575
39754
|
}, async (args) => {
|
|
39576
39755
|
const start = args.start ?? 1;
|
|
@@ -39578,7 +39757,7 @@ function registerJournalTools(server, client2) {
|
|
|
39578
39757
|
const data = await client2.request("GET", `/pub/v1/journals?start=${start}&max=${max}`);
|
|
39579
39758
|
return jsonResponse(data);
|
|
39580
39759
|
});
|
|
39581
|
-
server.registerTool("ofw_create_journal_entry", {
|
|
39760
|
+
if (allowWrites) server.registerTool("ofw_create_journal_entry", {
|
|
39582
39761
|
description: "Create a new journal entry in OurFamilyWizard",
|
|
39583
39762
|
annotations: { destructiveHint: false },
|
|
39584
39763
|
inputSchema: {
|
|
@@ -39604,7 +39783,7 @@ process.emit = function(event, ...args) {
|
|
|
39604
39783
|
};
|
|
39605
39784
|
await runMcp({
|
|
39606
39785
|
name: "ofw",
|
|
39607
|
-
version: "2.
|
|
39786
|
+
version: "2.4.0",
|
|
39608
39787
|
// x-release-please-version
|
|
39609
39788
|
deps: client,
|
|
39610
39789
|
tools: [
|
package/dist/config.js
CHANGED
|
@@ -54,6 +54,32 @@ export function getAttachmentsDir() {
|
|
|
54
54
|
export function parseBoolEnv(name) {
|
|
55
55
|
return parseBoolEnvUtil(name);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Gate for write-tool registration, read at registration time (startup).
|
|
59
|
+
*
|
|
60
|
+
* none No write tools are registered — pure read/sync/search surface.
|
|
61
|
+
* drafts Draft-level writes only (ofw_save_draft, ofw_delete_draft,
|
|
62
|
+
* ofw_upload_attachment). Nothing that lands on the court-visible
|
|
63
|
+
* record (send, calendar/expense/journal writes) is registered —
|
|
64
|
+
* the only way to send remains a human in the OFW web UI.
|
|
65
|
+
* all Every tool registers (the default; fully backward compatible).
|
|
66
|
+
*
|
|
67
|
+
* Unregistered tools cannot be invoked by any host permission setting or
|
|
68
|
+
* injected instruction — the gate is structural, not behavioral. An
|
|
69
|
+
* unrecognized value fails closed to 'none': this is a safety control, so a
|
|
70
|
+
* typo must never silently grant write access.
|
|
71
|
+
*/
|
|
72
|
+
export function getWriteMode() {
|
|
73
|
+
const raw = process.env.OFW_WRITE_MODE;
|
|
74
|
+
if (typeof raw !== 'string' || raw.trim().length === 0)
|
|
75
|
+
return 'all';
|
|
76
|
+
const mode = raw.trim().toLowerCase();
|
|
77
|
+
if (mode === 'none' || mode === 'drafts' || mode === 'all')
|
|
78
|
+
return mode;
|
|
79
|
+
// stdio transport: stderr only — stdout is reserved for JSON-RPC.
|
|
80
|
+
console.error(`[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" — failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`);
|
|
81
|
+
return 'none';
|
|
82
|
+
}
|
|
57
83
|
// Default for ofw_download_attachment's `inline` arg when the caller doesn't
|
|
58
84
|
// pass one. Set OFW_INLINE_ATTACHMENTS=true to have attachments returned as
|
|
59
85
|
// MCP content blocks by default (skipping disk) — useful on sandboxed MCP
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { registerJournalTools } from './tools/journal.js';
|
|
|
24
24
|
// always succeeds before any credential check runs.
|
|
25
25
|
await runMcp({
|
|
26
26
|
name: 'ofw',
|
|
27
|
-
version: '2.
|
|
27
|
+
version: '2.4.0', // x-release-please-version
|
|
28
28
|
deps: client,
|
|
29
29
|
tools: [
|
|
30
30
|
registerUserTools,
|