ofw-mcp 2.19.4 → 2.20.0
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 +27 -4
- package/dist/auth.js +4 -4
- package/dist/bundle.js +1199 -180
- package/dist/client.js +4 -1
- package/dist/config.js +37 -0
- package/dist/index.js +13 -9
- package/dist/tool-surface.js +14 -0
- package/dist/tools/_shared.js +5 -1
- package/dist/tools/delivery.js +42 -2
- package/dist/tools/expenses.js +563 -56
- package/dist/tools/healthcheck.js +2 -2
- package/dist/tools/messages.js +2 -1
- package/package.json +3 -3
- package/server.json +14 -2
- package/skills/ofw/SKILL.md +11 -4
- package/skills/ofw-fpx/SKILL.md +5 -2
- package/skills/ofw-fpx/references/requests.md +58 -4
|
@@ -62,7 +62,7 @@ resolve = resolveAuth) {
|
|
|
62
62
|
kind: 'transport',
|
|
63
63
|
// The upstream `.hint` rides along in `error.message` — it carries
|
|
64
64
|
// the actionable "click the toolbar icon" copy this cannot know.
|
|
65
|
-
hint: '
|
|
65
|
+
hint: 'ContextMint Bridge is down, so the browser path could not be tried. This is ' +
|
|
66
66
|
'not a credential problem: OFW_USERNAME/OFW_PASSWORD, if set, were not reached ' +
|
|
67
67
|
'either. See error.message for the extension-specific fix.',
|
|
68
68
|
}
|
|
@@ -71,7 +71,7 @@ resolve = resolveAuth) {
|
|
|
71
71
|
// Now means exactly what it says: nothing is set up. A configured path
|
|
72
72
|
// that was tried and failed no longer lands here.
|
|
73
73
|
no_credential: 'No OFW credential is configured. Either set OFW_USERNAME + OFW_PASSWORD, or install ' +
|
|
74
|
-
'
|
|
74
|
+
'ContextMint Bridge and sign in to ourfamilywizard.com in a tab (unsetting ' +
|
|
75
75
|
'OFW_DISABLE_FETCHPROXY if you set it).',
|
|
76
76
|
credential_rejected: 'OurFamilyWizard rejected the credential. If it came from `env`, the password changed or ' +
|
|
77
77
|
'the account is locked; if from `fetchproxy`, the browser session expired — sign in again ' +
|
package/dist/tools/messages.js
CHANGED
|
@@ -1526,7 +1526,7 @@ export function registerMessageTools(server, client, cacheProvider, attachmentIO
|
|
|
1526
1526
|
});
|
|
1527
1527
|
});
|
|
1528
1528
|
server.registerTool('ofw_download_attachment', {
|
|
1529
|
-
description: 'Download an OFW message attachment by fileId and return content you can actually read. Inline delivery walks a ladder and returns the first rung that works: (1) host-renderable images (PNG/JPEG/GIF/WEBP) come back as ImageContent; (2) .xlsx/.csv/.tsv, .pdf, .docx, .pptx and text files come back as EXTRACTED CONTENT — per-sheet CSV, per-page/slide text, document text — in the response JSON under `extracted`; (3) anything else comes back as an EmbeddedResource blob of the raw bytes. The meta block names the rung as `deliveredVia` and, when it falls through to bytes, lists what was tried in `deliveryAttempts`. Reported mime types are always normalized to a bare media type (no charset/name parameters). In disk mode the bytes are saved to ~/Downloads/ofw-mcp/ and the response carries the absolute path; pass extract:true to ALSO get the extracted content in that response. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var. On a hosted deployment with no filesystem, disk mode is unavailable, so inline is forced (forcedInline:true) rather than failing — a saveTo path never costs you the content. fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo; saveTo must stay inside the attachments directory, and an existing file is never overwritten unless force:true. Re-downloading to the same path is a no-op (disk mode only).',
|
|
1529
|
+
description: 'Download an OFW message attachment by fileId and return content you can actually read. Inline delivery walks a ladder and returns the first rung that works: (1) host-renderable images (PNG/JPEG/GIF/WEBP) come back as ImageContent; (2) .xlsx/.csv/.tsv, .pdf, .docx, .pptx and text files come back as EXTRACTED CONTENT — per-sheet CSV, per-page/slide text, document text — in the response JSON under `extracted`; (3) anything else comes back as an EmbeddedResource blob of the raw bytes. Images and raw bytes are only returned inline up to 10 MiB; a larger file fails with a tool error that says how to get it instead (extracted text is unaffected — it is bounded by maxChars). The meta block names the rung as `deliveredVia` and, when it falls through to bytes, lists what was tried in `deliveryAttempts`. Reported mime types are always normalized to a bare media type (no charset/name parameters). In disk mode the bytes are saved to ~/Downloads/ofw-mcp/ and the response carries the absolute path; pass extract:true to ALSO get the extracted content in that response. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var. On a hosted deployment with no filesystem, disk mode is unavailable, so inline is forced (forcedInline:true) rather than failing — a saveTo path never costs you the content. fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo; saveTo must stay inside the attachments directory, and an existing file is never overwritten unless force:true. Re-downloading to the same path is a no-op (disk mode only).',
|
|
1530
1530
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
1531
1531
|
inputSchema: z.object({
|
|
1532
1532
|
fileId: z.number().describe('Attachment file id (from ofw_get_message → attachments[].fileId)'),
|
|
@@ -1581,6 +1581,7 @@ export function registerMessageTools(server, client, cacheProvider, attachmentIO
|
|
|
1581
1581
|
// ends up holding something readable.
|
|
1582
1582
|
return await buildInlineDelivery({
|
|
1583
1583
|
fileId, fileName, mimeType, bytes, forcedInline, options: deliveryOptions,
|
|
1584
|
+
diskAvailable: attachmentIO.supportsDisk,
|
|
1584
1585
|
});
|
|
1585
1586
|
}
|
|
1586
1587
|
let dest;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"mcpName": "io.github.chrischall/ofw-mcp",
|
|
6
6
|
"description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@chrischall/mcp-utils": "^2.
|
|
38
|
-
"@fetchproxy/bootstrap": "^3.
|
|
37
|
+
"@chrischall/mcp-utils": "^2.8.0",
|
|
38
|
+
"@fetchproxy/bootstrap": "^3.4.1",
|
|
39
39
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
40
40
|
"dotenv": "^18.0.0",
|
|
41
41
|
"zod": "^4.6.5"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/ofw-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.20.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.20.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -64,6 +64,18 @@
|
|
|
64
64
|
"description": "Absolute path for the session cache file. Defaults to $MCP_DATA_DIR/.ofw-mcp/session.json.",
|
|
65
65
|
"isRequired": false,
|
|
66
66
|
"format": "string"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "OFW_EXPENSE_ONLY",
|
|
70
|
+
"description": "Set true to register only healthcheck plus expense tools. All profile/dashboard, message, calendar, and journal tools are structurally absent.",
|
|
71
|
+
"isRequired": false,
|
|
72
|
+
"format": "string"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "OFW_EXPENSE_UPLOAD_ONLY",
|
|
76
|
+
"description": "Set true for strict reimbursement upload mode: only healthcheck, ofw_upload_expense_pdf, ofw_create_expense, and ofw_update_expense register. Implies OFW_EXPENSE_ONLY.",
|
|
77
|
+
"isRequired": false,
|
|
78
|
+
"format": "string"
|
|
67
79
|
}
|
|
68
80
|
]
|
|
69
81
|
}
|
package/skills/ofw/SKILL.md
CHANGED
|
@@ -100,7 +100,7 @@ Always pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter
|
|
|
100
100
|
| `ofw_save_draft(subject, body, recipientIds?, messageId?, replyToId?, myFileIDs?, expectedRevision?, force?)` | Create a new draft. Pass `messageId` to **replace** an existing draft: the tool creates a fresh draft and deletes the old one (OFW's update-in-place endpoint silently no-ops). The returned `id` is the NEW id; the response leads with `draftKey`, which stays the same across every edit — **track that, not the id**. Note: OFW does **not** store recipients on drafts — `recipientIds` are accepted but come back empty (a one-line NOTE says so; supply them at send time instead). Threading warnings fire only on genuine drops — a draft echoing `inReplyTo`/`showContext` IS threaded. |
|
|
101
101
|
| `ofw_delete_draft(messageId)` | Delete a draft. |
|
|
102
102
|
| `ofw_upload_attachment(path, shareClass?, label?, description?)` | Upload a local file to My Files; returns a fileId to pass into `myFileIDs`. Only files inside the upload directory (`OFW_UPLOAD_DIR`, default `~/Downloads/ofw-mcp`) can be uploaded; hidden files are refused. `shareClass:"SHARED"` needs write mode `all`. |
|
|
103
|
-
| `ofw_download_attachment(fileId, inline?, saveTo?, force?, extract?, maxChars?, parts?)` | Download an attachment. Inline delivery returns the first rung that works: image → `ImageContent`; .xlsx/.csv/.pdf/.docx/.pptx/text → **extracted content** under `extracted` (per-sheet CSV, per-page/slide text); anything else → raw bytes. Default writes to `~/Downloads/ofw-mcp/` (add `extract:true` for content too). Use `parts:"1-2"` / a sheet name and `maxChars` on large files. |
|
|
103
|
+
| `ofw_download_attachment(fileId, inline?, saveTo?, force?, extract?, maxChars?, parts?)` | Download an attachment. Inline delivery returns the first rung that works: image → `ImageContent`; .xlsx/.csv/.pdf/.docx/.pptx/text → **extracted content** under `extracted` (per-sheet CSV, per-page/slide text); anything else → raw bytes. Default writes to `~/Downloads/ofw-mcp/` (add `extract:true` for content too). Use `parts:"1-2"` / a sheet name and `maxChars` on large files. Images and raw bytes over 10 MiB are not returned inline (tool error) — use disk mode or open the file in OFW. |
|
|
104
104
|
| `ofw_check_freshness(folders?, messageIds?, allowMarkRead?)` | Cheap live check that the cache still matches OFW — one request for folder counts plus one per id, no bodies, no sync. Each id gets a live `state` (`draft`/`sent`/`received`/`deleted`/`unknown`) plus `folder` and `sentAt`. Probes ids cached as drafts, as sent, or as already-read inbox messages freely; anything else needs `allowMarkRead:true` (it would mark an inbox message read). |
|
|
105
105
|
| `ofw_status(ids?, draftKeys?, includeDraftInventory?, allowMarkRead?)` | **The status call.** One live round trip. With no arguments: the full, server-verified draft inventory. With `ids`/`draftKeys`: each one's live lifecycle state. Top-level `complete` is true only when every part was verified live. |
|
|
106
106
|
|
|
@@ -112,12 +112,19 @@ Always pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter
|
|
|
112
112
|
| `ofw_update_event(eventId, ...)` | Partial update — only pass fields to change |
|
|
113
113
|
| `ofw_delete_event(eventId)` | Permanent delete |
|
|
114
114
|
|
|
115
|
+
### Restricted expense deployments
|
|
116
|
+
|
|
117
|
+
When `OFW_EXPENSE_ONLY=true`, only the healthcheck and expense tools exist. When `OFW_EXPENSE_UPLOAD_ONLY=true`, only `ofw_healthcheck`, `ofw_upload_expense_pdf`, `ofw_create_expense`, and `ofw_update_expense` exist. Do not suggest message/calendar/journal/profile tools in those deployments; they are structurally unregistered.
|
|
118
|
+
|
|
115
119
|
### Expenses
|
|
116
120
|
| Tool | Notes |
|
|
117
121
|
|------|-------|
|
|
118
122
|
| `ofw_get_expense_totals` | Summary of owed/paid totals |
|
|
119
|
-
| `ofw_list_expenses(
|
|
120
|
-
| `
|
|
123
|
+
| `ofw_list_expenses(page?, size?)` | Paginated (1-based page); default size 20; follow `nextPage` |
|
|
124
|
+
| `ofw_list_expense_categories` | Category ids and split metadata (use the id in create/update) |
|
|
125
|
+
| `ofw_upload_expense_pdf(path \| url, fileName?, label?, description?)` | Upload one receipt PDF to My Files (SHARED, co-parent-visible even on a private expense) for later attachment; confirm-gated; mode `all` only |
|
|
126
|
+
| `ofw_create_expense(title, amount, purchaseDate, categoryId, payerId, children, description?, privateExpense?, receiptFileId?)` | Log a new expense; confirm-gated; optionally private with one receipt |
|
|
127
|
+
| `ofw_update_expense(expenseId, …only the fields to change…)` | Reads the expense and keeps every field you omit; `privateExpense:false` publishes a private expense; `receiptFileId` replaces all receipts, `null` clears it or `description`; refuses as `EXPENSE_FIELDS_UNREADABLE` (naming the fields) rather than erase one it can't read; confirm-gated |
|
|
121
128
|
|
|
122
129
|
### Journal
|
|
123
130
|
| Tool | Notes |
|
|
@@ -166,7 +173,7 @@ Message and draft reads come from a local cache, so **a result can be stale with
|
|
|
166
173
|
|---|---|
|
|
167
174
|
| How old is this data? | `freshness` — `staleness` (`fresh`/`unverified`/`stale`), `asOf`, `ageSeconds`, a quotable `warning` |
|
|
168
175
|
| Is this the WHOLE answer? | `complete` on `ofw_list_messages` / `ofw_list_drafts` / `ofw_get_unread_sent` / `ofw_status` |
|
|
169
|
-
| If not, how do I get the rest? | `nextPage` (message tools) or `nextStart` (`
|
|
176
|
+
| If not, how do I get the rest? | `nextPage` (message tools and `ofw_list_expenses`) or `nextStart` (`ofw_list_journal_entries`) — null means there is no more |
|
|
170
177
|
| Is this entity still what I think it is? | `state` from `ofw_status` / `ofw_check_freshness` |
|
|
171
178
|
|
|
172
179
|
Rules:
|
package/skills/ofw-fpx/SKILL.md
CHANGED
|
@@ -33,13 +33,16 @@ dry-run/confirm here — curl just does it. Treat every write like the MCP's
|
|
|
33
33
|
npm install -g @fetchproxy/cli # provides `fpx`
|
|
34
34
|
fpx profile add ofw --domain ourfamilywizard.com
|
|
35
35
|
fpx profile declare ofw --local-storage auth --local-storage tokenExpiry
|
|
36
|
-
fpx pair -p ofw # prints a pair code → approve in
|
|
36
|
+
fpx pair -p ofw # prints a pair code → approve in ContextMint Bridge
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Requirements: the **
|
|
39
|
+
Requirements: the **ContextMint Bridge** browser extension installed
|
|
40
|
+
([releases](https://github.com/nullnet-app/contextmint-bridge/releases) —
|
|
41
|
+
Chrome: load the Chrome zip unpacked; Safari isn't available yet, so use Chrome for now), with an
|
|
40
42
|
open, signed-in `ofw.ourfamilywizard.com` (or `www.ourfamilywizard.com`)
|
|
41
43
|
tab, and its Chrome **Site access** allowing `ourfamilywizard.com`. Pairing
|
|
42
44
|
persists across invocations.
|
|
45
|
+
ContextMint Bridge is the fetchproxy extension renamed, same maintainer (see https://github.com/chrischall/fetchproxy#extension); source at https://github.com/nullnet-app/contextmint-bridge — build it, or verify a release zip with `shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256`.
|
|
43
46
|
|
|
44
47
|
## Capture the token (once per shell / whenever it goes stale)
|
|
45
48
|
|
|
@@ -197,18 +197,72 @@ curl -s -X DELETE "https://ofw.ourfamilywizard.com/pub/v1/calendar/events/${EVEN
|
|
|
197
197
|
curl -s 'https://ofw.ourfamilywizard.com/pub/v2/expense/expenses/totals' "${AUTH_HEADERS[@]}" | jq .
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
**List expenses** (
|
|
200
|
+
**List expenses** (page-based, 1-indexed `page`):
|
|
201
|
+
|
|
202
|
+
OFW currently ignores the legacy `start`/`max` query parameters and returns
|
|
203
|
+
page 1 repeatedly. Use `page`/`size` and follow the response metadata's
|
|
204
|
+
`last` flag / `currentPage`.
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
curl -s 'https://ofw.ourfamilywizard.com/pub/v2/expense/expenses?page=1&size=20' "${AUTH_HEADERS[@]}" | jq .
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Upload a receipt PDF** (write to My Files). It must be `SHARED` — that is the
|
|
211
|
+
share class OFW accepts in an expense's `fileIds` — and a SHARED file is
|
|
212
|
+
visible to the co-parent in My Files as soon as it lands:
|
|
201
213
|
|
|
202
214
|
```sh
|
|
203
|
-
curl -s 'https://ofw.ourfamilywizard.com/pub/
|
|
215
|
+
curl -s -X POST 'https://ofw.ourfamilywizard.com/pub/v3/myfiles/multipart' \
|
|
216
|
+
"${AUTH_HEADERS[@]}" \
|
|
217
|
+
-F "file=@/path/to/receipt.pdf;type=application/pdf" \
|
|
218
|
+
-F 'source=expense' \
|
|
219
|
+
-F 'description=receipt.pdf' \
|
|
220
|
+
-F 'label=receipt.pdf' \
|
|
221
|
+
-F 'fileName=receipt.pdf' \
|
|
222
|
+
-F 'shared=true' \
|
|
223
|
+
-F 'shareClass=SHARED' | jq .
|
|
204
224
|
```
|
|
205
225
|
|
|
226
|
+
Use the returned `fileId` as `receiptFileId` when creating the expense.
|
|
227
|
+
|
|
206
228
|
**Create an expense** (write):
|
|
207
229
|
|
|
230
|
+
The current OFW expense form requires a complete expense record. In addition to
|
|
231
|
+
the amount, send the expense title, purchase date, category id, parent who owes
|
|
232
|
+
(`payerId`), and at least one child user id. Description, visibility, and a
|
|
233
|
+
single receipt file are optional.
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
curl -s -X POST 'https://ofw.ourfamilywizard.com/pub/v2/expense' \
|
|
237
|
+
"${AUTH_HEADERS[@]}" -H 'Content-Type: application/json' \
|
|
238
|
+
--data '{
|
|
239
|
+
"title":"Soccer cleats",
|
|
240
|
+
"amount":45.00,
|
|
241
|
+
"purchaseDate":"2026-09-28",
|
|
242
|
+
"categoryId":1,
|
|
243
|
+
"payerId":<PAYER_USER_ID>,
|
|
244
|
+
"children":[<CHILD_USER_ID>],
|
|
245
|
+
"description":"Cleats for soccer",
|
|
246
|
+
"isPrivate":false,
|
|
247
|
+
"fileIds":[<RECEIPT_FILE_ID>]
|
|
248
|
+
}' | jq .
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Use OFW numeric ids, not display labels, for `categoryId`, `payerId`, and
|
|
252
|
+
`children`. For a private expense (visible only to you), send `isPrivate:true`;
|
|
253
|
+
the MCP tools expose this as `privateExpense`. The MCP tools attach at most one
|
|
254
|
+
receipt, sent as a one-element `fileIds` array.
|
|
255
|
+
|
|
256
|
+
**Update an expense** (write; full payload, not a patch — e.g. publish a
|
|
257
|
+
private expense by sending `isPrivate:false`). A field left out of the PUT is
|
|
258
|
+
erased, so GET the expense first and send back every field you are not
|
|
259
|
+
changing (the MCP's `ofw_update_expense` does this for you):
|
|
260
|
+
|
|
208
261
|
```sh
|
|
209
|
-
curl -s
|
|
262
|
+
curl -s "https://ofw.ourfamilywizard.com/pub/v2/expense/expenses/${EXPENSE_ID}" "${AUTH_HEADERS[@]}" | jq .
|
|
263
|
+
curl -s -X PUT "https://ofw.ourfamilywizard.com/pub/v2/expense/expenses/${EXPENSE_ID}" \
|
|
210
264
|
"${AUTH_HEADERS[@]}" -H 'Content-Type: application/json' \
|
|
211
|
-
--data '{
|
|
265
|
+
--data '{ …every current field…, "isPrivate":false }' | jq .
|
|
212
266
|
```
|
|
213
267
|
|
|
214
268
|
## 8. Journal
|