ofw-mcp 2.20.3 → 2.20.5

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.20.3"
9
+ "version": "2.20.5"
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.20.3",
17
+ "version": "2.20.5",
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.20.3",
4
+ "version": "2.20.5",
5
5
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall"
@@ -17,5 +17,5 @@
17
17
  "family"
18
18
  ],
19
19
  "skills": "./skills/",
20
- "mcp": "./.mcp.json"
20
+ "mcpServers": "./.mcp.json"
21
21
  }
package/README.md CHANGED
@@ -131,7 +131,7 @@ Read-only tools run automatically. Writes that reach your co-parent or the court
131
131
  | Tool | What it does | Permission | Write mode |
132
132
  |------|-------------|------------|------------|
133
133
  | `ofw_get_profile` | Your profile and co-parent info | Auto | any |
134
- | `ofw_get_notifications` | Dashboard counts (unread messages, upcoming events, outstanding expenses) | Auto | any |
134
+ | `ofw_get_notifications` | Dashboard counts (unread messages, upcoming events, outstanding expenses). Updates your co-parent-visible last-seen status, so it is not marked read-only | Confirm | any |
135
135
  | `ofw_list_message_folders` | Folders with unread counts — **get folder IDs here before listing messages** | Auto | any |
136
136
  | `ofw_list_messages` | Messages in a folder | Auto | any |
137
137
  | `ofw_get_message` | Full content of a single message | Auto | any |
@@ -293,7 +293,9 @@ Calendar events sit between the two message tiers: they have no draft stage (a c
293
293
 
294
294
  ### Egress allowlist
295
295
 
296
- Every outbound request passes its constructed URL through a host check before `fetch` — the server refuses to contact any host other than `ofw.ourfamilywizard.com`. Today it always passes, and that is the point: it makes "this server only ever talks to OFW" a structural invariant rather than a code-review promise, so a later refactor or a compromised dependency that pointed a request elsewhere throws instead of carrying your bearer token or messages off-host. It is a pre-flight check on the URL we build, not a redirect-following egress filter.
296
+ Every request that carries your OFW token or login passes its constructed URL through a host check before `fetch` — the server refuses to send them to any host other than `ofw.ourfamilywizard.com`. Today it always passes, and that is the point: it makes "the credential only ever goes to OFW" a structural invariant rather than a code-review promise, so a later refactor or a compromised dependency that pointed a request elsewhere throws instead of carrying your bearer token or messages off-host. It is a pre-flight check on the URL we build, not a redirect-following egress filter.
297
+
298
+ The one other host the server contacts is `*.oaiusercontent.com`, and only for `ofw_upload_expense_pdf`'s `url` mode: it downloads the receipt PDF from that signed file URL with no OFW credential attached, re-checking every redirect hop against the same suffix, capping the size and requiring a real PDF. The hosted registration's egress allowlist (`mint.yaml`) names exactly these two hosts.
297
299
 
298
300
  ## Troubleshooting
299
301
 
@@ -317,7 +319,7 @@ Every outbound request passes its constructed URL through a host check before `f
317
319
  - They are passed to the server as environment variables and never logged
318
320
  - The server authenticates with OFW using the same login flow as the web app
319
321
  - Use a strong, unique OFW password
320
- - Outbound requests are host-allowlisted to `ofw.ourfamilywizard.com` (see [Egress allowlist](#egress-allowlist))
322
+ - Requests carrying your OFW credential are host-allowlisted to `ofw.ourfamilywizard.com`; the only other host contacted is the receipt-download host for `ofw_upload_expense_pdf`'s `url` mode (see [Egress allowlist](#egress-allowlist))
321
323
 
322
324
  ### Local data, and how to remove it
323
325
 
@@ -339,6 +341,8 @@ rm -f ~/.ofw-mcp/session.json # or your OFW_SESSION_FILE
339
341
 
340
342
  The next start rebuilds the cache from OurFamilyWizard with `ofw_sync_messages`. A directory you pointed `OFW_ATTACHMENTS_DIR` at yourself keeps its own permissions (it may be shared on purpose); only what the server creates, and the default `~/Downloads/ofw-mcp`, are locked down.
341
343
 
344
+ **On a hosted connector (mcp-host)** the same data lives in the registration's persistent data directory instead of on your machine: the message cache under `$HOME/.cache/ofw-mcp/`, the session token at `$MCP_DATA_DIR/.ofw-mcp/session.json`, the browser-bridge identity under `$HOME/.fetchproxy/identity/`, and spent confirm-token nonces under `$MCP_DATA_DIR/.mcp-confirm/`. Downloads are returned inline and never written to disk. The commands above do not reach it — how long that directory is kept, and whether disconnecting or deleting the registration removes it, is the host's retention policy, not this server's.
345
+
342
346
  ## Development
343
347
 
344
348
  ```bash
@@ -118,9 +118,34 @@ export async function loginWithPassword(username, password) {
118
118
  const body = await response.text();
119
119
  throw new Error(`OFW login returned unexpected response (${contentType || 'no content-type'}): ${body.substring(0, 200)}`);
120
120
  }
121
- const data = (await response.json());
121
+ // A 200 JSON answer is not proof of a token: a lockout, MFA or captcha
122
+ // challenge can arrive the same way. Without this check the token would be
123
+ // the string "undefined", every request would 401, and the TokenManager
124
+ // would re-mint through another password POST — one OFW counts against the
125
+ // account. Not latched: this is not OFW judging the password.
126
+ const data = await response.json();
127
+ const record = typeof data === 'object' && data !== null ? data : null;
128
+ const token = record?.auth;
129
+ if (typeof token !== 'string' || token === '') {
130
+ throw new Error(describeTokenlessLogin(record));
131
+ }
122
132
  return {
123
- token: data.auth,
133
+ token,
124
134
  expiresAt: new Date(Date.now() + OFW_TOKEN_TTL_MS),
125
135
  };
126
136
  }
