mcpField
O
ofw-mcp

ofw-mcp

Local
github.com/chrischallProductivity14Updated today

About ofw-mcp

ofw-mcp connects Claude to an OurFamilyWizard account for co-parenting messages, calendar events, expenses, and journal entries. It provides read and write tools with configurable write modes and confirmation safeguards; the project warns that its implementation has not been human-audited.

Key features

  1. 01Reads messages, calendar events, expenses, and journal entries
  2. 02Creates or manages drafts, events, expenses, and journal entries
  3. 03Caches message data and reports freshness
  4. 04Downloads message attachments and uploads files
  5. 05Applies write modes and confirmation safeguards

Use cases

  1. 01Review recent co-parenting messages and unread counts
  2. 02Check upcoming family calendar events
  3. 03Summarize expenses and reimbursement status
  4. 04Draft a reply or record a journal entry

MCP support

ofw-mcp is an MCP server that exposes OurFamilyWizard account data and operations to Claude. Read tools and write tools have different approval behavior, with server-side confirmation for selected actions.

Configuration

Local server config is the same for every client. Copy it into your client’s MCP config file.

mcp.json · Universal config
{
"mcpServers": {
"chrischall-ofw": {
"command": "npx",
"args": [
"-y",
"ofw-mcp"
],
"env": {
"OFW_USERNAME": "<OFW_USERNAME>",
"OFW_PASSWORD": "<OFW_PASSWORD>",
"OFW_WRITE_MODE": "<OFW_WRITE_MODE>",
"OFW_CALENDAR_WRITES": "<OFW_CALENDAR_WRITES>",
"OFW_ALLOW_MARK_READ": "<OFW_ALLOW_MARK_READ>",
"OFW_FETCH_UNREAD_BODIES": "<OFW_FETCH_UNREAD_BODIES>",
"OFW_READ_DAYS": "<OFW_READ_DAYS>",
"OFW_NO_READ_DAYS": "<OFW_NO_READ_DAYS>",
"OFW_SESSION_CACHE": "<OFW_SESSION_CACHE>",
"OFW_SESSION_FILE": "<OFW_SESSION_FILE>",
"OFW_EXPENSE_ONLY": "<OFW_EXPENSE_ONLY>",
"OFW_EXPENSE_UPLOAD_ONLY": "<OFW_EXPENSE_UPLOAD_ONLY>",
"OFW_INLINE_ATTACHMENTS": "<OFW_INLINE_ATTACHMENTS>",
"OFW_ATTACHMENTS_DIR": "<OFW_ATTACHMENTS_DIR>",
"OFW_UPLOAD_DIR": "<OFW_UPLOAD_DIR>",
"OFW_CACHE_IDENTITY": "<OFW_CACHE_IDENTITY>",
"OFW_CACHE_DIR": "<OFW_CACHE_DIR>",
"OFW_DISABLE_FETCHPROXY": "<OFW_DISABLE_FETCHPROXY>",
"OFW_AUTO_REFRESH": "<OFW_AUTO_REFRESH>",
"OFW_FRESHNESS_TTL_SECONDS": "<OFW_FRESHNESS_TTL_SECONDS>",
"OFW_SYNC_MAX_REQUESTS": "<OFW_SYNC_MAX_REQUESTS>",
"OFW_REQUEST_TIMEOUT_MS": "<OFW_REQUEST_TIMEOUT_MS>",
"OFW_DEBUG_LOG": "<OFW_DEBUG_LOG>",
"DISPLAY_TZ": "<DISPLAY_TZ>",
"FETCHPROXY_WS_PORT": "<FETCHPROXY_WS_PORT>",
"FETCHPROXY_WS_HOST": "<FETCHPROXY_WS_HOST>",
"FETCHPROXY_IDENTITY_DIR": "<FETCHPROXY_IDENTITY_DIR>"
}
}
}
}
Run commandnpx -y ofw-mcp
Environment variables to set
OFW_USERNAMEYour OurFamilyWizard login email address. Optional — if omitted, the server falls back to the fetchproxy browser extension (requires being signed in to ourfamilywizard.com).
OFW_PASSWORDSecretYour OurFamilyWizard password. Optional — see OFW_USERNAME.
OFW_WRITE_MODEWrite-tool gate: "none" registers no write tools; "drafts" registers draft-level writes only (save/delete drafts, upload attachments); "all" registers everything (default). Unrecognized values fail closed to "none".
OFW_CALENDAR_WRITESSet to "true" to register calendar write tools (create/update/delete event) in "drafts" write mode. Events have no draft stage but are reversible. Never overrides "none".
OFW_ALLOW_MARK_READDefault "true". Set "false" to forbid any tool from marking a message read on OurFamilyWizard - reading a body for the first time stamps a co-parent-visible "First Viewed" time that cannot be undone. A ceiling: no per-call argument can raise it.
OFW_FETCH_UNREAD_BODIESDefault "false". Whether ofw_sync_messages fetches unread inbox bodies by default (each fetch stamps a First Viewed time). Capped by OFW_ALLOW_MARK_READ.
OFW_READ_DAYSAllow list of days message content may be read, e.g. "mon,thu" (weekdays, YYYY-MM-DD, or YYYY-MM-DD..YYYY-MM-DD, in DISPLAY_TZ). Other days: subjects only, nothing marked read. Unset = every day.
OFW_NO_READ_DAYSDeny list of days message content may NOT be read (same format as OFW_READ_DAYS; wins over it). Unset = none.
OFW_SESSION_CACHESet to false to disable the on-disk session cache and re-authenticate on every process start. Defaults to enabled.
OFW_SESSION_FILEAbsolute path for the session cache file. Defaults to $MCP_DATA_DIR/.ofw-mcp/session.json.
OFW_EXPENSE_ONLYSet true to register only healthcheck plus expense tools. All profile/dashboard, message, calendar, and journal tools are structurally absent.
OFW_EXPENSE_UPLOAD_ONLYSet true for strict reimbursement upload mode: only healthcheck, ofw_upload_expense_pdf, ofw_create_expense, and ofw_update_expense register. Excludes ofw_mark_expense_reimbursed. Implies OFW_EXPENSE_ONLY.
OFW_INLINE_ATTACHMENTSSet "true" to return attachments as MCP content blocks by default (images render directly; other files come back as embedded resources) instead of writing to disk. Callers can still override per call via the tool's `inline` argument.
OFW_ATTACHMENTS_DIRWhere ofw_download_attachment writes (default ~/Downloads/ofw-mcp). Every disk write is confined to it: a saveTo outside it (after resolving symlinks) is refused, and an existing file is replaced only with force:true.
OFW_UPLOAD_DIRThe only directory ofw_upload_attachment may read from (default: OFW_ATTACHMENTS_DIR). Paths outside it after resolving symlinks, hidden files/dirs, and files over 25 MiB are refused.
OFW_CACHE_IDENTITYExplicit cache-key label; overrides OFW_USERNAME for fetchproxy-only multi-account setups. If you have multiple OFW accounts and want them to use separate caches, set it to a label per profile.
OFW_CACHE_DIROverrides the message cache directory (default ~/.cache/ofw-mcp).
OFW_DISABLE_FETCHPROXYSkip the fetchproxy fallback, so missing credentials become a hard error (useful in headless CI).
OFW_AUTO_REFRESHWhen a cached read comes back EMPTY from a non-fresh cache, the read tools sync the backing folders and answer from the refreshed cache instead of refusing with UNVERIFIED_EMPTY (default off, i.e. refuse). Per-call autoRefresh overrides it. A refresh that does not make the read verifiable still refuses.
OFW_FRESHNESS_TTL_SECONDSHow long a verified-against-OFW folder stays labelled `fresh` in read tools' `freshness` block (default 300). Unusable values fall back to the default — a typo must never widen the window in which stale data reads as current.
OFW_SYNC_MAX_REQUESTSPer-call OFW-request budget for ofw_sync_messages: a positive integer caps how many OFW requests one sync call may make before pausing; the next call resumes where it left off. Unset means unbounded (local installs walk fully in one call).
OFW_REQUEST_TIMEOUT_MSPer-request timeout for OFW API calls, in milliseconds (default 30000). Each retry gets its own fresh window.
OFW_DEBUG_LOGLog every OFW request/response to stderr (Authorization redacted). Diagnostic only.
DISPLAY_TZIANA zone (e.g. America/New_York, the default) for every `<field>Display` value, and the zone a NAIVE source timestamp is assumed to be wall-clock in. OFW's API reports naive local times in the account's own zone, so this must match it. Unrecognized values fall back to the default.
FETCHPROXY_WS_PORTLocalhost port of the fetchproxy concentrator the ContextMint Bridge extension connects to (default 37149). Every MCP on one machine shares it, so move it for all of them or not at all.
FETCHPROXY_WS_HOSTBind interface for the fetchproxy concentrator, a literal IP only (default 127.0.0.1). Keep on loopback; it exists for hosted deployments that run each MCP in its own network namespace.
FETCHPROXY_IDENTITY_DIRAbsolute path where the fetchproxy long-term identity keypair is stored (default ~/.fetchproxy/identity/).

