ofw-mcp 2.10.1 → 2.10.2
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 +0 -11
- package/dist/auth-password.js +1 -1
- package/dist/bundle.js +8 -17
- package/dist/cache/node.js +2 -2
- package/dist/cache/store.js +8 -8
- package/dist/client.js +5 -5
- package/dist/config.js +4 -4
- package/dist/extract/inflate.js +2 -2
- package/dist/extract/xml.js +1 -1
- package/dist/extract/zip.js +5 -4
- package/dist/index.js +4 -4
- package/dist/sync.js +4 -4
- package/dist/tools/attachments.js +2 -2
- package/dist/tools/draft-freshness.js +1 -1
- package/dist/tools/lifecycle.js +7 -7
- package/dist/tools/messages.js +4 -4
- package/package.json +3 -12
- package/server.json +2 -2
- package/skills/ofw/SKILL.md +0 -1
- package/dist/ofw-auth.js +0 -26
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "OurFamilyWizard tools for Claude Code",
|
|
9
|
-
"version": "2.10.
|
|
9
|
+
"version": "2.10.2"
|
|
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.10.
|
|
17
|
+
"version": "2.10.2",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/README.md
CHANGED
|
@@ -124,17 +124,6 @@ Environment variables always take priority over the `.env` file. You can also pa
|
|
|
124
124
|
OFW_USERNAME=you@example.com OFW_PASSWORD=yourpass node dist/index.js
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
## Hosted connector (Cloudflare Worker)
|
|
128
|
-
|
|
129
|
-
Instead of running `ofw-mcp` locally, you can add it to [claude.ai](https://claude.ai) as a **remote MCP connector** — a hosted Cloudflare Worker you reach from Settings → Connectors on Claude web, desktop, or mobile (connectors sync across all three). The same tool registrars back both targets, so the tools and behaviour are identical to the local stdio install; the Worker just wraps them with [`@chrischall/mcp-connector`](https://www.npmjs.com/package/@chrischall/mcp-connector) (the shared OAuth + streamable-HTTP harness) and a per-user [Durable Object](src/cache/durable.ts) cache in place of the local SQLite file.
|
|
130
|
-
|
|
131
|
-
- **How you connect.** Each person you share the connector URL with logs in through the connector's own OAuth page with their **own** OurFamilyWizard email and password. Those credentials are stored (encrypted at rest) per user because OFW bearer tokens expire after ~6h with no refresh token, so the connector must be able to re-login on its own. One user can never see another's account or cache.
|
|
132
|
-
- **Attachments are inline-only.** The Worker has no local filesystem, so `ofw_download_attachment` always returns content as MCP content blocks (`OFW_INLINE_ATTACHMENTS=true`) rather than writing to disk. Spreadsheets, PDFs and Office documents come back as extracted text/CSV, so they are readable even though the host cannot render the file itself.
|
|
133
|
-
- **Write mode defaults to `all`.** The hosted connector registers every tool by default, configurable per deployment via `OFW_WRITE_MODE` / `OFW_CALENDAR_WRITES` in `wrangler.jsonc` — see [Write protection](#write-protection-ofw_write_mode).
|
|
134
|
-
- **Message sync is bounded and resumable.** To stay under Cloudflare's per-request subrequest cap, `ofw_sync_messages` on the hosted connector caps how many OFW requests one call makes (`OFW_SYNC_MAX_REQUESTS` in `wrangler.jsonc`, default `40`) and resumes across calls, so a large mailbox backfills over multiple `ofw_sync_messages` calls rather than one; the local stdio server is unbounded. See [`docs/DEPLOY-CONNECTOR.md`](docs/DEPLOY-CONNECTOR.md#sync--the-subrequest-limit).
|
|
135
|
-
|
|
136
|
-
Standing this up requires a Cloudflare account and is a one-time setup for whoever hosts it; after that the `deploy-connector` job in `release-please.yml` deploys each release automatically (and **Actions → deploy-connector → Run workflow** deploys any ref on demand) — see [`docs/DEPLOY-CONNECTOR.md`](docs/DEPLOY-CONNECTOR.md) for the full runbook. `wrangler.jsonc` serves the Worker at a custom domain (`https://connector.ofw.nullnet.app/mcp`) plus the account's `*.workers.dev` URL; whoever hosts it uses their own domain. The local stdio / `.mcpb` install above remains the desktop-only alternative if you'd rather run it against just your own account.
|
|
137
|
-
|
|
138
127
|
## Available tools
|
|
139
128
|
|
|
140
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.
|
package/dist/auth-password.js
CHANGED
|
@@ -47,7 +47,7 @@ export async function loginWithPassword(username, password) {
|
|
|
47
47
|
// OFW rejects bad credentials by re-serving its HTML login page (Spring
|
|
48
48
|
// Security re-renders the form rather than returning 401/JSON). Surface a
|
|
49
49
|
// clean, actionable message instead of dumping the HTML page — this is what
|
|
50
|
-
//
|
|
50
|
+
// a hosted deployment's login page shows the user on a failed sign-in.
|
|
51
51
|
if (contentType.includes('text/html')) {
|
|
52
52
|
throw new Error('OFW login failed — your OurFamilyWizard email or password was not accepted. Check them and try again.');
|
|
53
53
|
}
|
package/dist/bundle.js
CHANGED
|
@@ -39074,7 +39074,7 @@ async function loginWithPassword(username, password) {
|
|
|
39074
39074
|
// package.json
|
|
39075
39075
|
var package_default = {
|
|
39076
39076
|
name: "ofw-mcp",
|
|
39077
|
-
version: "2.10.
|
|
39077
|
+
version: "2.10.2",
|
|
39078
39078
|
license: "MIT",
|
|
39079
39079
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
39080
39080
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -39103,10 +39103,7 @@ var package_default = {
|
|
|
39103
39103
|
dev: "node --env-file=.env dist/index.js",
|
|
39104
39104
|
test: "vitest run",
|
|
39105
39105
|
"test:coverage": "vitest run --coverage",
|
|
39106
|
-
"test:watch": "vitest"
|
|
39107
|
-
"worker:dev": "wrangler dev",
|
|
39108
|
-
"worker:deploy": "wrangler deploy",
|
|
39109
|
-
"worker:test": "vitest run --config vitest.workers.config.ts"
|
|
39106
|
+
"test:watch": "vitest"
|
|
39110
39107
|
},
|
|
39111
39108
|
dependencies: {
|
|
39112
39109
|
"@chrischall/mcp-utils": "^0.14.0",
|
|
@@ -39116,17 +39113,11 @@ var package_default = {
|
|
|
39116
39113
|
zod: "^4.4.3"
|
|
39117
39114
|
},
|
|
39118
39115
|
devDependencies: {
|
|
39119
|
-
"@chrischall/mcp-connector": "^1.1.1",
|
|
39120
|
-
"@cloudflare/vitest-pool-workers": "^0.19.1",
|
|
39121
|
-
"@cloudflare/workers-oauth-provider": "^0.8.1",
|
|
39122
|
-
"@cloudflare/workers-types": "^5.20260708.1",
|
|
39123
39116
|
"@types/node": "^26.0.0",
|
|
39124
39117
|
"@vitest/coverage-v8": "^4.1.7",
|
|
39125
|
-
agents: "^0.19.0",
|
|
39126
39118
|
esbuild: "^0.28.0",
|
|
39127
39119
|
typescript: "^7.0.2",
|
|
39128
|
-
vitest: "^4.1.7"
|
|
39129
|
-
wrangler: "^4.110.0"
|
|
39120
|
+
vitest: "^4.1.7"
|
|
39130
39121
|
}
|
|
39131
39122
|
};
|
|
39132
39123
|
|
|
@@ -39232,7 +39223,7 @@ var OFWClient = class {
|
|
|
39232
39223
|
// Optional injected auth resolver. When set, the refresh callback uses it
|
|
39233
39224
|
// instead of the module-level global `resolveAuth` (env-var → fetchproxy
|
|
39234
39225
|
// priority). A hosted per-user deployment injects its own resolver so each
|
|
39235
|
-
// request carries that user's credentials — see the
|
|
39226
|
+
// request carries that user's credentials — see the per-user
|
|
39236
39227
|
// deployment. Left undefined by the stdio path, which falls back to the
|
|
39237
39228
|
// global resolver, keeping that behaviour byte-for-byte identical.
|
|
39238
39229
|
authResolver;
|
|
@@ -43302,7 +43293,7 @@ var OFWCacheCore = class {
|
|
|
43302
43293
|
}
|
|
43303
43294
|
/**
|
|
43304
43295
|
* Batch upsert every row in a single transaction — one round-trip's worth of
|
|
43305
|
-
* work (crucial
|
|
43296
|
+
* work (crucial where each round trip is a billed request).
|
|
43306
43297
|
* Empty array is a no-op (no transaction opened).
|
|
43307
43298
|
*/
|
|
43308
43299
|
upsertMessages(rows) {
|
|
@@ -43445,8 +43436,8 @@ var OFWCacheCore = class {
|
|
|
43445
43436
|
return r ? lineageFromDb(r) : null;
|
|
43446
43437
|
}
|
|
43447
43438
|
/**
|
|
43448
|
-
* Batch read — one query for a whole page of drafts.
|
|
43449
|
-
*
|
|
43439
|
+
* Batch read — one query for a whole page of drafts. Where the cache is
|
|
43440
|
+
* remote each cache call is a subrequest, so a per-draft lookup would spend
|
|
43450
43441
|
* the caller's sync budget on bookkeeping.
|
|
43451
43442
|
*/
|
|
43452
43443
|
getDraftLineageByIds(ids) {
|
|
@@ -43730,7 +43721,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
|
|
|
43730
43721
|
var nodeAttachmentIO = new NodeAttachmentIO();
|
|
43731
43722
|
await runMcp({
|
|
43732
43723
|
name: "ofw",
|
|
43733
|
-
version: "2.10.
|
|
43724
|
+
version: "2.10.2",
|
|
43734
43725
|
// x-release-please-version
|
|
43735
43726
|
deps: client,
|
|
43736
43727
|
tools: [
|
package/dist/cache/node.js
CHANGED
|
@@ -5,8 +5,8 @@ import { OFWCacheCore, LocalCacheStore } from './store.js';
|
|
|
5
5
|
// The `node:sqlite` backend for the OFW message cache — a local on-disk SQLite
|
|
6
6
|
// file used by the stdio/desktop server. The query logic lives in OFWCacheCore
|
|
7
7
|
// (src/cache/store.ts); this file only adapts `node:sqlite` to the SqlDriver
|
|
8
|
-
// surface and manages the file handle + permissions.
|
|
9
|
-
//
|
|
8
|
+
// surface and manages the file handle + permissions. Another deployment can
|
|
9
|
+
// adapt a different driver to the same surface.
|
|
10
10
|
/** Adapts a `node:sqlite` DatabaseSync to the driver surface the core needs. */
|
|
11
11
|
export class NodeSqlDriver {
|
|
12
12
|
db;
|
package/dist/cache/store.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// All message reads (list/get/drafts/unread-sent) are served from this cache;
|
|
4
4
|
// only ofw_sync_messages walks OFW for new content. The SQL lives here ONCE,
|
|
5
5
|
// over a tiny synchronous {@link SqlDriver}, so the same schema/queries back
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// any engine: `node:sqlite` is the one that ships (src/cache/node.ts), and
|
|
7
|
+
// another deployment can adapt a different driver to the same surface. This
|
|
8
|
+
// module imports nothing platform-specific.
|
|
9
9
|
function rowFromDb(r) {
|
|
10
10
|
return {
|
|
11
11
|
id: r.id,
|
|
@@ -131,7 +131,7 @@ export const SCHEMA_STATEMENTS = [
|
|
|
131
131
|
* every open. SQLite has no `ADD COLUMN IF NOT EXISTS`, so each statement runs
|
|
132
132
|
* inside a try/catch — re-running against an already-migrated DB throws
|
|
133
133
|
* "duplicate column name", which is swallowed. Driver-agnostic: both
|
|
134
|
-
* `node:sqlite`
|
|
134
|
+
* `node:sqlite` raises synchronously, as any conforming driver must.
|
|
135
135
|
*/
|
|
136
136
|
export const MIGRATIONS = [
|
|
137
137
|
// Resumable deep-sync cursor. Absent/NULL → SyncState.resumePage null.
|
|
@@ -222,7 +222,7 @@ export class OFWCacheCore {
|
|
|
222
222
|
}
|
|
223
223
|
/**
|
|
224
224
|
* Batch upsert every row in a single transaction — one round-trip's worth of
|
|
225
|
-
* work (crucial
|
|
225
|
+
* work (crucial where each round trip is a billed request).
|
|
226
226
|
* Empty array is a no-op (no transaction opened).
|
|
227
227
|
*/
|
|
228
228
|
upsertMessages(rows) {
|
|
@@ -351,8 +351,8 @@ export class OFWCacheCore {
|
|
|
351
351
|
return r ? lineageFromDb(r) : null;
|
|
352
352
|
}
|
|
353
353
|
/**
|
|
354
|
-
* Batch read — one query for a whole page of drafts.
|
|
355
|
-
*
|
|
354
|
+
* Batch read — one query for a whole page of drafts. Where the cache is
|
|
355
|
+
* remote each cache call is a subrequest, so a per-draft lookup would spend
|
|
356
356
|
* the caller's sync budget on bookkeeping.
|
|
357
357
|
*/
|
|
358
358
|
getDraftLineageByIds(ids) {
|
|
@@ -463,7 +463,7 @@ export class OFWCacheCore {
|
|
|
463
463
|
}
|
|
464
464
|
/**
|
|
465
465
|
* Adapts a synchronous {@link OFWCacheCore} to the async {@link CacheStore}
|
|
466
|
-
* interface. Used by the in-process node backend;
|
|
466
|
+
* interface. Used by the in-process node backend; a remote backend
|
|
467
467
|
* implements CacheStore over a real RPC boundary instead.
|
|
468
468
|
*/
|
|
469
469
|
export class LocalCacheStore {
|
package/dist/client.js
CHANGED
|
@@ -6,16 +6,16 @@ import { resolveAuth } from './auth.js';
|
|
|
6
6
|
import { BASE_URL, OFW_PROTOCOL_HEADERS, OFW_TOKEN_TTL_MS, OFW_TOKEN_EXPIRY_SKEW_MS } from './protocol.js';
|
|
7
7
|
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
|
|
8
8
|
// bundle). loadDotenvSafely applies override:false + quiet:true and swallows a
|
|
9
|
-
// missing dotenv module. The try/catch additionally guards
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// missing dotenv module. The try/catch additionally guards a runtime where
|
|
10
|
+
// `import.meta.url` is undefined and `fileURLToPath(undefined)` would
|
|
11
|
+
// otherwise throw at module init (a failure at
|
|
12
12
|
// startup validation) — there is no filesystem / .env to load there anyway.
|
|
13
13
|
try {
|
|
14
14
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
15
15
|
await loadDotenvSafely({ path: join(dir, '..', '.env') });
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
|
-
/* v8 ignore next -- only reached in a non-Node runtime
|
|
18
|
+
/* v8 ignore next -- only reached in a non-Node runtime: no .env to load */
|
|
19
19
|
}
|
|
20
20
|
// Parse a Content-Disposition header for a filename. Prefers RFC 6266
|
|
21
21
|
// `filename*=UTF-8''…` (percent-decoded) and falls back to `filename="…"`.
|
|
@@ -69,7 +69,7 @@ export class OFWClient {
|
|
|
69
69
|
// Optional injected auth resolver. When set, the refresh callback uses it
|
|
70
70
|
// instead of the module-level global `resolveAuth` (env-var → fetchproxy
|
|
71
71
|
// priority). A hosted per-user deployment injects its own resolver so each
|
|
72
|
-
// request carries that user's credentials — see the
|
|
72
|
+
// request carries that user's credentials — see the per-user
|
|
73
73
|
// deployment. Left undefined by the stdio path, which falls back to the
|
|
74
74
|
// global resolver, keeping that behaviour byte-for-byte identical.
|
|
75
75
|
authResolver;
|
package/dist/config.js
CHANGED
|
@@ -144,16 +144,16 @@ export function getAutoRefreshStaleReads() {
|
|
|
144
144
|
}
|
|
145
145
|
// Default for ofw_download_attachment's `inline` arg when the caller doesn't
|
|
146
146
|
// pass one. Set OFW_INLINE_ATTACHMENTS=true to have attachments returned as
|
|
147
|
-
// MCP content blocks by default (skipping disk) —
|
|
148
|
-
//
|
|
147
|
+
// MCP content blocks by default (skipping disk) — necessary wherever the
|
|
148
|
+
// caller cannot read the server's filesystem.
|
|
149
149
|
export function getDefaultInlineAttachments() {
|
|
150
150
|
return parseBoolEnv('OFW_INLINE_ATTACHMENTS');
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
153
|
* Per-invocation OFW-request budget for ofw_sync_messages.
|
|
154
154
|
*
|
|
155
|
-
*
|
|
156
|
-
* (every OFW API fetch and every
|
|
155
|
+
* A hosted deployment may enforce a request cap per call
|
|
156
|
+
* (every OFW API fetch and every cache round trip counts), so a deep
|
|
157
157
|
* backfill must be bounded and resumable there. Set OFW_SYNC_MAX_REQUESTS to a
|
|
158
158
|
* positive integer to cap the number of OFW requests one sync call may make
|
|
159
159
|
* before pausing; the next call resumes the walk (deep or not) where it left off.
|
package/dist/extract/inflate.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// decompressed stream chunk by chunk, and abort the moment the running total
|
|
13
13
|
// passes the limit. Peak memory is then bounded by the limit rather than by
|
|
14
14
|
// whatever the file felt like claiming.
|
|
15
|
-
/** 32 MiB. Sized to fit comfortably inside
|
|
15
|
+
/** 32 MiB. Sized to fit comfortably inside a constrained memory budget. */
|
|
16
16
|
export const MAX_DECOMPRESSED_BYTES = 32 * 1024 * 1024;
|
|
17
17
|
/**
|
|
18
18
|
* Thrown when decompression is aborted for exceeding its cap. Distinct from a
|
|
@@ -31,7 +31,7 @@ export class DecompressionLimitError extends Error {
|
|
|
31
31
|
*
|
|
32
32
|
* `deflate-raw` is the ZIP member format; `deflate` is the zlib-wrapped form a
|
|
33
33
|
* PDF `/FlateDecode` stream uses. Both go through the WHATWG
|
|
34
|
-
* `DecompressionStream` so this runs unchanged
|
|
34
|
+
* `DecompressionStream` so this runs unchanged wherever the standard exists.
|
|
35
35
|
*/
|
|
36
36
|
export async function inflateBounded(data, format, limit, label) {
|
|
37
37
|
const stream = new Blob([data]).stream()
|
package/dist/extract/xml.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The office formats are machine-generated XML with a known, flat shape per
|
|
4
4
|
// part (rows of cells, runs of text), so a scanning reader beats pulling in a
|
|
5
|
-
// DOM parser that has to run
|
|
5
|
+
// DOM parser that has to run anywhere. The one rule this file
|
|
6
6
|
// exists to enforce is that a tag match is anchored on the FULL tag name:
|
|
7
7
|
// naively scanning for `<w:p` also matches `<w:pPr>`, which silently turns
|
|
8
8
|
// paragraph properties into paragraphs.
|
package/dist/extract/zip.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// reading one is the first step of every office-document extractor. This is
|
|
5
5
|
// deliberately not a general ZIP library: it reads the central directory,
|
|
6
6
|
// slices an entry's bytes, and inflates DEFLATE members via the WHATWG
|
|
7
|
-
// `DecompressionStream` —
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// `DecompressionStream` — a web standard available in Node ≥18 and in
|
|
8
|
+
// sandboxed runtimes alike, so the
|
|
9
|
+
// same code runs on the stdio server and a hosted deployment. Using
|
|
10
|
+
// `node:zlib` here would tie this to Node; adding a userland inflate
|
|
10
11
|
// dependency would bloat it. Neither is necessary.
|
|
11
12
|
import { inflateBounded, MAX_DECOMPRESSED_BYTES } from './inflate.js';
|
|
12
13
|
const EOCD_SIG = 0x06054b50;
|
|
@@ -16,7 +17,7 @@ const ZIP64_SENTINEL = 0xffffffff;
|
|
|
16
17
|
/**
|
|
17
18
|
* Hard ceiling on a single decompressed member (32 MiB). An attachment is a
|
|
18
19
|
* co-parent-supplied file, so a zip bomb is a real (if unlikely) input, and the
|
|
19
|
-
*
|
|
20
|
+
* memory budget of a constrained runtime is what is being protected.
|
|
20
21
|
*
|
|
21
22
|
* The cap is enforced on the bytes as they arrive ({@link inflateBounded}), NOT
|
|
22
23
|
* on the size the archive declares for itself. The declared size is checked too
|
package/dist/index.js
CHANGED
|
@@ -21,9 +21,9 @@ import { getCacheDbPath } from './config.js';
|
|
|
21
21
|
import { NodeAttachmentIO } from './tools/attachments.js';
|
|
22
22
|
// The stdio server backs the message cache with a local `node:sqlite` file,
|
|
23
23
|
// opened lazily on first use (so the server still boots and answers the host's
|
|
24
|
-
// install-time tools/list probe when no cache path is configured).
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// install-time tools/list probe when no cache path is configured). Both the
|
|
25
|
+
// CacheStore and the AttachmentIO are injected, so a deployment with no usable
|
|
26
|
+
// disk supplies its own implementations to the same registrar instead.
|
|
27
27
|
let nodeCache;
|
|
28
28
|
const nodeCacheProvider = () => (nodeCache ??= OFWCache.open(getCacheDbPath()));
|
|
29
29
|
const nodeAttachmentIO = new NodeAttachmentIO();
|
|
@@ -35,7 +35,7 @@ const nodeAttachmentIO = new NodeAttachmentIO();
|
|
|
35
35
|
// always succeeds before any credential check runs.
|
|
36
36
|
await runMcp({
|
|
37
37
|
name: 'ofw',
|
|
38
|
-
version: '2.10.
|
|
38
|
+
version: '2.10.2', // x-release-please-version
|
|
39
39
|
deps: client,
|
|
40
40
|
tools: [
|
|
41
41
|
registerUserTools,
|
package/dist/sync.js
CHANGED
|
@@ -227,8 +227,8 @@ async function walkPages(client, folder, folderId, opts, store) {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
// Flush the page's rows in one transaction/RPC. Skipped entirely when the
|
|
230
|
-
// page held nothing new:
|
|
231
|
-
//
|
|
230
|
+
// page held nothing new: where the cache is remote this call is a round
|
|
231
|
+
// trip, counting against the same subrequest budget as an OFW fetch.
|
|
232
232
|
// A deep re-walk crosses page after page of already-cached messages, so an
|
|
233
233
|
// unconditional "no-op" write spends the caller's budget to store nothing.
|
|
234
234
|
if (toUpsert.length > 0)
|
|
@@ -294,7 +294,7 @@ export async function syncMessageFolder(client, folder, folderId, opts, store) {
|
|
|
294
294
|
// This was a real starvation bug. `fwd.nextPage` is just the start page
|
|
295
295
|
// (1) when nothing was fetched, so the `Math.min` below would silently
|
|
296
296
|
// reset a deep backfill — e.g. resumePage 87 → 1 — discarding 86 pages
|
|
297
|
-
// of progress.
|
|
297
|
+
// of progress. Under a request budget (OFW_SYNC_MAX_REQUESTS) a user
|
|
298
298
|
// with enough drafts to consume the whole budget, with drafts running
|
|
299
299
|
// first, hit this on EVERY call: inbox/sent never got budget, their
|
|
300
300
|
// cursor was reset every time, and the backfill could never advance.
|
|
@@ -502,7 +502,7 @@ export async function syncAll(client, opts, store) {
|
|
|
502
502
|
// Drafts go FIRST. They are the only folder a destructive tool
|
|
503
503
|
// (ofw_save_draft / ofw_delete_draft) reads as its base, and they are cheap
|
|
504
504
|
// and bounded — one list page plus one detail per draft. Running them last,
|
|
505
|
-
// behind inbox and sent, meant a bounded call (the
|
|
505
|
+
// behind inbox and sent, meant a bounded call (the
|
|
506
506
|
// OFW_SYNC_MAX_REQUESTS=40) spent its whole budget backfilling history and
|
|
507
507
|
// deferred drafts on every single call, so server-side draft edits stayed
|
|
508
508
|
// invisible indefinitely while the response reported `drafts: 0`.
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// writes downloaded bytes to disk (and reads them back for the inline-reuse
|
|
5
5
|
// path). Those are the ONLY node:fs touch points in the message tools — they
|
|
6
6
|
// live behind this {@link AttachmentIO} interface so the stdio server can use
|
|
7
|
-
// the disk-backed {@link NodeAttachmentIO} while
|
|
8
|
-
//
|
|
7
|
+
// the disk-backed {@link NodeAttachmentIO} while a deployment with no usable
|
|
8
|
+
// disk injects an inline, filesystem-free implementation.
|
|
9
9
|
// Keeping the interface here means src/tools/messages.ts imports nothing from
|
|
10
10
|
// node:fs.
|
|
11
11
|
import { readFileSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
@@ -7,7 +7,7 @@ export class DraftFreshnessError extends Error {
|
|
|
7
7
|
// FNV-1a (64-bit) over a canonical encoding. Not cryptographic — this is a
|
|
8
8
|
// change detector, and it is never the sole guard: an unsupplied token falls
|
|
9
9
|
// back to a full field-by-field comparison against the cached base.
|
|
10
|
-
// BigInt keeps it byte-identical
|
|
10
|
+
// BigInt keeps it byte-identical across runtimes.
|
|
11
11
|
const FNV_OFFSET = 0xcbf29ce484222325n;
|
|
12
12
|
const FNV_PRIME = 0x100000001b3n;
|
|
13
13
|
const MASK64 = 0xffffffffffffffffn;
|
package/dist/tools/lifecycle.js
CHANGED
|
@@ -164,10 +164,10 @@ function stateNote(state, cachedAsDraft, folderName) {
|
|
|
164
164
|
* request for the map plus one per probed id.
|
|
165
165
|
*/
|
|
166
166
|
export async function probeIds(client, store, ids, opts) {
|
|
167
|
-
// THREE cache reads for the whole batch, not three per id.
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
167
|
+
// THREE cache reads for the whole batch, not three per id. Where the cache
|
|
168
|
+
// is remote every call is a subrequest counting against the same hosting cap
|
|
169
|
+
// as the OFW fetches, so a per-id lookup would spend the caller's budget on
|
|
170
|
+
// bookkeeping before a single probe ran.
|
|
171
171
|
const draftsById = new Map((await store.getDrafts(ids)).map((d) => [d.id, d]));
|
|
172
172
|
const messagesById = new Map((await store.getMessages(ids)).map((m) => [m.id, m]));
|
|
173
173
|
const prepared = ids.map((id) => {
|
|
@@ -294,9 +294,9 @@ export async function resolveDraftKey(store, draftKey) {
|
|
|
294
294
|
return { currentId: chain[chain.length - 1].id, ids: chain.map((r) => r.id) };
|
|
295
295
|
}
|
|
296
296
|
/**
|
|
297
|
-
* Mint a new stable draft identity. Uses the Web Crypto global, which
|
|
298
|
-
*
|
|
299
|
-
*
|
|
297
|
+
* Mint a new stable draft identity. Uses the Web Crypto global, which Node ≥19
|
|
298
|
+
* provides natively — a `node:crypto` import would not bundle for a hosted
|
|
299
|
+
* deployment.
|
|
300
300
|
*/
|
|
301
301
|
export function newDraftKey() {
|
|
302
302
|
return `dk_${crypto.randomUUID()}`;
|
package/dist/tools/messages.js
CHANGED
|
@@ -81,7 +81,7 @@ const FolderCountsSchema = z.looseObject({
|
|
|
81
81
|
/**
|
|
82
82
|
* Cap on per-id probes in one ofw_check_freshness call.
|
|
83
83
|
*
|
|
84
|
-
* Each id costs one OFW request, and
|
|
84
|
+
* Each id costs one OFW request, and under a request budget every one counts
|
|
85
85
|
* against the subrequest cap (see OFW_SYNC_MAX_REQUESTS). The check has to stay
|
|
86
86
|
* cheap enough that a caller reaches for it freely — that is the entire point
|
|
87
87
|
* of it existing — so it truncates loudly rather than turning into a sync.
|
|
@@ -885,7 +885,7 @@ export function registerMessageTools(server, client, cacheProvider, attachmentIO
|
|
|
885
885
|
const { freshness, serverConfirmed, cacheStatus } = await draftsFreshness(cache);
|
|
886
886
|
const rows = await cache.listDrafts({ page, size });
|
|
887
887
|
const total = await cache.countDrafts();
|
|
888
|
-
// One batch lookup for the whole page —
|
|
888
|
+
// One batch lookup for the whole page — where the cache is remote a
|
|
889
889
|
// per-draft lineage read would be a subrequest each.
|
|
890
890
|
const keyById = new Map((await cache.getDraftLineageByIds(rows.map((d) => d.id))).map((l) => [l.id, l.draftKey]));
|
|
891
891
|
// Every draft carries the concurrency token to echo back on a write, its
|
|
@@ -1250,7 +1250,7 @@ export function registerMessageTools(server, client, cacheProvider, attachmentIO
|
|
|
1250
1250
|
},
|
|
1251
1251
|
}, async (args) => {
|
|
1252
1252
|
// Resolve the upload source through the injected attachment-I/O boundary
|
|
1253
|
-
// (disk read on node; an in-memory source on
|
|
1253
|
+
// (disk read on node; an in-memory source on a hosted deployment).
|
|
1254
1254
|
const { blob, fileName, mimeType: mime, sizeBytes } = await attachmentIO.resolveUpload(args.path);
|
|
1255
1255
|
// Build the multipart payload matching the OFW web UI's request shape.
|
|
1256
1256
|
const form = new FormData();
|
|
@@ -1298,7 +1298,7 @@ export function registerMessageTools(server, client, cacheProvider, attachmentIO
|
|
|
1298
1298
|
const fileId = args.fileId;
|
|
1299
1299
|
const cache = cacheProvider();
|
|
1300
1300
|
const requestedInline = args.inline ?? getDefaultInlineAttachments();
|
|
1301
|
-
// When the deployment has no filesystem
|
|
1301
|
+
// When the deployment has no filesystem, inline is the
|
|
1302
1302
|
// ONLY path to the bytes — force it rather than erroring on a disk write.
|
|
1303
1303
|
// `forcedInline` records that we overrode an explicit `inline:false` so the
|
|
1304
1304
|
// response is honest about it instead of silently ignoring the argument.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.2",
|
|
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)",
|
|
@@ -29,10 +29,7 @@
|
|
|
29
29
|
"dev": "node --env-file=.env dist/index.js",
|
|
30
30
|
"test": "vitest run",
|
|
31
31
|
"test:coverage": "vitest run --coverage",
|
|
32
|
-
"test:watch": "vitest"
|
|
33
|
-
"worker:dev": "wrangler dev",
|
|
34
|
-
"worker:deploy": "wrangler deploy",
|
|
35
|
-
"worker:test": "vitest run --config vitest.workers.config.ts"
|
|
32
|
+
"test:watch": "vitest"
|
|
36
33
|
},
|
|
37
34
|
"dependencies": {
|
|
38
35
|
"@chrischall/mcp-utils": "^0.14.0",
|
|
@@ -42,16 +39,10 @@
|
|
|
42
39
|
"zod": "^4.4.3"
|
|
43
40
|
},
|
|
44
41
|
"devDependencies": {
|
|
45
|
-
"@chrischall/mcp-connector": "^1.1.1",
|
|
46
|
-
"@cloudflare/vitest-pool-workers": "^0.19.1",
|
|
47
|
-
"@cloudflare/workers-oauth-provider": "^0.8.1",
|
|
48
|
-
"@cloudflare/workers-types": "^5.20260708.1",
|
|
49
42
|
"@types/node": "^26.0.0",
|
|
50
43
|
"@vitest/coverage-v8": "^4.1.7",
|
|
51
|
-
"agents": "^0.19.0",
|
|
52
44
|
"esbuild": "^0.28.0",
|
|
53
45
|
"typescript": "^7.0.2",
|
|
54
|
-
"vitest": "^4.1.7"
|
|
55
|
-
"wrangler": "^4.110.0"
|
|
46
|
+
"vitest": "^4.1.7"
|
|
56
47
|
}
|
|
57
48
|
}
|
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.10.
|
|
9
|
+
"version": "2.10.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.10.
|
|
14
|
+
"version": "2.10.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/skills/ofw/SKILL.md
CHANGED
|
@@ -10,7 +10,6 @@ MCP server for OurFamilyWizard — provides read/write access to messages, calen
|
|
|
10
10
|
- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)
|
|
11
11
|
- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)
|
|
12
12
|
|
|
13
|
-
> These tools are also available via the hosted [claude.ai](https://claude.ai) remote connector (a Cloudflare Worker) — the tool set and behaviour are identical to the local stdio install. See the repo's `docs/DEPLOY-CONNECTOR.md`.
|
|
14
13
|
|
|
15
14
|
## Setup
|
|
16
15
|
|
package/dist/ofw-auth.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { loginWithPassword } from './auth-password.js';
|
|
2
|
-
/**
|
|
3
|
-
* `ConnectorAuth` for the OurFamilyWizard remote connector: the login page
|
|
4
|
-
* collects the user's own OFW email/username + password, verifies them via the
|
|
5
|
-
* same Spring Security form login the stdio server uses (`loginWithPassword` in
|
|
6
|
-
* `auth-password.js`), and stores `{ username, password }` as the OAuth props
|
|
7
|
-
* that `worker.ts`'s `buildClient` turns into a per-user `OFWClient` capable of
|
|
8
|
-
* re-authenticating when its 6h token expires.
|
|
9
|
-
*/
|
|
10
|
-
export const ofwAuth = {
|
|
11
|
-
service: 'OurFamilyWizard',
|
|
12
|
-
accent: '#00A9A5',
|
|
13
|
-
privacyNote: 'Your OFW email and password are stored encrypted and used only to sign in to OurFamilyWizard on your behalf ' +
|
|
14
|
-
'(OFW sign-in tokens expire every few hours, so your password is needed to renew them).',
|
|
15
|
-
fields: [
|
|
16
|
-
{ name: 'username', label: 'OFW email or username' },
|
|
17
|
-
{ name: 'password', label: 'OFW password', type: 'password' },
|
|
18
|
-
],
|
|
19
|
-
async login(fields) {
|
|
20
|
-
// Verify the credentials up front — a bad password throws here, which the
|
|
21
|
-
// connector surfaces back on the login page. We deliberately discard the
|
|
22
|
-
// returned token: the per-user client logs in again from the stored creds.
|
|
23
|
-
await loginWithPassword(fields.username, fields.password);
|
|
24
|
-
return { username: fields.username, password: fields.password };
|
|
25
|
-
},
|
|
26
|
-
};
|