360 Secure SkillHub
360 安全技能中心

ofw

这是对接共同育儿平台OurFamilyWizard(OFW)的MCP服务,配置账号凭证后,可读写站内消息(含收发、草稿、附件)、共享日历、共同开销、育儿日记四类数据。工具采用本地缓存加实时新鲜度校验保障准确性,发送、删除等涉法律记录的敏感操作需用户确认,支持仅开放开销能力的受限部署,适配共同育儿场景的数据管理需求,输出结构化结果。

chrischall
chrischall
热度 62.03
最近更新 2026-10-10 07:29:27
分类:协作依赖:MCP 服务 / 第三方 API / 读写文件 / 内部调用大模型应用场景:API 集成 / 邮件起草 / 日程安排 / 财务 ...

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add ofw
Metadata
name
ofw
description
This skill should be used when the user asks about OurFamilyWizard (OFW) co-parenting data. Triggers on phrases like "check OFW", "OurFamilyWizard inbox", "OFW messages", "OFW calendar", "OFW expenses", "what did my co-parent say", "log an expense in OFW", "OFW journal", or any request involving co-parenting messages, calendar events, shared expenses, or journal entries.

ofw-mcp

MCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.

Setup

Option A — Claude Code (direct MCP, no mcporter)

Add to .mcp.json in your project or ~/.claude/mcp.json:

{
  "mcpServers": {
    "ofw": {
      "command": "npx",
      "args": ["-y", "ofw-mcp"],
      "env": {
        "OFW_USERNAME": "you@example.com",
        "OFW_PASSWORD": "yourpassword"
      }
    }
  }
}

Option B — mcporter

1. Install

npm install -g ofw-mcp

Or from source:

git clone https://github.com/chrischall/ofw-mcp
cd ofw-mcp
npm install && npm run build

2. Configure credentials

cp .env.example .env
# Edit .env: set OFW_USERNAME and OFW_PASSWORD

3. Register with mcporter

mcporter config add ofw \
  --command "ofw-mcp" \
  --env "OFW_USERNAME=you@example.com" \
  --env "OFW_PASSWORD=yourpassword" \
  --config ~/.mcporter/mcporter.json

4. Verify

mcporter list --config ~/.mcporter/mcporter.json
mcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json

Calling tools (mcporter)

mcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json

Always pass --config ~/.mcporter/mcporter.json unless a local config/mcporter.json exists.

Tools

User

Tool Description
ofw_get_profile Current user + co-parent info (IDs, contact details)
ofw_get_notifications Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status.

Messages