Local servers need a runtime on your machine (e.g. Node.js 18+). Config file locations differ by client; see your client’s docs.

Packages
npmofw-mcp2.23.0

Tools

28

Saved tool list · updated today

This tool takes no parameters.

Details

From the project README

OurFamilyWizard MCP

CI npm license

A Model Context Protocol server that connects Claude to OurFamilyWizard, giving you natural-language access to your co-parenting messages, calendar, expenses, and journal.

Warning

AI-developed project. This codebase was entirely built and is actively maintained by Claude Sonnet 4.6. No human has audited the implementation. Review all code and tool permissions before use.

What you can do

Ask Claude things like:

  • "Show me my recent OFW messages"
  • "What's on the kids' calendar next week?"
  • "List recent expenses and tell me what I owe"
  • "Add a journal entry about today's pickup"
  • "Draft a reply to the last message from my co-parent"

Requirements

  • Claude Desktop
  • Node.js 22.5 or later (node:sqlite is the cache backend)
  • An active OurFamilyWizard account

Acknowledgement of Terms

By using this MCP server, you acknowledge and agree to the following:

1. This server accesses your own OurFamilyWizard account. Auth happens via your own credentials. It does not — and cannot — access your co-parent's account, your children's accounts, or anyone else's.

2. OurFamilyWizard's Terms govern your use of this server, just as they govern your direct use of OFW. There is no explicit anti-scraping clause; the governing language is broader:

