ofw-mcp 2.4.3 → 2.4.4
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 -14
- package/dist/bundle.js +113 -100
- package/dist/client.js +4 -10
- package/dist/config.js +2 -21
- package/dist/index.js +1 -1
- package/dist/sync.js +7 -7
- package/dist/tools/_shared.js +12 -3
- package/dist/tools/messages.js +11 -6
- package/package.json +3 -3
- package/server.json +2 -2
- package/dist/validate.js +0 -35
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "OurFamilyWizard tools for Claude Code",
|
|
9
|
-
"version": "2.4.
|
|
9
|
+
"version": "2.4.4"
|
|
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.4.
|
|
17
|
+
"version": "2.4.4",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/dist/auth.js
CHANGED
|
@@ -45,22 +45,11 @@
|
|
|
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
|
+
import { parseBoolEnv, readEnvVar } from '@chrischall/mcp-utils';
|
|
49
49
|
import { bootstrap } from '@fetchproxy/bootstrap';
|
|
50
50
|
import { classifyBridgeError } from '@chrischall/mcp-utils/fetchproxy';
|
|
51
51
|
import { loginWithPassword } from './auth-password.js';
|
|
52
|
-
import { parseBoolEnv } from './config.js';
|
|
53
52
|
import pkg from '../package.json' with { type: 'json' };
|
|
54
|
-
/**
|
|
55
|
-
* Read an env var, trim, and treat blank / `${UNEXPANDED}` placeholders as
|
|
56
|
-
* unset. Defends against MCP hosts that pass `.mcp.json` env blocks through
|
|
57
|
-
* without variable expansion. Delegates to @chrischall/mcp-utils' `readEnvVar`,
|
|
58
|
-
* which applies the identical sanitization (blank, `undefined`/`null`,
|
|
59
|
-
* `${...}` placeholder → unset).
|
|
60
|
-
*/
|
|
61
|
-
function readEnv(key) {
|
|
62
|
-
return readEnvVar(key);
|
|
63
|
-
}
|
|
64
53
|
/** True if the user has explicitly disabled the fetchproxy fallback. */
|
|
65
54
|
function fetchproxyDisabled() {
|
|
66
55
|
return parseBoolEnv('OFW_DISABLE_FETCHPROXY');
|
|
@@ -75,8 +64,11 @@ function fetchproxyDisabled() {
|
|
|
75
64
|
*/
|
|
76
65
|
export async function resolveAuth() {
|
|
77
66
|
// ── Path 1: env-var credentials (unchanged from pre-fetchproxy behavior).
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
// `readEnvVar` trims and treats blank / `"undefined"` / `"null"` /
|
|
68
|
+
// `${UNEXPANDED}` placeholders as unset — defends against MCP hosts that
|
|
69
|
+
// pass `.mcp.json` env blocks through without variable expansion.
|
|
70
|
+
const username = readEnvVar('OFW_USERNAME');
|
|
71
|
+
const password = readEnvVar('OFW_PASSWORD');
|
|
80
72
|
if (username && password) {
|
|
81
73
|
const { token, expiresAt } = await loginWithPassword(username, password);
|
|
82
74
|
return { token, expiresAt, source: 'env' };
|
package/dist/bundle.js
CHANGED
|
@@ -34686,6 +34686,22 @@ async function runMcp(opts) {
|
|
|
34686
34686
|
}
|
|
34687
34687
|
|
|
34688
34688
|
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
34689
|
+
var McpToolError = class extends Error {
|
|
34690
|
+
/** Actionable remediation text, when one applies. */
|
|
34691
|
+
hint;
|
|
34692
|
+
constructor(message, opts) {
|
|
34693
|
+
super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
34694
|
+
this.name = "McpToolError";
|
|
34695
|
+
if (opts?.hint !== void 0)
|
|
34696
|
+
this.hint = opts.hint;
|
|
34697
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
34698
|
+
}
|
|
34699
|
+
};
|
|
34700
|
+
var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
34701
|
+
var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
34702
|
+
var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
|
|
34703
|
+
var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
|
|
34704
|
+
var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
|
|
34689
34705
|
var API_KEY_RE = new RegExp([
|
|
34690
34706
|
"sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
|
|
34691
34707
|
// OpenAI / Anthropic (incl. sk-ant-…)
|
|
@@ -34700,6 +34716,14 @@ var API_KEY_RE = new RegExp([
|
|
|
34700
34716
|
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
34701
34717
|
// webhook signing secret (Stripe-style)
|
|
34702
34718
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
34719
|
+
var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
34720
|
+
var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
34721
|
+
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
34722
|
+
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
34723
|
+
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
34724
|
+
function redactSecrets(text) {
|
|
34725
|
+
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
34726
|
+
}
|
|
34703
34727
|
|
|
34704
34728
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34705
34729
|
function textResult(data) {
|
|
@@ -34781,6 +34805,21 @@ async function fileBlob(path, opts = {}) {
|
|
|
34781
34805
|
return blob;
|
|
34782
34806
|
}
|
|
34783
34807
|
|
|
34808
|
+
// node_modules/@chrischall/mcp-utils/dist/zod/parse-lenient.js
|
|
34809
|
+
function parseLenient(schema, raw, opts) {
|
|
34810
|
+
const result = schema.safeParse(raw);
|
|
34811
|
+
if (result.success)
|
|
34812
|
+
return result.data;
|
|
34813
|
+
const issues = result.error.issues.map((issue2) => `${issue2.path.join(".") || "(root)"}: ${issue2.message}`).join("; ");
|
|
34814
|
+
if (opts.mode === "strict") {
|
|
34815
|
+
throw new McpToolError(`Unexpected ${opts.context} shape from the upstream API. ${issues}`, {
|
|
34816
|
+
hint: "The upstream API may have changed; the schema needs updating."
|
|
34817
|
+
});
|
|
34818
|
+
}
|
|
34819
|
+
console.error(`[${opts.label}] WARNING: unexpected ${opts.context} shape \u2014 proceeding with the raw response. ${issues}`);
|
|
34820
|
+
return raw;
|
|
34821
|
+
}
|
|
34822
|
+
|
|
34784
34823
|
// node_modules/@chrischall/mcp-utils/dist/zod/index.js
|
|
34785
34824
|
var PositiveInt = external_exports.number().int().positive();
|
|
34786
34825
|
var NonNegInt = external_exports.number().int().nonnegative();
|
|
@@ -34878,7 +34917,7 @@ var TokenManager = class {
|
|
|
34878
34917
|
};
|
|
34879
34918
|
|
|
34880
34919
|
// src/client.ts
|
|
34881
|
-
import { dirname, join as
|
|
34920
|
+
import { dirname, join as join3 } from "path";
|
|
34882
34921
|
import { fileURLToPath } from "url";
|
|
34883
34922
|
|
|
34884
34923
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
@@ -38243,53 +38282,10 @@ async function loginWithPassword(username, password) {
|
|
|
38243
38282
|
};
|
|
38244
38283
|
}
|
|
38245
38284
|
|
|
38246
|
-
// src/config.ts
|
|
38247
|
-
import { createHash } from "node:crypto";
|
|
38248
|
-
import { homedir as homedir3 } from "node:os";
|
|
38249
|
-
import { join as join3 } from "node:path";
|
|
38250
|
-
function readCacheIdentity() {
|
|
38251
|
-
const explicit = process.env.OFW_CACHE_IDENTITY;
|
|
38252
|
-
if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
|
|
38253
|
-
const username = process.env.OFW_USERNAME;
|
|
38254
|
-
if (typeof username === "string" && username.trim().length > 0) return username.trim();
|
|
38255
|
-
return "_default";
|
|
38256
|
-
}
|
|
38257
|
-
function getCacheDir() {
|
|
38258
|
-
const override = process.env.OFW_CACHE_DIR;
|
|
38259
|
-
if (override && override.trim().length > 0) return override.trim();
|
|
38260
|
-
return join3(homedir3(), ".cache", "ofw-mcp");
|
|
38261
|
-
}
|
|
38262
|
-
function getCacheDbPath() {
|
|
38263
|
-
const identity = readCacheIdentity();
|
|
38264
|
-
const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
38265
|
-
return join3(getCacheDir(), `${hash2}.db`);
|
|
38266
|
-
}
|
|
38267
|
-
function getAttachmentsDir() {
|
|
38268
|
-
const override = process.env.OFW_ATTACHMENTS_DIR;
|
|
38269
|
-
if (override && override.trim().length > 0) return override.trim();
|
|
38270
|
-
return join3(homedir3(), "Downloads", "ofw-mcp");
|
|
38271
|
-
}
|
|
38272
|
-
function parseBoolEnv2(name) {
|
|
38273
|
-
return parseBoolEnv(name);
|
|
38274
|
-
}
|
|
38275
|
-
function getWriteMode() {
|
|
38276
|
-
const raw = process.env.OFW_WRITE_MODE;
|
|
38277
|
-
if (typeof raw !== "string" || raw.trim().length === 0) return "all";
|
|
38278
|
-
const mode = raw.trim().toLowerCase();
|
|
38279
|
-
if (mode === "none" || mode === "drafts" || mode === "all") return mode;
|
|
38280
|
-
console.error(
|
|
38281
|
-
`[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" \u2014 failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`
|
|
38282
|
-
);
|
|
38283
|
-
return "none";
|
|
38284
|
-
}
|
|
38285
|
-
function getDefaultInlineAttachments() {
|
|
38286
|
-
return parseBoolEnv2("OFW_INLINE_ATTACHMENTS");
|
|
38287
|
-
}
|
|
38288
|
-
|
|
38289
38285
|
// package.json
|
|
38290
38286
|
var package_default = {
|
|
38291
38287
|
name: "ofw-mcp",
|
|
38292
|
-
version: "2.4.
|
|
38288
|
+
version: "2.4.4",
|
|
38293
38289
|
license: "MIT",
|
|
38294
38290
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
38295
38291
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -38321,14 +38317,14 @@ var package_default = {
|
|
|
38321
38317
|
"test:watch": "vitest"
|
|
38322
38318
|
},
|
|
38323
38319
|
dependencies: {
|
|
38324
|
-
"@chrischall/mcp-utils": "^0.
|
|
38320
|
+
"@chrischall/mcp-utils": "^0.12.0",
|
|
38325
38321
|
"@fetchproxy/bootstrap": "^1.3.0",
|
|
38326
38322
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38327
38323
|
dotenv: "^17.4.2",
|
|
38328
38324
|
zod: "^4.4.3"
|
|
38329
38325
|
},
|
|
38330
38326
|
devDependencies: {
|
|
38331
|
-
"@types/node": "^
|
|
38327
|
+
"@types/node": "^26.0.0",
|
|
38332
38328
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38333
38329
|
esbuild: "^0.28.0",
|
|
38334
38330
|
typescript: "^6.0.3",
|
|
@@ -38337,15 +38333,12 @@ var package_default = {
|
|
|
38337
38333
|
};
|
|
38338
38334
|
|
|
38339
38335
|
// src/auth.ts
|
|
38340
|
-
function readEnv(key) {
|
|
38341
|
-
return readEnvVar(key);
|
|
38342
|
-
}
|
|
38343
38336
|
function fetchproxyDisabled() {
|
|
38344
|
-
return
|
|
38337
|
+
return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
|
|
38345
38338
|
}
|
|
38346
38339
|
async function resolveAuth() {
|
|
38347
|
-
const username =
|
|
38348
|
-
const password =
|
|
38340
|
+
const username = readEnvVar("OFW_USERNAME");
|
|
38341
|
+
const password = readEnvVar("OFW_PASSWORD");
|
|
38349
38342
|
if (username && password) {
|
|
38350
38343
|
const { token, expiresAt } = await loginWithPassword(username, password);
|
|
38351
38344
|
return { token, expiresAt, source: "env" };
|
|
@@ -38402,7 +38395,7 @@ async function resolveAuth() {
|
|
|
38402
38395
|
|
|
38403
38396
|
// src/client.ts
|
|
38404
38397
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38405
|
-
await loadDotenvSafely({ path:
|
|
38398
|
+
await loadDotenvSafely({ path: join3(__dirname, "..", ".env") });
|
|
38406
38399
|
function parseContentDispositionFilename(cd) {
|
|
38407
38400
|
const extMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(cd);
|
|
38408
38401
|
if (extMatch) {
|
|
@@ -38417,12 +38410,7 @@ function parseContentDispositionFilename(cd) {
|
|
|
38417
38410
|
return m ? m[1] : null;
|
|
38418
38411
|
}
|
|
38419
38412
|
function debugLogEnabled() {
|
|
38420
|
-
return
|
|
38421
|
-
}
|
|
38422
|
-
function redactHeaders(h) {
|
|
38423
|
-
const out = { ...h };
|
|
38424
|
-
if (out.Authorization) out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}\u2026`;
|
|
38425
|
-
return out;
|
|
38413
|
+
return parseBoolEnv("OFW_DEBUG_LOG");
|
|
38426
38414
|
}
|
|
38427
38415
|
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
38428
38416
|
function getRequestTimeoutMs() {
|
|
@@ -38517,7 +38505,7 @@ var OFWClient = class {
|
|
|
38517
38505
|
if (debugLogEnabled()) {
|
|
38518
38506
|
const bodyPreview = body === void 0 ? "<none>" : isFormData ? `<FormData entries=${Array.from(body.keys()).join(",")}>` : JSON.stringify(body);
|
|
38519
38507
|
console.error(`[ofw-debug] \u2192 ${method} ${url2}${isRetry ? " (retry)" : ""}`);
|
|
38520
|
-
console.error(`[ofw-debug] headers: ${JSON.stringify(
|
|
38508
|
+
console.error(`[ofw-debug] headers: ${redactSecrets(JSON.stringify(headers))}`);
|
|
38521
38509
|
console.error(`[ofw-debug] body: ${bodyPreview}`);
|
|
38522
38510
|
}
|
|
38523
38511
|
const timeoutMs = getRequestTimeoutMs();
|
|
@@ -38557,17 +38545,6 @@ var OFWClient = class {
|
|
|
38557
38545
|
};
|
|
38558
38546
|
var client = new OFWClient();
|
|
38559
38547
|
|
|
38560
|
-
// src/validate.ts
|
|
38561
|
-
function parseOFW(schema, raw, ctx, mode = "lenient") {
|
|
38562
|
-
const result = schema.safeParse(raw);
|
|
38563
|
-
if (result.success) return result.data;
|
|
38564
|
-
const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
38565
|
-
const message = `OFW response for ${ctx} failed validation: ${issues}`;
|
|
38566
|
-
if (mode === "strict") throw new Error(message);
|
|
38567
|
-
console.error(`[ofw-mcp] WARNING: ${message} \u2014 continuing with the raw response; fields derived from it may be missing or wrong.`);
|
|
38568
|
-
return raw;
|
|
38569
|
-
}
|
|
38570
|
-
|
|
38571
38548
|
// src/tools/_shared.ts
|
|
38572
38549
|
var jsonResponse = textResult;
|
|
38573
38550
|
var textResponse = rawTextResult;
|
|
@@ -38599,19 +38576,17 @@ var PostMessagesResponseSchema = external_exports.looseObject({
|
|
|
38599
38576
|
entityId: external_exports.number().optional()
|
|
38600
38577
|
}).nullable();
|
|
38601
38578
|
async function postMessageAndRefetch(client2, payload, detailSchema, ctx) {
|
|
38602
|
-
const raw =
|
|
38579
|
+
const raw = parseLenient(
|
|
38603
38580
|
PostMessagesResponseSchema,
|
|
38604
38581
|
await client2.request("POST", "/pub/v3/messages", payload),
|
|
38605
|
-
`POST /pub/v3/messages (${ctx})`,
|
|
38606
|
-
"strict"
|
|
38582
|
+
{ label: "ofw-mcp", context: `POST /pub/v3/messages (${ctx})`, mode: "strict" }
|
|
38607
38583
|
);
|
|
38608
38584
|
const id = typeof raw?.id === "number" ? raw.id : typeof raw?.entityId === "number" ? raw.entityId : null;
|
|
38609
38585
|
if (id === null) return { id: null, detail: null, raw };
|
|
38610
|
-
const detail =
|
|
38586
|
+
const detail = parseLenient(
|
|
38611
38587
|
detailSchema,
|
|
38612
38588
|
await client2.request("GET", `/pub/v3/messages/${id}`),
|
|
38613
|
-
`GET /pub/v3/messages/{id} (${ctx})`,
|
|
38614
|
-
"strict"
|
|
38589
|
+
{ label: "ofw-mcp", context: `GET /pub/v3/messages/{id} (${ctx})`, mode: "strict" }
|
|
38615
38590
|
);
|
|
38616
38591
|
return { id, detail, raw };
|
|
38617
38592
|
}
|
|
@@ -38638,6 +38613,44 @@ function registerUserTools(server, client2) {
|
|
|
38638
38613
|
import { DatabaseSync } from "node:sqlite";
|
|
38639
38614
|
import { mkdirSync, chmodSync, existsSync } from "node:fs";
|
|
38640
38615
|
import { dirname as dirname2 } from "node:path";
|
|
38616
|
+
|
|
38617
|
+
// src/config.ts
|
|
38618
|
+
import { createHash } from "node:crypto";
|
|
38619
|
+
import { homedir as homedir3 } from "node:os";
|
|
38620
|
+
import { join as join4 } from "node:path";
|
|
38621
|
+
function readCacheIdentity() {
|
|
38622
|
+
return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
|
|
38623
|
+
}
|
|
38624
|
+
function getCacheDir() {
|
|
38625
|
+
const override = process.env.OFW_CACHE_DIR;
|
|
38626
|
+
if (override && override.trim().length > 0) return override.trim();
|
|
38627
|
+
return join4(homedir3(), ".cache", "ofw-mcp");
|
|
38628
|
+
}
|
|
38629
|
+
function getCacheDbPath() {
|
|
38630
|
+
const identity = readCacheIdentity();
|
|
38631
|
+
const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
38632
|
+
return join4(getCacheDir(), `${hash2}.db`);
|
|
38633
|
+
}
|
|
38634
|
+
function getAttachmentsDir() {
|
|
38635
|
+
const override = process.env.OFW_ATTACHMENTS_DIR;
|
|
38636
|
+
if (override && override.trim().length > 0) return override.trim();
|
|
38637
|
+
return join4(homedir3(), "Downloads", "ofw-mcp");
|
|
38638
|
+
}
|
|
38639
|
+
function getWriteMode() {
|
|
38640
|
+
const raw = process.env.OFW_WRITE_MODE;
|
|
38641
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return "all";
|
|
38642
|
+
const mode = raw.trim().toLowerCase();
|
|
38643
|
+
if (mode === "none" || mode === "drafts" || mode === "all") return mode;
|
|
38644
|
+
console.error(
|
|
38645
|
+
`[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" \u2014 failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`
|
|
38646
|
+
);
|
|
38647
|
+
return "none";
|
|
38648
|
+
}
|
|
38649
|
+
function getDefaultInlineAttachments() {
|
|
38650
|
+
return parseBoolEnv("OFW_INLINE_ATTACHMENTS");
|
|
38651
|
+
}
|
|
38652
|
+
|
|
38653
|
+
// src/cache.ts
|
|
38641
38654
|
var instance = null;
|
|
38642
38655
|
var SCHEMA_V1 = `
|
|
38643
38656
|
CREATE TABLE IF NOT EXISTS messages (
|
|
@@ -38990,10 +39003,10 @@ var FileMetaSchema = external_exports.looseObject({
|
|
|
38990
39003
|
fileSize: external_exports.number().optional()
|
|
38991
39004
|
});
|
|
38992
39005
|
async function fetchAttachmentMeta(client2, fileId, messageId) {
|
|
38993
|
-
const meta3 =
|
|
39006
|
+
const meta3 = parseLenient(
|
|
38994
39007
|
FileMetaSchema,
|
|
38995
39008
|
await client2.request("GET", `/pub/v1/myfiles/${fileId}`),
|
|
38996
|
-
"GET /pub/v1/myfiles/{fileId}"
|
|
39009
|
+
{ label: "ofw-mcp", context: "GET /pub/v1/myfiles/{fileId}" }
|
|
38997
39010
|
);
|
|
38998
39011
|
upsertAttachmentForMessage({
|
|
38999
39012
|
fileId: meta3.fileId ?? fileId,
|
|
@@ -39012,10 +39025,10 @@ var FoldersSchema = external_exports.looseObject({
|
|
|
39012
39025
|
systemFolders: external_exports.array(external_exports.looseObject({ id: external_exports.string(), folderType: external_exports.string() })).optional()
|
|
39013
39026
|
});
|
|
39014
39027
|
async function resolveFolderIds(client2) {
|
|
39015
|
-
const data =
|
|
39028
|
+
const data = parseLenient(
|
|
39016
39029
|
FoldersSchema,
|
|
39017
39030
|
await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true"),
|
|
39018
|
-
"GET /pub/v1/messageFolders"
|
|
39031
|
+
{ label: "ofw-mcp", context: "GET /pub/v1/messageFolders" }
|
|
39019
39032
|
);
|
|
39020
39033
|
const sys = data.systemFolders ?? [];
|
|
39021
39034
|
const find = (type) => {
|
|
@@ -39051,10 +39064,10 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
39051
39064
|
const unread = [];
|
|
39052
39065
|
while (true) {
|
|
39053
39066
|
const path = `/pub/v3/messages?folders=${encodeURIComponent(folderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
39054
|
-
const list =
|
|
39067
|
+
const list = parseLenient(
|
|
39055
39068
|
ListResponseSchema,
|
|
39056
39069
|
await client2.request("GET", path),
|
|
39057
|
-
`GET /pub/v3/messages?folders={${folder}}`
|
|
39070
|
+
{ label: "ofw-mcp", context: `GET /pub/v3/messages?folders={${folder}}` }
|
|
39058
39071
|
);
|
|
39059
39072
|
const items = list.data ?? [];
|
|
39060
39073
|
if (items.length === 0) break;
|
|
@@ -39070,10 +39083,10 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
39070
39083
|
let fetchedBodyAt = null;
|
|
39071
39084
|
let detailFileIds = [];
|
|
39072
39085
|
if (shouldFetchBody) {
|
|
39073
|
-
const detail =
|
|
39086
|
+
const detail = parseLenient(
|
|
39074
39087
|
DetailResponseSchema,
|
|
39075
39088
|
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
39076
|
-
"GET /pub/v3/messages/{id} (sync)"
|
|
39089
|
+
{ label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (sync)" }
|
|
39077
39090
|
);
|
|
39078
39091
|
body = detail.body ?? "";
|
|
39079
39092
|
fetchedBodyAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -39133,10 +39146,10 @@ async function syncDrafts(client2, draftsFolderId) {
|
|
|
39133
39146
|
let page = 1;
|
|
39134
39147
|
while (true) {
|
|
39135
39148
|
const path = `/pub/v3/messages?folders=${encodeURIComponent(draftsFolderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
39136
|
-
const list =
|
|
39149
|
+
const list = parseLenient(
|
|
39137
39150
|
DraftListResponseSchema,
|
|
39138
39151
|
await client2.request("GET", path),
|
|
39139
|
-
"GET /pub/v3/messages?folders={drafts}"
|
|
39152
|
+
{ label: "ofw-mcp", context: "GET /pub/v3/messages?folders={drafts}" }
|
|
39140
39153
|
);
|
|
39141
39154
|
const pageItems = list.data ?? [];
|
|
39142
39155
|
items.push(...pageItems);
|
|
@@ -39149,10 +39162,10 @@ async function syncDrafts(client2, draftsFolderId) {
|
|
|
39149
39162
|
seenIds.add(item.id);
|
|
39150
39163
|
const modifiedAt = item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
39151
39164
|
const existing = getDraft(item.id);
|
|
39152
|
-
const detail =
|
|
39165
|
+
const detail = parseLenient(
|
|
39153
39166
|
DraftDetailSchema,
|
|
39154
39167
|
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
39155
|
-
"GET /pub/v3/messages/{id} (drafts sync)"
|
|
39168
|
+
{ label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (drafts sync)" }
|
|
39156
39169
|
);
|
|
39157
39170
|
const row = {
|
|
39158
39171
|
id: item.id,
|
|
@@ -39352,10 +39365,10 @@ function registerMessageTools(server, client2) {
|
|
|
39352
39365
|
let attachments2 = listAttachmentsForMessage(id);
|
|
39353
39366
|
if (attachments2.length === 0 && listDataHintsAtFiles(cached2.listData)) {
|
|
39354
39367
|
try {
|
|
39355
|
-
const detail2 =
|
|
39368
|
+
const detail2 = parseLenient(
|
|
39356
39369
|
DetailFilesSchema,
|
|
39357
39370
|
await client2.request("GET", `/pub/v3/messages/${id}`),
|
|
39358
|
-
"GET /pub/v3/messages/{id} (attachment backfill)"
|
|
39371
|
+
{ label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (attachment backfill)" }
|
|
39359
39372
|
);
|
|
39360
39373
|
if (Array.isArray(detail2.files) && detail2.files.length > 0) {
|
|
39361
39374
|
await fetchAttachmentMetaForMessage(client2, id, detail2.files);
|
|
@@ -39366,10 +39379,10 @@ function registerMessageTools(server, client2) {
|
|
|
39366
39379
|
}
|
|
39367
39380
|
return jsonResponse({ ...cached2, attachments: attachments2 });
|
|
39368
39381
|
}
|
|
39369
|
-
const detail =
|
|
39382
|
+
const detail = parseLenient(
|
|
39370
39383
|
MessageDetailSchema,
|
|
39371
39384
|
await client2.request("GET", `/pub/v3/messages/${encodeURIComponent(args.messageId)}`),
|
|
39372
|
-
"GET /pub/v3/messages/{id} (ofw_get_message)"
|
|
39385
|
+
{ label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (ofw_get_message)" }
|
|
39373
39386
|
);
|
|
39374
39387
|
const folder = cached2?.folder ?? "inbox";
|
|
39375
39388
|
const row = {
|
|
@@ -39651,11 +39664,10 @@ ${text}` : text);
|
|
|
39651
39664
|
form.append("label", args.label ?? fileName);
|
|
39652
39665
|
form.append("fileName", fileName);
|
|
39653
39666
|
form.append("shareClass", args.shareClass ?? "PRIVATE");
|
|
39654
|
-
const meta3 =
|
|
39667
|
+
const meta3 = parseLenient(
|
|
39655
39668
|
UploadedFileSchema,
|
|
39656
39669
|
await client2.request("POST", "/pub/v3/myfiles/multipart", form),
|
|
39657
|
-
"POST /pub/v3/myfiles/multipart (ofw_upload_attachment)",
|
|
39658
|
-
"strict"
|
|
39670
|
+
{ label: "ofw-mcp", context: "POST /pub/v3/myfiles/multipart (ofw_upload_attachment)", mode: "strict" }
|
|
39659
39671
|
);
|
|
39660
39672
|
upsertAttachmentForMessage({
|
|
39661
39673
|
fileId: meta3.fileId,
|
|
@@ -39727,12 +39739,13 @@ ${text}` : text);
|
|
|
39727
39739
|
} }] };
|
|
39728
39740
|
}
|
|
39729
39741
|
let dest;
|
|
39742
|
+
const safeName = basename(cached2.fileName);
|
|
39730
39743
|
if (args.saveTo) {
|
|
39731
39744
|
const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
|
|
39732
39745
|
const abs = expandPath2(args.saveTo);
|
|
39733
|
-
dest = isDirArg ? join5(abs, `${fileId}-${
|
|
39746
|
+
dest = isDirArg ? join5(abs, `${fileId}-${safeName}`) : abs;
|
|
39734
39747
|
} else {
|
|
39735
|
-
dest = join5(getAttachmentsDir(), `${fileId}-${
|
|
39748
|
+
dest = join5(getAttachmentsDir(), `${fileId}-${safeName}`);
|
|
39736
39749
|
}
|
|
39737
39750
|
if (!args.force && cached2.downloadedPath === dest) {
|
|
39738
39751
|
return jsonResponse({
|
|
@@ -39926,7 +39939,7 @@ process.emit = function(event, ...args) {
|
|
|
39926
39939
|
};
|
|
39927
39940
|
await runMcp({
|
|
39928
39941
|
name: "ofw",
|
|
39929
|
-
version: "2.4.
|
|
39942
|
+
version: "2.4.4",
|
|
39930
39943
|
// x-release-please-version
|
|
39931
39944
|
deps: client,
|
|
39932
39945
|
tools: [
|
package/dist/client.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { loadDotenvSafely } from '@chrischall/mcp-utils';
|
|
1
|
+
import { loadDotenvSafely, parseBoolEnv, redactSecrets } from '@chrischall/mcp-utils';
|
|
2
2
|
import { TokenManager } from '@chrischall/mcp-utils/session';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { resolveAuth } from './auth.js';
|
|
6
|
-
import { parseBoolEnv } from './config.js';
|
|
7
6
|
import { BASE_URL, OFW_PROTOCOL_HEADERS, OFW_TOKEN_TTL_MS, OFW_TOKEN_EXPIRY_SKEW_MS } from './protocol.js';
|
|
8
7
|
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
|
|
9
8
|
// bundle). loadDotenvSafely applies override:false + quiet:true and swallows a
|
|
@@ -32,13 +31,6 @@ function parseContentDispositionFilename(cd) {
|
|
|
32
31
|
function debugLogEnabled() {
|
|
33
32
|
return parseBoolEnv('OFW_DEBUG_LOG');
|
|
34
33
|
}
|
|
35
|
-
function redactHeaders(h) {
|
|
36
|
-
const out = { ...h };
|
|
37
|
-
/* v8 ignore next -- request headers always carry Authorization (set in request()); the guard is defensive for arbitrary header maps */
|
|
38
|
-
if (out.Authorization)
|
|
39
|
-
out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}…`;
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
34
|
// Per-request timeout. Overridable via OFW_REQUEST_TIMEOUT_MS. The default
|
|
43
35
|
// (30s) is comfortably above OFW's typical p99 but low enough that a stuck
|
|
44
36
|
// upstream fails fast instead of burning the MCP client-side budget — which
|
|
@@ -148,7 +140,9 @@ export class OFWClient {
|
|
|
148
140
|
? `<FormData entries=${Array.from(body.keys()).join(',')}>`
|
|
149
141
|
: JSON.stringify(body);
|
|
150
142
|
console.error(`[ofw-debug] → ${method} ${url}${isRetry ? ' (retry)' : ''}`);
|
|
151
|
-
|
|
143
|
+
// redactSecrets scrubs the Bearer token (and any other secret shapes)
|
|
144
|
+
// from the serialized header map — shared fleet redaction, never bespoke.
|
|
145
|
+
console.error(`[ofw-debug] headers: ${redactSecrets(JSON.stringify(headers))}`);
|
|
152
146
|
console.error(`[ofw-debug] body: ${bodyPreview}`);
|
|
153
147
|
}
|
|
154
148
|
// AbortController + setTimeout (not AbortSignal.timeout) so vitest fake
|
package/dist/config.js
CHANGED
|
@@ -1,7 +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
|
|
4
|
+
import { parseBoolEnv, readEnvVar } from '@chrischall/mcp-utils';
|
|
5
5
|
// Cache identity drives the per-user SQLite DB filename. Order of preference:
|
|
6
6
|
// 1. OFW_CACHE_IDENTITY — explicit override for users who want to label the
|
|
7
7
|
// cache themselves (e.g. when authing via fetchproxy and OFW_USERNAME is
|
|
@@ -11,13 +11,7 @@ import { parseBoolEnv as parseBoolEnvUtil } from '@chrischall/mcp-utils';
|
|
|
11
11
|
// Single-user installs are fine on this; multi-account users should set
|
|
12
12
|
// OFW_CACHE_IDENTITY explicitly so their caches don't collide.
|
|
13
13
|
function readCacheIdentity() {
|
|
14
|
-
|
|
15
|
-
if (typeof explicit === 'string' && explicit.trim().length > 0)
|
|
16
|
-
return explicit.trim();
|
|
17
|
-
const username = process.env.OFW_USERNAME;
|
|
18
|
-
if (typeof username === 'string' && username.trim().length > 0)
|
|
19
|
-
return username.trim();
|
|
20
|
-
return '_default';
|
|
14
|
+
return readEnvVar('OFW_CACHE_IDENTITY') ?? readEnvVar('OFW_USERNAME') ?? '_default';
|
|
21
15
|
}
|
|
22
16
|
export function getCacheDir() {
|
|
23
17
|
const override = process.env.OFW_CACHE_DIR;
|
|
@@ -41,19 +35,6 @@ export function getAttachmentsDir() {
|
|
|
41
35
|
// location across macOS/Linux/Windows.
|
|
42
36
|
return join(homedir(), 'Downloads', 'ofw-mcp');
|
|
43
37
|
}
|
|
44
|
-
/**
|
|
45
|
-
* True when a boolean-shaped env var is set to "1", "true", "yes", or "on"
|
|
46
|
-
* (case-insensitive, trimmed). Anything else — unset, empty, or other
|
|
47
|
-
* values — is false. Used for OFW_INLINE_ATTACHMENTS, OFW_DISABLE_FETCHPROXY,
|
|
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).
|
|
53
|
-
*/
|
|
54
|
-
export function parseBoolEnv(name) {
|
|
55
|
-
return parseBoolEnvUtil(name);
|
|
56
|
-
}
|
|
57
38
|
/**
|
|
58
39
|
* Gate for write-tool registration, read at registration time (startup).
|
|
59
40
|
*
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { registerJournalTools } from './tools/journal.js';
|
|
|
24
24
|
// always succeeds before any credential check runs.
|
|
25
25
|
await runMcp({
|
|
26
26
|
name: 'ofw',
|
|
27
|
-
version: '2.4.
|
|
27
|
+
version: '2.4.4', // x-release-please-version
|
|
28
28
|
deps: client,
|
|
29
29
|
tools: [
|
|
30
30
|
registerUserTools,
|
package/dist/sync.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setMeta, upsertMessage, getMessage, deleteMessage, setSyncState, upsertDraft, getDraft, deleteDraft, listDraftIds, upsertAttachmentForMessage, } from './cache.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { ApiRecipientSchema, mapRecipients } from './tools/_shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { parseLenient } from '@chrischall/mcp-utils';
|
|
5
5
|
// Each OFW message detail returns `files: [fileId, ...]`. We fetch the metadata
|
|
6
6
|
// for each file id (cheap JSON call) so the model can see filenames/mime types
|
|
7
7
|
// without downloading bytes. Bytes are pulled lazily by ofw_download_attachment.
|
|
@@ -22,7 +22,7 @@ const FileMetaSchema = z.looseObject({
|
|
|
22
22
|
// best-effort helper below; callers that need the result (download tool) let
|
|
23
23
|
// the throw propagate.
|
|
24
24
|
export async function fetchAttachmentMeta(client, fileId, messageId) {
|
|
25
|
-
const meta =
|
|
25
|
+
const meta = parseLenient(FileMetaSchema, await client.request('GET', `/pub/v1/myfiles/${fileId}`), { label: 'ofw-mcp', context: 'GET /pub/v1/myfiles/{fileId}' });
|
|
26
26
|
upsertAttachmentForMessage({
|
|
27
27
|
fileId: meta.fileId ?? fileId,
|
|
28
28
|
fileName: meta.fileName ?? `file-${fileId}`,
|
|
@@ -44,7 +44,7 @@ const FoldersSchema = z.looseObject({
|
|
|
44
44
|
systemFolders: z.array(z.looseObject({ id: z.string(), folderType: z.string() })).optional(),
|
|
45
45
|
});
|
|
46
46
|
export async function resolveFolderIds(client) {
|
|
47
|
-
const data =
|
|
47
|
+
const data = parseLenient(FoldersSchema, await client.request('GET', '/pub/v1/messageFolders?includeFolderCounts=true'), { label: 'ofw-mcp', context: 'GET /pub/v1/messageFolders' });
|
|
48
48
|
const sys = data.systemFolders ?? [];
|
|
49
49
|
const find = (type) => {
|
|
50
50
|
const f = sys.find((x) => x.folderType === type);
|
|
@@ -83,7 +83,7 @@ export async function syncMessageFolder(client, folder, folderId, opts) {
|
|
|
83
83
|
const unread = [];
|
|
84
84
|
while (true) {
|
|
85
85
|
const path = `/pub/v3/messages?folders=${encodeURIComponent(folderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
86
|
-
const list =
|
|
86
|
+
const list = parseLenient(ListResponseSchema, await client.request('GET', path), { label: 'ofw-mcp', context: `GET /pub/v3/messages?folders={${folder}}` });
|
|
87
87
|
const items = list.data ?? [];
|
|
88
88
|
if (items.length === 0)
|
|
89
89
|
break;
|
|
@@ -101,7 +101,7 @@ export async function syncMessageFolder(client, folder, folderId, opts) {
|
|
|
101
101
|
let fetchedBodyAt = null;
|
|
102
102
|
let detailFileIds = [];
|
|
103
103
|
if (shouldFetchBody) {
|
|
104
|
-
const detail =
|
|
104
|
+
const detail = parseLenient(DetailResponseSchema, await client.request('GET', `/pub/v3/messages/${item.id}`), { label: 'ofw-mcp', context: 'GET /pub/v3/messages/{id} (sync)' });
|
|
105
105
|
body = detail.body ?? '';
|
|
106
106
|
fetchedBodyAt = new Date().toISOString();
|
|
107
107
|
if (Array.isArray(detail.files) && detail.files.length > 0) {
|
|
@@ -170,7 +170,7 @@ export async function syncDrafts(client, draftsFolderId) {
|
|
|
170
170
|
let page = 1;
|
|
171
171
|
while (true) {
|
|
172
172
|
const path = `/pub/v3/messages?folders=${encodeURIComponent(draftsFolderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
173
|
-
const list =
|
|
173
|
+
const list = parseLenient(DraftListResponseSchema, await client.request('GET', path), { label: 'ofw-mcp', context: 'GET /pub/v3/messages?folders={drafts}' });
|
|
174
174
|
const pageItems = list.data ?? [];
|
|
175
175
|
items.push(...pageItems);
|
|
176
176
|
if (pageItems.length < 50)
|
|
@@ -186,7 +186,7 @@ export async function syncDrafts(client, draftsFolderId) {
|
|
|
186
186
|
// timestamp for drafts — direct UI edits don't bump it — so we can't
|
|
187
187
|
// use it to skip the detail fetch. Always re-fetch; drafts are few.
|
|
188
188
|
const existing = getDraft(item.id);
|
|
189
|
-
const detail =
|
|
189
|
+
const detail = parseLenient(DraftDetailSchema, await client.request('GET', `/pub/v3/messages/${item.id}`), { label: 'ofw-mcp', context: 'GET /pub/v3/messages/{id} (drafts sync)' });
|
|
190
190
|
const row = {
|
|
191
191
|
id: item.id,
|
|
192
192
|
subject: detail.subject ?? item.subject ?? '(no subject)',
|
package/dist/tools/_shared.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expandPath as expandPathUtil, rawTextResult, textResult } from '@chrischall/mcp-utils';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { parseLenient } from '@chrischall/mcp-utils';
|
|
4
4
|
// Pretty-printed JSON tool result. Thin wrapper over @chrischall/mcp-utils'
|
|
5
5
|
// `textResult` so the rest of the codebase keeps the local name.
|
|
6
6
|
export const jsonResponse = textResult;
|
|
@@ -77,14 +77,23 @@ const PostMessagesResponseSchema = z.looseObject({
|
|
|
77
77
|
* `if (result.id !== null)`. When id is null (no id field in the
|
|
78
78
|
* response — never observed in production, but defensive), `raw`
|
|
79
79
|
* carries the POST response so the caller can still surface it.
|
|
80
|
+
*
|
|
81
|
+
* The generic is parametrized on the schema's OUTPUT type `T`
|
|
82
|
+
* (`detailSchema: z.ZodType<T>`, `detail: T`) rather than on the schema
|
|
83
|
+
* type itself. This mirrors `parseLenient`'s own signature
|
|
84
|
+
* (`<T>(schema: ZodType<T>, …): T`) exactly, so `T` is inferred straight
|
|
85
|
+
* from the schema and flows into the return type with no `as` cast — the
|
|
86
|
+
* compiler verifies that `detail` matches `detailSchema`'s output. (A
|
|
87
|
+
* `<S extends z.ZodType>` constraint would widen the output to `unknown`
|
|
88
|
+
* and force a cast at this call site.)
|
|
80
89
|
*/
|
|
81
90
|
export async function postMessageAndRefetch(client, payload, detailSchema, ctx) {
|
|
82
|
-
const raw =
|
|
91
|
+
const raw = parseLenient(PostMessagesResponseSchema, await client.request('POST', '/pub/v3/messages', payload), { label: 'ofw-mcp', context: `POST /pub/v3/messages (${ctx})`, mode: 'strict' });
|
|
83
92
|
const id = typeof raw?.id === 'number' ? raw.id
|
|
84
93
|
: typeof raw?.entityId === 'number' ? raw.entityId
|
|
85
94
|
: null;
|
|
86
95
|
if (id === null)
|
|
87
96
|
return { id: null, detail: null, raw };
|
|
88
|
-
const detail =
|
|
97
|
+
const detail = parseLenient(detailSchema, await client.request('GET', `/pub/v3/messages/${id}`), { label: 'ofw-mcp', context: `GET /pub/v3/messages/{id} (${ctx})`, mode: 'strict' });
|
|
89
98
|
return { id, detail, raw };
|
|
90
99
|
}
|
package/dist/tools/messages.js
CHANGED
|
@@ -6,7 +6,7 @@ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
|
6
6
|
import { basename, dirname, extname, join } from 'node:path';
|
|
7
7
|
import { fileBlob } from '@chrischall/mcp-utils';
|
|
8
8
|
import { ApiRecipientSchema, expandPath, jsonResponse, mapRecipients, postMessageAndRefetch, textResponse, verifyWriteLanded } from './_shared.js';
|
|
9
|
-
import {
|
|
9
|
+
import { parseLenient } from '@chrischall/mcp-utils';
|
|
10
10
|
// Schemas for the load-bearing fields of each /pub/v3 response this file
|
|
11
11
|
// reads (issue #83). Loose: unknown keys pass through into cached listData.
|
|
12
12
|
const DateSchema = z.looseObject({ dateTime: z.string() });
|
|
@@ -193,7 +193,7 @@ export function registerMessageTools(server, client) {
|
|
|
193
193
|
// OFW state isn't changing).
|
|
194
194
|
if (attachments.length === 0 && listDataHintsAtFiles(cached.listData)) {
|
|
195
195
|
try {
|
|
196
|
-
const detail =
|
|
196
|
+
const detail = parseLenient(DetailFilesSchema, await client.request('GET', `/pub/v3/messages/${id}`), { label: 'ofw-mcp', context: 'GET /pub/v3/messages/{id} (attachment backfill)' });
|
|
197
197
|
if (Array.isArray(detail.files) && detail.files.length > 0) {
|
|
198
198
|
await fetchAttachmentMetaForMessage(client, id, detail.files);
|
|
199
199
|
attachments = listAttachmentsForMessage(id);
|
|
@@ -205,7 +205,7 @@ export function registerMessageTools(server, client) {
|
|
|
205
205
|
}
|
|
206
206
|
return jsonResponse({ ...cached, attachments });
|
|
207
207
|
}
|
|
208
|
-
const detail =
|
|
208
|
+
const detail = parseLenient(MessageDetailSchema, await client.request('GET', `/pub/v3/messages/${encodeURIComponent(args.messageId)}`), { label: 'ofw-mcp', context: 'GET /pub/v3/messages/{id} (ofw_get_message)' });
|
|
209
209
|
const folder = cached?.folder ?? 'inbox';
|
|
210
210
|
const row = {
|
|
211
211
|
id: detail.id,
|
|
@@ -508,7 +508,7 @@ export function registerMessageTools(server, client) {
|
|
|
508
508
|
form.append('label', args.label ?? fileName);
|
|
509
509
|
form.append('fileName', fileName);
|
|
510
510
|
form.append('shareClass', args.shareClass ?? 'PRIVATE');
|
|
511
|
-
const meta =
|
|
511
|
+
const meta = parseLenient(UploadedFileSchema, await client.request('POST', '/pub/v3/myfiles/multipart', form), { label: 'ofw-mcp', context: 'POST /pub/v3/myfiles/multipart (ofw_upload_attachment)', mode: 'strict' });
|
|
512
512
|
// Cache metadata so subsequent ofw_get_message calls can surface it and
|
|
513
513
|
// ofw_download_attachment can short-circuit. messageId is 0 (the
|
|
514
514
|
// not-yet-linked sentinel) until a message actually references this file.
|
|
@@ -583,14 +583,19 @@ export function registerMessageTools(server, client) {
|
|
|
583
583
|
} }] };
|
|
584
584
|
}
|
|
585
585
|
let dest;
|
|
586
|
+
// The filename comes from OFW file metadata — i.e. it is controlled by the
|
|
587
|
+
// co-parent who uploaded the attachment. basename() it before interpolating
|
|
588
|
+
// into a path so a crafted `../…` name can't escape the target directory
|
|
589
|
+
// (the upload path at :549 already applies basename to its input).
|
|
590
|
+
const safeName = basename(cached.fileName);
|
|
586
591
|
if (args.saveTo) {
|
|
587
592
|
// Treat saveTo as a directory if it ends with a separator; otherwise as a full path.
|
|
588
593
|
const isDirArg = args.saveTo.endsWith('/') || args.saveTo.endsWith('\\');
|
|
589
594
|
const abs = expandPath(args.saveTo);
|
|
590
|
-
dest = isDirArg ? join(abs, `${fileId}-${
|
|
595
|
+
dest = isDirArg ? join(abs, `${fileId}-${safeName}`) : abs;
|
|
591
596
|
}
|
|
592
597
|
else {
|
|
593
|
-
dest = join(getAttachmentsDir(), `${fileId}-${
|
|
598
|
+
dest = join(getAttachmentsDir(), `${fileId}-${safeName}`);
|
|
594
599
|
}
|
|
595
600
|
if (!args.force && cached.downloadedPath === dest) {
|
|
596
601
|
return jsonResponse({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"mcpName": "io.github.chrischall/ofw-mcp",
|
|
6
6
|
"description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"test:watch": "vitest"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@chrischall/mcp-utils": "^0.
|
|
35
|
+
"@chrischall/mcp-utils": "^0.12.0",
|
|
36
36
|
"@fetchproxy/bootstrap": "^1.3.0",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38
38
|
"dotenv": "^17.4.2",
|
|
39
39
|
"zod": "^4.4.3"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/node": "^
|
|
42
|
+
"@types/node": "^26.0.0",
|
|
43
43
|
"@vitest/coverage-v8": "^4.1.7",
|
|
44
44
|
"esbuild": "^0.28.0",
|
|
45
45
|
"typescript": "^6.0.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.4.
|
|
9
|
+
"version": "2.4.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.4.
|
|
14
|
+
"version": "2.4.4",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/dist/validate.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validate an OFW API response against a zod schema at the call site.
|
|
3
|
-
*
|
|
4
|
-
* Every OFW endpoint is reverse-engineered and undocumented, so a backend
|
|
5
|
-
* change on their side would otherwise flow `undefined` silently into the
|
|
6
|
-
* SQLite cache and persist (issue #83). Schemas are `.looseObject(...)`
|
|
7
|
-
* covering ONLY the fields the code actually reads — cosmetic API additions
|
|
8
|
-
* pass through untouched (and stay present in the parsed output, which
|
|
9
|
-
* matters for `listData`/`metadata` blobs cached verbatim).
|
|
10
|
-
*
|
|
11
|
-
* Two modes, chosen per call site:
|
|
12
|
-
* - `'lenient'` (default) — read/sync paths. On mismatch, log a structured
|
|
13
|
-
* warning to stderr naming the endpoint and fields, then return the RAW
|
|
14
|
-
* response unchanged so the existing `??` fallbacks keep the tool useful.
|
|
15
|
-
* - `'strict'` — write paths (send/save_draft verification, upload). On
|
|
16
|
-
* mismatch, throw: proceeding on an unverifiable response risks deleting
|
|
17
|
-
* a draft, mis-reporting a send, or caching an unusable fileId.
|
|
18
|
-
*
|
|
19
|
-
* The error/warning text is deliberately precise ("date.dateTime: expected
|
|
20
|
-
* string…") — it's the failure signal a maintainer (human or Claude) fixes
|
|
21
|
-
* in one session, vs. "some messages show the wrong date sometimes".
|
|
22
|
-
*/
|
|
23
|
-
export function parseOFW(schema, raw, ctx, mode = 'lenient') {
|
|
24
|
-
const result = schema.safeParse(raw);
|
|
25
|
-
if (result.success)
|
|
26
|
-
return result.data;
|
|
27
|
-
const issues = result.error.issues
|
|
28
|
-
.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
29
|
-
.join('; ');
|
|
30
|
-
const message = `OFW response for ${ctx} failed validation: ${issues}`;
|
|
31
|
-
if (mode === 'strict')
|
|
32
|
-
throw new Error(message);
|
|
33
|
-
console.error(`[ofw-mcp] WARNING: ${message} — continuing with the raw response; fields derived from it may be missing or wrong.`);
|
|
34
|
-
return raw;
|
|
35
|
-
}
|