ofw-mcp 2.19.2 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +47 -9
- package/dist/auth-password.js +39 -1
- package/dist/bundle.js +1114 -245
- package/dist/config.js +16 -0
- package/dist/extract/spreadsheet.js +33 -10
- package/dist/index.js +1 -1
- package/dist/sync.js +3 -2
- package/dist/timestamps.js +42 -0
- package/dist/tools/_confirm.js +49 -0
- package/dist/tools/_shared.js +79 -2
- package/dist/tools/attachments.js +85 -8
- package/dist/tools/calendar.js +143 -15
- package/dist/tools/draft-freshness.js +1 -1
- package/dist/tools/expenses.js +39 -5
- package/dist/tools/journal.js +14 -2
- package/dist/tools/messages.js +201 -34
- package/mint.yaml +27 -1
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/ofw/SKILL.md +3 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "OurFamilyWizard tools for Claude Code",
|
|
9
|
-
"version": "2.19.
|
|
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.
|
|
17
|
+
"version": "2.19.4",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
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.
|
|
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 to My Files; returns a fileId to attach via `ofw_send_message`/`ofw_save_draft` | 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,14 +222,30 @@ 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
|
|------------------|------------------|
|
|
231
247
|
| `none` | Read/sync/search only. No write tools exist. |
|
|
232
|
-
| `drafts` | Adds draft-level writes: `ofw_save_draft`, `ofw_delete_draft`, `ofw_upload_attachment
|
|
248
|
+
| `drafts` | Adds draft-level writes: `ofw_save_draft`, `ofw_delete_draft`, `ofw_upload_attachment` (PRIVATE only). Nothing that lands on the court-visible record — the AI prepares, only a human signed into the OFW web UI can send. |
|
|
233
249
|
| `all` | Everything (the default — fully backward compatible). |
|
|
234
250
|
|
|
235
251
|
Unrecognized values fail closed to `none`, with a warning on stderr — a typo never silently grants write access.
|
|
@@ -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
|
package/dist/auth-password.js
CHANGED
|
@@ -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
|
-
|
|
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)}`);
|