Users may not obtain or attempt to obtain any materials or information through any means not intentionally made available.

And on credentials: "You are solely responsible for (1) maintaining the strict confidentiality of assigned Authentication Methods, (2) instructing any individual to whom the assigned Authentication Method is shared ('Authorized User') to not allow another person to use the Authentication Method." OFW does contemplate "Authorized Users" and third-party-enabled integrations — but the account holder remains responsible.

You are agreeing to those terms — read by the maintainer 2026-05-23 — every time you invoke a tool in this server.

3. Personal, family use only. This project is not affiliated with, endorsed by, sponsored by, or in partnership with OurFamilyWizard, LLC or its parent. It is a personal automation tool for the named account holder. Do not use it on behalf of a co-parent without their consent, do not share credentials with anyone, and do not use it to bulk-extract another family's data.

4. OFW is a court-of-record platform. Messages, expenses, calendar entries, and journal entries on OFW may be entered into legal proceedings — including custody, divorce, and parenting-plan-modification cases. Anything this server writes to OFW (drafts you save, events you create, expenses you log) will appear with the same legal weight as if you had typed it yourself. Do not let this MCP send a message, create an event, or log an expense that you have not read and approved. Review every write operation before confirming.

5. You accept full responsibility for any consequences — both technical (account warnings, suspension) and legal (anything OFW records about your account activity). The MCP author is not your attorney; if you're using OFW in connection with an active legal matter, talk to your actual attorney before automating anything.

This section is the maintainer's good-faith summary of the terms — it is not legal advice and does not modify or supersede OurFamilyWizard's actual ToS.

Installation

1. Clone and build
git clone https://github.com/chrischall/ofw-mcp.git
cd ofw-mcp
npm install
npm run build
2. Add to Claude Desktop

