ofw-mcp 2.3.0 → 2.3.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 +1 -1
- package/dist/auth.js +6 -13
- package/dist/bundle.js +245 -116
- package/dist/cache.js +1 -0
- package/dist/client.js +7 -9
- package/dist/config.js +6 -4
- package/dist/index.js +20 -11
- package/dist/tools/_shared.js +9 -12
- package/dist/tools/messages.js +6 -4
- package/package.json +3 -2
- package/server.json +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "OurFamilyWizard tools for Claude Code",
|
|
9
|
-
"version": "2.3.
|
|
9
|
+
"version": "2.3.1"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"displayName": "OurFamilyWizard",
|
|
15
15
|
"source": "./",
|
|
16
16
|
"description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
|
|
17
|
-
"version": "2.3.
|
|
17
|
+
"version": "2.3.1",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/dist/auth.js
CHANGED
|
@@ -45,28 +45,21 @@
|
|
|
45
45
|
// - `./auth-password.js` (loginWithPassword) is a separate module
|
|
46
46
|
// specifically so it can be mocked here too. This keeps the
|
|
47
47
|
// selection logic independent of either implementation.
|
|
48
|
+
import { readEnvVar } from '@chrischall/mcp-utils';
|
|
48
49
|
import { bootstrap } from '@fetchproxy/bootstrap';
|
|
49
|
-
import { classifyBridgeError } from '@fetchproxy
|
|
50
|
+
import { classifyBridgeError } from '@chrischall/mcp-utils/fetchproxy';
|
|
50
51
|
import { loginWithPassword } from './auth-password.js';
|
|
51
52
|
import { parseBoolEnv } from './config.js';
|
|
52
53
|
import pkg from '../package.json' with { type: 'json' };
|
|
53
54
|
/**
|
|
54
55
|
* Read an env var, trim, and treat blank / `${UNEXPANDED}` placeholders as
|
|
55
56
|
* unset. Defends against MCP hosts that pass `.mcp.json` env blocks through
|
|
56
|
-
* without variable expansion.
|
|
57
|
+
* without variable expansion. Delegates to @chrischall/mcp-utils' `readEnvVar`,
|
|
58
|
+
* which applies the identical sanitization (blank, `undefined`/`null`,
|
|
59
|
+
* `${...}` placeholder → unset).
|
|
57
60
|
*/
|
|
58
61
|
function readEnv(key) {
|
|
59
|
-
|
|
60
|
-
if (typeof raw !== 'string')
|
|
61
|
-
return undefined;
|
|
62
|
-
const trimmed = raw.trim();
|
|
63
|
-
if (trimmed.length === 0)
|
|
64
|
-
return undefined;
|
|
65
|
-
if (trimmed === 'undefined' || trimmed === 'null')
|
|
66
|
-
return undefined;
|
|
67
|
-
if (/^\$\{[^}]*\}$/.test(trimmed))
|
|
68
|
-
return undefined;
|
|
69
|
-
return trimmed;
|
|
62
|
+
return readEnvVar(key);
|
|
70
63
|
}
|
|
71
64
|
/** True if the user has explicitly disabled the fetchproxy fallback. */
|
|
72
65
|
function fetchproxyDisabled() {
|
package/dist/bundle.js
CHANGED
|
@@ -10342,10 +10342,10 @@ var require_websocket_server = __commonJS({
|
|
|
10342
10342
|
process.nextTick(emitClose, this);
|
|
10343
10343
|
}
|
|
10344
10344
|
} else {
|
|
10345
|
-
const
|
|
10345
|
+
const server = this._server;
|
|
10346
10346
|
this._removeListeners();
|
|
10347
10347
|
this._removeListeners = this._server = null;
|
|
10348
|
-
|
|
10348
|
+
server.close(() => {
|
|
10349
10349
|
emitClose(this);
|
|
10350
10350
|
});
|
|
10351
10351
|
}
|
|
@@ -10530,17 +10530,17 @@ var require_websocket_server = __commonJS({
|
|
|
10530
10530
|
}
|
|
10531
10531
|
};
|
|
10532
10532
|
module.exports = WebSocketServer2;
|
|
10533
|
-
function addListeners(
|
|
10534
|
-
for (const event of Object.keys(map2))
|
|
10533
|
+
function addListeners(server, map2) {
|
|
10534
|
+
for (const event of Object.keys(map2)) server.on(event, map2[event]);
|
|
10535
10535
|
return function removeListeners() {
|
|
10536
10536
|
for (const event of Object.keys(map2)) {
|
|
10537
|
-
|
|
10537
|
+
server.removeListener(event, map2[event]);
|
|
10538
10538
|
}
|
|
10539
10539
|
};
|
|
10540
10540
|
}
|
|
10541
|
-
function emitClose(
|
|
10542
|
-
|
|
10543
|
-
|
|
10541
|
+
function emitClose(server) {
|
|
10542
|
+
server._state = CLOSED;
|
|
10543
|
+
server.emit("close");
|
|
10544
10544
|
}
|
|
10545
10545
|
function socketOnError() {
|
|
10546
10546
|
this.destroy();
|
|
@@ -10559,11 +10559,11 @@ var require_websocket_server = __commonJS({
|
|
|
10559
10559
|
` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
|
|
10560
10560
|
);
|
|
10561
10561
|
}
|
|
10562
|
-
function abortHandshakeOrEmitwsClientError(
|
|
10563
|
-
if (
|
|
10562
|
+
function abortHandshakeOrEmitwsClientError(server, req, socket, code, message, headers) {
|
|
10563
|
+
if (server.listenerCount("wsClientError")) {
|
|
10564
10564
|
const err = new Error(message);
|
|
10565
10565
|
Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
|
|
10566
|
-
|
|
10566
|
+
server.emit("wsClientError", err, socket, req);
|
|
10567
10567
|
} else {
|
|
10568
10568
|
abortHandshake(socket, code, message, headers);
|
|
10569
10569
|
}
|
|
@@ -32189,11 +32189,11 @@ var Protocol = class {
|
|
|
32189
32189
|
*
|
|
32190
32190
|
* The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
32191
32191
|
*/
|
|
32192
|
-
async connect(
|
|
32192
|
+
async connect(transport) {
|
|
32193
32193
|
if (this._transport) {
|
|
32194
32194
|
throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
|
|
32195
32195
|
}
|
|
32196
|
-
this._transport =
|
|
32196
|
+
this._transport = transport;
|
|
32197
32197
|
const _onclose = this.transport?.onclose;
|
|
32198
32198
|
this._transport.onclose = () => {
|
|
32199
32199
|
_onclose?.();
|
|
@@ -33783,8 +33783,8 @@ var McpServer = class {
|
|
|
33783
33783
|
*
|
|
33784
33784
|
* The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
33785
33785
|
*/
|
|
33786
|
-
async connect(
|
|
33787
|
-
return await this.server.connect(
|
|
33786
|
+
async connect(transport) {
|
|
33787
|
+
return await this.server.connect(transport);
|
|
33788
33788
|
}
|
|
33789
33789
|
/**
|
|
33790
33790
|
* Closes the connection.
|
|
@@ -34634,8 +34634,152 @@ var StdioServerTransport = class {
|
|
|
34634
34634
|
}
|
|
34635
34635
|
};
|
|
34636
34636
|
|
|
34637
|
+
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
34638
|
+
async function createMcpServer(opts) {
|
|
34639
|
+
const server = new McpServer({ name: opts.name, version: opts.version });
|
|
34640
|
+
if (opts.banner !== void 0) {
|
|
34641
|
+
console.error(opts.banner);
|
|
34642
|
+
}
|
|
34643
|
+
const deps = opts.deps;
|
|
34644
|
+
for (const register of opts.tools) {
|
|
34645
|
+
await register(server, deps);
|
|
34646
|
+
}
|
|
34647
|
+
return server;
|
|
34648
|
+
}
|
|
34649
|
+
function withGracefulShutdown(server, opts = {}) {
|
|
34650
|
+
const shouldExit = opts.exit ?? true;
|
|
34651
|
+
let shuttingDown = false;
|
|
34652
|
+
const handler = (signal) => {
|
|
34653
|
+
if (shuttingDown)
|
|
34654
|
+
return;
|
|
34655
|
+
shuttingDown = true;
|
|
34656
|
+
void (async () => {
|
|
34657
|
+
try {
|
|
34658
|
+
if (opts.onSignal)
|
|
34659
|
+
await opts.onSignal(signal);
|
|
34660
|
+
await server.close();
|
|
34661
|
+
} catch (err) {
|
|
34662
|
+
console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
|
|
34663
|
+
} finally {
|
|
34664
|
+
if (shouldExit)
|
|
34665
|
+
process.exit(0);
|
|
34666
|
+
}
|
|
34667
|
+
})();
|
|
34668
|
+
};
|
|
34669
|
+
process.on("SIGINT", () => handler("SIGINT"));
|
|
34670
|
+
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
34671
|
+
}
|
|
34672
|
+
async function runMcp(opts) {
|
|
34673
|
+
const server = await createMcpServer(opts);
|
|
34674
|
+
const shutdown = opts.shutdown ?? true;
|
|
34675
|
+
if (shutdown !== false) {
|
|
34676
|
+
withGracefulShutdown(server, shutdown === true ? {} : shutdown);
|
|
34677
|
+
}
|
|
34678
|
+
const spec = opts.transport ?? "stdio";
|
|
34679
|
+
const transport = spec === "stdio" ? new StdioServerTransport() : spec;
|
|
34680
|
+
await server.connect(transport);
|
|
34681
|
+
return server;
|
|
34682
|
+
}
|
|
34683
|
+
|
|
34684
|
+
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34685
|
+
function textResult(data) {
|
|
34686
|
+
return {
|
|
34687
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
34688
|
+
};
|
|
34689
|
+
}
|
|
34690
|
+
function rawTextResult(text) {
|
|
34691
|
+
return { content: [{ type: "text", text }] };
|
|
34692
|
+
}
|
|
34693
|
+
|
|
34694
|
+
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
34695
|
+
import { homedir } from "node:os";
|
|
34696
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
34697
|
+
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
34698
|
+
function readEnvVar(key, opts = {}) {
|
|
34699
|
+
const env = opts.env ?? process.env;
|
|
34700
|
+
const raw = env[key];
|
|
34701
|
+
if (typeof raw === "string") {
|
|
34702
|
+
const trimmed = raw.trim();
|
|
34703
|
+
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
34704
|
+
return trimmed;
|
|
34705
|
+
}
|
|
34706
|
+
}
|
|
34707
|
+
return opts.default;
|
|
34708
|
+
}
|
|
34709
|
+
var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
34710
|
+
var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
34711
|
+
function parseBoolEnv(key, opts = {}) {
|
|
34712
|
+
const fallback = opts.default ?? false;
|
|
34713
|
+
const raw = readEnvVar(key, { env: opts.env });
|
|
34714
|
+
if (raw === void 0)
|
|
34715
|
+
return fallback;
|
|
34716
|
+
const token = raw.toLowerCase();
|
|
34717
|
+
if (TRUE_TOKENS.has(token))
|
|
34718
|
+
return true;
|
|
34719
|
+
if (FALSE_TOKENS.has(token))
|
|
34720
|
+
return false;
|
|
34721
|
+
return fallback;
|
|
34722
|
+
}
|
|
34723
|
+
function expandPath(p) {
|
|
34724
|
+
let expanded = p;
|
|
34725
|
+
if (p === "~") {
|
|
34726
|
+
expanded = homedir();
|
|
34727
|
+
} else if (p.startsWith("~/")) {
|
|
34728
|
+
expanded = join(homedir(), p.slice(2));
|
|
34729
|
+
}
|
|
34730
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
34731
|
+
}
|
|
34732
|
+
async function loadDotenvSafely(opts = {}) {
|
|
34733
|
+
try {
|
|
34734
|
+
const mod = await import(
|
|
34735
|
+
/* @vite-ignore */
|
|
34736
|
+
"dotenv"
|
|
34737
|
+
);
|
|
34738
|
+
const result = mod.config({
|
|
34739
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
34740
|
+
override: opts.override ?? false,
|
|
34741
|
+
quiet: true
|
|
34742
|
+
});
|
|
34743
|
+
return result.error === void 0;
|
|
34744
|
+
} catch {
|
|
34745
|
+
return false;
|
|
34746
|
+
}
|
|
34747
|
+
}
|
|
34748
|
+
|
|
34749
|
+
// node_modules/@chrischall/mcp-utils/dist/fs/index.js
|
|
34750
|
+
import { openAsBlob } from "node:fs";
|
|
34751
|
+
async function fileBlob(path, opts = {}) {
|
|
34752
|
+
let blob;
|
|
34753
|
+
try {
|
|
34754
|
+
blob = await openAsBlob(path, opts.type !== void 0 ? { type: opts.type } : void 0);
|
|
34755
|
+
} catch {
|
|
34756
|
+
throw new Error(`Cannot read file for upload: ${path}`);
|
|
34757
|
+
}
|
|
34758
|
+
if (opts.maxBytes !== void 0 && blob.size > opts.maxBytes) {
|
|
34759
|
+
throw new Error(`${opts.label ?? "File"} is ${blob.size} bytes, over the ${opts.maxBytes}-byte limit: ${path}`);
|
|
34760
|
+
}
|
|
34761
|
+
return blob;
|
|
34762
|
+
}
|
|
34763
|
+
|
|
34764
|
+
// node_modules/@chrischall/mcp-utils/dist/zod/index.js
|
|
34765
|
+
var PositiveInt = external_exports.number().int().positive();
|
|
34766
|
+
var NonNegInt = external_exports.number().int().nonnegative();
|
|
34767
|
+
var NonEmptyString = external_exports.string().min(1);
|
|
34768
|
+
var IsoDate = external_exports.iso.date();
|
|
34769
|
+
var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
|
|
34770
|
+
var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
|
|
34771
|
+
var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
|
|
34772
|
+
var paginationSchema = {
|
|
34773
|
+
offset: NonNegInt.default(0).describe("Number of items to skip (0-based)."),
|
|
34774
|
+
limit: external_exports.number().int().min(1).max(200).default(50).describe("Maximum number of items to return (1-200).")
|
|
34775
|
+
};
|
|
34776
|
+
var pageSchema = {
|
|
34777
|
+
page_num: PositiveInt.default(1).describe("1-based page number."),
|
|
34778
|
+
page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
|
|
34779
|
+
};
|
|
34780
|
+
|
|
34637
34781
|
// src/client.ts
|
|
34638
|
-
import { dirname, join as
|
|
34782
|
+
import { dirname, join as join4 } from "path";
|
|
34639
34783
|
import { fileURLToPath } from "url";
|
|
34640
34784
|
|
|
34641
34785
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
@@ -35494,13 +35638,13 @@ async function openEncryptedFrame(sessionKey, frame) {
|
|
|
35494
35638
|
// node_modules/@fetchproxy/server/dist/election.js
|
|
35495
35639
|
import { createServer, Server as HttpServer } from "node:http";
|
|
35496
35640
|
async function electRole(opts) {
|
|
35497
|
-
const
|
|
35641
|
+
const server = createServer();
|
|
35498
35642
|
return new Promise((resolve2, reject) => {
|
|
35499
35643
|
const onError = (e) => {
|
|
35500
|
-
|
|
35644
|
+
server.removeListener("listening", onListening);
|
|
35501
35645
|
if (e.code === "EADDRINUSE") {
|
|
35502
35646
|
try {
|
|
35503
|
-
|
|
35647
|
+
server.close();
|
|
35504
35648
|
} catch {
|
|
35505
35649
|
}
|
|
35506
35650
|
resolve2({ role: "peer" });
|
|
@@ -35509,12 +35653,12 @@ async function electRole(opts) {
|
|
|
35509
35653
|
}
|
|
35510
35654
|
};
|
|
35511
35655
|
const onListening = () => {
|
|
35512
|
-
|
|
35513
|
-
resolve2({ role: "host", server
|
|
35656
|
+
server.removeListener("error", onError);
|
|
35657
|
+
resolve2({ role: "host", server });
|
|
35514
35658
|
};
|
|
35515
|
-
|
|
35516
|
-
|
|
35517
|
-
|
|
35659
|
+
server.once("error", onError);
|
|
35660
|
+
server.once("listening", onListening);
|
|
35661
|
+
server.listen(opts.port, opts.host);
|
|
35518
35662
|
});
|
|
35519
35663
|
}
|
|
35520
35664
|
|
|
@@ -35924,19 +36068,19 @@ async function startPeer(opts) {
|
|
|
35924
36068
|
|
|
35925
36069
|
// node_modules/@fetchproxy/server/dist/identity.js
|
|
35926
36070
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
35927
|
-
import { join } from "node:path";
|
|
35928
|
-
import { homedir } from "node:os";
|
|
36071
|
+
import { join as join2 } from "node:path";
|
|
36072
|
+
import { homedir as homedir2 } from "node:os";
|
|
35929
36073
|
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
35930
36074
|
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
35931
36075
|
function defaultIdentityDir() {
|
|
35932
|
-
return
|
|
36076
|
+
return join2(homedir2(), ".fetchproxy", "identity");
|
|
35933
36077
|
}
|
|
35934
36078
|
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
35935
36079
|
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
35936
36080
|
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
35937
36081
|
}
|
|
35938
36082
|
const safeFile = serverName.replace(/\//g, "_");
|
|
35939
|
-
const path =
|
|
36083
|
+
const path = join2(dir, `${safeFile}.json`);
|
|
35940
36084
|
await mkdir(dir, { recursive: true, mode: 448 });
|
|
35941
36085
|
try {
|
|
35942
36086
|
const raw = await readFile(path, "utf8");
|
|
@@ -37343,7 +37487,7 @@ async function bootstrap(opts) {
|
|
|
37343
37487
|
const sessionStorageKeys = new Set(opts.declare.sessionStorage);
|
|
37344
37488
|
for (const p of sessionStoragePointers)
|
|
37345
37489
|
sessionStorageKeys.add(p.storageKey);
|
|
37346
|
-
const
|
|
37490
|
+
const server = factory({
|
|
37347
37491
|
serverName: opts.serverName,
|
|
37348
37492
|
version: opts.version,
|
|
37349
37493
|
domains: [...opts.domains],
|
|
@@ -37378,10 +37522,10 @@ async function bootstrap(opts) {
|
|
|
37378
37522
|
if (opts.storageSubdomain !== void 0)
|
|
37379
37523
|
storageDomainOpts.subdomain = opts.storageSubdomain;
|
|
37380
37524
|
try {
|
|
37381
|
-
await
|
|
37525
|
+
await server.listen();
|
|
37382
37526
|
const cookies = {};
|
|
37383
37527
|
if (opts.declare.cookies.length > 0) {
|
|
37384
|
-
const joined = await
|
|
37528
|
+
const joined = await server.readCookies({
|
|
37385
37529
|
keys: opts.declare.cookies,
|
|
37386
37530
|
...storageDomainOpts
|
|
37387
37531
|
});
|
|
@@ -37401,7 +37545,7 @@ async function bootstrap(opts) {
|
|
|
37401
37545
|
for (const p of localStoragePointers) {
|
|
37402
37546
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
37403
37547
|
}
|
|
37404
|
-
localStorage = await
|
|
37548
|
+
localStorage = await server.readLocalStorage({
|
|
37405
37549
|
keys: allKeys,
|
|
37406
37550
|
...storageDomainOpts,
|
|
37407
37551
|
...localStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -37414,7 +37558,7 @@ async function bootstrap(opts) {
|
|
|
37414
37558
|
for (const p of sessionStoragePointers) {
|
|
37415
37559
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
37416
37560
|
}
|
|
37417
|
-
sessionStorage = await
|
|
37561
|
+
sessionStorage = await server.readSessionStorage({
|
|
37418
37562
|
keys: allKeys,
|
|
37419
37563
|
...storageDomainOpts,
|
|
37420
37564
|
...sessionStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -37430,14 +37574,14 @@ async function bootstrap(opts) {
|
|
|
37430
37574
|
opts.onWaiting(`waiting to capture ${h.headerName} \u2014 interact with the page in your browser`);
|
|
37431
37575
|
}
|
|
37432
37576
|
}
|
|
37433
|
-
capturedHeaders[h.headerName] = await
|
|
37577
|
+
capturedHeaders[h.headerName] = await server.captureRequestHeader({
|
|
37434
37578
|
urlPattern: h.urlPattern,
|
|
37435
37579
|
headerName: h.headerName
|
|
37436
37580
|
});
|
|
37437
37581
|
}
|
|
37438
37582
|
const indexedDbBucket = {};
|
|
37439
37583
|
for (const d of indexedDb) {
|
|
37440
|
-
const values = await
|
|
37584
|
+
const values = await server.readIndexedDb({
|
|
37441
37585
|
database: d.database,
|
|
37442
37586
|
store: d.store,
|
|
37443
37587
|
keys: [...d.keys],
|
|
@@ -37454,7 +37598,7 @@ async function bootstrap(opts) {
|
|
|
37454
37598
|
};
|
|
37455
37599
|
} finally {
|
|
37456
37600
|
try {
|
|
37457
|
-
await
|
|
37601
|
+
await server.close();
|
|
37458
37602
|
} catch {
|
|
37459
37603
|
}
|
|
37460
37604
|
}
|
|
@@ -37519,8 +37663,8 @@ async function loginWithPassword(username, password) {
|
|
|
37519
37663
|
|
|
37520
37664
|
// src/config.ts
|
|
37521
37665
|
import { createHash } from "node:crypto";
|
|
37522
|
-
import { homedir as
|
|
37523
|
-
import { join as
|
|
37666
|
+
import { homedir as homedir3 } from "node:os";
|
|
37667
|
+
import { join as join3 } from "node:path";
|
|
37524
37668
|
function readCacheIdentity() {
|
|
37525
37669
|
const explicit = process.env.OFW_CACHE_IDENTITY;
|
|
37526
37670
|
if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
|
|
@@ -37531,31 +37675,29 @@ function readCacheIdentity() {
|
|
|
37531
37675
|
function getCacheDir() {
|
|
37532
37676
|
const override = process.env.OFW_CACHE_DIR;
|
|
37533
37677
|
if (override && override.trim().length > 0) return override.trim();
|
|
37534
|
-
return
|
|
37678
|
+
return join3(homedir3(), ".cache", "ofw-mcp");
|
|
37535
37679
|
}
|
|
37536
37680
|
function getCacheDbPath() {
|
|
37537
37681
|
const identity = readCacheIdentity();
|
|
37538
37682
|
const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
37539
|
-
return
|
|
37683
|
+
return join3(getCacheDir(), `${hash2}.db`);
|
|
37540
37684
|
}
|
|
37541
37685
|
function getAttachmentsDir() {
|
|
37542
37686
|
const override = process.env.OFW_ATTACHMENTS_DIR;
|
|
37543
37687
|
if (override && override.trim().length > 0) return override.trim();
|
|
37544
|
-
return
|
|
37688
|
+
return join3(homedir3(), "Downloads", "ofw-mcp");
|
|
37545
37689
|
}
|
|
37546
|
-
function
|
|
37547
|
-
|
|
37548
|
-
if (typeof raw !== "string") return false;
|
|
37549
|
-
return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
|
|
37690
|
+
function parseBoolEnv2(name) {
|
|
37691
|
+
return parseBoolEnv(name);
|
|
37550
37692
|
}
|
|
37551
37693
|
function getDefaultInlineAttachments() {
|
|
37552
|
-
return
|
|
37694
|
+
return parseBoolEnv2("OFW_INLINE_ATTACHMENTS");
|
|
37553
37695
|
}
|
|
37554
37696
|
|
|
37555
37697
|
// package.json
|
|
37556
37698
|
var package_default = {
|
|
37557
37699
|
name: "ofw-mcp",
|
|
37558
|
-
version: "2.3.
|
|
37700
|
+
version: "2.3.1",
|
|
37559
37701
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
37560
37702
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
37561
37703
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -37579,11 +37721,12 @@ var package_default = {
|
|
|
37579
37721
|
bundle: `esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js`,
|
|
37580
37722
|
dev: "node --env-file=.env dist/index.js",
|
|
37581
37723
|
test: "vitest run",
|
|
37724
|
+
"test:coverage": "vitest run --coverage",
|
|
37582
37725
|
"test:watch": "vitest"
|
|
37583
37726
|
},
|
|
37584
37727
|
dependencies: {
|
|
37728
|
+
"@chrischall/mcp-utils": "^0.4.0",
|
|
37585
37729
|
"@fetchproxy/bootstrap": "^0.11.0",
|
|
37586
|
-
"@fetchproxy/server": "^0.11.0",
|
|
37587
37730
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37588
37731
|
dotenv: "^17.4.2",
|
|
37589
37732
|
zod: "^4.4.3"
|
|
@@ -37599,16 +37742,10 @@ var package_default = {
|
|
|
37599
37742
|
|
|
37600
37743
|
// src/auth.ts
|
|
37601
37744
|
function readEnv(key) {
|
|
37602
|
-
|
|
37603
|
-
if (typeof raw !== "string") return void 0;
|
|
37604
|
-
const trimmed = raw.trim();
|
|
37605
|
-
if (trimmed.length === 0) return void 0;
|
|
37606
|
-
if (trimmed === "undefined" || trimmed === "null") return void 0;
|
|
37607
|
-
if (/^\$\{[^}]*\}$/.test(trimmed)) return void 0;
|
|
37608
|
-
return trimmed;
|
|
37745
|
+
return readEnvVar(key);
|
|
37609
37746
|
}
|
|
37610
37747
|
function fetchproxyDisabled() {
|
|
37611
|
-
return
|
|
37748
|
+
return parseBoolEnv2("OFW_DISABLE_FETCHPROXY");
|
|
37612
37749
|
}
|
|
37613
37750
|
async function resolveAuth() {
|
|
37614
37751
|
const username = readEnv("OFW_USERNAME");
|
|
@@ -37668,12 +37805,8 @@ async function resolveAuth() {
|
|
|
37668
37805
|
}
|
|
37669
37806
|
|
|
37670
37807
|
// src/client.ts
|
|
37671
|
-
|
|
37672
|
-
|
|
37673
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37674
|
-
config2({ path: join3(__dirname, "..", ".env"), override: false, quiet: true });
|
|
37675
|
-
} catch {
|
|
37676
|
-
}
|
|
37808
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37809
|
+
await loadDotenvSafely({ path: join4(__dirname, "..", ".env") });
|
|
37677
37810
|
function parseContentDispositionFilename(cd) {
|
|
37678
37811
|
const extMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(cd);
|
|
37679
37812
|
if (extMatch) {
|
|
@@ -37688,7 +37821,7 @@ function parseContentDispositionFilename(cd) {
|
|
|
37688
37821
|
return m ? m[1] : null;
|
|
37689
37822
|
}
|
|
37690
37823
|
function debugLogEnabled() {
|
|
37691
|
-
return
|
|
37824
|
+
return parseBoolEnv2("OFW_DEBUG_LOG");
|
|
37692
37825
|
}
|
|
37693
37826
|
function redactHeaders(h) {
|
|
37694
37827
|
const out = { ...h };
|
|
@@ -37817,13 +37950,8 @@ var OFWClient = class {
|
|
|
37817
37950
|
var client = new OFWClient();
|
|
37818
37951
|
|
|
37819
37952
|
// src/tools/_shared.ts
|
|
37820
|
-
|
|
37821
|
-
|
|
37822
|
-
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
37823
|
-
}
|
|
37824
|
-
function textResponse(text) {
|
|
37825
|
-
return { content: [{ type: "text", text }] };
|
|
37826
|
-
}
|
|
37953
|
+
var jsonResponse = textResult;
|
|
37954
|
+
var textResponse = rawTextResult;
|
|
37827
37955
|
function mapRecipients(items) {
|
|
37828
37956
|
return (items ?? []).map((r) => ({
|
|
37829
37957
|
userId: r.user?.id ?? 0,
|
|
@@ -37831,10 +37959,7 @@ function mapRecipients(items) {
|
|
|
37831
37959
|
viewedAt: r.viewed?.dateTime ?? null
|
|
37832
37960
|
}));
|
|
37833
37961
|
}
|
|
37834
|
-
|
|
37835
|
-
const expanded = p.startsWith("~/") ? join4(process.env.HOME ?? "", p.slice(2)) : p;
|
|
37836
|
-
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
37837
|
-
}
|
|
37962
|
+
var expandPath2 = expandPath;
|
|
37838
37963
|
async function postMessageAndRefetch(client2, payload) {
|
|
37839
37964
|
const raw = await client2.request(
|
|
37840
37965
|
"POST",
|
|
@@ -37848,15 +37973,15 @@ async function postMessageAndRefetch(client2, payload) {
|
|
|
37848
37973
|
}
|
|
37849
37974
|
|
|
37850
37975
|
// src/tools/user.ts
|
|
37851
|
-
function registerUserTools(
|
|
37852
|
-
|
|
37976
|
+
function registerUserTools(server, client2) {
|
|
37977
|
+
server.registerTool("ofw_get_profile", {
|
|
37853
37978
|
description: "Get current user and co-parent profile information from OurFamilyWizard",
|
|
37854
37979
|
annotations: { readOnlyHint: true }
|
|
37855
37980
|
}, async () => {
|
|
37856
37981
|
const data = await client2.request("GET", "/pub/v2/profiles");
|
|
37857
37982
|
return jsonResponse(data);
|
|
37858
37983
|
});
|
|
37859
|
-
|
|
37984
|
+
server.registerTool("ofw_get_notifications", {
|
|
37860
37985
|
description: "Get OurFamilyWizard dashboard summary: unread message count, upcoming events, outstanding expenses. Note: updates your last-seen status.",
|
|
37861
37986
|
annotations: { readOnlyHint: false }
|
|
37862
37987
|
}, async () => {
|
|
@@ -38397,15 +38522,15 @@ function listDataHintsAtFiles(listData) {
|
|
|
38397
38522
|
if (Array.isArray(ld.files)) return ld.files.length > 0;
|
|
38398
38523
|
return false;
|
|
38399
38524
|
}
|
|
38400
|
-
function registerMessageTools(
|
|
38401
|
-
|
|
38525
|
+
function registerMessageTools(server, client2) {
|
|
38526
|
+
server.registerTool("ofw_list_message_folders", {
|
|
38402
38527
|
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.",
|
|
38403
38528
|
annotations: { readOnlyHint: true }
|
|
38404
38529
|
}, async () => {
|
|
38405
38530
|
const data = await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true");
|
|
38406
38531
|
return jsonResponse(data);
|
|
38407
38532
|
});
|
|
38408
|
-
|
|
38533
|
+
server.registerTool("ofw_list_messages", {
|
|
38409
38534
|
description: "List messages from the local OurFamilyWizard cache. Supports filtering by folder, date range, and a substring query on subject+body. Pagination is offset-based but if you know what you want (a date range, a topic), prefer the filters over walking pages \u2014 the cache may have 1000+ messages. Call ofw_sync_messages first if the cache is empty or stale.",
|
|
38410
38535
|
annotations: { readOnlyHint: true },
|
|
38411
38536
|
inputSchema: {
|
|
@@ -38441,7 +38566,7 @@ function registerMessageTools(server2, client2) {
|
|
|
38441
38566
|
}
|
|
38442
38567
|
return jsonResponse(payload);
|
|
38443
38568
|
});
|
|
38444
|
-
|
|
38569
|
+
server.registerTool("ofw_get_message", {
|
|
38445
38570
|
description: 'Get a single OurFamilyWizard message OR draft by ID. Reads from local cache when available; otherwise fetches from OFW (which will mark unread inbox messages as read on OFW). For ids that match a draft (in the drafts cache), the response carries folder="drafts" and the body/subject/recipients reflect the drafts cache (which ofw_sync_messages keeps fresh) \u2014 drafts have no `fromUser`, and `sentAt`/`fetchedBodyAt` mirror the draft\'s `modifiedAt`. For inbox/sent messages, folder is "inbox" or "sent" as before.',
|
|
38446
38571
|
annotations: { readOnlyHint: false },
|
|
38447
38572
|
inputSchema: {
|
|
@@ -38506,7 +38631,7 @@ function registerMessageTools(server2, client2) {
|
|
|
38506
38631
|
const attachments = listAttachmentsForMessage(detail.id);
|
|
38507
38632
|
return jsonResponse({ ...row, attachments });
|
|
38508
38633
|
});
|
|
38509
|
-
|
|
38634
|
+
server.registerTool("ofw_send_message", {
|
|
38510
38635
|
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.",
|
|
38511
38636
|
annotations: { destructiveHint: true },
|
|
38512
38637
|
inputSchema: {
|
|
@@ -38616,7 +38741,7 @@ function registerMessageTools(server2, client2) {
|
|
|
38616
38741
|
|
|
38617
38742
|
${text}` : text);
|
|
38618
38743
|
});
|
|
38619
|
-
|
|
38744
|
+
server.registerTool("ofw_list_drafts", {
|
|
38620
38745
|
description: "List draft messages from the local OurFamilyWizard cache. Call ofw_sync_messages first if the cache is empty.",
|
|
38621
38746
|
annotations: { readOnlyHint: true },
|
|
38622
38747
|
inputSchema: {
|
|
@@ -38630,7 +38755,7 @@ ${text}` : text);
|
|
|
38630
38755
|
const payload = drafts.length === 0 ? { drafts: [], note: "Cache empty. Call ofw_sync_messages to populate." } : { drafts };
|
|
38631
38756
|
return jsonResponse(payload);
|
|
38632
38757
|
});
|
|
38633
|
-
|
|
38758
|
+
server.registerTool("ofw_save_draft", {
|
|
38634
38759
|
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.",
|
|
38635
38760
|
annotations: { readOnlyHint: false },
|
|
38636
38761
|
inputSchema: {
|
|
@@ -38692,7 +38817,7 @@ ${text}` : text);
|
|
|
38692
38817
|
|
|
38693
38818
|
${text}` : text);
|
|
38694
38819
|
});
|
|
38695
|
-
|
|
38820
|
+
server.registerTool("ofw_delete_draft", {
|
|
38696
38821
|
description: "Delete a draft message from OurFamilyWizard. Also removes the draft from the local cache.",
|
|
38697
38822
|
annotations: { destructiveHint: true },
|
|
38698
38823
|
inputSchema: {
|
|
@@ -38703,7 +38828,7 @@ ${text}` : text);
|
|
|
38703
38828
|
deleteDraft(args.messageId);
|
|
38704
38829
|
return data ? jsonResponse(data) : textResponse("Draft deleted.");
|
|
38705
38830
|
});
|
|
38706
|
-
|
|
38831
|
+
server.registerTool("ofw_get_unread_sent", {
|
|
38707
38832
|
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.",
|
|
38708
38833
|
annotations: { readOnlyHint: true },
|
|
38709
38834
|
inputSchema: {
|
|
@@ -38729,7 +38854,7 @@ ${text}` : text);
|
|
|
38729
38854
|
}
|
|
38730
38855
|
return jsonResponse(unread);
|
|
38731
38856
|
});
|
|
38732
|
-
|
|
38857
|
+
server.registerTool("ofw_upload_attachment", {
|
|
38733
38858
|
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.`,
|
|
38734
38859
|
annotations: { destructiveHint: false },
|
|
38735
38860
|
inputSchema: {
|
|
@@ -38739,14 +38864,13 @@ ${text}` : text);
|
|
|
38739
38864
|
description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional()
|
|
38740
38865
|
}
|
|
38741
38866
|
}, async (args) => {
|
|
38742
|
-
const abs =
|
|
38867
|
+
const abs = expandPath2(args.path);
|
|
38743
38868
|
const stat = statSync(abs);
|
|
38744
38869
|
if (!stat.isFile()) throw new Error(`Not a file: ${abs}`);
|
|
38745
|
-
const buf = readFileSync(abs);
|
|
38746
38870
|
const fileName = basename(abs);
|
|
38747
38871
|
const mime = mimeFromName(fileName);
|
|
38748
38872
|
const form = new FormData();
|
|
38749
|
-
form.append("file",
|
|
38873
|
+
form.append("file", await fileBlob(abs, { type: mime }), fileName);
|
|
38750
38874
|
form.append("source", "message");
|
|
38751
38875
|
form.append("description", args.description ?? fileName);
|
|
38752
38876
|
form.append("label", args.label ?? fileName);
|
|
@@ -38758,7 +38882,7 @@ ${text}` : text);
|
|
|
38758
38882
|
fileName: meta3.fileName ?? fileName,
|
|
38759
38883
|
label: meta3.label ?? args.label ?? fileName,
|
|
38760
38884
|
mimeType: meta3.fileType ?? mime,
|
|
38761
|
-
sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes :
|
|
38885
|
+
sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes : stat.size,
|
|
38762
38886
|
metadata: meta3,
|
|
38763
38887
|
messageId: 0
|
|
38764
38888
|
});
|
|
@@ -38766,12 +38890,12 @@ ${text}` : text);
|
|
|
38766
38890
|
fileId: meta3.fileId,
|
|
38767
38891
|
fileName: meta3.fileName ?? fileName,
|
|
38768
38892
|
mimeType: meta3.fileType ?? mime,
|
|
38769
|
-
sizeBytes: meta3.sizeInBytes ??
|
|
38893
|
+
sizeBytes: meta3.sizeInBytes ?? stat.size,
|
|
38770
38894
|
shareClass: meta3.shareClass ?? args.shareClass ?? "PRIVATE",
|
|
38771
38895
|
note: "Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it."
|
|
38772
38896
|
});
|
|
38773
38897
|
});
|
|
38774
|
-
|
|
38898
|
+
server.registerTool("ofw_download_attachment", {
|
|
38775
38899
|
description: 'Download an OFW message attachment by fileId. By default, bytes are saved to disk (~/Downloads/ofw-mcp/) and the response carries the absolute path, mime type, and size for the caller to read back. Pass inline:true to skip disk entirely and return the bytes as MCP content blocks \u2014 images come back as ImageContent (the model sees them directly); other files come back as an EmbeddedResource blob. Use inline for small files where you want the model to read content immediately and the host is sandboxed; use disk for large files or when you want a persistent local copy. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var (set to "true" to make inline the default). fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo. Re-downloading to the same path is a no-op (disk mode only).',
|
|
38776
38900
|
annotations: { readOnlyHint: false },
|
|
38777
38901
|
inputSchema: {
|
|
@@ -38825,7 +38949,7 @@ ${text}` : text);
|
|
|
38825
38949
|
let dest;
|
|
38826
38950
|
if (args.saveTo) {
|
|
38827
38951
|
const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
|
|
38828
|
-
const abs =
|
|
38952
|
+
const abs = expandPath2(args.saveTo);
|
|
38829
38953
|
dest = isDirArg ? join5(abs, `${fileId}-${cached2.fileName}`) : abs;
|
|
38830
38954
|
} else {
|
|
38831
38955
|
dest = join5(getAttachmentsDir(), `${fileId}-${cached2.fileName}`);
|
|
@@ -38852,7 +38976,7 @@ ${text}` : text);
|
|
|
38852
38976
|
fileName: response.suggestedFileName ?? cached2.fileName
|
|
38853
38977
|
});
|
|
38854
38978
|
});
|
|
38855
|
-
|
|
38979
|
+
server.registerTool("ofw_sync_messages", {
|
|
38856
38980
|
description: "Sync messages from OurFamilyWizard into the local cache. Returns counts per folder and a list of unread inbox messages whose bodies were NOT fetched (to avoid mark-as-read on OFW). Call ofw_get_message(id) on those to read them. Pass deep:true to walk all OFW pages instead of stopping at the first all-cached page (use to backfill suspected gaps).",
|
|
38857
38981
|
annotations: { readOnlyHint: false },
|
|
38858
38982
|
inputSchema: {
|
|
@@ -38876,8 +39000,8 @@ async function deleteOFWMessages(client2, ids) {
|
|
|
38876
39000
|
}
|
|
38877
39001
|
|
|
38878
39002
|
// src/tools/calendar.ts
|
|
38879
|
-
function registerCalendarTools(
|
|
38880
|
-
|
|
39003
|
+
function registerCalendarTools(server, client2) {
|
|
39004
|
+
server.registerTool("ofw_list_events", {
|
|
38881
39005
|
description: "List OurFamilyWizard calendar events in a date range",
|
|
38882
39006
|
annotations: { readOnlyHint: true },
|
|
38883
39007
|
inputSchema: {
|
|
@@ -38893,7 +39017,7 @@ function registerCalendarTools(server2, client2) {
|
|
|
38893
39017
|
);
|
|
38894
39018
|
return jsonResponse(data);
|
|
38895
39019
|
});
|
|
38896
|
-
|
|
39020
|
+
server.registerTool("ofw_create_event", {
|
|
38897
39021
|
description: "Create a calendar event in OurFamilyWizard",
|
|
38898
39022
|
annotations: { destructiveHint: false },
|
|
38899
39023
|
inputSchema: {
|
|
@@ -38913,7 +39037,7 @@ function registerCalendarTools(server2, client2) {
|
|
|
38913
39037
|
const data = await client2.request("POST", "/pub/v1/calendar/events", args);
|
|
38914
39038
|
return jsonResponse(data);
|
|
38915
39039
|
});
|
|
38916
|
-
|
|
39040
|
+
server.registerTool("ofw_update_event", {
|
|
38917
39041
|
description: "Update an existing OurFamilyWizard calendar event",
|
|
38918
39042
|
annotations: { destructiveHint: true },
|
|
38919
39043
|
inputSchema: {
|
|
@@ -38931,7 +39055,7 @@ function registerCalendarTools(server2, client2) {
|
|
|
38931
39055
|
const data = await client2.request("PUT", `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
|
|
38932
39056
|
return jsonResponse(data);
|
|
38933
39057
|
});
|
|
38934
|
-
|
|
39058
|
+
server.registerTool("ofw_delete_event", {
|
|
38935
39059
|
description: "Delete an OurFamilyWizard calendar event",
|
|
38936
39060
|
annotations: { destructiveHint: true },
|
|
38937
39061
|
inputSchema: {
|
|
@@ -38944,15 +39068,15 @@ function registerCalendarTools(server2, client2) {
|
|
|
38944
39068
|
}
|
|
38945
39069
|
|
|
38946
39070
|
// src/tools/expenses.ts
|
|
38947
|
-
function registerExpenseTools(
|
|
38948
|
-
|
|
39071
|
+
function registerExpenseTools(server, client2) {
|
|
39072
|
+
server.registerTool("ofw_get_expense_totals", {
|
|
38949
39073
|
description: "Get OurFamilyWizard expense summary totals (owed/paid)",
|
|
38950
39074
|
annotations: { readOnlyHint: true }
|
|
38951
39075
|
}, async () => {
|
|
38952
39076
|
const data = await client2.request("GET", "/pub/v2/expense/expenses/totals");
|
|
38953
39077
|
return jsonResponse(data);
|
|
38954
39078
|
});
|
|
38955
|
-
|
|
39079
|
+
server.registerTool("ofw_list_expenses", {
|
|
38956
39080
|
description: "List OurFamilyWizard expenses with pagination",
|
|
38957
39081
|
annotations: { readOnlyHint: true },
|
|
38958
39082
|
inputSchema: {
|
|
@@ -38965,7 +39089,7 @@ function registerExpenseTools(server2, client2) {
|
|
|
38965
39089
|
const data = await client2.request("GET", `/pub/v2/expense/expenses?start=${start}&max=${max}`);
|
|
38966
39090
|
return jsonResponse(data);
|
|
38967
39091
|
});
|
|
38968
|
-
|
|
39092
|
+
server.registerTool("ofw_create_expense", {
|
|
38969
39093
|
description: "Log a new expense in OurFamilyWizard",
|
|
38970
39094
|
annotations: { destructiveHint: false },
|
|
38971
39095
|
inputSchema: {
|
|
@@ -38979,8 +39103,8 @@ function registerExpenseTools(server2, client2) {
|
|
|
38979
39103
|
}
|
|
38980
39104
|
|
|
38981
39105
|
// src/tools/journal.ts
|
|
38982
|
-
function registerJournalTools(
|
|
38983
|
-
|
|
39106
|
+
function registerJournalTools(server, client2) {
|
|
39107
|
+
server.registerTool("ofw_list_journal_entries", {
|
|
38984
39108
|
description: "List OurFamilyWizard journal entries",
|
|
38985
39109
|
annotations: { readOnlyHint: true },
|
|
38986
39110
|
inputSchema: {
|
|
@@ -38993,7 +39117,7 @@ function registerJournalTools(server2, client2) {
|
|
|
38993
39117
|
const data = await client2.request("GET", `/pub/v1/journals?start=${start}&max=${max}`);
|
|
38994
39118
|
return jsonResponse(data);
|
|
38995
39119
|
});
|
|
38996
|
-
|
|
39120
|
+
server.registerTool("ofw_create_journal_entry", {
|
|
38997
39121
|
description: "Create a new journal entry in OurFamilyWizard",
|
|
38998
39122
|
annotations: { destructiveHint: false },
|
|
38999
39123
|
inputSchema: {
|
|
@@ -39017,12 +39141,17 @@ process.emit = function(event, ...args) {
|
|
|
39017
39141
|
}
|
|
39018
39142
|
return originalEmit(event, ...args);
|
|
39019
39143
|
};
|
|
39020
|
-
|
|
39021
|
-
|
|
39022
|
-
|
|
39023
|
-
|
|
39024
|
-
|
|
39025
|
-
|
|
39026
|
-
|
|
39027
|
-
|
|
39028
|
-
|
|
39144
|
+
await runMcp({
|
|
39145
|
+
name: "ofw",
|
|
39146
|
+
version: "2.3.1",
|
|
39147
|
+
// x-release-please-version
|
|
39148
|
+
deps: client,
|
|
39149
|
+
tools: [
|
|
39150
|
+
registerUserTools,
|
|
39151
|
+
registerMessageTools,
|
|
39152
|
+
registerCalendarTools,
|
|
39153
|
+
registerExpenseTools,
|
|
39154
|
+
registerJournalTools
|
|
39155
|
+
],
|
|
39156
|
+
banner: "[ofw-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion."
|
|
39157
|
+
});
|
package/dist/cache.js
CHANGED
|
@@ -182,6 +182,7 @@ export function countMessages(opts) {
|
|
|
182
182
|
const { where, params } = buildMessageFilter(opts);
|
|
183
183
|
const r = db.prepare(`SELECT COUNT(*) as n FROM messages ${where}`)
|
|
184
184
|
.get(...params);
|
|
185
|
+
/* v8 ignore next -- SELECT COUNT(*) always returns exactly one row; the ?./?? are defensive */
|
|
185
186
|
return r?.n ?? 0;
|
|
186
187
|
}
|
|
187
188
|
function draftFromDb(r) {
|
package/dist/client.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
+
import { loadDotenvSafely } from '@chrischall/mcp-utils';
|
|
1
2
|
import { dirname, join } from 'path';
|
|
2
3
|
import { fileURLToPath } from 'url';
|
|
3
4
|
import { resolveAuth } from './auth.js';
|
|
4
5
|
import { parseBoolEnv } from './config.js';
|
|
5
6
|
import { BASE_URL, OFW_PROTOCOL_HEADERS, OFW_TOKEN_TTL_MS, OFW_TOKEN_EXPIRY_SKEW_MS } from './protocol.js';
|
|
6
|
-
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
// not available — rely on process.env (mcpb sets credentials via mcp_config.env)
|
|
14
|
-
}
|
|
7
|
+
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
|
|
8
|
+
// bundle). loadDotenvSafely applies override:false + quiet:true and swallows a
|
|
9
|
+
// missing dotenv module, matching the prior inline try/catch exactly.
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
await loadDotenvSafely({ path: join(__dirname, '..', '.env') });
|
|
15
12
|
// Parse a Content-Disposition header for a filename. Prefers RFC 6266
|
|
16
13
|
// `filename*=UTF-8''…` (percent-decoded) and falls back to `filename="…"`.
|
|
17
14
|
function parseContentDispositionFilename(cd) {
|
|
@@ -36,6 +33,7 @@ function debugLogEnabled() {
|
|
|
36
33
|
}
|
|
37
34
|
function redactHeaders(h) {
|
|
38
35
|
const out = { ...h };
|
|
36
|
+
/* v8 ignore next -- request headers always carry Authorization (set in request()); the guard is defensive for arbitrary header maps */
|
|
39
37
|
if (out.Authorization)
|
|
40
38
|
out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}…`;
|
|
41
39
|
return out;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { parseBoolEnv as parseBoolEnvUtil } from '@chrischall/mcp-utils';
|
|
4
5
|
// Cache identity drives the per-user SQLite DB filename. Order of preference:
|
|
5
6
|
// 1. OFW_CACHE_IDENTITY — explicit override for users who want to label the
|
|
6
7
|
// cache themselves (e.g. when authing via fetchproxy and OFW_USERNAME is
|
|
@@ -45,12 +46,13 @@ export function getAttachmentsDir() {
|
|
|
45
46
|
* (case-insensitive, trimmed). Anything else — unset, empty, or other
|
|
46
47
|
* values — is false. Used for OFW_INLINE_ATTACHMENTS, OFW_DISABLE_FETCHPROXY,
|
|
47
48
|
* OFW_DEBUG_LOG, etc.
|
|
49
|
+
*
|
|
50
|
+
* Delegates to @chrischall/mcp-utils' `parseBoolEnv` (which also recognizes
|
|
51
|
+
* the falsy set 0/false/no/off — behavior-equivalent here since callers only
|
|
52
|
+
* care about the truthy case and everything else defaults to false).
|
|
48
53
|
*/
|
|
49
54
|
export function parseBoolEnv(name) {
|
|
50
|
-
|
|
51
|
-
if (typeof raw !== 'string')
|
|
52
|
-
return false;
|
|
53
|
-
return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase());
|
|
55
|
+
return parseBoolEnvUtil(name);
|
|
54
56
|
}
|
|
55
57
|
// Default for ofw_download_attachment's `inline` arg when the caller doesn't
|
|
56
58
|
// pass one. Set OFW_INLINE_ATTACHMENTS=true to have attachments returned as
|
package/dist/index.js
CHANGED
|
@@ -9,20 +9,29 @@ process.emit = function (event, ...args) {
|
|
|
9
9
|
}
|
|
10
10
|
return originalEmit(event, ...args);
|
|
11
11
|
};
|
|
12
|
-
import {
|
|
13
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import { runMcp } from '@chrischall/mcp-utils';
|
|
14
13
|
import { client } from './client.js';
|
|
15
14
|
import { registerUserTools } from './tools/user.js';
|
|
16
15
|
import { registerMessageTools } from './tools/messages.js';
|
|
17
16
|
import { registerCalendarTools } from './tools/calendar.js';
|
|
18
17
|
import { registerExpenseTools } from './tools/expenses.js';
|
|
19
18
|
import { registerJournalTools } from './tools/journal.js';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
// runMcp builds the McpServer, applies the registrars (with `client` threaded
|
|
20
|
+
// through as deps), prints the banner to stderr, wires SIGINT/SIGTERM graceful
|
|
21
|
+
// shutdown, and connects the stdio transport. The deferred-config-error pattern
|
|
22
|
+
// is preserved: `client` is constructed at module load in ./client.js (auth is
|
|
23
|
+
// resolved lazily on the first tool call), so the host's initial tools/list
|
|
24
|
+
// always succeeds before any credential check runs.
|
|
25
|
+
await runMcp({
|
|
26
|
+
name: 'ofw',
|
|
27
|
+
version: '2.3.1', // x-release-please-version
|
|
28
|
+
deps: client,
|
|
29
|
+
tools: [
|
|
30
|
+
registerUserTools,
|
|
31
|
+
registerMessageTools,
|
|
32
|
+
registerCalendarTools,
|
|
33
|
+
registerExpenseTools,
|
|
34
|
+
registerJournalTools,
|
|
35
|
+
],
|
|
36
|
+
banner: '[ofw-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.',
|
|
37
|
+
});
|
package/dist/tools/_shared.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
1
|
+
import { expandPath as expandPathUtil, rawTextResult, textResult } from '@chrischall/mcp-utils';
|
|
2
|
+
// Pretty-printed JSON tool result. Thin wrapper over @chrischall/mcp-utils'
|
|
3
|
+
// `textResult` so the rest of the codebase keeps the local name.
|
|
4
|
+
export const jsonResponse = textResult;
|
|
5
|
+
// Raw-string tool result. Wrapper over @chrischall/mcp-utils' `rawTextResult`.
|
|
6
|
+
export const textResponse = rawTextResult;
|
|
8
7
|
// Translates OFW API recipient shape into the cache's normalized Recipient.
|
|
9
8
|
// Used wherever we surface or persist recipients (sync, get_message, send,
|
|
10
9
|
// save_draft) — all five call sites had near-identical inline mappings.
|
|
@@ -15,11 +14,9 @@ export function mapRecipients(items) {
|
|
|
15
14
|
viewedAt: r.viewed?.dateTime ?? null,
|
|
16
15
|
}));
|
|
17
16
|
}
|
|
18
|
-
// Expand a user-provided path: ~ →
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
22
|
-
}
|
|
17
|
+
// Expand a user-provided path: ~ → home, relative → absolute. Re-exports
|
|
18
|
+
// @chrischall/mcp-utils' `expandPath`.
|
|
19
|
+
export const expandPath = expandPathUtil;
|
|
23
20
|
/**
|
|
24
21
|
* POST a payload to /pub/v3/messages, then immediately GET the detail
|
|
25
22
|
* endpoint for the resulting message id. This is the only correct way to
|
package/dist/tools/messages.js
CHANGED
|
@@ -4,6 +4,7 @@ import { listMessages, countMessages, listDrafts, getMessage, upsertMessage, ups
|
|
|
4
4
|
import { getAttachmentsDir, getDefaultInlineAttachments } from '../config.js';
|
|
5
5
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { basename, dirname, extname, join } from 'node:path';
|
|
7
|
+
import { fileBlob } from '@chrischall/mcp-utils';
|
|
7
8
|
import { expandPath, jsonResponse, mapRecipients, postMessageAndRefetch, textResponse } from './_shared.js';
|
|
8
9
|
// Lightweight mime sniff from extension. OFW re-derives mime from the filename
|
|
9
10
|
// server-side anyway, so this is just a polite Content-Type for the Blob.
|
|
@@ -425,12 +426,12 @@ export function registerMessageTools(server, client) {
|
|
|
425
426
|
const stat = statSync(abs); // throws if missing
|
|
426
427
|
if (!stat.isFile())
|
|
427
428
|
throw new Error(`Not a file: ${abs}`);
|
|
428
|
-
const buf = readFileSync(abs);
|
|
429
429
|
const fileName = basename(abs);
|
|
430
430
|
const mime = mimeFromName(fileName);
|
|
431
431
|
// Build the multipart payload matching the OFW web UI's request shape.
|
|
432
432
|
const form = new FormData();
|
|
433
|
-
|
|
433
|
+
// fileBlob streams the file off disk (a file-backed Blob) instead of buffering it.
|
|
434
|
+
form.append('file', await fileBlob(abs, { type: mime }), fileName);
|
|
434
435
|
form.append('source', 'message');
|
|
435
436
|
form.append('description', args.description ?? fileName);
|
|
436
437
|
form.append('label', args.label ?? fileName);
|
|
@@ -445,7 +446,7 @@ export function registerMessageTools(server, client) {
|
|
|
445
446
|
fileName: meta.fileName ?? fileName,
|
|
446
447
|
label: meta.label ?? args.label ?? fileName,
|
|
447
448
|
mimeType: meta.fileType ?? mime,
|
|
448
|
-
sizeBytes: typeof meta.sizeInBytes === 'number' ? meta.sizeInBytes :
|
|
449
|
+
sizeBytes: typeof meta.sizeInBytes === 'number' ? meta.sizeInBytes : stat.size,
|
|
449
450
|
metadata: meta,
|
|
450
451
|
messageId: 0,
|
|
451
452
|
});
|
|
@@ -453,7 +454,7 @@ export function registerMessageTools(server, client) {
|
|
|
453
454
|
fileId: meta.fileId,
|
|
454
455
|
fileName: meta.fileName ?? fileName,
|
|
455
456
|
mimeType: meta.fileType ?? mime,
|
|
456
|
-
sizeBytes: meta.sizeInBytes ??
|
|
457
|
+
sizeBytes: meta.sizeInBytes ?? stat.size,
|
|
457
458
|
shareClass: meta.shareClass ?? args.shareClass ?? 'PRIVATE',
|
|
458
459
|
note: 'Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it.',
|
|
459
460
|
});
|
|
@@ -476,6 +477,7 @@ export function registerMessageTools(server, client) {
|
|
|
476
477
|
// sentinel — gets re-linked if a message later references this file.
|
|
477
478
|
await fetchAttachmentMeta(client, fileId, 0);
|
|
478
479
|
cached = getAttachment(fileId);
|
|
480
|
+
/* v8 ignore next -- fetchAttachmentMeta persists the row it just fetched; a still-null read here is an unreachable storage failure */
|
|
479
481
|
if (!cached)
|
|
480
482
|
throw new Error(`failed to fetch metadata for fileId ${fileId}`);
|
|
481
483
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"mcpName": "io.github.chrischall/ofw-mcp",
|
|
5
5
|
"description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from \"module\"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js",
|
|
25
25
|
"dev": "node --env-file=.env dist/index.js",
|
|
26
26
|
"test": "vitest run",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
27
28
|
"test:watch": "vitest"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
31
|
+
"@chrischall/mcp-utils": "^0.4.0",
|
|
30
32
|
"@fetchproxy/bootstrap": "^0.11.0",
|
|
31
|
-
"@fetchproxy/server": "^0.11.0",
|
|
32
33
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
34
|
"dotenv": "^17.4.2",
|
|
34
35
|
"zod": "^4.4.3"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/ofw-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.3.
|
|
9
|
+
"version": "2.3.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.3.
|
|
14
|
+
"version": "2.3.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|