ofw-mcp 2.0.13 → 2.0.15
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/README.md +20 -9
- package/dist/auth-password.js +57 -0
- package/dist/auth.js +135 -0
- package/dist/bundle.js +11602 -5743
- package/dist/client.js +43 -60
- package/dist/config.js +18 -8
- package/dist/index.js +1 -1
- package/dist/sync.js +9 -4
- package/dist/tools/messages.js +56 -26
- package/package.json +3 -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.0.
|
|
9
|
+
"version": "2.0.15"
|
|
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.
|
|
17
|
+
"version": "2.0.15",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
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
|
-
##
|
|
69
|
+
## Authentication
|
|
70
70
|
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|