Edit your Claude Desktop config file:

  • Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add the ofw entry inside "mcpServers" (create the key if it doesn't exist):

{
  "mcpServers": {
    "ofw": {
      "command": "node",
      "args": ["/absolute/path/to/ofw-mcp/dist/index.js"],
      "env": {
        "OFW_USERNAME": "[email protected]",
        "OFW_PASSWORD": "your-ofw-password"
      }
    }
  }
}

Replace /absolute/path/to/ofw-mcp with the actual path where you cloned the repo. On Mac, run pwd inside the cloned directory to get it.

3. Restart Claude Desktop

Quit completely (Cmd+Q on Mac, not just close the window) and relaunch.

4. Verify

Ask Claude: "What does my OFW dashboard look like?" — it should show your unread message count, upcoming events, and outstanding expenses.

Authentication

ofw-mcp tries three auth paths in order; whichever succeeds first is used. Existing setups keep working unchanged.

  1. Env-var credentials (legacy, recommended for Claude Desktop). Set OFW_USERNAME + OFW_PASSWORD and the server logs in via OFW's form endpoint. This is the path shown in the Claude Desktop config above.
  2. fetchproxy fallback (no env vars needed). When the credentials are absent, the server reads localStorage["auth"] once at startup from your already-signed-in ourfamilywizard.com tab via the ContextMint Bridge browser extension (built on fetchproxy). After that one read, all OFW API calls go directly from Node — the extension is not in the request hot path. Install ContextMint Bridge from its releases page (Chrome: download the Chrome zip, unzip it, and load it unpacked at chrome://extensions with Developer mode on; Safari is not available yet — it will ship inside the ContextMint app, which has no public download — so use Chrome for now), sign into OurFamilyWizard once, and the MCP just works. ContextMint Bridge is the fetchproxy browser extension under its new name, from the same maintainer — fetchproxy's own README (https://github.com/chrischall/fetchproxy#extension) points to it. Its source is public at https://github.com/nullnet-app/contextmint-bridge: build it yourself, or check a release zip against the .sha256 file published beside it (shasum -a 256 -c contextmint-bridge-chrome-<version>.zip.sha256). If you have multiple OFW accounts and want them to use separate caches, set OFW_CACHE_IDENTITY to a label per profile.
  3. Error. If neither path is available, the server tells you exactly which fix to apply. Set OFW_DISABLE_FETCHPROXY=1 to skip the fetchproxy fallback entirely (turns missing credentials into a hard error — useful in headless CI).
Credential options (env-var path)

Option A — env block in Claude Desktop config (shown above, recommended):

"env": {
  "OFW_USERNAME": "[email protected]",
  "OFW_PASSWORD": "your-ofw-password"
}

Option B — .env file in the project directory:

cp .env.example .env
# edit .env and fill in your credentials

Environment variables always take priority over the .env file. You can also pass them directly on the command line:

[email protected] OFW_PASSWORD=yourpass node dist/index.js

Available tools

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); 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 below.

ToolWhat it doesPermissionWrite mode
ofw_get_profileYour profile and co-parent infoAutoany
ofw_get_notificationsDashboard counts (unread messages, upcoming events, outstanding expenses). Updates your co-parent-visible last-seen status, so it is not marked read-onlyConfirmany
ofw_list_message_foldersFolders with unread counts — get folder IDs here before listing messagesAutoany
ofw_list_messagesMessages in a folderAutoany
ofw_get_messageFull content of a single messageAutoany
ofw_sync_messagesSync messages into the local cache (unread bodies left unfetched to avoid read receipts)Autoany
ofw_get_unread_sentSent messages a recipient hasn't read yet (from local cache)Autoany
ofw_check_freshnessCheap live check that the cache still matches OFW — per id, whether it is still a draft or was sent/deleted, without a full syncAutoany
ofw_statusOne live call for "where does everything stand?" — the full verified draft inventory, and the current state of any ids or draft keysAutoany
ofw_download_attachmentDownload a message attachment to disk, or inline as extracted content / bytesAutoany
ofw_send_messageSend a messageConfirm (server)all
ofw_list_draftsDraft messagesAutoany
ofw_save_draftCreate or update a draftConfirmdrafts
ofw_delete_draftDelete a draftConfirmdrafts
ofw_upload_attachmentUpload 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 allAuto (PRIVATE) / Confirm (server) (SHARED)drafts
ofw_list_eventsCalendar events in a date rangeAutoany
ofw_create_eventCreate a calendar eventConfirm (server) unless privateall (or drafts + OFW_CALENDAR_WRITES)
ofw_update_eventUpdate a calendar eventConfirm (server) if sharedall (or drafts + OFW_CALENDAR_WRITES)
ofw_delete_eventDelete a calendar eventConfirm (server) if sharedall (or drafts + OFW_CALENDAR_WRITES)
ofw_get_expense_totalsExpense summary totalsAutoany
ofw_list_expensesExpense historyAutoany
ofw_list_expense_categoriesExpense category ids and split metadataAutoany
ofw_upload_expense_pdfUpload a receipt PDF to My Files for an expense (SHARED — the co-parent sees it in My Files at once, even on a private expense)Confirm (server)all
ofw_create_expenseLog a new expense; supports private entries and one receipt PDFConfirm (server)all
ofw_mark_expense_reimbursedIrreversibly record full remaining external reimbursement; no transfer or partial payment; unavailable in upload-only modeConfirm (server)all
ofw_update_expenseChange an expense, e.g. publish a private one. Pass only the fields to change; it reads the expense first so the rest are keptConfirm (server)all
ofw_list_journal_entriesJournal entriesAutoany
ofw_create_journal_entryCreate a journal entryConfirmall
Data freshness (OFW_FRESHNESS_TTL_SECONDS)

Message and draft reads are served from the local cache, which means a result can look authoritative while being minutes or months out of date. The cache also cannot detect some changes on its own: editing a draft in the OFW web app bumps no timestamp at all, so "nothing changed" and "we didn't look" are indistinguishable unless the server says which happened.

So every read tool (ofw_list_messages, ofw_list_drafts, ofw_get_message, ofw_list_message_folders, ofw_sync_messages) returns a freshness block alongside its data:

"freshness": {
  "source": "cache",
  "asOf": "2026-07-20T12:40:00.000Z",
  "ageSeconds": 5231,
  "staleness": "unverified",
  "lastServerSyncAt": "2026-07-20T13:59:00.000Z",
  "syncComplete": false,
  "historyComplete": true,
  "warning": "Served from cache last verified 87 min ago; the last sync did not finish checking drafts. Re-read before asserting current state — call ofw_check_freshness for a cheap live confirmation, or ofw_sync_messages to refresh."
}

staleness is fresh only when the data was fetched live in that call, or verified against OFW within the threshold by a sync that actually reached that folder. It degrades to unverified when it ages out or a sync skipped the folder, and stale when the folder has never been checked at all. Anything other than fresh always carries a human-readable warning stating the age and the reason. The bias is deliberate and one-directional: a false unverified costs one extra call, whereas a false fresh lets remembered state be narrated as present fact.

Drafts additionally carry per-item cacheStatus, asOf, and serverConfirmed — true only when a completed drafts walk verified them inside the threshold. serverConfirmed: false means a draft's existence and unsent status are remembered, not known, and should not be stated as current fact without calling ofw_check_freshness first.

ofw_check_freshness is the cheap way to re-verify: one request for a folder count comparison plus one per message id, no bodies, no full sync. Draft ids are compared by content revision, not timestamp, for the reason above. It probes ids that are cached as drafts, as sent messages, or as already-read inbox messages freely, because none of those can change anything; any other id would mark an unread inbox message as read on OurFamilyWizard — an irreversible change to a court-visible record — so those are skipped unless you pass allowMarkRead: true.

Set OFW_FRESHNESS_TTL_SECONDS to tune the threshold (default 300, i.e. 5 minutes). Unusable values fall back to the default rather than widening the window.

Is it still what I think it is? (ofw_status)

Freshness answers how old is this data. It does not answer is this entity still what I think it is — and that is a different failure. A draft that has been sent still exists on the server, so "does this id exist?" comes back true for the one case where the answer matters most.

ofw_status is the call that should back any status summary:

{
  "checkedAt": "2026-07-28T09:12:00.000Z",
  "requested": [
    { "id": 538279699, "state": "sent", "sentAt": "2026-07-27T23:31:09", "inSync": false },
    { "id": 538086428, "state": "draft", "inSync": true }
  ],
  "complete": true
}
  • state is draft, sent, received, deleted or unknown, read live from OFW. unknown means the question was not answered — it is not a synonym for "fine".
  • With no arguments it returns the full draft inventory, verified against OFW first.
  • complete: true means every part of the snapshot was confirmed live. If it is false, incompleteReasons says what wasn't, and the payload is not a basis for stating a count.

Draft keys. Editing a draft mints a new OFW id every time (ofw_save_draft replaces by create-then-delete, because OFW's update-in-place endpoint silently no-ops). ofw_save_draft therefore also returns a draftKey that stays constant across every edit and follows the message into Sent. ofw_status(draftKeys: ["dk_…"]) resolves it to the current id and state — including state: "sent" with sentMessageId — so "what happened to the draft I was working on?" is one call, not a guess about which id is current.

Absence is never reported from a stale cache

A cached read that comes back empty is shaped identically to a verified "nothing there". ofw_list_messages, ofw_list_drafts and ofw_get_unread_sent therefore refuse rather than answer when the result is empty and the backing cache is not fresh:

{
  "result": "UNVERIFIED_EMPTY",
  "reason": "No drafts were found, but the backing cache is \"unverified\" — it was last verified 207 min ago. Refusing to report absence from unverified data…",
  "remedy": "Call ofw_sync_messages(folders:[\"drafts\"]) and retry, re-call with autoRefresh:true, or use ofw_status(includeDraftInventory:true) for a single live answer.",
  "complete": false
}

A false negative ("no, that was never sent") is more dangerous than a refusal, because it reads as a definitive answer. Non-empty results are never withheld — a stale cache that did find something is still evidence of presence, labelled with its age as before. Pass autoRefresh: true (or set OFW_AUTO_REFRESH=true) to have the tool sync and answer instead of refusing; a refresh that still cannot make the read verifiable refuses anyway.

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.

Recording an external expense reimbursement

ofw_mark_expense_reimbursed records money already reimbursed outside OFW. It never transfers money, calls /payments, or initiates a bank action. OFW warns that marking reimbursed cannot be undone. The tool is registered only in OFW_WRITE_MODE=all, including expense-only mode, but never in OFW_EXPENSE_UPLOAD_ONLY=true; that restricted roster is unchanged.

Pass an expense id and expectedRemainingAmount in currency units, positive and exact to cents. For example, a synthetic expense totaling 96.99 with totalOwed=48.50 and leftToPay=48.50 needs { "expenseId": 81001, "expectedRemainingAmount": 48.50 } — not 96.99 and not a partial amount. The first call previews the title, author/payer user ids, split/category, children, privacy, total, owed amount, remaining balance and current status. Verify the parties and that the external reimbursement actually happened, then explicitly approve the preview before returning its confirmToken with the same arguments (or approve the supported client's confirmation prompt).

Only OPEN, APPROVED, or PARTIAL with subStatus=NONE and a positive balance are supported. Missing/malformed detail, unknown or pending status, inconsistent balances, id mismatch, or amount mismatch refuses without writing. A record already PAID with zero remaining balance returns ALREADY_PAID without a write, regardless of its payment method; this does not relabel an electronic payment.

Confirmation binds the entire detail record. A fresh detail GET immediately before the write rejects any drift, including receipt or metadata edits. The only mutation is PUT /pub/v2/expense/expenses/{id}/status with { "status": "MARKED_REIMBURSED" }. A subsequent detail GET must show PAID, subStatus=NONE, zero remaining balance, paymentType="Marked reimbursed", and unchanged fields outside the expected status/balance transition before success is reported. Known derived balance-display fields may change normally; numeric purchase/owed totals and other protected fields must remain unchanged. The public web application's status request contract and the resulting status transition were manually accepted against the real provider with one user-authorized record. Automated tests remain mock-only and contain synthetic records. Unexpected backend changes outside the supported transition still fail conservatively rather than claim verification.

These are best-effort preconditions, not an atomic conditional update: OFW exposes no conditional status-write contract here, so a concurrent edit after the last GET cannot be prevented. An ambiguous mutation failure (timeout/network/5xx) or failed/mismatched readback returns EXPENSE_REIMBURSEMENT_UNCONFIRMED with mayHaveLanded=true. Do not automatically replay or roll back. Inspect the expense in the Expenses log on ourfamilywizard.com and reconcile its status before any new attempt. Definitive OFW rejections remain errors. No live account or private family/financial data is used in the automated tests.

Server-side confirmation (MCP_CONFIRM_MODE)

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, unless MCP_CONFIRM_ELICITATION=off. 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.

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.

VariableDefault
MCP_CONFIRM_MODEask-userWhat 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.
MCP_CONFIRM_ELICITATIONonoff never shows a confirmation prompt, so every client gets the MCP_CONFIRM_MODE path. Set it for a client that claims to support prompts but never shows one (the gated write hangs — opencode 2.0.x). Any other value stays on, with a warning on stderr.
MCP_CONFIRM_TTL_SECONDS600How long a token stays valid.
MCP_CONFIRM_SECRETrandom per processSigning key; set it only if tokens must survive a server restart. On mcp-host the host supplies a stable per-child key (MCP_HOST_CONFIRM_SECRET) and spent tokens are recorded under MCP_DATA_DIR, so an approval survives an idle restart.

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.

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.

Write protection (OFW_WRITE_MODE)

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.

OFW_WRITE_MODEWhat's available
noneRead/sync/search only. No write tools exist.
draftsAdds 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.
allEverything (the default — fully backward compatible).

Unrecognized values fail closed to none, with a warning on stderr — a typo never silently grants write access.

Expense-only deployments

For a dedicated reimbursement integration, you can also structurally remove unrelated OFW capabilities:

SettingRegistered surface
OFW_EXPENSE_ONLY=trueofw_healthcheck plus the expense tools only. Profile/dashboard, messages, calendar, and journal tools do not exist.
OFW_EXPENSE_UPLOAD_ONLY=trueStrictest mode: ofw_healthcheck, ofw_upload_expense_pdf, ofw_create_expense, and ofw_update_expense only. Expense totals/listing are removed too. This flag implies OFW_EXPENSE_ONLY.

These flags are registration-time restrictions, not prompt instructions. A host cannot call a tool that was never registered. Unrecognized non-empty flag values fail closed to the restricted state. OFW_WRITE_MODE still applies underneath; for the strict upload workflow set OFW_WRITE_MODE=all so expense creation is available.

Recommended reimbursement-only deployment:

OFW_EXPENSE_UPLOAD_ONLY=true
OFW_WRITE_MODE=all
OFW_ALLOW_MARK_READ=false
Reading is a write, too (OFW_ALLOW_MARK_READ)

Fetching a message body for the first time marks it read on OurFamilyWizard and stamps a "First Viewed" timestamp your co-parent can see. That is part of the record and cannot be undone — and it happens as a side effect of an ordinary read, so OFW_WRITE_MODE does not govern it.

By default nothing changes: reads behave exactly as they always have. Two controls exist if you want them:

SettingEffect
ofw_get_message(allowMarkRead: false)Refuses a fetch that would stamp an unread inbox message, returning a structured MARK_READ_BLOCKED payload instead. Cached bodies, sent messages and already-read messages still return normally — none of them can stamp anything.
OFW_ALLOW_MARK_READ=falseDeployment-wide ceiling. No tool may stamp: ofw_get_message refuses, ofw_check_freshness / ofw_status ignore allowMarkRead:true, ofw_sync_messages ignores fetchUnreadBodies:true. A per-call argument cannot raise it.

OFW_FETCH_UNREAD_BODIES=true flips ofw_sync_messages to fetch unread bodies by default (off unless set) — useful where read receipts are routine. It is capped by OFW_ALLOW_MARK_READ.

Message-reading days (OFW_READ_DAYS / OFW_NO_READ_DAYS)

If you may only read OFW messages on certain days, list them. On every other day you can still see that a message arrived — sender, date, subject — but bodies and attachments are withheld and nothing is marked read on OurFamilyWizard.

OFW_READ_DAYS=mon,thu                       # allow list: read only on these days
OFW_NO_READ_DAYS=2026-12-24..2026-12-26     # deny list: never on these (wins over the allow list)

Tokens are weekday names (mon, Monday, …), dates (2026-12-25) or inclusive ranges (2026-12-24..2026-12-31), separated by commas or spaces, and "today" is decided in DISPLAY_TZ. Unset means every day is a reading day. A token that cannot be parsed fails closed (content withheld every day, warning on stderr). Like OFW_ALLOW_MARK_READ, no per-call argument can override it. Refusals come back as READING_BLOCKED with nextReadableDay. ofw_sync_messages downloads no message content on those days (only the list: sender, date, subject); the first sync on a reading day fills the bodies in, except for unread inbox messages, which still wait for you to open them. Drafts stay readable, and sending is not affected.

Calendar opt-in (OFW_CALENDAR_WRITES)

Calendar events sit between the two message tiers: they have no draft stage (a created event is immediately visible on the shared record), but unlike a sent message they are reversible — an event can be edited or deleted afterward. If you run in drafts mode but are comfortable with direct calendar writes, set OFW_CALENDAR_WRITES=true to additionally register ofw_create_event, ofw_update_event, and ofw_delete_event. The flag is redundant in all mode and never overrides none.

Egress allowlist

Every request that carries your OFW token or login passes its constructed URL through a host check before fetch — the server refuses to send them to any host other than ofw.ourfamilywizard.com. Today it always passes, and that is the point: it makes "the credential only ever goes to OFW" a structural invariant rather than a code-review promise, so a later refactor or a compromised dependency that pointed a request elsewhere throws instead of carrying your bearer token or messages off-host. It is a pre-flight check on the URL we build, not a redirect-following egress filter.

The one other host the server contacts is *.oaiusercontent.com, and only for ofw_upload_expense_pdf's url mode: it downloads the receipt PDF from that signed file URL with no OFW credential attached, re-checking every redirect hop against the same suffix, capping the size and requiring a real PDF. The hosted registration's egress allowlist (mint.yaml) names exactly these two hosts.

Troubleshooting

"0 messages" — Claude may have read the notification counts rather than the actual messages. Ask explicitly: "List the messages in my OFW inbox" or "Use ofw_list_message_folders then ofw_list_messages".

"OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install ContextMint Bridge…" — neither auth path is configured. Either fill in the env block in your Claude Desktop config, or install ContextMint Bridge and sign into ourfamilywizard.com in your browser.

"fetchproxy fallback failed" / "ContextMint Bridge is down" — the env-var path wasn't configured and the extension couldn't be reached. Confirm ContextMint Bridge 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.

"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.

403 Forbidden — wrong credentials. Verify your username/password at ofw.ourfamilywizard.com.

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.

Can't find the config file on Mac — in Finder press Cmd+Shift+G and paste ~/Library/Application Support/Claude/.

Security

  • Credentials live only in your local config file or .env
  • They are passed to the server as environment variables and never logged
  • The server authenticates with OFW using the same login flow as the web app
  • Use a strong, unique OFW password
  • Requests carrying your OFW credential are host-allowlisted to ofw.ourfamilywizard.com; the only other host contacted is the receipt-download host for ofw_upload_expense_pdf's url mode (see Egress allowlist)
Local data, and how to remove it

The server keeps co-parenting data on this machine, readable only by your user account:

WhatWherePermissions
Message cache (message bodies, recipients, drafts, attachment records)~/.cache/ofw-mcp/<hash>.db (plus -wal/-shm), or OFW_CACHE_DIRdir 0700, files 0600
Downloaded attachments~/Downloads/ofw-mcp/, or OFW_ATTACHMENTS_DIRdirs the server creates 0700, files 0600
Session token$MCP_DATA_DIR/.ofw-mcp/session.json, or OFW_SESSION_FILE0600

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:

rm -rf ~/.cache/ofw-mcp        # or your OFW_CACHE_DIR
rm -rf ~/Downloads/ofw-mcp     # or your OFW_ATTACHMENTS_DIR
rm -f  ~/.ofw-mcp/session.json # or your OFW_SESSION_FILE

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.

On a hosted connector (mcp-host) the same data lives in the registration's persistent data directory instead of on your machine: the message cache under $HOME/.cache/ofw-mcp/, the session token at $MCP_DATA_DIR/.ofw-mcp/session.json, the browser-bridge identity under $HOME/.fetchproxy/identity/, and spent confirm-token nonces under $MCP_DATA_DIR/.mcp-confirm/. Downloads are returned inline and never written to disk. The commands above do not reach it — how long that directory is kept, and whether disconnecting or deleting the registration removes it, is the host's retention policy, not this server's.

Development

npm test         # tsc typecheck, then the vitest suite
npm run build    # tsc → dist/, then esbuild bundle → dist/bundle.js
npm run dev      # node --env-file=.env dist/index.js (requires built dist)

Main is protected. All changes land via PR — open with gh pr create --label <release-notes-label> and add ready-to-merge once you're satisfied with the auto-review feedback. See CLAUDE.md for the full PR + release flow.

Project structure
src/
  index.ts          MCP server entry (McpServer + StdioServerTransport)
  client.ts         OFW HTTP client with Bearer token + 401/429 retry
  auth.ts           resolveAuth(): env-var creds → fetchproxy → error
  auth-password.ts  Spring Security form login (legacy env-var path)
  cache.ts          SQLite cache (messages, drafts, attachments, sync state)
  sync.ts           Folder ID resolution + per-folder sync logic
  config.ts         Cache dir, attachment dir, env parsing
  tools/
    _shared.ts      Recipient mapping, response helpers, path expansion
    user.ts         ofw_get_profile, ofw_get_notifications
    messages.ts     Folders, list, get, send, drafts, sync, attachments
    calendar.ts     List, create, update, delete events
    expenses.ts     Totals, list, create
    journal.ts      List, create entries
tests/              Mirrors src/; mocks OFWClient.request via vi.spyOn
Auth flow

Auth resolution lives in src/auth.ts. Three paths, in priority order:

  1. Env vars present → src/auth-password.ts does the legacy OFW Spring Security form login:
    1. GET /ofw/login.form — establishes a session cookie
    2. POST /ofw/login — submits credentials, returns { auth: "<token>" }
  2. Env vars absent (and OFW_DISABLE_FETCHPROXY unset) → @fetchproxy/bootstrap reads localStorage["auth"] + localStorage["tokenExpiry"] once from the user's signed-in ourfamilywizard.com tab, then closes the bridge.
  3. Nothing configured → throws with both fixes spelled out.

Either path returns a Bearer token to OFWClient, which then operates from Node with Authorization: Bearer <token> — fetchproxy is not in the request hot path. On 401 the client re-resolves auth and replays once. Tokens are cached for 6h (env-var path) or until tokenExpiry (fetchproxy path).

Also see the fetchproxy README for extension install instructions.

License

MIT

Comments

Markdown supported

New to MCP?

From what the protocol is to installing your first server, these answers cover most first questions.

An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.