ofw-mcp 2.3.2 → 2.4.1
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 +37 -2
- package/.mcp.json +5 -2
- package/LICENSE +21 -0
- package/README.md +42 -22
- package/dist/auth-password.js +7 -2
- package/dist/bundle.js +250 -48
- package/dist/client.js +6 -0
- package/dist/config.js +26 -0
- package/dist/env-bootstrap.js +28 -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
|
@@ -13,7 +13,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
13
13
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
14
|
});
|
|
15
15
|
var __commonJS = (cb, mod) => function __require2() {
|
|
16
|
-
|
|
16
|
+
try {
|
|
17
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
throw mod = 0, e;
|
|
20
|
+
}
|
|
17
21
|
};
|
|
18
22
|
var __export = (target, all) => {
|
|
19
23
|
for (var name in all)
|
|
@@ -38069,8 +38073,7 @@ async function loginWithPassword(username, password) {
|
|
|
38069
38073
|
headers: { ...OFW_PROTOCOL_HEADERS },
|
|
38070
38074
|
redirect: "manual"
|
|
38071
38075
|
});
|
|
38072
|
-
const
|
|
38073
|
-
const sessionCookie = setCookie.split(";")[0];
|
|
38076
|
+
const sessionCookie = initResponse.headers.getSetCookie().map((c) => c.split(";")[0]).join("; ");
|
|
38074
38077
|
const response = await fetch(`${BASE_URL}/ofw/login`, {
|
|
38075
38078
|
method: "POST",
|
|
38076
38079
|
headers: {
|
|
@@ -38130,6 +38133,16 @@ function getAttachmentsDir() {
|
|
|
38130
38133
|
function parseBoolEnv2(name) {
|
|
38131
38134
|
return parseBoolEnv(name);
|
|
38132
38135
|
}
|
|
38136
|
+
function getWriteMode() {
|
|
38137
|
+
const raw = process.env.OFW_WRITE_MODE;
|
|
38138
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return "all";
|
|
38139
|
+
const mode = raw.trim().toLowerCase();
|
|
38140
|
+
if (mode === "none" || mode === "drafts" || mode === "all") return mode;
|
|
38141
|
+
console.error(
|
|
38142
|
+
`[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" \u2014 failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`
|
|
38143
|
+
);
|
|
38144
|
+
return "none";
|
|
38145
|
+
}
|
|
38133
38146
|
function getDefaultInlineAttachments() {
|
|
38134
38147
|
return parseBoolEnv2("OFW_INLINE_ATTACHMENTS");
|
|
38135
38148
|
}
|
|
@@ -38137,7 +38150,8 @@ function getDefaultInlineAttachments() {
|
|
|
38137
38150
|
// package.json
|
|
38138
38151
|
var package_default = {
|
|
38139
38152
|
name: "ofw-mcp",
|
|
38140
|
-
version: "2.
|
|
38153
|
+
version: "2.4.1",
|
|
38154
|
+
license: "MIT",
|
|
38141
38155
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
38142
38156
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
38143
38157
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38146,6 +38160,9 @@ var package_default = {
|
|
|
38146
38160
|
url: "git+https://github.com/chrischall/ofw-mcp.git"
|
|
38147
38161
|
},
|
|
38148
38162
|
type: "module",
|
|
38163
|
+
engines: {
|
|
38164
|
+
node: ">=22.5.0"
|
|
38165
|
+
},
|
|
38149
38166
|
bin: {
|
|
38150
38167
|
"ofw-mcp": "dist/index.js"
|
|
38151
38168
|
},
|
|
@@ -38244,7 +38261,26 @@ async function resolveAuth() {
|
|
|
38244
38261
|
);
|
|
38245
38262
|
}
|
|
38246
38263
|
|
|
38264
|
+
// src/env-bootstrap.ts
|
|
38265
|
+
var USER_CONFIG_KEYS = [
|
|
38266
|
+
"OFW_USERNAME",
|
|
38267
|
+
"OFW_PASSWORD",
|
|
38268
|
+
"OFW_INLINE_ATTACHMENTS",
|
|
38269
|
+
"OFW_ATTACHMENTS_DIR",
|
|
38270
|
+
"OFW_WRITE_MODE"
|
|
38271
|
+
];
|
|
38272
|
+
function clearBlankInjectedEnv(env = process.env, keys = USER_CONFIG_KEYS) {
|
|
38273
|
+
for (const key of keys) {
|
|
38274
|
+
const value = env[key];
|
|
38275
|
+
if (value === void 0) continue;
|
|
38276
|
+
if (value.trim() === "" || value.includes("${")) {
|
|
38277
|
+
delete env[key];
|
|
38278
|
+
}
|
|
38279
|
+
}
|
|
38280
|
+
}
|
|
38281
|
+
|
|
38247
38282
|
// src/client.ts
|
|
38283
|
+
clearBlankInjectedEnv();
|
|
38248
38284
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38249
38285
|
await loadDotenvSafely({ path: join4(__dirname, "..", ".env") });
|
|
38250
38286
|
function parseContentDispositionFilename(cd) {
|
|
@@ -38401,9 +38437,24 @@ var OFWClient = class {
|
|
|
38401
38437
|
};
|
|
38402
38438
|
var client = new OFWClient();
|
|
38403
38439
|
|
|
38440
|
+
// src/validate.ts
|
|
38441
|
+
function parseOFW(schema, raw, ctx, mode = "lenient") {
|
|
38442
|
+
const result = schema.safeParse(raw);
|
|
38443
|
+
if (result.success) return result.data;
|
|
38444
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
38445
|
+
const message = `OFW response for ${ctx} failed validation: ${issues}`;
|
|
38446
|
+
if (mode === "strict") throw new Error(message);
|
|
38447
|
+
console.error(`[ofw-mcp] WARNING: ${message} \u2014 continuing with the raw response; fields derived from it may be missing or wrong.`);
|
|
38448
|
+
return raw;
|
|
38449
|
+
}
|
|
38450
|
+
|
|
38404
38451
|
// src/tools/_shared.ts
|
|
38405
38452
|
var jsonResponse = textResult;
|
|
38406
38453
|
var textResponse = rawTextResult;
|
|
38454
|
+
var ApiRecipientSchema = external_exports.looseObject({
|
|
38455
|
+
user: external_exports.looseObject({ id: external_exports.number().optional(), name: external_exports.string().optional() }).optional(),
|
|
38456
|
+
viewed: external_exports.looseObject({ dateTime: external_exports.string() }).nullable().optional()
|
|
38457
|
+
});
|
|
38407
38458
|
function mapRecipients(items) {
|
|
38408
38459
|
return (items ?? []).map((r) => ({
|
|
38409
38460
|
userId: r.user?.id ?? 0,
|
|
@@ -38412,15 +38463,36 @@ function mapRecipients(items) {
|
|
|
38412
38463
|
}));
|
|
38413
38464
|
}
|
|
38414
38465
|
var expandPath2 = expandPath;
|
|
38415
|
-
|
|
38416
|
-
const
|
|
38417
|
-
|
|
38418
|
-
"
|
|
38419
|
-
|
|
38466
|
+
function verifyWriteLanded(kind, sent, persisted) {
|
|
38467
|
+
const mismatches = [];
|
|
38468
|
+
if (typeof persisted.subject !== "string" || !persisted.subject.includes(sent.subject)) {
|
|
38469
|
+
mismatches.push("subject");
|
|
38470
|
+
}
|
|
38471
|
+
if (typeof persisted.body !== "string" || !persisted.body.includes(sent.body)) {
|
|
38472
|
+
mismatches.push("body");
|
|
38473
|
+
}
|
|
38474
|
+
if (mismatches.length === 0) return null;
|
|
38475
|
+
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.`;
|
|
38476
|
+
}
|
|
38477
|
+
var PostMessagesResponseSchema = external_exports.looseObject({
|
|
38478
|
+
id: external_exports.number().optional(),
|
|
38479
|
+
entityId: external_exports.number().optional()
|
|
38480
|
+
}).nullable();
|
|
38481
|
+
async function postMessageAndRefetch(client2, payload, detailSchema, ctx) {
|
|
38482
|
+
const raw = parseOFW(
|
|
38483
|
+
PostMessagesResponseSchema,
|
|
38484
|
+
await client2.request("POST", "/pub/v3/messages", payload),
|
|
38485
|
+
`POST /pub/v3/messages (${ctx})`,
|
|
38486
|
+
"strict"
|
|
38420
38487
|
);
|
|
38421
38488
|
const id = typeof raw?.id === "number" ? raw.id : typeof raw?.entityId === "number" ? raw.entityId : null;
|
|
38422
38489
|
if (id === null) return { id: null, detail: null, raw };
|
|
38423
|
-
const detail =
|
|
38490
|
+
const detail = parseOFW(
|
|
38491
|
+
detailSchema,
|
|
38492
|
+
await client2.request("GET", `/pub/v3/messages/${id}`),
|
|
38493
|
+
`GET /pub/v3/messages/{id} (${ctx})`,
|
|
38494
|
+
"strict"
|
|
38495
|
+
);
|
|
38424
38496
|
return { id, detail, raw };
|
|
38425
38497
|
}
|
|
38426
38498
|
|
|
@@ -38789,8 +38861,20 @@ function markAttachmentDownloaded(fileId, path) {
|
|
|
38789
38861
|
}
|
|
38790
38862
|
|
|
38791
38863
|
// src/sync.ts
|
|
38864
|
+
var FileMetaSchema = external_exports.looseObject({
|
|
38865
|
+
fileId: external_exports.number(),
|
|
38866
|
+
label: external_exports.string().optional(),
|
|
38867
|
+
fileName: external_exports.string().optional(),
|
|
38868
|
+
fileType: external_exports.string().optional(),
|
|
38869
|
+
// MIME
|
|
38870
|
+
fileSize: external_exports.number().optional()
|
|
38871
|
+
});
|
|
38792
38872
|
async function fetchAttachmentMeta(client2, fileId, messageId) {
|
|
38793
|
-
const meta3 =
|
|
38873
|
+
const meta3 = parseOFW(
|
|
38874
|
+
FileMetaSchema,
|
|
38875
|
+
await client2.request("GET", `/pub/v1/myfiles/${fileId}`),
|
|
38876
|
+
"GET /pub/v1/myfiles/{fileId}"
|
|
38877
|
+
);
|
|
38794
38878
|
upsertAttachmentForMessage({
|
|
38795
38879
|
fileId: meta3.fileId ?? fileId,
|
|
38796
38880
|
fileName: meta3.fileName ?? `file-${fileId}`,
|
|
@@ -38804,10 +38888,14 @@ async function fetchAttachmentMeta(client2, fileId, messageId) {
|
|
|
38804
38888
|
async function fetchAttachmentMetaForMessage(client2, messageId, fileIds) {
|
|
38805
38889
|
await Promise.allSettled(fileIds.map((fid) => fetchAttachmentMeta(client2, fid, messageId)));
|
|
38806
38890
|
}
|
|
38891
|
+
var FoldersSchema = external_exports.looseObject({
|
|
38892
|
+
systemFolders: external_exports.array(external_exports.looseObject({ id: external_exports.string(), folderType: external_exports.string() })).optional()
|
|
38893
|
+
});
|
|
38807
38894
|
async function resolveFolderIds(client2) {
|
|
38808
|
-
const data =
|
|
38809
|
-
|
|
38810
|
-
"/pub/v1/messageFolders?includeFolderCounts=true"
|
|
38895
|
+
const data = parseOFW(
|
|
38896
|
+
FoldersSchema,
|
|
38897
|
+
await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true"),
|
|
38898
|
+
"GET /pub/v1/messageFolders"
|
|
38811
38899
|
);
|
|
38812
38900
|
const sys = data.systemFolders ?? [];
|
|
38813
38901
|
const find = (type) => {
|
|
@@ -38823,6 +38911,19 @@ async function resolveFolderIds(client2) {
|
|
|
38823
38911
|
setMeta("drafts_folder_id", ids.drafts);
|
|
38824
38912
|
return ids;
|
|
38825
38913
|
}
|
|
38914
|
+
var ListItemSchema = external_exports.looseObject({
|
|
38915
|
+
id: external_exports.number(),
|
|
38916
|
+
subject: external_exports.string(),
|
|
38917
|
+
date: external_exports.looseObject({ dateTime: external_exports.string() }),
|
|
38918
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
38919
|
+
showNeverViewed: external_exports.boolean(),
|
|
38920
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
38921
|
+
});
|
|
38922
|
+
var ListResponseSchema = external_exports.looseObject({ data: external_exports.array(ListItemSchema).optional() });
|
|
38923
|
+
var DetailResponseSchema = external_exports.looseObject({
|
|
38924
|
+
body: external_exports.string().optional(),
|
|
38925
|
+
files: external_exports.array(external_exports.number()).optional()
|
|
38926
|
+
});
|
|
38826
38927
|
async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
38827
38928
|
let page = 1;
|
|
38828
38929
|
let synced = 0;
|
|
@@ -38830,7 +38931,11 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
38830
38931
|
const unread = [];
|
|
38831
38932
|
while (true) {
|
|
38832
38933
|
const path = `/pub/v3/messages?folders=${encodeURIComponent(folderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
38833
|
-
const list =
|
|
38934
|
+
const list = parseOFW(
|
|
38935
|
+
ListResponseSchema,
|
|
38936
|
+
await client2.request("GET", path),
|
|
38937
|
+
`GET /pub/v3/messages?folders={${folder}}`
|
|
38938
|
+
);
|
|
38834
38939
|
const items = list.data ?? [];
|
|
38835
38940
|
if (items.length === 0) break;
|
|
38836
38941
|
let pageHadNewItem = false;
|
|
@@ -38845,7 +38950,11 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
38845
38950
|
let fetchedBodyAt = null;
|
|
38846
38951
|
let detailFileIds = [];
|
|
38847
38952
|
if (shouldFetchBody) {
|
|
38848
|
-
const detail =
|
|
38953
|
+
const detail = parseOFW(
|
|
38954
|
+
DetailResponseSchema,
|
|
38955
|
+
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
38956
|
+
"GET /pub/v3/messages/{id} (sync)"
|
|
38957
|
+
);
|
|
38849
38958
|
body = detail.body ?? "";
|
|
38850
38959
|
fetchedBodyAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38851
38960
|
if (Array.isArray(detail.files) && detail.files.length > 0) {
|
|
@@ -38887,17 +38996,44 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
38887
38996
|
});
|
|
38888
38997
|
return { synced, unread };
|
|
38889
38998
|
}
|
|
38999
|
+
var DraftListItemSchema = external_exports.looseObject({
|
|
39000
|
+
id: external_exports.number(),
|
|
39001
|
+
subject: external_exports.string(),
|
|
39002
|
+
date: external_exports.looseObject({ dateTime: external_exports.string() }),
|
|
39003
|
+
replyToId: external_exports.number().nullable().optional(),
|
|
39004
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39005
|
+
});
|
|
39006
|
+
var DraftListResponseSchema = external_exports.looseObject({ data: external_exports.array(DraftListItemSchema).optional() });
|
|
39007
|
+
var DraftDetailSchema = external_exports.looseObject({
|
|
39008
|
+
body: external_exports.string().optional(),
|
|
39009
|
+
subject: external_exports.string().optional()
|
|
39010
|
+
});
|
|
38890
39011
|
async function syncDrafts(client2, draftsFolderId) {
|
|
38891
|
-
const
|
|
38892
|
-
|
|
38893
|
-
|
|
39012
|
+
const items = [];
|
|
39013
|
+
let page = 1;
|
|
39014
|
+
while (true) {
|
|
39015
|
+
const path = `/pub/v3/messages?folders=${encodeURIComponent(draftsFolderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
39016
|
+
const list = parseOFW(
|
|
39017
|
+
DraftListResponseSchema,
|
|
39018
|
+
await client2.request("GET", path),
|
|
39019
|
+
"GET /pub/v3/messages?folders={drafts}"
|
|
39020
|
+
);
|
|
39021
|
+
const pageItems = list.data ?? [];
|
|
39022
|
+
items.push(...pageItems);
|
|
39023
|
+
if (pageItems.length < 50) break;
|
|
39024
|
+
page++;
|
|
39025
|
+
}
|
|
38894
39026
|
const seenIds = /* @__PURE__ */ new Set();
|
|
38895
39027
|
let synced = 0;
|
|
38896
39028
|
for (const item of items) {
|
|
38897
39029
|
seenIds.add(item.id);
|
|
38898
39030
|
const modifiedAt = item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
38899
39031
|
const existing = getDraft(item.id);
|
|
38900
|
-
const detail =
|
|
39032
|
+
const detail = parseOFW(
|
|
39033
|
+
DraftDetailSchema,
|
|
39034
|
+
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
39035
|
+
"GET /pub/v3/messages/{id} (drafts sync)"
|
|
39036
|
+
);
|
|
38901
39037
|
const row = {
|
|
38902
39038
|
id: item.id,
|
|
38903
39039
|
subject: detail.subject ?? item.subject ?? "(no subject)",
|
|
@@ -38949,6 +39085,39 @@ async function syncAll(client2, opts) {
|
|
|
38949
39085
|
// src/tools/messages.ts
|
|
38950
39086
|
import { mkdirSync as mkdirSync2, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
38951
39087
|
import { basename, dirname as dirname3, extname, join as join5 } from "node:path";
|
|
39088
|
+
var DateSchema = external_exports.looseObject({ dateTime: external_exports.string() });
|
|
39089
|
+
var SentDetailSchema = external_exports.looseObject({
|
|
39090
|
+
subject: external_exports.string().optional(),
|
|
39091
|
+
body: external_exports.string().optional(),
|
|
39092
|
+
date: DateSchema.optional(),
|
|
39093
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
39094
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39095
|
+
});
|
|
39096
|
+
var SavedDraftDetailSchema = external_exports.looseObject({
|
|
39097
|
+
subject: external_exports.string().optional(),
|
|
39098
|
+
body: external_exports.string().optional(),
|
|
39099
|
+
date: DateSchema.optional(),
|
|
39100
|
+
replyToId: external_exports.number().nullable().optional(),
|
|
39101
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39102
|
+
});
|
|
39103
|
+
var MessageDetailSchema = external_exports.looseObject({
|
|
39104
|
+
id: external_exports.number(),
|
|
39105
|
+
subject: external_exports.string(),
|
|
39106
|
+
body: external_exports.string().optional(),
|
|
39107
|
+
date: DateSchema,
|
|
39108
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
39109
|
+
files: external_exports.array(external_exports.number()).optional(),
|
|
39110
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39111
|
+
});
|
|
39112
|
+
var DetailFilesSchema = external_exports.looseObject({ files: external_exports.array(external_exports.number()).optional() });
|
|
39113
|
+
var UploadedFileSchema = external_exports.looseObject({
|
|
39114
|
+
fileId: external_exports.number(),
|
|
39115
|
+
fileName: external_exports.string().optional(),
|
|
39116
|
+
label: external_exports.string().optional(),
|
|
39117
|
+
fileType: external_exports.string().optional(),
|
|
39118
|
+
sizeInBytes: external_exports.number().optional(),
|
|
39119
|
+
shareClass: external_exports.string().optional()
|
|
39120
|
+
});
|
|
38952
39121
|
var MIME_BY_EXT = {
|
|
38953
39122
|
".pdf": "application/pdf",
|
|
38954
39123
|
".png": "image/png",
|
|
@@ -38984,6 +39153,9 @@ function listDataHintsAtFiles(listData) {
|
|
|
38984
39153
|
return false;
|
|
38985
39154
|
}
|
|
38986
39155
|
function registerMessageTools(server, client2) {
|
|
39156
|
+
const writeMode = getWriteMode();
|
|
39157
|
+
const allowSend = writeMode === "all";
|
|
39158
|
+
const allowDrafts = writeMode !== "none";
|
|
38987
39159
|
server.registerTool("ofw_list_message_folders", {
|
|
38988
39160
|
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
39161
|
annotations: { readOnlyHint: true }
|
|
@@ -38996,8 +39168,8 @@ function registerMessageTools(server, client2) {
|
|
|
38996
39168
|
annotations: { readOnlyHint: true },
|
|
38997
39169
|
inputSchema: {
|
|
38998
39170
|
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(),
|
|
39171
|
+
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
39172
|
+
size: external_exports.number().int().min(1).describe("Messages per page (default 50)").optional(),
|
|
39001
39173
|
since: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at >= since (inclusive)").optional(),
|
|
39002
39174
|
until: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at < until (exclusive)").optional(),
|
|
39003
39175
|
q: external_exports.string().describe("Substring match on subject AND body (case-insensitive). Use to find messages on a specific topic.").optional()
|
|
@@ -39060,7 +39232,11 @@ function registerMessageTools(server, client2) {
|
|
|
39060
39232
|
let attachments2 = listAttachmentsForMessage(id);
|
|
39061
39233
|
if (attachments2.length === 0 && listDataHintsAtFiles(cached2.listData)) {
|
|
39062
39234
|
try {
|
|
39063
|
-
const detail2 =
|
|
39235
|
+
const detail2 = parseOFW(
|
|
39236
|
+
DetailFilesSchema,
|
|
39237
|
+
await client2.request("GET", `/pub/v3/messages/${id}`),
|
|
39238
|
+
"GET /pub/v3/messages/{id} (attachment backfill)"
|
|
39239
|
+
);
|
|
39064
39240
|
if (Array.isArray(detail2.files) && detail2.files.length > 0) {
|
|
39065
39241
|
await fetchAttachmentMetaForMessage(client2, id, detail2.files);
|
|
39066
39242
|
attachments2 = listAttachmentsForMessage(id);
|
|
@@ -39070,7 +39246,11 @@ function registerMessageTools(server, client2) {
|
|
|
39070
39246
|
}
|
|
39071
39247
|
return jsonResponse({ ...cached2, attachments: attachments2 });
|
|
39072
39248
|
}
|
|
39073
|
-
const detail =
|
|
39249
|
+
const detail = parseOFW(
|
|
39250
|
+
MessageDetailSchema,
|
|
39251
|
+
await client2.request("GET", `/pub/v3/messages/${encodeURIComponent(args.messageId)}`),
|
|
39252
|
+
"GET /pub/v3/messages/{id} (ofw_get_message)"
|
|
39253
|
+
);
|
|
39074
39254
|
const folder = cached2?.folder ?? "inbox";
|
|
39075
39255
|
const row = {
|
|
39076
39256
|
id: detail.id,
|
|
@@ -39092,7 +39272,7 @@ function registerMessageTools(server, client2) {
|
|
|
39092
39272
|
const attachments = listAttachmentsForMessage(detail.id);
|
|
39093
39273
|
return jsonResponse({ ...row, attachments });
|
|
39094
39274
|
});
|
|
39095
|
-
server.registerTool("ofw_send_message", {
|
|
39275
|
+
if (allowSend) server.registerTool("ofw_send_message", {
|
|
39096
39276
|
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
39277
|
annotations: { destructiveHint: true },
|
|
39098
39278
|
inputSchema: {
|
|
@@ -39162,9 +39342,11 @@ function registerMessageTools(server, client2) {
|
|
|
39162
39342
|
draft: false,
|
|
39163
39343
|
includeOriginal: resolvedReplyTo !== null,
|
|
39164
39344
|
replyToId: resolvedReplyTo
|
|
39165
|
-
});
|
|
39345
|
+
}, SentDetailSchema, "ofw_send_message");
|
|
39166
39346
|
let persisted = null;
|
|
39347
|
+
let verifyNote = null;
|
|
39167
39348
|
if (newId !== null) {
|
|
39349
|
+
verifyNote = verifyWriteLanded("message", { subject, body }, detail);
|
|
39168
39350
|
persisted = {
|
|
39169
39351
|
id: newId,
|
|
39170
39352
|
folder: "sent",
|
|
@@ -39192,13 +39374,18 @@ function registerMessageTools(server, client2) {
|
|
|
39192
39374
|
});
|
|
39193
39375
|
}
|
|
39194
39376
|
}
|
|
39195
|
-
|
|
39377
|
+
let unconfirmedNote = null;
|
|
39378
|
+
if (newId === null) {
|
|
39379
|
+
const draftClause = draftRef !== void 0 ? `Draft ${draftRef} was NOT deleted \u2014 check` : "Check";
|
|
39380
|
+
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.`;
|
|
39381
|
+
} else if (draftRef !== void 0) {
|
|
39196
39382
|
await deleteOFWMessages(client2, [draftRef]);
|
|
39197
39383
|
deleteDraft(draftRef);
|
|
39198
39384
|
}
|
|
39199
39385
|
const responseObj = persisted ?? raw;
|
|
39200
39386
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
39201
|
-
|
|
39387
|
+
const notes = [rewriteNote, verifyNote, unconfirmedNote].filter((n) => n !== null).join("\n\n");
|
|
39388
|
+
return textResponse(notes ? `${notes}
|
|
39202
39389
|
|
|
39203
39390
|
${text}` : text);
|
|
39204
39391
|
});
|
|
@@ -39206,8 +39393,8 @@ ${text}` : text);
|
|
|
39206
39393
|
description: "List draft messages from the local OurFamilyWizard cache. Call ofw_sync_messages first if the cache is empty.",
|
|
39207
39394
|
annotations: { readOnlyHint: true },
|
|
39208
39395
|
inputSchema: {
|
|
39209
|
-
page: external_exports.number().describe("Page number (default 1)").optional(),
|
|
39210
|
-
size: external_exports.number().describe("Drafts per page (default 50)").optional()
|
|
39396
|
+
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
39397
|
+
size: external_exports.number().int().min(1).describe("Drafts per page (default 50)").optional()
|
|
39211
39398
|
}
|
|
39212
39399
|
}, async (args) => {
|
|
39213
39400
|
const page = args.page ?? 1;
|
|
@@ -39216,7 +39403,7 @@ ${text}` : text);
|
|
|
39216
39403
|
const payload = drafts.length === 0 ? { drafts: [], note: "Cache empty. Call ofw_sync_messages to populate." } : { drafts };
|
|
39217
39404
|
return jsonResponse(payload);
|
|
39218
39405
|
});
|
|
39219
|
-
server.registerTool("ofw_save_draft", {
|
|
39406
|
+
if (allowDrafts) server.registerTool("ofw_save_draft", {
|
|
39220
39407
|
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
39408
|
annotations: { readOnlyHint: false },
|
|
39222
39409
|
inputSchema: {
|
|
@@ -39247,10 +39434,17 @@ ${text}` : text);
|
|
|
39247
39434
|
includeOriginal: resolvedReplyTo !== null,
|
|
39248
39435
|
replyToId: resolvedReplyTo
|
|
39249
39436
|
};
|
|
39250
|
-
const { id: newId, detail, raw } = await postMessageAndRefetch(
|
|
39437
|
+
const { id: newId, detail, raw } = await postMessageAndRefetch(
|
|
39438
|
+
client2,
|
|
39439
|
+
payload,
|
|
39440
|
+
SavedDraftDetailSchema,
|
|
39441
|
+
"ofw_save_draft"
|
|
39442
|
+
);
|
|
39251
39443
|
let persisted = null;
|
|
39252
39444
|
let replaceNote = null;
|
|
39445
|
+
let verifyNote = null;
|
|
39253
39446
|
if (newId !== null) {
|
|
39447
|
+
verifyNote = verifyWriteLanded("draft", { subject: args.subject, body: args.body }, detail);
|
|
39254
39448
|
persisted = {
|
|
39255
39449
|
id: newId,
|
|
39256
39450
|
subject: detail.subject ?? args.subject,
|
|
@@ -39273,12 +39467,12 @@ ${text}` : text);
|
|
|
39273
39467
|
}
|
|
39274
39468
|
const responseObj = persisted ?? raw;
|
|
39275
39469
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Draft saved.";
|
|
39276
|
-
const notes = [rewriteNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
39470
|
+
const notes = [rewriteNote, verifyNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
39277
39471
|
return textResponse(notes ? `${notes}
|
|
39278
39472
|
|
|
39279
39473
|
${text}` : text);
|
|
39280
39474
|
});
|
|
39281
|
-
server.registerTool("ofw_delete_draft", {
|
|
39475
|
+
if (allowDrafts) server.registerTool("ofw_delete_draft", {
|
|
39282
39476
|
description: "Delete a draft message from OurFamilyWizard. Also removes the draft from the local cache.",
|
|
39283
39477
|
annotations: { destructiveHint: true },
|
|
39284
39478
|
inputSchema: {
|
|
@@ -39293,8 +39487,8 @@ ${text}` : text);
|
|
|
39293
39487
|
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
39488
|
annotations: { readOnlyHint: true },
|
|
39295
39489
|
inputSchema: {
|
|
39296
|
-
page: external_exports.number().describe("Page (default 1)").optional(),
|
|
39297
|
-
size: external_exports.number().describe("Per page (default 50)").optional()
|
|
39490
|
+
page: external_exports.number().int().min(1).describe("Page (default 1)").optional(),
|
|
39491
|
+
size: external_exports.number().int().min(1).describe("Per page (default 50)").optional()
|
|
39298
39492
|
}
|
|
39299
39493
|
}, async (args) => {
|
|
39300
39494
|
const page = args.page ?? 1;
|
|
@@ -39315,7 +39509,7 @@ ${text}` : text);
|
|
|
39315
39509
|
}
|
|
39316
39510
|
return jsonResponse(unread);
|
|
39317
39511
|
});
|
|
39318
|
-
server.registerTool("ofw_upload_attachment", {
|
|
39512
|
+
if (allowDrafts) server.registerTool("ofw_upload_attachment", {
|
|
39319
39513
|
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
39514
|
annotations: { destructiveHint: false },
|
|
39321
39515
|
inputSchema: {
|
|
@@ -39337,7 +39531,12 @@ ${text}` : text);
|
|
|
39337
39531
|
form.append("label", args.label ?? fileName);
|
|
39338
39532
|
form.append("fileName", fileName);
|
|
39339
39533
|
form.append("shareClass", args.shareClass ?? "PRIVATE");
|
|
39340
|
-
const meta3 =
|
|
39534
|
+
const meta3 = parseOFW(
|
|
39535
|
+
UploadedFileSchema,
|
|
39536
|
+
await client2.request("POST", "/pub/v3/myfiles/multipart", form),
|
|
39537
|
+
"POST /pub/v3/myfiles/multipart (ofw_upload_attachment)",
|
|
39538
|
+
"strict"
|
|
39539
|
+
);
|
|
39341
39540
|
upsertAttachmentForMessage({
|
|
39342
39541
|
fileId: meta3.fileId,
|
|
39343
39542
|
fileName: meta3.fileName ?? fileName,
|
|
@@ -39462,6 +39661,7 @@ async function deleteOFWMessages(client2, ids) {
|
|
|
39462
39661
|
|
|
39463
39662
|
// src/tools/calendar.ts
|
|
39464
39663
|
function registerCalendarTools(server, client2) {
|
|
39664
|
+
const allowWrites = getWriteMode() === "all";
|
|
39465
39665
|
server.registerTool("ofw_list_events", {
|
|
39466
39666
|
description: "List OurFamilyWizard calendar events in a date range",
|
|
39467
39667
|
annotations: { readOnlyHint: true },
|
|
@@ -39478,7 +39678,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39478
39678
|
);
|
|
39479
39679
|
return jsonResponse(data);
|
|
39480
39680
|
});
|
|
39481
|
-
server.registerTool("ofw_create_event", {
|
|
39681
|
+
if (allowWrites) server.registerTool("ofw_create_event", {
|
|
39482
39682
|
description: "Create a calendar event in OurFamilyWizard",
|
|
39483
39683
|
annotations: { destructiveHint: false },
|
|
39484
39684
|
inputSchema: {
|
|
@@ -39498,7 +39698,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39498
39698
|
const data = await client2.request("POST", "/pub/v1/calendar/events", args);
|
|
39499
39699
|
return jsonResponse(data);
|
|
39500
39700
|
});
|
|
39501
|
-
server.registerTool("ofw_update_event", {
|
|
39701
|
+
if (allowWrites) server.registerTool("ofw_update_event", {
|
|
39502
39702
|
description: "Update an existing OurFamilyWizard calendar event",
|
|
39503
39703
|
annotations: { destructiveHint: true },
|
|
39504
39704
|
inputSchema: {
|
|
@@ -39516,7 +39716,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39516
39716
|
const data = await client2.request("PUT", `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
|
|
39517
39717
|
return jsonResponse(data);
|
|
39518
39718
|
});
|
|
39519
|
-
server.registerTool("ofw_delete_event", {
|
|
39719
|
+
if (allowWrites) server.registerTool("ofw_delete_event", {
|
|
39520
39720
|
description: "Delete an OurFamilyWizard calendar event",
|
|
39521
39721
|
annotations: { destructiveHint: true },
|
|
39522
39722
|
inputSchema: {
|
|
@@ -39530,6 +39730,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39530
39730
|
|
|
39531
39731
|
// src/tools/expenses.ts
|
|
39532
39732
|
function registerExpenseTools(server, client2) {
|
|
39733
|
+
const allowWrites = getWriteMode() === "all";
|
|
39533
39734
|
server.registerTool("ofw_get_expense_totals", {
|
|
39534
39735
|
description: "Get OurFamilyWizard expense summary totals (owed/paid)",
|
|
39535
39736
|
annotations: { readOnlyHint: true }
|
|
@@ -39541,8 +39742,8 @@ function registerExpenseTools(server, client2) {
|
|
|
39541
39742
|
description: "List OurFamilyWizard expenses with pagination",
|
|
39542
39743
|
annotations: { readOnlyHint: true },
|
|
39543
39744
|
inputSchema: {
|
|
39544
|
-
start: external_exports.number().describe("Start offset (default 0)").optional(),
|
|
39545
|
-
max: external_exports.number().describe("Max results (default 20)").optional()
|
|
39745
|
+
start: external_exports.number().int().min(0).describe("Start offset (default 0)").optional(),
|
|
39746
|
+
max: external_exports.number().int().min(1).describe("Max results (default 20)").optional()
|
|
39546
39747
|
}
|
|
39547
39748
|
}, async (args) => {
|
|
39548
39749
|
const start = args.start ?? 0;
|
|
@@ -39550,7 +39751,7 @@ function registerExpenseTools(server, client2) {
|
|
|
39550
39751
|
const data = await client2.request("GET", `/pub/v2/expense/expenses?start=${start}&max=${max}`);
|
|
39551
39752
|
return jsonResponse(data);
|
|
39552
39753
|
});
|
|
39553
|
-
server.registerTool("ofw_create_expense", {
|
|
39754
|
+
if (allowWrites) server.registerTool("ofw_create_expense", {
|
|
39554
39755
|
description: "Log a new expense in OurFamilyWizard",
|
|
39555
39756
|
annotations: { destructiveHint: false },
|
|
39556
39757
|
inputSchema: {
|
|
@@ -39565,12 +39766,13 @@ function registerExpenseTools(server, client2) {
|
|
|
39565
39766
|
|
|
39566
39767
|
// src/tools/journal.ts
|
|
39567
39768
|
function registerJournalTools(server, client2) {
|
|
39769
|
+
const allowWrites = getWriteMode() === "all";
|
|
39568
39770
|
server.registerTool("ofw_list_journal_entries", {
|
|
39569
39771
|
description: "List OurFamilyWizard journal entries",
|
|
39570
39772
|
annotations: { readOnlyHint: true },
|
|
39571
39773
|
inputSchema: {
|
|
39572
|
-
start: external_exports.number().describe("Start offset (default 1)").optional(),
|
|
39573
|
-
max: external_exports.number().describe("Max results (default 10)").optional()
|
|
39774
|
+
start: external_exports.number().int().min(1).describe("Start offset (default 1)").optional(),
|
|
39775
|
+
max: external_exports.number().int().min(1).describe("Max results (default 10)").optional()
|
|
39574
39776
|
}
|
|
39575
39777
|
}, async (args) => {
|
|
39576
39778
|
const start = args.start ?? 1;
|
|
@@ -39578,7 +39780,7 @@ function registerJournalTools(server, client2) {
|
|
|
39578
39780
|
const data = await client2.request("GET", `/pub/v1/journals?start=${start}&max=${max}`);
|
|
39579
39781
|
return jsonResponse(data);
|
|
39580
39782
|
});
|
|
39581
|
-
server.registerTool("ofw_create_journal_entry", {
|
|
39783
|
+
if (allowWrites) server.registerTool("ofw_create_journal_entry", {
|
|
39582
39784
|
description: "Create a new journal entry in OurFamilyWizard",
|
|
39583
39785
|
annotations: { destructiveHint: false },
|
|
39584
39786
|
inputSchema: {
|
|
@@ -39604,7 +39806,7 @@ process.emit = function(event, ...args) {
|
|
|
39604
39806
|
};
|
|
39605
39807
|
await runMcp({
|
|
39606
39808
|
name: "ofw",
|
|
39607
|
-
version: "2.
|
|
39809
|
+
version: "2.4.1",
|
|
39608
39810
|
// x-release-please-version
|
|
39609
39811
|
deps: client,
|
|
39610
39812
|
tools: [
|
package/dist/client.js
CHANGED
|
@@ -4,7 +4,13 @@ import { dirname, join } from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { resolveAuth } from './auth.js';
|
|
6
6
|
import { parseBoolEnv } from './config.js';
|
|
7
|
+
import { clearBlankInjectedEnv } from './env-bootstrap.js';
|
|
7
8
|
import { BASE_URL, OFW_PROTOCOL_HEADERS, OFW_TOKEN_TTL_MS, OFW_TOKEN_EXPIRY_SKEW_MS } from './protocol.js';
|
|
9
|
+
// When the plugin runs via a host that maps creds from optional
|
|
10
|
+
// `${user_config.*}` fields, an unset field can be injected as a blank /
|
|
11
|
+
// placeholder env value. Clear those first so the next step's .env (and the
|
|
12
|
+
// shell env) can still populate them — a filled field keeps its real value.
|
|
13
|
+
clearBlankInjectedEnv();
|
|
8
14
|
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
|
|
9
15
|
// bundle). loadDotenvSafely applies override:false + quiet:true and swallows a
|
|
10
16
|
// missing dotenv module, matching the prior inline try/catch exactly.
|
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
|