ofw-mcp 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/auth.js +34 -4
- package/dist/bundle.js +210 -7
- package/dist/index.js +3 -1
- package/dist/tools/healthcheck.js +81 -0
- package/package.json +2 -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.
|
|
9
|
+
"version": "2.14.0"
|
|
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.
|
|
17
|
+
"version": "2.14.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/dist/auth.js
CHANGED
|
@@ -69,6 +69,38 @@ function fetchproxyDisabled() {
|
|
|
69
69
|
* return value as opaque credentials — they should not branch on `source`.
|
|
70
70
|
* The field exists for logging / future cache-keying only.
|
|
71
71
|
*/
|
|
72
|
+
/**
|
|
73
|
+
* The message raised when NEITHER auth path is configured.
|
|
74
|
+
*
|
|
75
|
+
* Exported with {@link isNoAuthConfigured} because a second reader needs to
|
|
76
|
+
* tell this case apart from every other auth failure: "nothing is set up" and
|
|
77
|
+
* "the password was rejected" or "the bridge is down" need opposite advice, and
|
|
78
|
+
* `ofw_healthcheck` gives the wrong one if it confuses them. A prefix match on
|
|
79
|
+
* a copy of this string in the other module would pass its own test while
|
|
80
|
+
* silently stopping matching the day this wording changed.
|
|
81
|
+
*/
|
|
82
|
+
export const NO_AUTH_CONFIGURED = 'OFW auth: set OFW_USERNAME + OFW_PASSWORD, ' +
|
|
83
|
+
'or install the fetchproxy extension and sign into ourfamilywizard.com ' +
|
|
84
|
+
'(unset OFW_DISABLE_FETCHPROXY if it is set).';
|
|
85
|
+
/**
|
|
86
|
+
* Prefix of the message raised when the fetchproxy bridge is unreachable.
|
|
87
|
+
*
|
|
88
|
+
* A PREFIX rather than a whole constant because the upstream `.hint` — the
|
|
89
|
+
* actionable "click the toolbar icon" copy — is appended per failure. Exported
|
|
90
|
+
* with {@link isBridgeDown} for the reason {@link NO_AUTH_CONFIGURED} is: a
|
|
91
|
+
* reader that matched a copy of this text would keep its own test green on the
|
|
92
|
+
* day the wording changed, while silently misreporting a downed bridge as an
|
|
93
|
+
* unconfigured server.
|
|
94
|
+
*/
|
|
95
|
+
export const BRIDGE_DOWN_PREFIX = 'OFW auth: fetchproxy bridge is down';
|
|
96
|
+
/** True for the {@link BRIDGE_DOWN_PREFIX} failure and nothing else. */
|
|
97
|
+
export function isBridgeDown(e) {
|
|
98
|
+
return e instanceof Error && e.message.startsWith(BRIDGE_DOWN_PREFIX);
|
|
99
|
+
}
|
|
100
|
+
/** True for the {@link NO_AUTH_CONFIGURED} failure and nothing else. */
|
|
101
|
+
export function isNoAuthConfigured(e) {
|
|
102
|
+
return e instanceof Error && e.message === NO_AUTH_CONFIGURED;
|
|
103
|
+
}
|
|
72
104
|
export async function resolveAuth() {
|
|
73
105
|
// Which paths are CONFIGURED. `resolveAuthPattern` runs the first one
|
|
74
106
|
// provided, in the fleet's fixed priority order (token → oauth →
|
|
@@ -125,7 +157,7 @@ export async function resolveAuth() {
|
|
|
125
157
|
// FetchproxyBridgeDownError only escapes bootstrap() after the lazy-revive retry fails — surface .hint verbatim (actionable "click toolbar icon" copy).
|
|
126
158
|
if (classifyBridgeError(e) === 'bridge_down') {
|
|
127
159
|
const downErr = e;
|
|
128
|
-
throw new Error(
|
|
160
|
+
throw new Error(`${BRIDGE_DOWN_PREFIX} (extension service worker unreachable after retry). ${downErr.hint}`);
|
|
129
161
|
}
|
|
130
162
|
const msg = e instanceof Error ? e.message : String(e);
|
|
131
163
|
throw new Error(`OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`);
|
|
@@ -137,9 +169,7 @@ export async function resolveAuth() {
|
|
|
137
169
|
// because this one names OFW's own two fixes side-by-side and the generic
|
|
138
170
|
// one cannot.
|
|
139
171
|
if (!pattern.sessionScrape && !pattern.fetchproxy) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
'or install the fetchproxy extension and sign into ourfamilywizard.com ' +
|
|
142
|
-
'(unset OFW_DISABLE_FETCHPROXY if it is set).');
|
|
172
|
+
throw new Error(NO_AUTH_CONFIGURED);
|
|
143
173
|
}
|
|
144
174
|
// Errors from the winning path propagate UNWRAPPED, which is what keeps the
|
|
145
175
|
// bridge-down `.hint` above intact.
|
package/dist/bundle.js
CHANGED
|
@@ -34678,6 +34678,7 @@ var StdioServerTransport = class {
|
|
|
34678
34678
|
};
|
|
34679
34679
|
|
|
34680
34680
|
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
34681
|
+
var DEFAULT_ERROR_MESSAGE_MAX = 500;
|
|
34681
34682
|
var McpToolError = class extends Error {
|
|
34682
34683
|
/** Actionable remediation text, when one applies. */
|
|
34683
34684
|
hint;
|
|
@@ -34719,6 +34720,18 @@ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*('
|
|
|
34719
34720
|
function redactSecrets(text) {
|
|
34720
34721
|
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]");
|
|
34721
34722
|
}
|
|
34723
|
+
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
34724
|
+
const str = text === null || text === void 0 ? "" : String(text);
|
|
34725
|
+
const redacted = redactSecrets(str);
|
|
34726
|
+
if (redacted.length <= max)
|
|
34727
|
+
return redacted;
|
|
34728
|
+
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
34729
|
+
}
|
|
34730
|
+
function messageOf(err) {
|
|
34731
|
+
if (err instanceof Error)
|
|
34732
|
+
return err.message;
|
|
34733
|
+
return String(err);
|
|
34734
|
+
}
|
|
34722
34735
|
|
|
34723
34736
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34724
34737
|
function textResult(data) {
|
|
@@ -39377,6 +39390,144 @@ var BootstrapDisabledError = class extends Error {
|
|
|
39377
39390
|
}
|
|
39378
39391
|
};
|
|
39379
39392
|
|
|
39393
|
+
// node_modules/@chrischall/mcp-utils/dist/healthcheck/index.js
|
|
39394
|
+
function statusOf(err) {
|
|
39395
|
+
if (typeof err !== "object" || err === null)
|
|
39396
|
+
return void 0;
|
|
39397
|
+
const s = err.status ?? err.statusCode;
|
|
39398
|
+
return typeof s === "number" ? s : void 0;
|
|
39399
|
+
}
|
|
39400
|
+
var CREDENTIAL_ARMS = /* @__PURE__ */ new Set([
|
|
39401
|
+
"ok",
|
|
39402
|
+
"no_credential",
|
|
39403
|
+
"credential_rejected",
|
|
39404
|
+
"timeout",
|
|
39405
|
+
"http",
|
|
39406
|
+
"transport",
|
|
39407
|
+
"unknown"
|
|
39408
|
+
]);
|
|
39409
|
+
function isArm(kind) {
|
|
39410
|
+
return kind !== void 0 && CREDENTIAL_ARMS.has(kind);
|
|
39411
|
+
}
|
|
39412
|
+
function credentialHint(arm, prefix, hostLabel, source) {
|
|
39413
|
+
switch (arm) {
|
|
39414
|
+
case "ok":
|
|
39415
|
+
return `Credential from '${source}' works: ${hostLabel} accepted an authenticated request. If a real tool still fails, the problem is that tool, not auth.`;
|
|
39416
|
+
case "no_credential":
|
|
39417
|
+
return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
|
|
39418
|
+
case "credential_rejected":
|
|
39419
|
+
return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
|
|
39420
|
+
case "timeout":
|
|
39421
|
+
return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
|
|
39422
|
+
case "http":
|
|
39423
|
+
return `${hostLabel} answered with an error status that is not an auth rejection. That is USUALLY a ${hostLabel}-side problem rather than an auth one \u2014 but a 404 here more often means the probe path is wrong than that ${hostLabel} is broken, so check error.message and probe.url before concluding anything about the credential.`;
|
|
39424
|
+
case "transport":
|
|
39425
|
+
return `Could not reach ${hostLabel} at all. Check network egress; the credential itself was never judged.`;
|
|
39426
|
+
default:
|
|
39427
|
+
return `Unexpected failure \u2014 see error.message.`;
|
|
39428
|
+
}
|
|
39429
|
+
}
|
|
39430
|
+
function registerCredentialHealthcheckTool(args) {
|
|
39431
|
+
const { server, prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
|
|
39432
|
+
const probeUrl = probePath ? `https://${hostLabel}${probePath}` : void 0;
|
|
39433
|
+
server.registerTool(`${prefix}_healthcheck`, {
|
|
39434
|
+
title: "Verify credentials and upstream reachability",
|
|
39435
|
+
description: `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Call this when a real tool fails and you want to know which hop broke. Read-only; never returns the credential itself.`,
|
|
39436
|
+
annotations: {
|
|
39437
|
+
title: "Verify credentials and upstream reachability",
|
|
39438
|
+
readOnlyHint: true,
|
|
39439
|
+
idempotentHint: true,
|
|
39440
|
+
openWorldHint: true
|
|
39441
|
+
},
|
|
39442
|
+
inputSchema: {}
|
|
39443
|
+
}, async () => {
|
|
39444
|
+
let probeStarted = 0;
|
|
39445
|
+
let state;
|
|
39446
|
+
try {
|
|
39447
|
+
state = await resolveCredential();
|
|
39448
|
+
} catch (e) {
|
|
39449
|
+
const classified = classifyThrown?.(e);
|
|
39450
|
+
const result2 = {
|
|
39451
|
+
ok: false,
|
|
39452
|
+
// Still false, and still no source: a classification explains WHY
|
|
39453
|
+
// nothing resolved, it does not invent a credential that did.
|
|
39454
|
+
credential: { source: null, resolved: false },
|
|
39455
|
+
// No `url`: nothing was probed, and naming one implies it was tried.
|
|
39456
|
+
probe: { elapsed_ms: 0 },
|
|
39457
|
+
error: {
|
|
39458
|
+
kind: classified?.kind ?? "no_credential",
|
|
39459
|
+
message: truncateErrorMessage(messageOf(e)),
|
|
39460
|
+
...classified?.detail !== void 0 ? { detail: classified.detail } : {}
|
|
39461
|
+
},
|
|
39462
|
+
// The hint must follow the KIND beside it. Falling back to
|
|
39463
|
+
// `no_credential`'s copy under a classified kind would state a cause
|
|
39464
|
+
// the kind contradicts — the same disagreement this path exists to
|
|
39465
|
+
// remove. So: an inline hint wins; else the classified arm's own
|
|
39466
|
+
// copy (consumer override first); else, for a kind this module has
|
|
39467
|
+
// no copy for, the neutral `unknown` text rather than one that
|
|
39468
|
+
// asserts a cause; else the unclassified `no_credential` default.
|
|
39469
|
+
hint: classified?.hint ?? (isArm(classified?.kind) ? hints?.[classified.kind] ?? credentialHint(classified.kind, prefix, hostLabel, null) : classified !== void 0 ? credentialHint("unknown", prefix, hostLabel, null) : hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null))
|
|
39470
|
+
};
|
|
39471
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
39472
|
+
}
|
|
39473
|
+
const credential = {
|
|
39474
|
+
source: state.source,
|
|
39475
|
+
resolved: state.source !== null,
|
|
39476
|
+
...state.detail !== void 0 ? { detail: state.detail } : {}
|
|
39477
|
+
};
|
|
39478
|
+
if (!credential.resolved) {
|
|
39479
|
+
const result2 = {
|
|
39480
|
+
ok: false,
|
|
39481
|
+
credential,
|
|
39482
|
+
probe: { elapsed_ms: 0 },
|
|
39483
|
+
error: { kind: "no_credential", message: "no credential source resolved" },
|
|
39484
|
+
hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
|
|
39485
|
+
};
|
|
39486
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
39487
|
+
}
|
|
39488
|
+
let arm = "ok";
|
|
39489
|
+
let error51;
|
|
39490
|
+
let status;
|
|
39491
|
+
let customHint;
|
|
39492
|
+
probeStarted = Date.now();
|
|
39493
|
+
try {
|
|
39494
|
+
await probeFn();
|
|
39495
|
+
} catch (e) {
|
|
39496
|
+
status = statusOf(e);
|
|
39497
|
+
const aborted2 = e instanceof Error && e.name === "AbortError";
|
|
39498
|
+
arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
|
|
39499
|
+
let kind = arm;
|
|
39500
|
+
let detail;
|
|
39501
|
+
const custom2 = classifyThrown?.(e);
|
|
39502
|
+
if (custom2) {
|
|
39503
|
+
kind = custom2.kind;
|
|
39504
|
+
customHint = custom2.hint;
|
|
39505
|
+
detail = custom2.detail;
|
|
39506
|
+
}
|
|
39507
|
+
error51 = {
|
|
39508
|
+
kind,
|
|
39509
|
+
// Redacted AND bounded before it reaches the result: an upstream
|
|
39510
|
+
// failure routinely quotes what it was sent, and a healthcheck is
|
|
39511
|
+
// the tool people paste into a chat when something is broken.
|
|
39512
|
+
message: truncateErrorMessage(messageOf(e)),
|
|
39513
|
+
...detail !== void 0 ? { detail } : {}
|
|
39514
|
+
};
|
|
39515
|
+
}
|
|
39516
|
+
const result = {
|
|
39517
|
+
ok: error51 === void 0,
|
|
39518
|
+
credential,
|
|
39519
|
+
probe: {
|
|
39520
|
+
...probeUrl ? { url: probeUrl } : {},
|
|
39521
|
+
elapsed_ms: Date.now() - probeStarted,
|
|
39522
|
+
...status !== void 0 ? { status } : {}
|
|
39523
|
+
},
|
|
39524
|
+
...error51 ? { error: error51 } : {},
|
|
39525
|
+
hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
|
|
39526
|
+
};
|
|
39527
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
39528
|
+
});
|
|
39529
|
+
}
|
|
39530
|
+
|
|
39380
39531
|
// src/protocol.ts
|
|
39381
39532
|
var BASE_URL = "https://ofw.ourfamilywizard.com";
|
|
39382
39533
|
var OFW_PROTOCOL_HEADERS = {
|
|
@@ -39431,7 +39582,7 @@ async function loginWithPassword(username, password) {
|
|
|
39431
39582
|
// package.json
|
|
39432
39583
|
var package_default = {
|
|
39433
39584
|
name: "ofw-mcp",
|
|
39434
|
-
version: "2.
|
|
39585
|
+
version: "2.14.0",
|
|
39435
39586
|
license: "MIT",
|
|
39436
39587
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
39437
39588
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -39465,7 +39616,7 @@ var package_default = {
|
|
|
39465
39616
|
typecheck: "tsc -p tsconfig.json --noEmit"
|
|
39466
39617
|
},
|
|
39467
39618
|
dependencies: {
|
|
39468
|
-
"@chrischall/mcp-utils": "^0.
|
|
39619
|
+
"@chrischall/mcp-utils": "^0.19.3",
|
|
39469
39620
|
"@fetchproxy/bootstrap": "^2.2.0",
|
|
39470
39621
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39471
39622
|
dotenv: "^17.4.2",
|
|
@@ -39484,6 +39635,14 @@ var package_default = {
|
|
|
39484
39635
|
function fetchproxyDisabled() {
|
|
39485
39636
|
return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
|
|
39486
39637
|
}
|
|
39638
|
+
var NO_AUTH_CONFIGURED = "OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension and sign into ourfamilywizard.com (unset OFW_DISABLE_FETCHPROXY if it is set).";
|
|
39639
|
+
var BRIDGE_DOWN_PREFIX = "OFW auth: fetchproxy bridge is down";
|
|
39640
|
+
function isBridgeDown(e) {
|
|
39641
|
+
return e instanceof Error && e.message.startsWith(BRIDGE_DOWN_PREFIX);
|
|
39642
|
+
}
|
|
39643
|
+
function isNoAuthConfigured(e) {
|
|
39644
|
+
return e instanceof Error && e.message === NO_AUTH_CONFIGURED;
|
|
39645
|
+
}
|
|
39487
39646
|
async function resolveAuth() {
|
|
39488
39647
|
const pattern = {};
|
|
39489
39648
|
const username = readEnvVar("OFW_USERNAME");
|
|
@@ -39531,7 +39690,7 @@ async function resolveAuth() {
|
|
|
39531
39690
|
if (classifyBridgeError(e) === "bridge_down") {
|
|
39532
39691
|
const downErr = e;
|
|
39533
39692
|
throw new Error(
|
|
39534
|
-
|
|
39693
|
+
`${BRIDGE_DOWN_PREFIX} (extension service worker unreachable after retry). ${downErr.hint}`
|
|
39535
39694
|
);
|
|
39536
39695
|
}
|
|
39537
39696
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -39542,9 +39701,7 @@ async function resolveAuth() {
|
|
|
39542
39701
|
};
|
|
39543
39702
|
}
|
|
39544
39703
|
if (!pattern.sessionScrape && !pattern.fetchproxy) {
|
|
39545
|
-
throw new Error(
|
|
39546
|
-
"OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension and sign into ourfamilywizard.com (unset OFW_DISABLE_FETCHPROXY if it is set)."
|
|
39547
|
-
);
|
|
39704
|
+
throw new Error(NO_AUTH_CONFIGURED);
|
|
39548
39705
|
}
|
|
39549
39706
|
const { credential, source, expiresAt } = await resolveAuthPattern(pattern);
|
|
39550
39707
|
return {
|
|
@@ -40068,6 +40225,51 @@ function registerUserTools(server, client2) {
|
|
|
40068
40225
|
});
|
|
40069
40226
|
}
|
|
40070
40227
|
|
|
40228
|
+
// src/tools/healthcheck.ts
|
|
40229
|
+
function registerHealthcheckTools(server, client2, resolve3 = resolveAuth) {
|
|
40230
|
+
registerCredentialHealthcheckTool({
|
|
40231
|
+
server,
|
|
40232
|
+
prefix: "ofw",
|
|
40233
|
+
hostLabel: "ourfamilywizard.com",
|
|
40234
|
+
// The same read `ofw_get_profile` makes: authenticated, cheap, and it
|
|
40235
|
+
// changes nothing. A healthcheck that marked a message read would be
|
|
40236
|
+
// co-parent-visible and irreversible.
|
|
40237
|
+
probePath: "/pub/v2/profiles",
|
|
40238
|
+
resolveCredential: async () => {
|
|
40239
|
+
try {
|
|
40240
|
+
const auth = await resolve3();
|
|
40241
|
+
return {
|
|
40242
|
+
source: auth.source,
|
|
40243
|
+
// Never the token. Expiry is the fact that explains a connector
|
|
40244
|
+
// that worked an hour ago and does not now.
|
|
40245
|
+
detail: auth.expiresAt ? { expires_at: auth.expiresAt.toISOString() } : void 0
|
|
40246
|
+
};
|
|
40247
|
+
} catch (e) {
|
|
40248
|
+
if (isNoAuthConfigured(e)) return { source: null };
|
|
40249
|
+
throw e;
|
|
40250
|
+
}
|
|
40251
|
+
},
|
|
40252
|
+
probeFn: () => client2.request("GET", "/pub/v2/profiles"),
|
|
40253
|
+
// A downed bridge is not a missing credential, and since mcp-utils 0.19.3
|
|
40254
|
+
// the helper consults this for a `resolveCredential` failure too — so it
|
|
40255
|
+
// gets its own arm instead of the `no_credential` copy. That copy could
|
|
40256
|
+
// previously only hedge across both cases and point at `error.message`;
|
|
40257
|
+
// now each answer names one cause and one fix.
|
|
40258
|
+
classifyThrown: (err) => isBridgeDown(err) ? {
|
|
40259
|
+
kind: "transport",
|
|
40260
|
+
// The upstream `.hint` rides along in `error.message` — it carries
|
|
40261
|
+
// the actionable "click the toolbar icon" copy this cannot know.
|
|
40262
|
+
hint: "The fetchproxy bridge is down, so the browser path could not be tried. This is not a credential problem: OFW_USERNAME/OFW_PASSWORD, if set, were not reached either. See error.message for the extension-specific fix."
|
|
40263
|
+
} : void 0,
|
|
40264
|
+
hints: {
|
|
40265
|
+
// Now means exactly what it says: nothing is set up. A configured path
|
|
40266
|
+
// that was tried and failed no longer lands here.
|
|
40267
|
+
no_credential: "No OFW credential is configured. Either set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension and sign in to ourfamilywizard.com in a tab (unsetting OFW_DISABLE_FETCHPROXY if you set it).",
|
|
40268
|
+
credential_rejected: "OurFamilyWizard rejected the credential. If it came from `env`, the password changed or the account is locked; if from `fetchproxy`, the browser session expired \u2014 sign in again in the tab. Retrying will not fix either."
|
|
40269
|
+
}
|
|
40270
|
+
});
|
|
40271
|
+
}
|
|
40272
|
+
|
|
40071
40273
|
// src/sync.ts
|
|
40072
40274
|
var FileMetaSchema = external_exports.looseObject({
|
|
40073
40275
|
fileId: external_exports.number(),
|
|
@@ -44244,10 +44446,11 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
|
|
|
44244
44446
|
var nodeAttachmentIO = new NodeAttachmentIO();
|
|
44245
44447
|
await runMcp({
|
|
44246
44448
|
name: "ofw",
|
|
44247
|
-
version: "2.
|
|
44449
|
+
version: "2.14.0",
|
|
44248
44450
|
// x-release-please-version
|
|
44249
44451
|
deps: client,
|
|
44250
44452
|
tools: [
|
|
44453
|
+
registerHealthcheckTools,
|
|
44251
44454
|
registerUserTools,
|
|
44252
44455
|
(server, deps) => registerMessageTools(server, deps, nodeCacheProvider, nodeAttachmentIO),
|
|
44253
44456
|
registerCalendarTools,
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ process.emit = function (event, ...args) {
|
|
|
12
12
|
import { runMcp } from '@chrischall/mcp-utils';
|
|
13
13
|
import { client } from './client.js';
|
|
14
14
|
import { registerUserTools } from './tools/user.js';
|
|
15
|
+
import { registerHealthcheckTools } from './tools/healthcheck.js';
|
|
15
16
|
import { registerMessageTools } from './tools/messages.js';
|
|
16
17
|
import { registerCalendarTools } from './tools/calendar.js';
|
|
17
18
|
import { registerExpenseTools } from './tools/expenses.js';
|
|
@@ -35,9 +36,10 @@ const nodeAttachmentIO = new NodeAttachmentIO();
|
|
|
35
36
|
// always succeeds before any credential check runs.
|
|
36
37
|
await runMcp({
|
|
37
38
|
name: 'ofw',
|
|
38
|
-
version: '2.
|
|
39
|
+
version: '2.14.0', // x-release-please-version
|
|
39
40
|
deps: client,
|
|
40
41
|
tools: [
|
|
42
|
+
registerHealthcheckTools,
|
|
41
43
|
registerUserTools,
|
|
42
44
|
(server, deps) => registerMessageTools(server, deps, nodeCacheProvider, nodeAttachmentIO),
|
|
43
45
|
registerCalendarTools,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { registerCredentialHealthcheckTool } from '@chrischall/mcp-utils/healthcheck';
|
|
2
|
+
import { resolveAuth, isNoAuthConfigured, isBridgeDown } from '../auth.js';
|
|
3
|
+
/**
|
|
4
|
+
* `ofw_healthcheck` — the one call that answers "is this connector working?".
|
|
5
|
+
*
|
|
6
|
+
* OFW had no such tool. `ofw_status` looks like one and is not: it is a
|
|
7
|
+
* heavyweight draft-inventory call, `readOnlyHint: false`, that answers "where
|
|
8
|
+
* do my drafts stand?". Asking it whether auth works spends a drafts sync and
|
|
9
|
+
* still cannot separate "no credential" from "OFW rejected it".
|
|
10
|
+
*
|
|
11
|
+
* The distinction matters most for the two-path auth here: the token comes
|
|
12
|
+
* from either OFW_USERNAME/OFW_PASSWORD or a signed-in browser tab via
|
|
13
|
+
* fetchproxy, and "which of those actually supplied it" is the first thing
|
|
14
|
+
* anyone needs when the connector misbehaves. That is why `source` is
|
|
15
|
+
* reported.
|
|
16
|
+
*/
|
|
17
|
+
export function registerHealthcheckTools(server, client,
|
|
18
|
+
/** Seam: the auth resolver, injectable so tests need no network. */
|
|
19
|
+
resolve = resolveAuth) {
|
|
20
|
+
registerCredentialHealthcheckTool({
|
|
21
|
+
server,
|
|
22
|
+
prefix: 'ofw',
|
|
23
|
+
hostLabel: 'ourfamilywizard.com',
|
|
24
|
+
// The same read `ofw_get_profile` makes: authenticated, cheap, and it
|
|
25
|
+
// changes nothing. A healthcheck that marked a message read would be
|
|
26
|
+
// co-parent-visible and irreversible.
|
|
27
|
+
probePath: '/pub/v2/profiles',
|
|
28
|
+
resolveCredential: async () => {
|
|
29
|
+
try {
|
|
30
|
+
const auth = await resolve();
|
|
31
|
+
return {
|
|
32
|
+
source: auth.source,
|
|
33
|
+
// Never the token. Expiry is the fact that explains a connector
|
|
34
|
+
// that worked an hour ago and does not now.
|
|
35
|
+
detail: auth.expiresAt ? { expires_at: auth.expiresAt.toISOString() } : undefined,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
// "Nothing is configured" is a CREDENTIAL state, not a failure to
|
|
40
|
+
// check — it earns the `no_credential` arm and its advice. Every
|
|
41
|
+
// other error (a rejected password, a bridge that is down) is a real
|
|
42
|
+
// failure and must keep its own message rather than being flattened
|
|
43
|
+
// into "no credential", which would send someone to set variables
|
|
44
|
+
// that are already set.
|
|
45
|
+
// `isNoAuthConfigured` rather than a prefix match on a copy of the
|
|
46
|
+
// message: the copy would pass this module's own test while silently
|
|
47
|
+
// stopping matching the day auth.ts reworded it, and the failure mode
|
|
48
|
+
// is giving a rejected password the advice meant for a blank setup.
|
|
49
|
+
if (isNoAuthConfigured(e))
|
|
50
|
+
return { source: null };
|
|
51
|
+
throw e;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
probeFn: () => client.request('GET', '/pub/v2/profiles'),
|
|
55
|
+
// A downed bridge is not a missing credential, and since mcp-utils 0.19.3
|
|
56
|
+
// the helper consults this for a `resolveCredential` failure too — so it
|
|
57
|
+
// gets its own arm instead of the `no_credential` copy. That copy could
|
|
58
|
+
// previously only hedge across both cases and point at `error.message`;
|
|
59
|
+
// now each answer names one cause and one fix.
|
|
60
|
+
classifyThrown: (err) => isBridgeDown(err)
|
|
61
|
+
? {
|
|
62
|
+
kind: 'transport',
|
|
63
|
+
// The upstream `.hint` rides along in `error.message` — it carries
|
|
64
|
+
// the actionable "click the toolbar icon" copy this cannot know.
|
|
65
|
+
hint: 'The fetchproxy bridge is down, so the browser path could not be tried. This is ' +
|
|
66
|
+
'not a credential problem: OFW_USERNAME/OFW_PASSWORD, if set, were not reached ' +
|
|
67
|
+
'either. See error.message for the extension-specific fix.',
|
|
68
|
+
}
|
|
69
|
+
: undefined,
|
|
70
|
+
hints: {
|
|
71
|
+
// Now means exactly what it says: nothing is set up. A configured path
|
|
72
|
+
// that was tried and failed no longer lands here.
|
|
73
|
+
no_credential: 'No OFW credential is configured. Either set OFW_USERNAME + OFW_PASSWORD, or install ' +
|
|
74
|
+
'the fetchproxy extension and sign in to ourfamilywizard.com in a tab (unsetting ' +
|
|
75
|
+
'OFW_DISABLE_FETCHPROXY if you set it).',
|
|
76
|
+
credential_rejected: 'OurFamilyWizard rejected the credential. If it came from `env`, the password changed or ' +
|
|
77
|
+
'the account is locked; if from `fetchproxy`, the browser session expired — sign in again ' +
|
|
78
|
+
'in the tab. Retrying will not fix either.',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
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)",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@chrischall/mcp-utils": "^0.
|
|
37
|
+
"@chrischall/mcp-utils": "^0.19.3",
|
|
38
38
|
"@fetchproxy/bootstrap": "^2.2.0",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
40
|
"dotenv": "^17.4.2",
|
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.
|
|
9
|
+
"version": "2.14.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.14.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|