137
+ /**
138
+ * An actionable message for a login answer that carried no token. It names
139
+ * the response's KEYS and OFW's own `message` text, never any other value —
140
+ * a challenge payload can carry ids or tokens that do not belong in an error.
141
+ */
142
+ function describeTokenlessLogin(record) {
143
+ const base = 'OFW login returned no token, so it did not sign in';
144
+ if (record === null) {
145
+ return `${base} (the response was not a JSON object). Sign in to OurFamilyWizard in a browser to check the account, then try again.`;
146
+ }
147
+ const keys = Object.keys(record);
148
+ const said = typeof record.message === 'string' ? ` OFW said: "${record.message.substring(0, 200)}".` : '';
149
+ return `${base} (response fields: ${keys.length > 0 ? keys.join(', ') : 'none'}).${said} `
150
+ + 'The account may be locked or need a verification step: sign in to OurFamilyWizard in a browser to clear it, then try again.';
151
+ }
package/dist/auth.js CHANGED
@@ -56,7 +56,7 @@ import { parseBoolEnv, readEnvVar, resolveAuthPattern } from '@chrischall/mcp-ut
56
56
  import { bootstrap } from '@fetchproxy/bootstrap';
57
57
  import { classifyBridgeError } from '@chrischall/mcp-utils/fetchproxy';
58
58
  import { loginWithPassword } from './auth-password.js';
59
- import pkg from '../package.json' with { type: 'json' };
59
+ import { SERVER_NAME, VERSION } from './version.js';
60
60
  /** True if the user has explicitly disabled the fetchproxy fallback. */
61
61
  function fetchproxyDisabled() {
62
62
  return parseBoolEnv('OFW_DISABLE_FETCHPROXY');
@@ -124,8 +124,8 @@ export async function resolveAuth() {
124
124
  pattern.fetchproxy = async () => {
125
125
  try {
126
126
  const session = await bootstrap({
127
- serverName: pkg.name,
128
- version: pkg.version,
127
+ serverName: SERVER_NAME,
128
+ version: VERSION,
129
129
  // OFW serves both ofw.ourfamilywizard.com and www.ourfamilywizard.com;
130
130
  // the API + auth token live on the apex. The extension matches on
131
131
  // suffix, so listing the apex covers both.