ofw-mcp 2.19.4 → 2.19.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/dist/auth.js +4 -4
- package/dist/bundle.js +722 -133
- package/dist/index.js +1 -1
- package/dist/tools/delivery.js +42 -2
- package/dist/tools/healthcheck.js +2 -2
- package/dist/tools/messages.js +2 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/ofw/SKILL.md +1 -1
- package/skills/ofw-fpx/SKILL.md +5 -2
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ const nodeAttachmentIO = new NodeAttachmentIO();
|
|
|
36
36
|
// always succeeds before any credential check runs.
|
|
37
37
|
await runMcp({
|
|
38
38
|
name: 'ofw',
|
|
39
|
-
version: '2.19.
|
|
39
|
+
version: '2.19.5', // x-release-please-version
|
|
40
40
|
deps: client,
|
|
41
41
|
tools: [
|
|
42
42
|
registerHealthcheckTools,
|
package/dist/tools/delivery.js
CHANGED
|
@@ -14,12 +14,29 @@
|
|
|
14
14
|
// 2. extractable document → the FILE'S TEXT, as text (see src/extract)
|
|
15
15
|
// 3. raw bytes → base64 EmbeddedResource, as before
|
|
16
16
|
//
|
|
17
|
+
// Rungs 1 and 3 are bounded by MAX_INLINE_BYTES (10 MiB raw; see below).
|
|
17
18
|
// Rung 3 never disappears, so nothing regresses; rung 2 is what makes a
|
|
18
19
|
// spreadsheet, PDF, Word or PowerPoint attachment readable at all. When a rung
|
|
19
20
|
// is skipped or fails, the response says so by name in `deliveryAttempts` —
|
|
20
21
|
// a caller must never be left guessing why it got bytes instead of content.
|
|
21
22
|
import { extractAttachment } from '../extract/index.js';
|
|
22
23
|
import { isHostRenderableImage } from './attachments.js';
|
|
24
|
+
/**
|
|
25
|
+
* Upper bound on the RAW bytes this tool will return inline (as ImageContent
|
|
26
|
+
* or a base64 EmbeddedResource). Extracted text is not subject to it — that is
|
|
27
|
+
* already bounded by `maxChars`.
|
|
28
|
+
*
|
|
29
|
+
* Why 10 MiB: mcp-host, the hosted runtime that runs this MCP for claude.ai,
|
|
30
|
+
* caps any single child result at 14 MiB of serialized JSON-RPC
|
|
31
|
+
* (`CHILD_RESULT_MAX_BYTES`, chrischall/mcp-host#952) and replaces anything
|
|
32
|
+
* larger with a generic "result too large" tool error. 10 MiB raw is ~13.3 MiB
|
|
33
|
+
* as base64, which with the JSON-RPC envelope still fits under 14 MiB — so this
|
|
34
|
+
* bound fires first, with a message that says what to do instead. claude.ai
|
|
35
|
+
* itself accepts images up to 10 MB each (measured 2026-09-27), and before
|
|
36
|
+
* #952 anything over 10 MiB killed the child process outright. Real traffic
|
|
37
|
+
* for this tool has peaked at ~3 MB, so normal use never reaches it.
|
|
38
|
+
*/
|
|
39
|
+
export const MAX_INLINE_BYTES = 10 * 1024 * 1024;
|
|
23
40
|
/**
|
|
24
41
|
* Attempt extraction, converting every failure into a REASON rather than an
|
|
25
42
|
* exception: a format we cannot read must still be delivered as bytes, and the
|
|
@@ -42,6 +59,26 @@ export async function tryExtract(bytes, mimeType, fileName, opts) {
|
|
|
42
59
|
return { reason: `extraction failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
43
60
|
}
|
|
44
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Refuse to inline more than {@link MAX_INLINE_BYTES}. This is a tool error
|
|
64
|
+
* rather than a fallback: the only non-inline channel is disk mode, which the
|
|
65
|
+
* hosted runtime (the one the cap protects) does not have, and silently
|
|
66
|
+
* writing to disk when the caller asked for content would not be honest.
|
|
67
|
+
*/
|
|
68
|
+
function assertInlineSize(input, rawRequested) {
|
|
69
|
+
const { bytes, fileName, fileId, diskAvailable } = input;
|
|
70
|
+
if (bytes.length <= MAX_INLINE_BYTES)
|
|
71
|
+
return;
|
|
72
|
+
const hints = [];
|
|
73
|
+
if (rawRequested)
|
|
74
|
+
hints.push('omit extract:false to get the file\'s extracted text instead, if it is a readable document type');
|
|
75
|
+
if (diskAvailable)
|
|
76
|
+
hints.push('pass inline:false to save it to disk and get the path');
|
|
77
|
+
hints.push('or open it directly in OurFamilyWizard');
|
|
78
|
+
throw new Error(`Attachment ${fileId} (${fileName}) is ${bytes.length} bytes, over the ${MAX_INLINE_BYTES / (1024 * 1024)} MiB `
|
|
79
|
+
+ `limit for returning a file inline (a larger result would exceed the host's response size limit). `
|
|
80
|
+
+ `To get it: ${hints.join('; ')}.`);
|
|
81
|
+
}
|
|
45
82
|
/**
|
|
46
83
|
* Build the content blocks for an inline download by walking the ladder.
|
|
47
84
|
* The first block is always a JSON meta block naming `deliveredVia`, so the
|
|
@@ -57,6 +94,7 @@ export async function buildInlineDelivery(input) {
|
|
|
57
94
|
const block = () => ({ type: 'text', text: JSON.stringify(meta, null, 2) });
|
|
58
95
|
// Rung 1 — the host renders these itself, and a picture beats a description.
|
|
59
96
|
if (isHostRenderableImage(mimeType)) {
|
|
97
|
+
assertInlineSize(input, false);
|
|
60
98
|
meta.deliveredVia = 'image';
|
|
61
99
|
return { content: [block(), { type: 'image', data: bytes.toString('base64'), mimeType }] };
|
|
62
100
|
}
|
|
@@ -80,8 +118,10 @@ export async function buildInlineDelivery(input) {
|
|
|
80
118
|
/* v8 ignore next -- tryExtract always sets `reason` when it returns no extraction */
|
|
81
119
|
attempts.push(outcome.reason ?? 'extraction produced no content');
|
|
82
120
|
}
|
|
83
|
-
// Rung 3 — the bytes themselves. Always available
|
|
84
|
-
// never ends with the caller holding nothing
|
|
121
|
+
// Rung 3 — the bytes themselves. Always available up to MAX_INLINE_BYTES, so
|
|
122
|
+
// a fetch that succeeded never ends with the caller holding nothing; beyond
|
|
123
|
+
// it the caller gets a tool error that says how to reach the file instead.
|
|
124
|
+
assertInlineSize(input, options.extract === false);
|
|
85
125
|
meta.deliveredVia = 'blob';
|
|
86
126
|
meta.deliveryAttempts = attempts;
|
|
87
127
|
meta.note = 'Returned as raw bytes. Some hosts cannot render an embedded resource of this type; '
|
|
@@ -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.19.
|
|
3
|
+
"version": "2.19.5",
|
|
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.19.
|
|
9
|
+
"version": "2.19.5",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.19.
|
|
14
|
+
"version": "2.19.5",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
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
|
|
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
|
|