ofw-mcp 2.19.3 → 2.19.4

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.3"
9
+ "version": "2.19.4"
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.3",
17
+ "version": "2.19.4",
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.3",
4
+ "version": "2.19.4",
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
@@ -126,7 +126,7 @@ OFW_USERNAME=you@example.com OFW_PASSWORD=yourpass node dist/index.js
126
126
 
127
127
  ## Available tools
128
128
 
129
- Read-only tools run automatically. Write tools ask for your confirmation first. The *Write mode* column shows the minimum `OFW_WRITE_MODE` a tool needs to be available at all — see [Write protection](#write-protection-ofw_write_mode) below.
129
+ Read-only tools run automatically. Writes that reach your co-parent or the court-visible record — **Confirm (server)** below — are confirmed by the server itself before anything is sent (see [Server-side confirmation](#server-side-confirmation-mcp_confirm_mode)); the other writes rely on your MCP host's own approval prompt (**Confirm**). The *Write mode* column shows the minimum `OFW_WRITE_MODE` a tool needs to be available at all — see [Write protection](#write-protection-ofw_write_mode) below.
130
130
 
131
131
  | Tool | What it does | Permission | Write mode |
132
132
  |------|-------------|------------|------------|
@@ -140,18 +140,18 @@ Read-only tools run automatically. Write tools ask for your confirmation first.
140
140
  | `ofw_check_freshness` | Cheap live check that the cache still matches OFW — per id, whether it is still a `draft` or was `sent`/`deleted`, without a full sync | Auto | any |
141
141
  | `ofw_status` | **One live call for "where does everything stand?"** — the full verified draft inventory, and the current state of any ids or draft keys | Auto | any |
142
142
  | `ofw_download_attachment` | Download a message attachment to disk, or inline as extracted content / bytes | Auto | any |
143
- | `ofw_send_message` | Send a message | Confirm | `all` |
143
+ | `ofw_send_message` | Send a message | Confirm (server) | `all` |
144
144
  | `ofw_list_drafts` | Draft messages | Auto | any |
145
145
  | `ofw_save_draft` | Create or update a draft | Confirm | `drafts` |
146
146
  | `ofw_delete_draft` | Delete a draft | Confirm | `drafts` |
147
- | `ofw_upload_attachment` | Upload a local file from the upload directory (`OFW_UPLOAD_DIR`, default `~/Downloads/ofw-mcp`) to My Files; returns a fileId to attach via `ofw_send_message`/`ofw_save_draft`. Sharing (`shareClass: SHARED`) needs mode `all` | Auto | `drafts` |
147
+ | `ofw_upload_attachment` | Upload a local file from the upload directory (`OFW_UPLOAD_DIR`, default `~/Downloads/ofw-mcp`) to My Files; returns a fileId to attach via `ofw_send_message`/`ofw_save_draft`. Sharing (`shareClass: SHARED`) needs mode `all` | Auto (PRIVATE) / Confirm (server) (SHARED) | `drafts` |
148
148
  | `ofw_list_events` | Calendar events in a date range | Auto | any |
149
- | `ofw_create_event` | Create a calendar event | Confirm | `all` (or `drafts` + `OFW_CALENDAR_WRITES`) |
150
- | `ofw_update_event` | Update a calendar event | Confirm | `all` (or `drafts` + `OFW_CALENDAR_WRITES`) |
151
- | `ofw_delete_event` | Delete a calendar event | Confirm | `all` (or `drafts` + `OFW_CALENDAR_WRITES`) |
149
+ | `ofw_create_event` | Create a calendar event | Confirm (server) unless private | `all` (or `drafts` + `OFW_CALENDAR_WRITES`) |
150
+ | `ofw_update_event` | Update a calendar event | Confirm (server) if shared | `all` (or `drafts` + `OFW_CALENDAR_WRITES`) |
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 | `all` |
154
+ | `ofw_create_expense` | Log a new expense | Confirm (server) | `all` |
155
155
  | `ofw_list_journal_entries` | Journal entries | Auto | any |
156
156
  | `ofw_create_journal_entry` | Create a journal entry | Confirm | `all` |
157
157
 
@@ -222,9 +222,25 @@ A false negative ("no, that was never sent") is more dangerous than a refusal, b
222
222
 
223
223
  Every list read also carries an explicit **`complete`** boolean describing the *result set* — "this is every matching item on OurFamilyWizard as of `asOf`" — with a `completeNote` naming what is missing when it is false. Check it before stating a count.
224
224
 
225
+ ### Server-side confirmation (`MCP_CONFIRM_MODE`)
226
+
227
+ The writes marked **Confirm (server)** above — sending a message, logging an expense, creating/updating/deleting a *shared* calendar event, and uploading a file as `SHARED` — are confirmed by this server, not left to the host. A client that can show a confirmation prompt (Claude Code) gets one, with a preview of exactly what would happen. A client that cannot (claude.ai, Claude Desktop) gets two steps: the first call writes **nothing** and returns that preview plus a `confirmToken`, and only a repeat call with the same arguments and that token proceeds.
228
+
229
+ Previews name what you are approving — recipients by name, subject and full body, the reply target, attachment file names; the expense amount and description; the event's title, date, time, visibility and (for an update) before and after. The token is bound to exactly that: a different body, amount or recipient is refused, and so is a draft or event that changed on OurFamilyWizard after the preview (say, the co-parent edited the event), even with `force: true`. A token works once and expires.
230
+
231
+ | Variable | Default | |
232
+ |---|---|---|
233
+ | `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
+ | `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. |
236
+
237
+ 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
+
239
+ **A write that times out is not a write that failed.** If `ofw_send_message`, `ofw_create_expense`, `ofw_create_event` or `ofw_create_journal_entry` loses its connection or times out without a definitive answer from OFW, the result is `SEND_UNCONFIRMED` / `EXPENSE_UNCONFIRMED` / `EVENT_UNCONFIRMED` / `JOURNAL_UNCONFIRMED`: it may have landed. Check the matching list tool (or ourfamilywizard.com) before retrying, or the co-parent sees it twice.
240
+
225
241
  ### Write protection (`OFW_WRITE_MODE`)
226
242
 
227
- The "Confirm" permission above is a *hint* to the MCP host — a host configured to auto-approve tools (or a user who clicked "always allow" once) would leave nothing between model output and a sent message. Because OurFamilyWizard is a court-of-record platform, the server also supports a structural gate: set `OFW_WRITE_MODE` in the server's `env` block and tools above your chosen level are **never registered**, so no host setting or prompt-injected instruction can invoke them.
243
+ The host's "Confirm" permission above is a *hint* to the MCP host — a host configured to auto-approve tools (or a user who clicked "always allow" once) would leave nothing between model output and a sent message. Because OurFamilyWizard is a court-of-record platform, the server also supports a structural gate: set `OFW_WRITE_MODE` in the server's `env` block and tools above your chosen level are **never registered**, so no host setting or prompt-injected instruction can invoke them.
228
244
 
229
245
  | `OFW_WRITE_MODE` | What's available |
230
246
  |------------------|------------------|
@@ -263,6 +279,8 @@ Every outbound request passes its constructed URL through a host check before `f
263
279
 
264
280
  **"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`.
265
281
 
282
+ **"OFW login not attempted — this OurFamilyWizard email and password were already rejected"** — OFW refused these credentials earlier in this session. OFW counts failed sign-ins against the account, so the server does not re-send a rejected password on every call. Update `OFW_USERNAME` / `OFW_PASSWORD` to your current login (or restart the server) and try again.
283
+
266
284
  **403 Forbidden** — wrong credentials. Verify your username/password at [ofw.ourfamilywizard.com](https://ofw.ourfamilywizard.com).
267
285
 
268
286
  **Tools not appearing in Claude** — go to **Claude Desktop → Settings → Developer** to see connected servers and any error output. Make sure you fully quit and relaunched after editing the config.
@@ -277,6 +295,26 @@ Every outbound request passes its constructed URL through a host check before `f
277
295
  - Use a strong, unique OFW password
278
296
  - Outbound requests are host-allowlisted to `ofw.ourfamilywizard.com` (see [Egress allowlist](#egress-allowlist))
279
297
 
298
+ ### Local data, and how to remove it
299
+
300
+ The server keeps co-parenting data on this machine, readable only by your user account:
301
+
302
+ | What | Where | Permissions |
303
+ |---|---|---|
304
+ | Message cache (message bodies, recipients, drafts, attachment records) | `~/.cache/ofw-mcp/<hash>.db` (plus `-wal`/`-shm`), or `OFW_CACHE_DIR` | dir `0700`, files `0600` |
305
+ | Downloaded attachments | `~/Downloads/ofw-mcp/`, or `OFW_ATTACHMENTS_DIR` | dirs the server creates `0700`, files `0600` |
306
+ | Session token | `$MCP_DATA_DIR/.ofw-mcp/session.json`, or `OFW_SESSION_FILE` | `0600` |
307
+
308
+ The cache keeps everything it has synced until you remove it; nothing expires it. When you no longer need it — the account is closed, the case is over, or you are handing the machine on — quit your MCP host and delete it:
309
+
310
+ ```bash
311
+ rm -rf ~/.cache/ofw-mcp # or your OFW_CACHE_DIR
312
+ rm -rf ~/Downloads/ofw-mcp # or your OFW_ATTACHMENTS_DIR
313
+ rm -f ~/.ofw-mcp/session.json # or your OFW_SESSION_FILE
314
+ ```
315
+
316
+ 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.
317
+
280
318
  ## Development
281
319
 
282
320
  ```bash
@@ -9,9 +9,45 @@
9
9
  // This file exists as a standalone helper (not a method on `OFWClient`) so
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
+ import { createHash } from 'node:crypto';
12
13
  import { currentCallSignal } from '@chrischall/mcp-utils';
13
14
  import { BASE_URL, OFW_PROTOCOL_HEADERS, OFW_TOKEN_TTL_MS, assertOfwUrl } from './protocol.js';
15
+ /**
16
+ * OFW definitively refused this username/password (it re-rendered its login
17
+ * page). Distinct from a transient failure (5xx, timeout), which is retried.
18
+ */
19
+ export class CredentialsRejectedError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = 'CredentialsRejectedError';
23
+ }
24
+ }
25
+ // ── Credential-rejection latch ───────────────────────────────────────────
26
+ // OFW counts failed sign-ins against the account, and nothing upstream of this
27
+ // function remembers a rejection: the TokenManager caches no failure, so every
28
+ // later tool call (and every healthcheck) would re-POST the same stale
29
+ // password — the realistic trigger being a password changed in the OFW web app
30
+ // while this process keeps the old one in its env. So a DEFINITIVE rejection is
31
+ // latched here, per credential pair, for the life of the process: the pair is
32
+ // refused locally until OFW_USERNAME/OFW_PASSWORD change or the server
33
+ // restarts. Only a digest of the pair is held, never the password itself.
34
+ const rejectedPairs = new Set();
35
+ // Synchronous on purpose: an extra await ahead of the first fetch would shift
36
+ // every caller's timing (config.ts already hashes with node:crypto).
37
+ function pairDigest(username, password) {
38
+ return createHash('sha256').update(`${username.length}:${username}\u0000${password}`).digest('hex');
39
+ }
40
+ /** Test hook: forget every latched rejection. */
41
+ export function resetCredentialRejections() {
42
+ rejectedPairs.clear();
43
+ }
14
44
  export async function loginWithPassword(username, password) {
45
+ const digest = pairDigest(username, password);
46
+ if (rejectedPairs.has(digest)) {
47
+ throw new CredentialsRejectedError('OFW login not attempted — this OurFamilyWizard email and password were already rejected by OFW '
48
+ + 'earlier in this session, and OFW counts failed sign-ins against the account, so they are not re-sent. '
49
+ + 'Update OFW_USERNAME / OFW_PASSWORD to the current values (or restart the server) and try again.');
50
+ }
15
51
  // Step 1: get a SESSION cookie (Spring Security refuses the POST without it).
16
52
  const initUrl = `${BASE_URL}/ofw/login.form`;
17
53
  assertOfwUrl(initUrl);
@@ -59,7 +95,9 @@ export async function loginWithPassword(username, password) {
59
95
  // clean, actionable message instead of dumping the HTML page — this is what
60
96
  // a hosted deployment's login page shows the user on a failed sign-in.
61
97
  if (contentType.includes('text/html')) {
62
- throw new Error('OFW login failed — your OurFamilyWizard email or password was not accepted. Check them and try again.');
98
+ rejectedPairs.add(digest);
99
+ throw new CredentialsRejectedError('OFW login failed — your OurFamilyWizard email or password was not accepted. Check them and try again. '
100
+ + 'They will not be re-sent until OFW_USERNAME / OFW_PASSWORD change or the server restarts.');
63
101
  }
64
102
  const body = await response.text();
65
103
  throw new Error(`OFW login returned unexpected response (${contentType || 'no content-type'}): ${body.substring(0, 200)}`);