ofw-mcp 2.12.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 +231 -17
- package/dist/index.js +3 -1
- package/dist/tools/healthcheck.js +81 -0
- package/package.json +3 -3
- 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
|
@@ -3314,8 +3314,8 @@ var require_utils = __commonJS({
|
|
|
3314
3314
|
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3315
3315
|
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3316
3316
|
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3317
|
-
function reescapeHostDelimiters(host,
|
|
3318
|
-
const re =
|
|
3317
|
+
function reescapeHostDelimiters(host, isIP2) {
|
|
3318
|
+
const re = isIP2 ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3319
3319
|
re.lastIndex = 0;
|
|
3320
3320
|
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3321
3321
|
}
|
|
@@ -3804,7 +3804,7 @@ var require_fast_uri = __commonJS({
|
|
|
3804
3804
|
fragment: void 0
|
|
3805
3805
|
};
|
|
3806
3806
|
let malformedAuthorityOrPort = false;
|
|
3807
|
-
let
|
|
3807
|
+
let isIP2 = false;
|
|
3808
3808
|
if (options.reference === "suffix") {
|
|
3809
3809
|
if (options.scheme) {
|
|
3810
3810
|
uri = options.scheme + ":" + uri;
|
|
@@ -3853,9 +3853,9 @@ var require_fast_uri = __commonJS({
|
|
|
3853
3853
|
if (ipv4result === false) {
|
|
3854
3854
|
const ipv6result = normalizeIPv6(parsed.host);
|
|
3855
3855
|
parsed.host = ipv6result.host.toLowerCase();
|
|
3856
|
-
|
|
3856
|
+
isIP2 = ipv6result.isIPV6;
|
|
3857
3857
|
} else {
|
|
3858
|
-
|
|
3858
|
+
isIP2 = true;
|
|
3859
3859
|
}
|
|
3860
3860
|
}
|
|
3861
3861
|
if (parsed.scheme === void 0 && parsed.userinfo === void 0 && parsed.host === void 0 && parsed.port === void 0 && parsed.query === void 0 && !parsed.path) {
|
|
@@ -3872,7 +3872,7 @@ var require_fast_uri = __commonJS({
|
|
|
3872
3872
|
}
|
|
3873
3873
|
const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
|
|
3874
3874
|
if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
|
|
3875
|
-
if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) &&
|
|
3875
|
+
if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP2 === false && nonSimpleDomain(parsed.host)) {
|
|
3876
3876
|
try {
|
|
3877
3877
|
parsed.host = new URL("http://" + parsed.host).hostname;
|
|
3878
3878
|
} catch (e) {
|
|
@@ -3886,7 +3886,7 @@ var require_fast_uri = __commonJS({
|
|
|
3886
3886
|
parsed.scheme = unescape(parsed.scheme);
|
|
3887
3887
|
}
|
|
3888
3888
|
if (parsed.host !== void 0) {
|
|
3889
|
-
parsed.host = reescapeHostDelimiters(unescape(parsed.host),
|
|
3889
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP2);
|
|
3890
3890
|
}
|
|
3891
3891
|
}
|
|
3892
3892
|
if (parsed.path) {
|
|
@@ -7199,7 +7199,7 @@ var require_permessage_deflate = __commonJS({
|
|
|
7199
7199
|
acceptAsServer(offers) {
|
|
7200
7200
|
const opts = this._options;
|
|
7201
7201
|
const accepted = offers.find((params) => {
|
|
7202
|
-
if (opts.serverNoContextTakeover === false && params.server_no_context_takeover || params.server_max_window_bits && (opts.serverMaxWindowBits === false || typeof opts.serverMaxWindowBits === "number" && opts.serverMaxWindowBits > params.server_max_window_bits) || typeof opts.clientMaxWindowBits === "number" && !params.client_max_window_bits) {
|
|
7202
|
+
if (opts.serverNoContextTakeover === false && params.server_no_context_takeover || params.server_max_window_bits && (opts.serverMaxWindowBits === false || typeof opts.serverMaxWindowBits === "number" && opts.serverMaxWindowBits > params.server_max_window_bits) || typeof opts.clientMaxWindowBits === "number" && (typeof params.client_max_window_bits === "number" ? opts.clientMaxWindowBits > params.client_max_window_bits : !params.client_max_window_bits)) {
|
|
7203
7203
|
return false;
|
|
7204
7204
|
}
|
|
7205
7205
|
return true;
|
|
@@ -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) {
|
|
@@ -37331,6 +37344,7 @@ function classifyBridgeError(err) {
|
|
|
37331
37344
|
}
|
|
37332
37345
|
|
|
37333
37346
|
// node_modules/@fetchproxy/server/dist/ws-server.js
|
|
37347
|
+
import { isIP } from "node:net";
|
|
37334
37348
|
function envWsPort() {
|
|
37335
37349
|
const raw = process.env.FETCHPROXY_WS_PORT;
|
|
37336
37350
|
if (raw === void 0 || raw.trim() === "")
|
|
@@ -37342,6 +37356,15 @@ function envWsPort() {
|
|
|
37342
37356
|
return void 0;
|
|
37343
37357
|
return port;
|
|
37344
37358
|
}
|
|
37359
|
+
function envWsHost() {
|
|
37360
|
+
const raw = process.env.FETCHPROXY_WS_HOST;
|
|
37361
|
+
if (raw === void 0)
|
|
37362
|
+
return void 0;
|
|
37363
|
+
const host = raw.trim();
|
|
37364
|
+
if (host === "" || isIP(host) === 0)
|
|
37365
|
+
return void 0;
|
|
37366
|
+
return host;
|
|
37367
|
+
}
|
|
37345
37368
|
var FetchproxyProtocolError = class extends Error {
|
|
37346
37369
|
constructor(message) {
|
|
37347
37370
|
super(message);
|
|
@@ -37590,7 +37613,7 @@ var FetchproxyServer = class {
|
|
|
37590
37613
|
}
|
|
37591
37614
|
this.opts = {
|
|
37592
37615
|
port: opts.port ?? envWsPort() ?? 37149,
|
|
37593
|
-
host: opts.host ?? "127.0.0.1",
|
|
37616
|
+
host: opts.host ?? envWsHost() ?? "127.0.0.1",
|
|
37594
37617
|
serverName: opts.serverName,
|
|
37595
37618
|
version: opts.version,
|
|
37596
37619
|
domains: [...opts.domains],
|
|
@@ -37876,6 +37899,7 @@ var FetchproxyServer = class {
|
|
|
37876
37899
|
return {
|
|
37877
37900
|
role: this.role,
|
|
37878
37901
|
port: this.opts.port,
|
|
37902
|
+
host: this.opts.host,
|
|
37879
37903
|
serverVersion: this.opts.version,
|
|
37880
37904
|
fetchTimeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
37881
37905
|
bridgeReviveDelayMs: this.opts.bridgeReviveDelayMs ?? 0,
|
|
@@ -39366,6 +39390,144 @@ var BootstrapDisabledError = class extends Error {
|
|
|
39366
39390
|
}
|
|
39367
39391
|
};
|
|
39368
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
|
+
|
|
39369
39531
|
// src/protocol.ts
|
|
39370
39532
|
var BASE_URL = "https://ofw.ourfamilywizard.com";
|
|
39371
39533
|
var OFW_PROTOCOL_HEADERS = {
|
|
@@ -39420,7 +39582,7 @@ async function loginWithPassword(username, password) {
|
|
|
39420
39582
|
// package.json
|
|
39421
39583
|
var package_default = {
|
|
39422
39584
|
name: "ofw-mcp",
|
|
39423
|
-
version: "2.
|
|
39585
|
+
version: "2.14.0",
|
|
39424
39586
|
license: "MIT",
|
|
39425
39587
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
39426
39588
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -39454,8 +39616,8 @@ var package_default = {
|
|
|
39454
39616
|
typecheck: "tsc -p tsconfig.json --noEmit"
|
|
39455
39617
|
},
|
|
39456
39618
|
dependencies: {
|
|
39457
|
-
"@chrischall/mcp-utils": "^0.
|
|
39458
|
-
"@fetchproxy/bootstrap": "^2.
|
|
39619
|
+
"@chrischall/mcp-utils": "^0.19.3",
|
|
39620
|
+
"@fetchproxy/bootstrap": "^2.2.0",
|
|
39459
39621
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39460
39622
|
dotenv: "^17.4.2",
|
|
39461
39623
|
zod: "^4.4.3"
|
|
@@ -39473,6 +39635,14 @@ var package_default = {
|
|
|
39473
39635
|
function fetchproxyDisabled() {
|
|
39474
39636
|
return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
|
|
39475
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
|
+
}
|
|
39476
39646
|
async function resolveAuth() {
|
|
39477
39647
|
const pattern = {};
|
|
39478
39648
|
const username = readEnvVar("OFW_USERNAME");
|
|
@@ -39520,7 +39690,7 @@ async function resolveAuth() {
|
|
|
39520
39690
|
if (classifyBridgeError(e) === "bridge_down") {
|
|
39521
39691
|
const downErr = e;
|
|
39522
39692
|
throw new Error(
|
|
39523
|
-
|
|
39693
|
+
`${BRIDGE_DOWN_PREFIX} (extension service worker unreachable after retry). ${downErr.hint}`
|
|
39524
39694
|
);
|
|
39525
39695
|
}
|
|
39526
39696
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -39531,9 +39701,7 @@ async function resolveAuth() {
|
|
|
39531
39701
|
};
|
|
39532
39702
|
}
|
|
39533
39703
|
if (!pattern.sessionScrape && !pattern.fetchproxy) {
|
|
39534
|
-
throw new Error(
|
|
39535
|
-
"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)."
|
|
39536
|
-
);
|
|
39704
|
+
throw new Error(NO_AUTH_CONFIGURED);
|
|
39537
39705
|
}
|
|
39538
39706
|
const { credential, source, expiresAt } = await resolveAuthPattern(pattern);
|
|
39539
39707
|
return {
|
|
@@ -40057,6 +40225,51 @@ function registerUserTools(server, client2) {
|
|
|
40057
40225
|
});
|
|
40058
40226
|
}
|
|
40059
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
|
+
|
|
40060
40273
|
// src/sync.ts
|
|
40061
40274
|
var FileMetaSchema = external_exports.looseObject({
|
|
40062
40275
|
fileId: external_exports.number(),
|
|
@@ -44233,10 +44446,11 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
|
|
|
44233
44446
|
var nodeAttachmentIO = new NodeAttachmentIO();
|
|
44234
44447
|
await runMcp({
|
|
44235
44448
|
name: "ofw",
|
|
44236
|
-
version: "2.
|
|
44449
|
+
version: "2.14.0",
|
|
44237
44450
|
// x-release-please-version
|
|
44238
44451
|
deps: client,
|
|
44239
44452
|
tools: [
|
|
44453
|
+
registerHealthcheckTools,
|
|
44240
44454
|
registerUserTools,
|
|
44241
44455
|
(server, deps) => registerMessageTools(server, deps, nodeCacheProvider, nodeAttachmentIO),
|
|
44242
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,8 +34,8 @@
|
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@chrischall/mcp-utils": "^0.
|
|
38
|
-
"@fetchproxy/bootstrap": "^2.
|
|
37
|
+
"@chrischall/mcp-utils": "^0.19.3",
|
|
38
|
+
"@fetchproxy/bootstrap": "^2.2.0",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
40
|
"dotenv": "^17.4.2",
|
|
41
41
|
"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.
|
|
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
|
},
|