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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "OurFamilyWizard tools for Claude Code",
9
- "version": "2.13.0"
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.13.0",
17
+ "version": "2.14.0",
18
18
  "author": {
19
19
  "name": "Chris Chall"
20
20
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ofw",
3
3
  "displayName": "OurFamilyWizard",
4
- "version": "2.13.0",
4
+ "version": "2.14.0",
5
5
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall"
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(`OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
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('OFW auth: set OFW_USERNAME + OFW_PASSWORD, ' +
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.13.0",
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.18.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
- `OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
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.13.0",
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.13.0', // x-release-please-version
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.13.0",
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.18.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.13.0",
9
+ "version": "2.14.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ofw-mcp",
14
- "version": "2.13.0",
14
+ "version": "2.14.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },