Back to Browse

Ofw MCP Server

Developer ToolsLow Risk9.3Local
Free

OurFamilyWizard co-parenting for Claude — messages, calendar, expenses, and journal

About

OurFamilyWizard co-parenting for Claude — messages, calendar, expenses, and journal

Security Report

9.3
Low Risk9.3Low Risk

Valid MCP server (2 strong, 3 medium validity signals). 2 known CVEs in dependencies (0 critical, 1 high severity) Package registry verified. Imported from the Official MCP Registry. Trust signals: trusted author (105/106 approved); 4 highly-trusted packages.

4 files analyzed · 3 issues found

Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.

Permissions Required

This plugin requests these system permissions. Most are normal for its category.

file_system

Check that this permission is expected for this type of plugin.

env_vars

Check that this permission is expected for this type of plugin.

What You'll Need

Set these up before or after installing:

Your OurFamilyWizard login email address. Optional — if omitted, the server falls back to the fetchproxy browser extension (requires being signed in to ourfamilywizard.com).Optional

Environment variable: OFW_USERNAME

Your OurFamilyWizard password. Optional — see OFW_USERNAME.Required

Environment variable: OFW_PASSWORD

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

Environment variable: OFW_WRITE_MODE

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

Environment variable: OFW_CALENDAR_WRITES

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

Environment variable: OFW_ALLOW_MARK_READ

Default "false". Whether ofw_sync_messages fetches unread inbox bodies by default (each fetch stamps a First Viewed time). Capped by OFW_ALLOW_MARK_READ.Optional

Environment variable: OFW_FETCH_UNREAD_BODIES

Set to false to disable the on-disk session cache and re-authenticate on every process start. Defaults to enabled.Optional

Environment variable: OFW_SESSION_CACHE

Absolute path for the session cache file. Defaults to $MCP_DATA_DIR/.ofw-mcp/session.json.Optional

Environment variable: OFW_SESSION_FILE

Set true to register only healthcheck plus expense tools. All profile/dashboard, message, calendar, and journal tools are structurally absent.Optional

Environment variable: OFW_EXPENSE_ONLY

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

Environment variable: OFW_EXPENSE_UPLOAD_ONLY

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

Environment variable: OFW_INLINE_ATTACHMENTS

Where 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.Optional

Environment variable: OFW_ATTACHMENTS_DIR

The 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.Optional

Environment variable: OFW_UPLOAD_DIR

Explicit 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.Optional

Environment variable: OFW_CACHE_IDENTITY

Overrides the message cache directory (default ~/.cache/ofw-mcp).Optional

Environment variable: OFW_CACHE_DIR

Skip the fetchproxy fallback, so missing credentials become a hard error (useful in headless CI).Optional

Environment variable: OFW_DISABLE_FETCHPROXY

When 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.Optional

Environment variable: OFW_AUTO_REFRESH

How 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.Optional

Environment variable: OFW_FRESHNESS_TTL_SECONDS

Per-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).Optional

Environment variable: OFW_SYNC_MAX_REQUESTS

Per-request timeout for OFW API calls, in milliseconds (default 30000). Each retry gets its own fresh window.Optional

Environment variable: OFW_REQUEST_TIMEOUT_MS

Log every OFW request/response to stderr (Authorization redacted). Diagnostic only.Optional

Environment variable: OFW_DEBUG_LOG

IANA 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.Optional

Environment variable: DISPLAY_TZ

Localhost 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.Optional

Environment variable: FETCHPROXY_WS_PORT

Bind 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.Optional

Environment variable: FETCHPROXY_WS_HOST

Absolute path where the fetchproxy long-term identity keypair is stored (default ~/.fetchproxy/identity/).Optional

Environment variable: FETCHPROXY_IDENTITY_DIR

How to Install

Add this to your MCP configuration file:

{
  "mcpServers": {
    "io-github-chrischall-ofw-mcp": {
      "env": {
        "OFW_PASSWORD": "your-ofw-password-here",
        "OFW_USERNAME": "your-ofw-username-here",
        "OFW_WRITE_MODE": "your-ofw-write-mode-here",
        "OFW_EXPENSE_ONLY": "your-ofw-expense-only-here",
        "OFW_SESSION_FILE": "your-ofw-session-file-here",
        "OFW_SESSION_CACHE": "your-ofw-session-cache-here",
        "OFW_ALLOW_MARK_READ": "your-ofw-allow-mark-read-here",
        "OFW_CALENDAR_WRITES": "your-ofw-calendar-writes-here",
        "OFW_EXPENSE_UPLOAD_ONLY": "your-ofw-expense-upload-only-here",
        "OFW_FETCH_UNREAD_BODIES": "your-ofw-fetch-unread-bodies-here"
      },
      "args": [
        "-y",
        "ofw-mcp"
      ],
      "command": "npx"
    }
  }
}

Documentation

View on GitHub

From the project's GitHub 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": "your-email@example.com",
        "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": "your-email@example.com",
  "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:

OFW_USERNAME=you@example.com 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_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.

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.

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 outbound request passes its constructed URL through a host check before fetch — the server refuses to contact any host other than ofw.ourfamilywizard.com. Today it always passes, and that is the point: it makes "this server only ever talks 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.

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
  • Outbound requests are host-allowlisted to ofw.ourfamilywizard.com (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.

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

Reviews

No reviews yet

Be the first to review this server!