Tool Notes
ofw_sync_messages(folders?, deep?, fetchUnreadBodies?) Sync OFW → local cache. Call first if the cache might be stale. Returns unread inbox hints (bodies not fetched, to avoid mark-as-read).
ofw_list_message_folders List OFW folders with unread counts. Most reads use the cache; this is mainly for folder IDs and live unread counts.
ofw_list_messages(folderId?, since?, until?, q?, sort?, page?, size?, autoRefresh?, view?) Cache-backed list. Supports folder ("inbox"/"sent"/"both"), date range, and substring search. sort:"oldest" starts at the old end of a range instead of paging to it (default "newest"). Returns complete for the RESULT SET plus nextPage (null when done) — and the paging keys come FIRST in the JSON, before messages. returned carries the record count as a scalar, beside total. An empty result from a non-fresh cache is refused (UNVERIFIED_EMPTY) — pass autoRefresh:true to sync and answer instead.
ofw_get_message(messageId, allowMarkRead?, view?) Read a message OR draft body. Cache-first. Ids in the drafts cache return folder: "drafts". ⚠️ Falls through to OFW for unread inbox messages, which marks them read AND stamps a "First Viewed" time the co-parent can see — irreversible. Pass allowMarkRead:false to refuse that fetch instead; cached, sent and already-read messages are unaffected.
ofw_send_message(draftId?, subject?, body?, recipientIds?, replyToId?, expectedRevision?, deleteDraftOnSuccess?, myFileIDs?, force?) Send a message — the one irreversible operation. Preferred path: pass draftId (+ expectedRevision) to send an existing draft as it exists on the server — the tool re-reads it from OFW first and refuses if it changed since you read it (or was already sent/deleted); subject/body become optional overrides. recipientIds is usually still required: OFW does not store recipients on drafts. After a confirmed send the draft is auto-deleted (deleteDraftOnSuccess:false to keep it); on any failure or ambiguity it is retained and the response says why (draftRetained). Response leads with sentMessageId, draftKey, threaded, draftDeleted. Compose from scratch by passing subject/body/recipientIds with no draftId.
ofw_get_unread_sent(page?, size?, autoRefresh?) Sent messages your co-parent hasn't read yet (from cache). Leads with complete/hasMore/nextPage, then scanned/total, then unread; an empty sent cache that is not fresh is refused rather than reported as "nothing sent".
ofw_list_drafts(page?, size?, verify?, autoRefresh?, view?) Leads with complete/hasMore/nextPage; drafts comes last. List saved drafts, auto-verified: when the cache is not verified-fresh a cheap drafts sync runs first (default verify:true), so one call answers server-confirmed. verify:false serves straight from cache. Each draft carries serverConfirmed, revision and draftKey. Returns complete — check it before saying "you have N drafts". See Freshness.
ofw_save_draft(subject, body, recipientIds?, messageId?, replyToId?, myFileIDs?, expectedRevision?, force?) Create a new draft. Pass messageId to replace an existing draft: the tool creates a fresh draft and deletes the old one (OFW's update-in-place endpoint silently no-ops). The returned id is the NEW id; the response leads with draftKey, which stays the same across every edit — track that, not the id. Note: OFW does not store recipients on drafts — recipientIds are accepted but come back empty (a one-line NOTE says so; supply them at send time instead). Threading warnings fire only on genuine drops — a draft echoing inReplyTo/showContext IS threaded.
ofw_delete_draft(messageId) Delete a draft.
ofw_upload_attachment(path, shareClass?, label?, description?) Upload a local file to My Files; returns a fileId to pass into myFileIDs. Only files inside the upload directory (OFW_UPLOAD_DIR, default ~/Downloads/ofw-mcp) can be uploaded; hidden files are refused. shareClass:"SHARED" needs write mode all.
ofw_download_attachment(fileId, inline?, saveTo?, force?, extract?, maxChars?, parts?) Download an attachment. Inline delivery returns the first rung that works: image → ImageContent; .xlsx/.csv/.pdf/.docx/.pptx/text → extracted content under extracted (per-sheet CSV, per-page/slide text); anything else → raw bytes. Default writes to ~/Downloads/ofw-mcp/ (add extract:true for content too). Use parts:"1-2" / a sheet name and maxChars on large files. Images and raw bytes over 10 MiB are not returned inline (tool error) — use disk mode or open the file in OFW.
ofw_check_freshness(folders?, messageIds?, allowMarkRead?) Cheap live check that the cache still matches OFW — one request for folder counts plus one per id, no bodies, no sync. Each id gets a live state (draft/sent/received/deleted/unknown) plus folder and sentAt. Probes ids cached as drafts, as sent, or as already-read inbox messages freely; anything else needs allowMarkRead:true (it would mark an inbox message read).
ofw_status(ids?, draftKeys?, includeDraftInventory?, allowMarkRead?) The status call. One live round trip. With no arguments: the full, server-verified draft inventory. With ids/draftKeys: each one's live lifecycle state. Top-level complete is true only when every part was verified live.

Calendar

Tool Notes
ofw_list_events(startDate, endDate, detailed?) Dates as YYYY-MM-DD
ofw_create_event(title, startDate, endDate, ...) startDate/endDate as ISO datetime
ofw_update_event(eventId, ...) Partial update — only pass fields to change
ofw_delete_event(eventId) Permanent delete

Restricted expense deployments

When OFW_EXPENSE_ONLY=true, only the healthcheck and expense tools exist. When OFW_EXPENSE_UPLOAD_ONLY=true, only ofw_healthcheck, ofw_upload_expense_pdf, ofw_create_expense, and ofw_update_expense exist. Do not suggest message/calendar/journal/profile tools in those deployments; they are structurally unregistered.

Expenses

Tool Notes
ofw_get_expense_totals Summary of owed/paid totals
ofw_list_expenses(page?, size?) Paginated (1-based page); default size 20; follow nextPage
ofw_list_expense_categories Category ids and split metadata (use the id in create/update)
ofw_upload_expense_pdf(path | url, fileName?, label?, description?) Upload one receipt PDF to My Files (SHARED, co-parent-visible even on a private expense) for later attachment; confirm-gated; mode all only
ofw_create_expense(title, amount, purchaseDate, categoryId, payerId, children, description?, privateExpense?, receiptFileId?) Log a new expense; confirm-gated; optionally private with one receipt
ofw_update_expense(expenseId, …only the fields to change…) Reads the expense and keeps every field you omit; privateExpense:false publishes a private expense; receiptFileId replaces all receipts, null clears it or description; refuses as EXPENSE_FIELDS_UNREADABLE (naming the fields) rather than erase one it can't read; confirm-gated

Journal

Tool Notes
ofw_list_journal_entries(start?, max?) 1-based offset; default max 10
ofw_create_journal_entry(title, body) Create a new entry

Response shape (view)

ofw_list_messages, ofw_get_message and ofw_list_drafts take view: "compact" | "full", and compact is the default — you get the slim shape without asking. It measured 135.1 KB → 41.1 KB on a real 50-message page.

What compact drops is listData, OFW's echo of the list payload. That blob was 58% of the response and 78% of it duplicated fields the same object already emits at the top level — eleven pre-formatted renderings of one timestamp, eight fields per recipient beside the three the row already normalised, and a preview that is a truncation of the body in the same object.

Two things about compact that will surprise you if you don't know them:

  • The sender is from, not fromUser. fromUser is the empty string on every row — inbox and sent alike — because OFW names the sender only inside listData.author. So compact promotes it. Dropping listData without that would have taken the sender off every message; this is the one field the fat blob was carrying alone.
  • On a draft, ofw_get_message returns from: null — a draft is unsent, so there is no sender, and "" would read as one we failed to find.

Pass view: "full" when you need a field compact dropped — listData and everything in it. There is deliberately no raw rung: a message here is assembled from a list item, a detail GET and derived fields, so there is no single upstream payload to hand back, and a rung that skipped normalisation would put naive local times back beside UTC ones on exactly the rung you reach for when something already looks wrong.

ofw_get_unread_sent takes no view: it emits a verdict list ({id, subject, sentAt, unreadBy}) that is already narrower than the projection.

Freshness, completeness, and lifecycle

Message and draft reads come from a local cache, so a result can be stale without looking stale. Three separate questions, three separate signals — do not substitute one for another:

Question Signal
How old is this data? freshness — staleness (fresh/unverified/stale), asOf, ageSeconds, a quotable warning
Is this the WHOLE answer? complete on ofw_list_messages / ofw_list_drafts / ofw_get_unread_sent / ofw_status
If not, how do I get the rest? nextPage (message tools and ofw_list_expenses) or nextStart (ofw_list_journal_entries) — null means there is no more
Is this entity still what I think it is? state from ofw_status / ofw_check_freshness

Rules:

  • Verification is cheaper than recollection. Use it. Any status summary about drafts costs exactly one ofw_status() call. There is no situation in which recalling an earlier tool result is the better option.
  • Never state current state from memory. A draft you saved earlier in the session is not evidence it still exists unsent now — the user may have sent, edited or deleted it in the web app since. This has gone wrong twice: drafts described as "still sitting unsent" that had already been sent.
  • existsOnServer does not mean "still a draft". A draft that was SENT still exists on the server. Only state distinguishes them.
  • Check complete before quoting a count. complete: false means the result set is a slice, or unverified, or both — completeNote says which. "You have 3 drafts" requires complete: true.
  • serverConfirmed: false means "remembered, not known." Call ofw_status / ofw_check_freshness first, or say plainly that you are reporting cached state and give its age.
  • A refusal is a good outcome. result: "UNVERIFIED_EMPTY" means the tool declined to report an absence it could not verify. Do the remedy — never re-report it as "nothing found". A wrong "no, that was never sent" is far costlier here than one extra call.
  • state: "unknown" is not "fine". It means the question was not answered.
  • OFW does not bump a draft's timestamp when it is edited in the web app, which is why freshness is compared by content revision. "Nothing changed" and "we didn't look" are otherwise indistinguishable.
  • A missing folder count in ofw_sync_messages output means that folder was not checked — it is never "no changes". Check notRefreshed.

Draft identity (draftKey)

Editing a draft mints a new OFW id every time — ofw_save_draft replaces by create-then-delete, so one message can burn through ten ids in a session. Track the draftKey it returns, not the id:

  • ofw_status(draftKeys: ["dk_…"]) resolves the key to the chain's current id and state.
  • The key keeps resolving after the draft is sent: state: "sent" with sentMessageId and sentAt.
  • draftKey: null on a draft means it was authored outside this tool; it is adopted into a chain the first time you save over it.

Workflows

Check inbox:

  1. ofw_list_message_folders → find inbox folder ID
  2. ofw_list_messages(folderId) → list messages
  3. ofw_get_message(messageId) → read a specific message

Send a message:

  1. ofw_get_profile → get co-parent's user ID
  2. ofw_send_message(subject, body, [coParentId])

Reply to a message (with thread history):

  1. ofw_get_message(messageId) → read the message to reply to
  2. ofw_send_message(subject, body, [coParentId], replyToId: messageId) — original message is included in the thread

Draft before sending (sensitive messages):

  1. ofw_save_draft(subject, body) → review with user; note the returned draftKey and revision
  2. ofw_send_message(draftId, recipientIds, expectedRevision) after approval — sends the server's copy of the draft (no body re-supply), refuses if it changed since review, and deletes the draft only after the send is confirmed

Check what's coming up:

  • ofw_get_notifications for a quick summary
  • ofw_list_events(today, +30days) for calendar detail

Caution

  • Always confirm before sending messages or deleting anything — OFW is a legal co-parenting record.
  • The server enforces it for co-parent-visible writes. ofw_send_message, ofw_create_expense, shared-event ofw_create_event/ofw_update_event/ofw_delete_event and SHARED ofw_upload_attachment either raise a confirmation prompt (unless the server sets MCP_CONFIRM_ELICITATION=off) or, on clients that cannot (claude.ai, Claude Desktop) and whenever that is set, return status: "confirmation-required" with a preview and a confirmToken and write nothing. Show the user the preview, get their approval, then repeat the SAME call with confirmToken. A DRAFT_CHANGED refusal means the arguments or the target changed since the preview — show the fresh preview it returns and ask again.
  • *_UNCONFIRMED means it may have landed. Never retry a SEND_/EXPENSE_/EVENT_/JOURNAL_UNCONFIRMED write until the matching list/sync shows it did not.
  • ofw_get_notifications updates last-seen status — avoid calling silently in the background.
  • ofw_get_message marks messages read — warn the user if they want to keep something unread.
  • Do not narrate cached state as present fact. Before saying what "is" true on OFW right now, call ofw_status — one live round trip that answers drafts, ids and draft keys at once. Never assemble a status summary from earlier tool results in the conversation; re-read.