ofw-mcp 2.0.12 → 2.0.14

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.0.12"
9
+ "version": "2.0.14"
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.0.12",
17
+ "version": "2.0.14",
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.0.12",
4
+ "version": "2.0.14",
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/README.md CHANGED
@@ -66,9 +66,15 @@ Quit completely (Cmd+Q on Mac, not just close the window) and relaunch.
66
66
 
67
67
  Ask Claude: *"What does my OFW dashboard look like?"* — it should show your unread message count, upcoming events, and outstanding expenses.
68
68
 
69
- ## Credentials
69
+ ## Authentication
70
70
 
71
- Credentials are read from environment variables, with two ways to provide them:
71
+ `ofw-mcp` tries three auth paths in order; whichever succeeds first is used. Existing setups keep working unchanged.
72
+
73
+ 1. **Env-var credentials (legacy, recommended for Claude Desktop).** Set `OFW_USERNAME` + `OFW_PASSWORD` and the server logs in via OFW's form endpoint. This is the path shown in the Claude Desktop config above.
74
+ 2. **fetchproxy fallback (no env vars needed).** When the credentials are absent, the server reads `localStorage["auth"]` once at startup from your already-signed-in `ourfamilywizard.com` tab via the [fetchproxy](https://github.com/chrischall/fetchproxy) browser extension. After that one read, all OFW API calls go directly from Node — the extension is **not** in the request hot path. Install the fetchproxy extension (Chrome Web Store / Safari `.dmg`), sign into OurFamilyWizard once, and the MCP just works. If you have multiple OFW accounts and want them to use separate caches, set `OFW_CACHE_IDENTITY` to a label per profile.
75
+ 3. **Error.** If neither path is available, the server tells you exactly which fix to apply. Set `OFW_DISABLE_FETCHPROXY=1` to skip the fetchproxy fallback entirely (turns missing credentials into a hard error — useful in headless CI).
76
+
77
+ ### Credential options (env-var path)
72
78
 
73
79
  **Option A — env block in Claude Desktop config** (shown above, recommended):
74
80
 
@@ -121,7 +127,9 @@ Read-only tools run automatically. Write tools ask for your confirmation first.
121
127
 
122
128
  **"0 messages"** — Claude may have read the notification counts rather than the actual messages. Ask explicitly: *"List the messages in my OFW inbox"* or *"Use ofw_list_message_folders then ofw_list_messages"*.
123
129
 
124
- **"OFW_USERNAME and OFW_PASSWORD must be set"** — credentials are missing. Check the `env` block in your Claude Desktop config or your `.env` file.
130
+ **"OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension…"** — neither auth path is configured. Either fill in the `env` block in your Claude Desktop config, or install the [fetchproxy extension](https://github.com/chrischall/fetchproxy) and sign into `ourfamilywizard.com` in your browser.
131
+
132
+ **"fetchproxy fallback failed"** — the env-var path wasn't configured and the extension couldn't be reached. Confirm the fetchproxy extension is installed, signed into OFW, and that it's running (open the extension popup). If you want to disable the fallback entirely, set `OFW_DISABLE_FETCHPROXY=1`.
125
133
 
126
134
  **403 Forbidden** — wrong credentials. Verify your username/password at [ofw.ourfamilywizard.com](https://ofw.ourfamilywizard.com).
127
135
 
@@ -162,14 +170,17 @@ tests/
162
170
 
163
171
  ### Auth flow
164
172
 
165
- OFW uses Spring Security form login:
173
+ Auth resolution lives in `src/auth.ts`. Three paths, in priority order:
174
+
175
+ 1. **Env vars present** → `src/auth-password.ts` does the legacy OFW Spring Security form login:
176
+ 1. `GET /ofw/login.form` — establishes a session cookie
177
+ 2. `POST /ofw/login` — submits credentials, returns `{ auth: "<token>" }`
178
+ 2. **Env vars absent (and `OFW_DISABLE_FETCHPROXY` unset)** → `@fetchproxy/bootstrap` reads `localStorage["auth"]` + `localStorage["tokenExpiry"]` once from the user's signed-in `ourfamilywizard.com` tab, then closes the bridge.
179
+ 3. **Nothing configured** → throws with both fixes spelled out.
166
180
 
167
- 1. `GET /ofw/login.form` — establishes a session cookie
168
- 2. `POST /ofw/login` — submits credentials, returns `{ auth: "<token>" }`
169
- 3. All API calls use `Authorization: Bearer <token>`
170
- 4. On 401, re-authenticates automatically and retries once
181
+ Either path returns a Bearer token to `OFWClient`, which then operates from Node with `Authorization: Bearer <token>` fetchproxy is **not** in the request hot path. On 401 the client re-resolves auth and replays once. Tokens are cached for 6h (env-var path) or until `tokenExpiry` (fetchproxy path).
171
182
 
172
- Tokens are cached for 6 hours.
183
+ Also see the [fetchproxy README](https://github.com/chrischall/fetchproxy) for extension install instructions.
173
184
 
174
185
  ## License
175
186
 
@@ -0,0 +1,57 @@
1
+ // OFW's existing password-login path.
2
+ //
3
+ // `POST /ofw/login` is Spring Security form-urlencoded; it requires a SESSION
4
+ // cookie that we capture from `GET /ofw/login.form` first. The response body
5
+ // is JSON `{ auth: "<Bearer token>", redirectUrl: "..." }`. OFW does not return
6
+ // a token expiry, so we synthesize a 6h lifetime — long enough to be useful,
7
+ // short enough that a 401 re-auth replay is rare.
8
+ //
9
+ // This file exists as a standalone helper (not a method on `OFWClient`) so
10
+ // `resolveAuth()` in `./auth.ts` can call it without a Client instance, and
11
+ // so tests can mock it at the module boundary.
12
+ const BASE_URL = 'https://ofw.ourfamilywizard.com';
13
+ const OFW_PROTOCOL_HEADERS = {
14
+ 'ofw-client': 'WebApplication',
15
+ 'ofw-version': '1.0.0',
16
+ };
17
+ export async function loginWithPassword(username, password) {
18
+ // Step 1: get a SESSION cookie (Spring Security refuses the POST without it).
19
+ const initResponse = await fetch(`${BASE_URL}/ofw/login.form`, {
20
+ headers: { ...OFW_PROTOCOL_HEADERS },
21
+ redirect: 'manual',
22
+ });
23
+ const setCookie = initResponse.headers.get('set-cookie') ?? '';
24
+ const sessionCookie = setCookie.split(';')[0];
25
+ // Step 2: submit the form.
26
+ const response = await fetch(`${BASE_URL}/ofw/login`, {
27
+ method: 'POST',
28
+ headers: {
29
+ ...OFW_PROTOCOL_HEADERS,
30
+ Accept: 'application/json',
31
+ 'Content-Type': 'application/x-www-form-urlencoded',
32
+ ...(sessionCookie ? { Cookie: sessionCookie } : {}),
33
+ },
34
+ body: new URLSearchParams({
35
+ submit: 'Sign In',
36
+ _eventId: 'submit',
37
+ username,
38
+ password,
39
+ }).toString(),
40
+ });
41
+ if (!response.ok) {
42
+ throw new Error(`OFW login failed: ${response.status} ${response.statusText}`);
43
+ }
44
+ const contentType = response.headers.get('content-type') ?? '';
45
+ if (!contentType.includes('application/json')) {
46
+ const body = await response.text();
47
+ throw new Error(`OFW login returned unexpected response (${contentType}): ${body.substring(0, 200)}`);
48
+ }
49
+ const data = (await response.json());
50
+ return {
51
+ token: data.auth,
52
+ // OFW's login endpoint omits expiry. 6h is the empirical TTL and matches
53
+ // the historical behavior of this client (a single 401 → re-auth + replay
54
+ // covers the edge case where this estimate is wrong).
55
+ expiresAt: new Date(Date.now() + 6 * 60 * 60 * 1000),
56
+ };
57
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,135 @@
1
+ // ────────────────────────────────────────────────────────────────────────────
2
+ // Auth resolution — Pattern A template
3
+ // ────────────────────────────────────────────────────────────────────────────
4
+ //
5
+ // This file is the canonical shape for "browser-bootstrap + Node-direct"
6
+ // auth used across our MCP servers. The other six MCPs in this family
7
+ // (resy-mcp, opentable-mcp, splitwise-mcp, …) will model their auth
8
+ // resolution after this one — keep the structure flat, the path-selection
9
+ // explicit, and the error messages actionable.
10
+ //
11
+ // THE THREE PATHS, in priority order:
12
+ //
13
+ // 1. Env-var credentials (existing behavior)
14
+ // OFW_USERNAME + OFW_PASSWORD set → POST the login form, get a token.
15
+ // This is the legacy path. It runs unchanged when both vars are set
16
+ // so existing users (Claude Desktop with mcpb env config, etc.) are
17
+ // not disrupted.
18
+ //
19
+ // 2. fetchproxy fallback (new)
20
+ // When credentials are absent, we try to lift the user's session
21
+ // out of their signed-in browser tab via the fetchproxy extension.
22
+ // The `@fetchproxy/bootstrap` helper spins up a one-shot WebSocket
23
+ // bridge, asks the extension for `localStorage["auth"]` and
24
+ // `localStorage["tokenExpiry"]` from any ourfamilywizard.com tab,
25
+ // then closes the bridge. From here on, all OFW API calls go out
26
+ // via plain Node `fetch()` — fetchproxy is NOT in the hot path.
27
+ //
28
+ // Users opt out with OFW_DISABLE_FETCHPROXY=1 (anyone who wants the
29
+ // old behavior of "fail loudly when creds are missing").
30
+ //
31
+ // 3. Error
32
+ // Nothing to authenticate with. We throw a message that tells the
33
+ // user exactly what to do: set creds, OR install the extension and
34
+ // sign in.
35
+ //
36
+ // Why fetchproxy is only a one-shot read:
37
+ // The bootstrap call snapshots the session blob and returns. The MCP
38
+ // then operates from Node with direct fetch + Authorization header,
39
+ // so latency and reliability are not coupled to the browser bridge
40
+ // for normal tool calls. Pre-PR mcp-chrome and tab-routing concerns
41
+ // (see opentable-mcp/CLAUDE.md "Bridge selection") do not apply here.
42
+ //
43
+ // Testability:
44
+ // - `@fetchproxy/bootstrap` is mocked at the module boundary in tests.
45
+ // - `./auth-password.js` (loginWithPassword) is a separate module
46
+ // specifically so it can be mocked here too. This keeps the
47
+ // selection logic independent of either implementation.
48
+ import { bootstrap } from '@fetchproxy/bootstrap';
49
+ import { loginWithPassword } from './auth-password.js';
50
+ import pkg from '../package.json' with { type: 'json' };
51
+ /**
52
+ * Read an env var, trim, and treat blank / `${UNEXPANDED}` placeholders as
53
+ * unset. Defends against MCP hosts that pass `.mcp.json` env blocks through
54
+ * without variable expansion.
55
+ */
56
+ function readEnv(key) {
57
+ const raw = process.env[key];
58
+ if (typeof raw !== 'string')
59
+ return undefined;
60
+ const trimmed = raw.trim();
61
+ if (trimmed.length === 0)
62
+ return undefined;
63
+ if (trimmed === 'undefined' || trimmed === 'null')
64
+ return undefined;
65
+ if (/^\$\{[^}]*\}$/.test(trimmed))
66
+ return undefined;
67
+ return trimmed;
68
+ }
69
+ /** True if the user has explicitly disabled the fetchproxy fallback. */
70
+ function fetchproxyDisabled() {
71
+ const raw = readEnv('OFW_DISABLE_FETCHPROXY');
72
+ if (raw === undefined)
73
+ return false;
74
+ return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase());
75
+ }
76
+ /**
77
+ * Resolve OFW auth using the three-path priority described at the top of
78
+ * this file. Throws with an actionable error message when no path succeeds.
79
+ *
80
+ * Callers (i.e. `OFWClient.login()`) treat the return value as opaque
81
+ * credentials — they should not branch on `source`. The field exists for
82
+ * logging / future cache-keying only.
83
+ */
84
+ export async function resolveAuth() {
85
+ // ── Path 1: env-var credentials (unchanged from pre-fetchproxy behavior).
86
+ const username = readEnv('OFW_USERNAME');
87
+ const password = readEnv('OFW_PASSWORD');
88
+ if (username && password) {
89
+ const { token, expiresAt } = await loginWithPassword(username, password);
90
+ return { token, expiresAt, source: 'env' };
91
+ }
92
+ // ── Path 2: fetchproxy fallback (new).
93
+ if (!fetchproxyDisabled()) {
94
+ try {
95
+ const session = await bootstrap({
96
+ serverName: pkg.name,
97
+ version: pkg.version,
98
+ // OFW serves both ofw.ourfamilywizard.com and www.ourfamilywizard.com;
99
+ // the API + auth token live on the apex. The extension matches on
100
+ // suffix, so listing the apex covers both.
101
+ domains: ['ourfamilywizard.com'],
102
+ declare: {
103
+ cookies: [],
104
+ // The web app stores the Bearer token in localStorage["auth"] and
105
+ // its expiry (ISO string) in localStorage["tokenExpiry"]. Mirroring
106
+ // both means our 401-replay logic can be slightly smarter, and the
107
+ // expiry surfaces correctly in diagnostics.
108
+ localStorage: ['auth', 'tokenExpiry'],
109
+ sessionStorage: [],
110
+ captureHeaders: [],
111
+ },
112
+ });
113
+ const token = session.localStorage['auth'];
114
+ const expiryRaw = session.localStorage['tokenExpiry'];
115
+ if (!token) {
116
+ throw new Error('localStorage["auth"] missing on ourfamilywizard.com. ' +
117
+ 'Sign into OFW in your browser (with the fetchproxy extension installed) and retry.');
118
+ }
119
+ return {
120
+ token,
121
+ expiresAt: expiryRaw ? new Date(expiryRaw) : undefined,
122
+ source: 'fetchproxy',
123
+ };
124
+ }
125
+ catch (e) {
126
+ const msg = e instanceof Error ? e.message : String(e);
127
+ throw new Error(`OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`);
128
+ }
129
+ }
130
+ // ── Path 3: nothing configured. Surface both fixes side-by-side so the
131
+ // user can pick whichever fits their setup.
132
+ throw new Error('OFW auth: set OFW_USERNAME + OFW_PASSWORD, ' +
133
+ 'or install the fetchproxy extension and sign into ourfamilywizard.com ' +
134
+ '(unset OFW_DISABLE_FETCHPROXY if it is set).');
135
+ }