ofw-mcp 2.19.5 → 2.20.1

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.19.5"
9
+ "version": "2.20.1"
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.19.5",
17
+ "version": "2.20.1",
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.19.5",
4
+ "version": "2.20.1",
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
@@ -151,7 +151,10 @@ Read-only tools run automatically. Writes that reach your co-parent or the court
151
151
  | `ofw_delete_event` | Delete a calendar event | Confirm (server) if shared | `all` (or `drafts` + `OFW_CALENDAR_WRITES`) |
152
152
  | `ofw_get_expense_totals` | Expense summary totals | Auto | any |
153
153
  | `ofw_list_expenses` | Expense history | Auto | any |
154
- | `ofw_create_expense` | Log a new expense | Confirm (server) | `all` |
154
+ | `ofw_list_expense_categories` | Expense category ids and split metadata | Auto | any |
155
+ | `ofw_upload_expense_pdf` | Upload a receipt PDF to My Files for an expense (SHARED — the co-parent sees it in My Files at once, even on a private expense) | Confirm (server) | `all` |
156
+ | `ofw_create_expense` | Log a new expense; supports private entries and one receipt PDF | Confirm (server) | `all` |
157
+ | `ofw_update_expense` | Change an expense, e.g. publish a private one. Pass only the fields to change; it reads the expense first so the rest are kept | Confirm (server) | `all` |
155
158
  | `ofw_list_journal_entries` | Journal entries | Auto | any |
156
159
  | `ofw_create_journal_entry` | Create a journal entry | Confirm | `all` |
157
160
 
@@ -232,7 +235,7 @@ Previews name what you are approving — recipients by name, subject and full bo
232
235
  |---|---|---|
233
236
  | `MCP_CONFIRM_MODE` | `ask-user` | What a gated write does on a client that cannot show a prompt. `ask-user`: two steps, and the model must get your approval in chat before using the token. `auto`: two steps, but the model may use the token after reviewing the preview itself. `refuse`: such writes are refused (do them on ourfamilywizard.com). An unrecognised value is treated as `refuse`. |
234
237
  | `MCP_CONFIRM_TTL_SECONDS` | `600` | How long a token stays valid. |
235
- | `MCP_CONFIRM_SECRET` | random per process | Signing key; set it only if tokens must survive a server restart. |
238
+ | `MCP_CONFIRM_SECRET` | random per process | Signing key; set it only if tokens must survive a server restart. On mcp-host the host supplies a stable per-child key (`MCP_HOST_CONFIRM_SECRET`) and spent tokens are recorded under `MCP_DATA_DIR`, so an approval survives an idle restart. |
236
239
 
237
240
  Private events and `PRIVATE` uploads — which the co-parent never sees — are not gated. `OFW_WRITE_MODE` below stays the structural layer underneath: a tool your write mode excludes does not exist at all.
238
241
 
@@ -250,6 +253,26 @@ The host's "Confirm" permission above is a *hint* to the MCP host — a host con
250
253
 
251
254
  Unrecognized values fail closed to `none`, with a warning on stderr — a typo never silently grants write access.
252
255
 
256
+ #### Expense-only deployments
257
+
258
+ For a dedicated reimbursement integration, you can also structurally remove unrelated OFW capabilities:
259
+
260
+ | Setting | Registered surface |
261
+ |---|---|
262
+ | `OFW_EXPENSE_ONLY=true` | `ofw_healthcheck` plus the expense tools only. Profile/dashboard, messages, calendar, and journal tools do not exist. |
263
+ | `OFW_EXPENSE_UPLOAD_ONLY=true` | Strictest mode: `ofw_healthcheck`, `ofw_upload_expense_pdf`, `ofw_create_expense`, and `ofw_update_expense` only. Expense totals/listing are removed too. This flag implies `OFW_EXPENSE_ONLY`. |
264
+
265
+ These flags are registration-time restrictions, not prompt instructions. A host cannot call a tool that was never registered. Unrecognized non-empty flag values fail closed to the restricted state. `OFW_WRITE_MODE` still applies underneath; for the strict upload workflow set `OFW_WRITE_MODE=all` so expense creation is available.
266
+
267
+ Recommended reimbursement-only deployment:
268
+
269
+ ```env
270
+ OFW_EXPENSE_UPLOAD_ONLY=true
271
+ OFW_WRITE_MODE=all
272
+ OFW_ALLOW_MARK_READ=false
273
+ ```
274
+
275
+
253
276
  ### Reading is a write, too (`OFW_ALLOW_MARK_READ`)
254
277
 
255
278
  Fetching a message body for the first time marks it read on OurFamilyWizard and stamps a **"First Viewed" timestamp your co-parent can see**. That is part of the record and cannot be undone — and it happens as a side effect of an ordinary read, so `OFW_WRITE_MODE` does not govern it.
@@ -10,7 +10,7 @@
10
10
  // `resolveAuth()` in `./auth.ts` can call it without a Client instance, and
11
11
  // so tests can mock it at the module boundary.
12
12
  import { createHash } from 'node:crypto';
13
- import { currentCallSignal } from '@chrischall/mcp-utils';
13
+ import { EdgeBlockedError, currentCallSignal, detectEdgeBlock } from '@chrischall/mcp-utils';
14
14
  import { BASE_URL, OFW_PROTOCOL_HEADERS, OFW_TOKEN_TTL_MS, assertOfwUrl } from './protocol.js';
15
15
  /**
16
16
  * OFW definitively refused this username/password (it re-rendered its login
@@ -86,6 +86,22 @@ export async function loginWithPassword(username, password) {
86
86
  }).toString(),
87
87
  });
88
88
  if (!response.ok) {
89
+ // A CDN/WAF refusal page is not OFW judging the password: name it, so the
90
+ // healthcheck reports edge_blocked instead of a missing credential, and
91
+ // nothing is latched as rejected (chrischall/mcp-host#1015). The page
92
+ // itself stays out of the message.
93
+ const edge = detectEdgeBlock({
94
+ body: await response.text().catch(() => ''),
95
+ headers: response.headers,
96
+ status: response.status,
97
+ });
98
+ if (edge !== null) {
99
+ throw new EdgeBlockedError(response.status, edge.vendor, {
100
+ service: 'OurFamilyWizard',
101
+ method: 'POST',
102
+ path: '/ofw/login',
103
+ });
104
+ }
89
105
  throw new Error(`OFW login failed: ${response.status} ${response.statusText}`);
90
106
  }
91
107
  const contentType = response.headers.get('content-type') ?? '';