{"id":"a2d45d20-4aa0-4fb4-a1f1-720e5a470e1c","entityType":"agent","slug":"clawhub-chrischall-ofw","name":"ofw","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-ofw","canonicalPath":"/agent/clawhub-chrischall-ofw","generatedAt":"2026-10-10T23:46:45.565Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-10T21:39:28.519Z","emptyReason":null},"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. Skill: ofw Owner: chrischall Summary: 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. Tags: la","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.3K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:ofw","sourceUrl":"https://clawhub.ai/chrischall/ofw","homepage":"https://clawhub.ai/chrischall/skills/ofw","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/ofw","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/ofw","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":62,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"This skill should be used when the user asks about OurFamilyWizard (OFW) co-parenting data. Triggers on phrases like \"check OFW\", \"OurFamilyWizard inbox\", \"OFW "},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T21:39:28.519Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T21:39:28.519Z","emptyReason":null},"stars":null,"forks":null,"downloads":1250,"packageName":null,"latestVersion":"2.20.4","tractionLabel":"1.3K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T21:39:28.444Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T21:39:28.519Z","lastCrawledAt":"2026-10-10T21:39:28.444Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T21:39:28.444Z","lastVerifiedAt":null,"highlights":[{"version":"2.20.4","createdAt":"2026-10-09T23:26:57.831Z","changelog":"- Removed redundant skill-card.md file. - No user-facing feature changes; documentation and usage instructions remain unchanged.","fileCount":3,"zipByteSize":8900},{"version":"2.20.3","createdAt":"2026-10-07T13:37:56.734Z","changelog":"- Removed the file skill-card.md. - No other changes to functionality or documentation.","fileCount":3,"zipByteSize":8806},{"version":"2.20.2","createdAt":"2026-10-05T03:26:48.663Z","changelog":"- Removed the file: skill-card.md - No other changes to functionality or features in this release.","fileCount":3,"zipByteSize":8709},{"version":"2.20.1","createdAt":"2026-10-03T01:45:02.869Z","changelog":"- Removed the file: skill-card.md - No other functional or descriptive changes detected.","fileCount":3,"zipByteSize":8772},{"version":"2.20.0","createdAt":"2026-09-29T00:43:02.503Z","changelog":"ofw 2.20.0 - Removed the file: skill-card.md - Updated SKILL.md; no user-facing functionality changes documented.","fileCount":3,"zipByteSize":8843},{"version":"2.19.5","createdAt":"2026-09-28T05:11:54.794Z","changelog":"- Removed the file: skill-card.md - Updated SKILL.md with minor documentation changes, including improved details about attachment size limits for inline downloads.","fileCount":3,"zipByteSize":8361},{"version":"2.19.4","createdAt":"2026-09-25T15:35:51.052Z","changelog":"- Removed the skill-card.md file. - Updated SKILL.md; content changes are not detailed, but overall documentation was modified.","fileCount":3,"zipByteSize":8230},{"version":"2.19.3","createdAt":"2026-09-23T21:44:43.726Z","changelog":"- Removed obsolete skill-card.md file. - Updated SKILL.md: revised the description for ofw_upload_attachment to specify upload restrictions (only files inside the upload directory allowed; hidden files refused; \"SHARED\" files require permission). - No functional changes to the API documented. - Documentation is now more explicit about file upload requirements.","fileCount":3,"zipByteSize":7824}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:ofw","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T23:46:45.560Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-ofw/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-10T21:39:28.519Z","emptyReason":null},"readme":"Skill: ofw\n\nOwner: chrischall\n\nSummary: 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.\n\nTags: latest:2.20.4\n\nVersion history:\n\nv2.20.4 | 2026-10-09T23:26:57.831Z | auto\n\n- Removed redundant skill-card.md file.\n- No user-facing feature changes; documentation and usage instructions remain unchanged.\n\nv2.20.3 | 2026-10-07T13:37:56.734Z | auto\n\n- Removed the file skill-card.md.  \n- No other changes to functionality or documentation.\n\nv2.20.2 | 2026-10-05T03:26:48.663Z | auto\n\n- Removed the file: skill-card.md\n- No other changes to functionality or features in this release.\n\nv2.20.1 | 2026-10-03T01:45:02.869Z | auto\n\n- Removed the file: skill-card.md\n- No other functional or descriptive changes detected.\n\nv2.20.0 | 2026-09-29T00:43:02.503Z | auto\n\nofw 2.20.0\n\n- Removed the file: skill-card.md\n- Updated SKILL.md; no user-facing functionality changes documented.\n\nv2.19.5 | 2026-09-28T05:11:54.794Z | auto\n\n- Removed the file: skill-card.md\n- Updated SKILL.md with minor documentation changes, including improved details about attachment size limits for inline downloads.\n\nv2.19.4 | 2026-09-25T15:35:51.052Z | auto\n\n- Removed the skill-card.md file.\n- Updated SKILL.md; content changes are not detailed, but overall documentation was modified.\n\nv2.19.3 | 2026-09-23T21:44:43.726Z | auto\n\n- Removed obsolete skill-card.md file.\n- Updated SKILL.md: revised the description for ofw_upload_attachment to specify upload restrictions (only files inside the upload directory allowed; hidden files refused; \"SHARED\" files require permission).\n- No functional changes to the API documented.\n- Documentation is now more explicit about file upload requirements.\n\nv2.19.2 | 2026-09-23T15:42:00.010Z | auto\n\n- Removed the file: skill-card.md\n- No functional or interface changes to the skill itself.\n\nv2.19.1 | 2026-09-21T04:12:09.766Z | auto\n\n- Removed the file skill-card.md from the project.\n- No changes to functionality or user features.\n\nv2.19.0 | 2026-09-20T18:28:37.204Z | auto\n\n- Removed the file skill-card.md.\n- No other changes to core functionality or documentation.\n\nv2.18.0 | 2026-09-20T02:50:06.028Z | auto\n\n- Removed the file: skill-card.md\n- No other changes to features, usage, or tools.\n- This update is a minor cleanup removing a documentation file.\n\nv2.17.0 | 2026-09-17T12:27:30.771Z | auto\n\n- Removed the file skill-card.md.\n- No changes to core features or functionality.\n- No user-facing impact; this is a documentation cleanup.\n\nv2.16.3 | 2026-09-15T19:21:57.103Z | auto\n\n- Removed the documentation file skill-card.md.\n- No changes to code or core functionality.\n\nv2.16.2 | 2026-09-14T14:13:04.587Z | auto\n\n- Removed the file: skill-card.md\n- No changes to skill functionality, setup, or user tools.\n\nv2.16.1 | 2026-09-10T17:51:09.011Z | auto\n\nVersion 2.16.1\n\n- Removed the file: skill-card.md\n- No other user-facing changes documented.\n\nv2.16.0 | 2026-09-08T00:35:48.388Z | auto\n\n- Removed the file: skill-card.md\n- No changes to functionality or setup documented.\n\nv2.15.1 | 2026-09-05T00:51:30.714Z | auto\n\n- Added view? parameter to ofw_list_messages, ofw_get_message, and ofw_list_drafts tool definitions.\n- Removed the file skill-card.md.\n\nv2.15.0 | 2026-09-04T22:22:26.218Z | auto\n\n- Removed the file: skill-card.md\n- No other changes to features, tools, or setup documented.\n\nv2.14.0 | 2026-08-31T16:38:29.441Z | auto\n\n- Removed the file skill-card.md.\n- No other changes to functionality or features.\n\nv2.13.0 | 2026-08-29T13:54:44.576Z | auto\n\n- Removed the skill-card.md file.\n- No other functional or breaking changes.\n\nv2.12.0 | 2026-08-28T11:35:09.888Z | auto\n\nofw 2.12.0 changelog:\n\n- Added detailed SKILL.md with tool descriptions, setup instructions, and usage examples.\n- Clarified tool triggers for handling co-parenting messages, events, expenses, and journals from OurFamilyWizard.\n- Documented all supported actions for messages (sync, list, send, drafts, attachments), calendar events, and expenses.\n- Provided step-by-step setup for both direct MCP and mcporter integration.\n- Outlined safety precautions, cache behaviors, and irreversible actions for message management.\n\nArchive index:\n\nArchive v2.20.4: 3 files, 8900 bytes\n\nFiles: skill-card.md (2376b), SKILL.md (17159b), _meta.json (123b)\n\nFile v2.20.4:SKILL.md\n\n---\nname: ofw\ndescription: 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.\n---\n\n# ofw-mcp\n\nMCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.\n\n- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)\n- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)\n\n\n## Setup\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g ofw-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\n## Tools\n\n### User\n| Tool | Description |\n|------|-------------|\n| `ofw_get_profile` | Current user + co-parent info (IDs, contact details) |\n| `ofw_get_notifications` | Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status. |\n\n### Messages\n| Tool | Notes |\n|------|-------|\n| `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). |\n| `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. |\n| `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. |\n| `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. |\n| `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`. |\n| `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\". |\n| `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](#freshness). |\n| `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. |\n| `ofw_delete_draft(messageId)` | Delete a draft. |\n| `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`. |\n| `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. |\n| `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). |\n| `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. |\n\n### Calendar\n| Tool | Notes |\n|------|-------|\n| `ofw_list_events(startDate, endDate, detailed?)` | Dates as `YYYY-MM-DD` |\n| `ofw_create_event(title, startDate, endDate, ...)` | `startDate`/`endDate` as ISO datetime |\n| `ofw_update_event(eventId, ...)` | Partial update — only pass fields to change |\n| `ofw_delete_event(eventId)` | Permanent delete |\n\n### Restricted expense deployments\n\nWhen `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.\n\n### Expenses\n| Tool | Notes |\n|------|-------|\n| `ofw_get_expense_totals` | Summary of owed/paid totals |\n| `ofw_list_expenses(page?, size?)` | Paginated (1-based page); default size 20; follow `nextPage` |\n| `ofw_list_expense_categories` | Category ids and split metadata (use the id in create/update) |\n| `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 |\n| `ofw_create_expense(title, amount, purchaseDate, categoryId, payerId, children, description?, privateExpense?, receiptFileId?)` | Log a new expense; confirm-gated; optionally private with one receipt |\n| `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 |\n\n### Journal\n| Tool | Notes |\n|------|-------|\n| `ofw_list_journal_entries(start?, max?)` | 1-based offset; default max 10 |\n| `ofw_create_journal_entry(title, body)` | Create a new entry |\n\n## Response shape (`view`)\n\n`ofw_list_messages`, `ofw_get_message` and `ofw_list_drafts` take\n`view: \"compact\" | \"full\"`, and **`compact` is the default** — you get the slim\nshape without asking. It measured 135.1 KB → 41.1 KB on a real 50-message page.\n\nWhat compact drops is `listData`, OFW's echo of the list payload. That blob was\n58% of the response and 78% of it duplicated fields the same object already\nemits at the top level — eleven pre-formatted renderings of one timestamp,\neight fields per recipient beside the three the row already normalised, and a\n`preview` that is a truncation of the `body` in the same object.\n\n**Two things about compact that will surprise you if you don't know them:**\n\n- **The sender is `from`, not `fromUser`.** `fromUser` is the empty string on\n  every row — inbox and sent alike — because OFW names the sender only inside\n  `listData.author`. So compact promotes it. Dropping `listData` without that\n  would have taken the sender off every message; this is the one field the fat\n  blob was carrying alone.\n- **On a draft, `ofw_get_message` returns `from: null`** — a draft is unsent, so\n  there is no sender, and `\"\"` would read as one we failed to find.\n\nPass `view: \"full\"` when you need a field compact dropped — `listData` and\neverything in it. There is deliberately **no `raw` rung**: a message here is\nassembled from a list item, a detail GET and derived fields, so there is no\nsingle upstream payload to hand back, and a rung that skipped normalisation\nwould put naive local times back beside UTC ones on exactly the rung you reach\nfor when something already looks wrong.\n\n`ofw_get_unread_sent` takes no `view`: it emits a verdict list\n(`{id, subject, sentAt, unreadBy}`) that is already narrower than the\nprojection.\n\n## Freshness, completeness, and lifecycle\n\nMessage 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:\n\n| Question | Signal |\n|---|---|\n| How old is this data? | `freshness` — `staleness` (`fresh`/`unverified`/`stale`), `asOf`, `ageSeconds`, a quotable `warning` |\n| Is this the WHOLE answer? | `complete` on `ofw_list_messages` / `ofw_list_drafts` / `ofw_get_unread_sent` / `ofw_status` |\n| 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 |\n| Is this entity still what I think it is? | `state` from `ofw_status` / `ofw_check_freshness` |\n\nRules:\n\n- **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.\n- **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.\n- **`existsOnServer` does not mean \"still a draft\".** A draft that was SENT still exists on the server. Only `state` distinguishes them.\n- **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`.\n- **`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.\n- **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.\n- **`state: \"unknown\"` is not \"fine\".** It means the question was not answered.\n- 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.\n- A missing folder count in `ofw_sync_messages` output means that folder was **not checked** — it is never \"no changes\". Check `notRefreshed`.\n\n### Draft identity (`draftKey`)\n\nEditing 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:\n\n- `ofw_status(draftKeys: [\"dk_…\"])` resolves the key to the chain's **current** id and state.\n- The key keeps resolving after the draft is sent: `state: \"sent\"` with `sentMessageId` and `sentAt`.\n- `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.\n\n\n## Workflows\n\n**Check inbox:**\n1. `ofw_list_message_folders` → find inbox folder ID\n2. `ofw_list_messages(folderId)` → list messages\n3. `ofw_get_message(messageId)` → read a specific message\n\n**Send a message:**\n1. `ofw_get_profile` → get co-parent's user ID\n2. `ofw_send_message(subject, body, [coParentId])`\n\n**Reply to a message (with thread history):**\n1. `ofw_get_message(messageId)` → read the message to reply to\n2. `ofw_send_message(subject, body, [coParentId], replyToId: messageId)` — original message is included in the thread\n\n**Draft before sending (sensitive messages):**\n1. `ofw_save_draft(subject, body)` → review with user; note the returned `draftKey` and `revision`\n2. `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\n\n**Check what's coming up:**\n- `ofw_get_notifications` for a quick summary\n- `ofw_list_events(today, +30days)` for calendar detail\n\n## Caution\n\n- **Always confirm before sending messages or deleting anything** — OFW is a legal co-parenting record.\n- **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.\n- **`*_UNCONFIRMED` means it may have landed.** Never retry a `SEND_`/`EXPENSE_`/`EVENT_`/`JOURNAL_UNCONFIRMED` write until the matching list/sync shows it did not.\n- `ofw_get_notifications` updates last-seen status — avoid calling silently in the background.\n- `ofw_get_message` marks messages read — warn the user if they want to keep something unread.\n- **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.\n\nFile v2.20.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"ofw\",\n  \"version\": \"2.20.4\",\n  \"publishedAt\": 1791588417831\n}\n\nFile v2.20.4:skill-card.md\n\n## Description:\n\nHelps agents work with OurFamilyWizard co-parenting messages, calendars, expenses, and journal entries through an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople using OurFamilyWizard and their assistants can check co-parenting records, draft or send messages, manage calendar events and expenses, and review journal entries.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The connected server can access sensitive co-parenting messages, calendar entries, expenses, journal entries, attachments, and profile details.\n\nMitigation: Review access before installation and connect only accounts whose records you are comfortable sharing with the server.\n\nRisk: Example setup stores the OFW password in project configuration or an .env file, where it could be exposed or committed.\n\nMitigation: Use a protected secret manager or tightly permissioned user-only configuration instead of project-local or committed credentials.\n\nRisk: Sending messages, deleting records, sharing uploads, and changing events or expenses can affect other people or permanently change records.\n\nMitigation: Show the intended action and obtain explicit user approval before each send, delete, shared upload, event change, or expense change.\n\nRisk: Some reads can mark messages as read or notifications as seen.\n\nMitigation: Warn users before reads that could change read or seen status, and avoid fetching them silently.\n\n## Reference(s):\n\n- [OFW skill on ClawHub](https://clawhub.ai/chrischall/skills/ofw)\n- [ofw-mcp package on npm](https://www.npmjs.com/package/ofw-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Guidance, Shell commands, Configuration instructions]\n\n**Output Format:** [Markdown with configuration and command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Can guide agent use of OFW tools to retrieve or change account records.]\n\n## Skill Version(s):\n\n2.20.4 (source: ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.20.3: 3 files, 8806 bytes\n\nFiles: skill-card.md (2293b), SKILL.md (17079b), _meta.json (123b)\n\nFile v2.20.3:SKILL.md\n\n---\nname: ofw\ndescription: 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.\n---\n\n# ofw-mcp\n\nMCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.\n\n- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)\n- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)\n\n\n## Setup\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g ofw-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\n## Tools\n\n### User\n| Tool | Description |\n|------|-------------|\n| `ofw_get_profile` | Current user + co-parent info (IDs, contact details) |\n| `ofw_get_notifications` | Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status. |\n\n### Messages\n| Tool | Notes |\n|------|-------|\n| `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). |\n| `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. |\n| `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. |\n| `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. |\n| `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`. |\n| `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\". |\n| `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](#freshness). |\n| `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. |\n| `ofw_delete_draft(messageId)` | Delete a draft. |\n| `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`. |\n| `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. |\n| `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). |\n| `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. |\n\n### Calendar\n| Tool | Notes |\n|------|-------|\n| `ofw_list_events(startDate, endDate, detailed?)` | Dates as `YYYY-MM-DD` |\n| `ofw_create_event(title, startDate, endDate, ...)` | `startDate`/`endDate` as ISO datetime |\n| `ofw_update_event(eventId, ...)` | Partial update — only pass fields to change |\n| `ofw_delete_event(eventId)` | Permanent delete |\n\n### Restricted expense deployments\n\nWhen `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.\n\n### Expenses\n| Tool | Notes |\n|------|-------|\n| `ofw_get_expense_totals` | Summary of owed/paid totals |\n| `ofw_list_expenses(page?, size?)` | Paginated (1-based page); default size 20; follow `nextPage` |\n| `ofw_list_expense_categories` | Category ids and split metadata (use the id in create/update) |\n| `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 |\n| `ofw_create_expense(title, amount, purchaseDate, categoryId, payerId, children, description?, privateExpense?, receiptFileId?)` | Log a new expense; confirm-gated; optionally private with one receipt |\n| `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 |\n\n### Journal\n| Tool | Notes |\n|------|-------|\n| `ofw_list_journal_entries(start?, max?)` | 1-based offset; default max 10 |\n| `ofw_create_journal_entry(title, body)` | Create a new entry |\n\n## Response shape (`view`)\n\n`ofw_list_messages`, `ofw_get_message` and `ofw_list_drafts` take\n`view: \"compact\" | \"full\"`, and **`compact` is the default** — you get the slim\nshape without asking. It measured 135.1 KB → 41.1 KB on a real 50-message page.\n\nWhat compact drops is `listData`, OFW's echo of the list payload. That blob was\n58% of the response and 78% of it duplicated fields the same object already\nemits at the top level — eleven pre-formatted renderings of one timestamp,\neight fields per recipient beside the three the row already normalised, and a\n`preview` that is a truncation of the `body` in the same object.\n\n**Two things about compact that will surprise you if you don't know them:**\n\n- **The sender is `from`, not `fromUser`.** `fromUser` is the empty string on\n  every row — inbox and sent alike — because OFW names the sender only inside\n  `listData.author`. So compact promotes it. Dropping `listData` without that\n  would have taken the sender off every message; this is the one field the fat\n  blob was carrying alone.\n- **On a draft, `ofw_get_message` returns `from: null`** — a draft is unsent, so\n  there is no sender, and `\"\"` would read as one we failed to find.\n\nPass `view: \"full\"` when you need a field compact dropped — `listData` and\neverything in it. There is deliberately **no `raw` rung**: a message here is\nassembled from a list item, a detail GET and derived fields, so there is no\nsingle upstream payload to hand back, and a rung that skipped normalisation\nwould put naive local times back beside UTC ones on exactly the rung you reach\nfor when something already looks wrong.\n\n`ofw_get_unread_sent` takes no `view`: it emits a verdict list\n(`{id, subject, sentAt, unreadBy}`) that is already narrower than the\nprojection.\n\n## Freshness, completeness, and lifecycle\n\nMessage 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:\n\n| Question | Signal |\n|---|---|\n| How old is this data? | `freshness` — `staleness` (`fresh`/`unverified`/`stale`), `asOf`, `ageSeconds`, a quotable `warning` |\n| Is this the WHOLE answer? | `complete` on `ofw_list_messages` / `ofw_list_drafts` / `ofw_get_unread_sent` / `ofw_status` |\n| 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 |\n| Is this entity still what I think it is? | `state` from `ofw_status` / `ofw_check_freshness` |\n\nRules:\n\n- **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.\n- **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.\n- **`existsOnServer` does not mean \"still a draft\".** A draft that was SENT still exists on the server. Only `state` distinguishes them.\n- **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`.\n- **`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.\n- **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.\n- **`state: \"unknown\"` is not \"fine\".** It means the question was not answered.\n- 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.\n- A missing folder count in `ofw_sync_messages` output means that folder was **not checked** — it is never \"no changes\". Check `notRefreshed`.\n\n### Draft identity (`draftKey`)\n\nEditing 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:\n\n- `ofw_status(draftKeys: [\"dk_…\"])` resolves the key to the chain's **current** id and state.\n- The key keeps resolving after the draft is sent: `state: \"sent\"` with `sentMessageId` and `sentAt`.\n- `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.\n\n\n## Workflows\n\n**Check inbox:**\n1. `ofw_list_message_folders` → find inbox folder ID\n2. `ofw_list_messages(folderId)` → list messages\n3. `ofw_get_message(messageId)` → read a specific message\n\n**Send a message:**\n1. `ofw_get_profile` → get co-parent's user ID\n2. `ofw_send_message(subject, body, [coParentId])`\n\n**Reply to a message (with thread history):**\n1. `ofw_get_message(messageId)` → read the message to reply to\n2. `ofw_send_message(subject, body, [coParentId], replyToId: messageId)` — original message is included in the thread\n\n**Draft before sending (sensitive messages):**\n1. `ofw_save_draft(subject, body)` → review with user; note the returned `draftKey` and `revision`\n2. `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\n\n**Check what's coming up:**\n- `ofw_get_notifications` for a quick summary\n- `ofw_list_events(today, +30days)` for calendar detail\n\n## Caution\n\n- **Always confirm before sending messages or deleting anything** — OFW is a legal co-parenting record.\n- **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 or, on clients that cannot (claude.ai, Claude Desktop), 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.\n- **`*_UNCONFIRMED` means it may have landed.** Never retry a `SEND_`/`EXPENSE_`/`EVENT_`/`JOURNAL_UNCONFIRMED` write until the matching list/sync shows it did not.\n- `ofw_get_notifications` updates last-seen status — avoid calling silently in the background.\n- `ofw_get_message` marks messages read — warn the user if they want to keep something unread.\n- **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.\n\nFile v2.20.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"ofw\",\n  \"version\": \"2.20.3\",\n  \"publishedAt\": 1791380276734\n}\n\nFile v2.20.3:skill-card.md\n\n## Description:\n\nHelps an agent read and manage OurFamilyWizard messages, calendars, expenses, attachments, and journal entries for co-parenting tasks.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople using an agent with their OurFamilyWizard account can review co-parenting records and, with appropriate approval, draft or make changes to messages, calendar events, expenses, attachments, and journal entries.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Broad co-parenting requests may expose sensitive records or lead to unintended account actions.\n\nMitigation: Invoke only for explicit OFW requests and protect account credentials.\n\nRisk: Messages, shared uploads, calendar changes, expenses, deletions, and journal entries can affect sensitive co-parenting records.\n\nMitigation: Show the proposed action and obtain user confirmation before writing, sending, sharing, or deleting.\n\nRisk: Opening an unread message can mark it read and expose a first-viewed time to the co-parent.\n\nMitigation: Warn before fetching unread messages and avoid marking them read without approval.\n\nRisk: Cached or incomplete results can misstate whether a draft, message, or other record is current.\n\nMitigation: Check live status, freshness, and pagination before asserting the current state or retrying uncertain writes.\n\n## Reference(s):\n\n- [ClawHub OFW skill release](https://clawhub.ai/chrischall/skills/ofw)\n- [ofw-mcp npm package (linked in skill)](https://www.npmjs.com/package/ofw-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration instructions]\n\n**Output Format:** [Markdown responses and setup examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Account-dependent results may require a fresh check before reporting current status.]\n\n## Skill Version(s):\n\n2.20.3 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.20.2: 3 files, 8709 bytes\n\nFiles: skill-card.md (2008b), SKILL.md (17079b), _meta.json (123b)\n\nFile v2.20.2:SKILL.md\n\n---\nname: ofw\ndescription: 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.\n---\n\n# ofw-mcp\n\nMCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.\n\n- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)\n- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)\n\n\n## Setup\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g ofw-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\n## Tools\n\n### User\n| Tool | Description |\n|------|-------------|\n| `ofw_get_profile` | Current user + co-parent info (IDs, contact details) |\n| `ofw_get_notifications` | Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status. |\n\n### Messages\n| Tool | Notes |\n|------|-------|\n| `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). |\n| `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. |\n| `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. |\n| `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. |\n| `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`. |\n| `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\". |\n| `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](#freshness). |\n| `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. |\n| `ofw_delete_draft(messageId)` | Delete a draft. |\n| `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`. |\n| `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. |\n| `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). |\n| `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. |\n\n### Calendar\n| Tool | Notes |\n|------|-------|\n| `ofw_list_events(startDate, endDate, detailed?)` | Dates as `YYYY-MM-DD` |\n| `ofw_create_event(title, startDate, endDate, ...)` | `startDate`/`endDate` as ISO datetime |\n| `ofw_update_event(eventId, ...)` | Partial update — only pass fields to change |\n| `ofw_delete_event(eventId)` | Permanent delete |\n\n### Restricted expense deployments\n\nWhen `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.\n\n### Expenses\n| Tool | Notes |\n|------|-------|\n| `ofw_get_expense_totals` | Summary of owed/paid totals |\n| `ofw_list_expenses(page?, size?)` | Paginated (1-based page); default size 20; follow `nextPage` |\n| `ofw_list_expense_categories` | Category ids and split metadata (use the id in create/update) |\n| `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 |\n| `ofw_create_expense(title, amount, purchaseDate, categoryId, payerId, children, description?, privateExpense?, receiptFileId?)` | Log a new expense; confirm-gated; optionally private with one receipt |\n| `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 |\n\n### Journal\n| Tool | Notes |\n|------|-------|\n| `ofw_list_journal_entries(start?, max?)` | 1-based offset; default max 10 |\n| `ofw_create_journal_entry(title, body)` | Create a new entry |\n\n## Response shape (`view`)\n\n`ofw_list_messages`, `ofw_get_message` and `ofw_list_drafts` take\n`view: \"compact\" | \"full\"`, and **`compact` is the default** — you get the slim\nshape without asking. It measured 135.1 KB → 41.1 KB on a real 50-message page.\n\nWhat compact drops is `listData`, OFW's echo of the list payload. That blob was\n58% of the response and 78% of it duplicated fields the same object already\nemits at the top level — eleven pre-formatted renderings of one timestamp,\neight fields per recipient beside the three the row already normalised, and a\n`preview` that is a truncation of the `body` in the same object.\n\n**Two things about compact that will surprise you if you don't know them:**\n\n- **The sender is `from`, not `fromUser`.** `fromUser` is the empty string on\n  every row — inbox and sent alike — because OFW names the sender only inside\n  `listData.author`. So compact promotes it. Dropping `listData` without that\n  would have taken the sender off every message; this is the one field the fat\n  blob was carrying alone.\n- **On a draft, `ofw_get_message` returns `from: null`** — a draft is unsent, so\n  there is no sender, and `\"\"` would read as one we failed to find.\n\nPass `view: \"full\"` when you need a field compact dropped — `listData` and\neverything in it. There is deliberately **no `raw` rung**: a message here is\nassembled from a list item, a detail GET and derived fields, so there is no\nsingle upstream payload to hand back, and a rung that skipped normalisation\nwould put naive local times back beside UTC ones on exactly the rung you reach\nfor when something already looks wrong.\n\n`ofw_get_unread_sent` takes no `view`: it emits a verdict list\n(`{id, subject, sentAt, unreadBy}`) that is already narrower than the\nprojection.\n\n## Freshness, completeness, and lifecycle\n\nMessage 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:\n\n| Question | Signal |\n|---|---|\n| How old is this data? | `freshness` — `staleness` (`fresh`/`unverified`/`stale`), `asOf`, `ageSeconds`, a quotable `warning` |\n| Is this the WHOLE answer? | `complete` on `ofw_list_messages` / `ofw_list_drafts` / `ofw_get_unread_sent` / `ofw_status` |\n| 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 |\n| Is this entity still what I think it is? | `state` from `ofw_status` / `ofw_check_freshness` |\n\nRules:\n\n- **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.\n- **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.\n- **`existsOnServer` does not mean \"still a draft\".** A draft that was SENT still exists on the server. Only `state` distinguishes them.\n- **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`.\n- **`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.\n- **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.\n- **`state: \"unknown\"` is not \"fine\".** It means the question was not answered.\n- 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.\n- A missing folder count in `ofw_sync_messages` output means that folder was **not checked** — it is never \"no changes\". Check `notRefreshed`.\n\n### Draft identity (`draftKey`)\n\nEditing 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:\n\n- `ofw_status(draftKeys: [\"dk_…\"])` resolves the key to the chain's **current** id and state.\n- The key keeps resolving after the draft is sent: `state: \"sent\"` with `sentMessageId` and `sentAt`.\n- `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.\n\n\n## Workflows\n\n**Check inbox:**\n1. `ofw_list_message_folders` → find inbox folder ID\n2. `ofw_list_messages(folderId)` → list messages\n3. `ofw_get_message(messageId)` → read a specific message\n\n**Send a message:**\n1. `ofw_get_profile` → get co-parent's user ID\n2. `ofw_send_message(subject, body, [coParentId])`\n\n**Reply to a message (with thread history):**\n1. `ofw_get_message(messageId)` → read the message to reply to\n2. `ofw_send_message(subject, body, [coParentId], replyToId: messageId)` — original message is included in the thread\n\n**Draft before sending (sensitive messages):**\n1. `ofw_save_draft(subject, body)` → review with user; note the returned `draftKey` and `revision`\n2. `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\n\n**Check what's coming up:**\n- `ofw_get_notifications` for a quick summary\n- `ofw_list_events(today, +30days)` for calendar detail\n\n## Caution\n\n- **Always confirm before sending messages or deleting anything** — OFW is a legal co-parenting record.\n- **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 or, on clients that cannot (claude.ai, Claude Desktop), 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.\n- **`*_UNCONFIRMED` means it may have landed.** Never retry a `SEND_`/`EXPENSE_`/`EVENT_`/`JOURNAL_UNCONFIRMED` write until the matching list/sync shows it did not.\n- `ofw_get_notifications` updates last-seen status — avoid calling silently in the background.\n- `ofw_get_message` marks messages read — warn the user if they want to keep something unread.\n- **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.\n\nFile v2.20.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"ofw\",\n  \"version\": \"2.20.2\",\n  \"publishedAt\": 1791170808663\n}\n\nFile v2.20.2:skill-card.md\n\n## Description:\n\nHelps an agent access and manage OurFamilyWizard co-parenting messages, calendar events, expenses, and journal entries.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople using an agent with their OurFamilyWizard account can review co-parenting records and, with approval, compose messages or update shared calendars and expenses.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Access to sensitive co-parenting records and account credentials.\n\nMitigation: Connect only an intended account, protect credentials, review the package before installation, and choose the narrowest available deployment mode.\n\nRisk: Messages, deletions, shared expenses, calendar changes, and uploads can affect other people or create lasting records.\n\nMitigation: Show the proposed action and require explicit user confirmation before each write or deletion.\n\nRisk: Some reads can mark messages as read or update last-seen status; cached results may be stale.\n\nMitigation: Warn before state-changing reads and check live status before reporting current account state.\n\n## Reference(s):\n\n- [OFW skill on ClawHub](https://clawhub.ai/chrischall/skills/ofw)\n- [ofw-mcp npm package](https://www.npmjs.com/package/ofw-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Configuration instructions, Text]\n\n**Output Format:** [Markdown with command and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May surface sensitive account records and propose account changes.]\n\n## Skill Version(s):\n\n2.20.2 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.20.1: 3 files, 8772 bytes\n\nFiles: skill-card.md (2096b), SKILL.md (17079b), _meta.json (123b)\n\nFile v2.20.1:SKILL.md\n\n---\nname: ofw\ndescription: 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.\n---\n\n# ofw-mcp\n\nMCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.\n\n- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)\n- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)\n\n\n## Setup\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g ofw-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\n## Tools\n\n### User\n| Tool | Description |\n|------|-------------|\n| `ofw_get_profile` | Current user + co-parent info (IDs, contact details) |\n| `ofw_get_notifications` | Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status. |\n\n### Messages\n| Tool | Notes |\n|------|-------|\n| `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). |\n| `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. |\n| `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. |\n| `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. |\n| `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`. |\n| `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\". |\n| `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](#freshness). |\n| `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. |\n| `ofw_delete_draft(messageId)` | Delete a draft. |\n| `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`. |\n| `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. |\n| `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). |\n| `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. |\n\n### Calendar\n| Tool | Notes |\n|------|-------|\n| `ofw_list_events(startDate, endDate, detailed?)` | Dates as `YYYY-MM-DD` |\n| `ofw_create_event(title, startDate, endDate, ...)` | `startDate`/`endDate` as ISO datetime |\n| `ofw_update_event(eventId, ...)` | Partial update — only pass fields to change |\n| `ofw_delete_event(eventId)` | Permanent delete |\n\n### Restricted expense deployments\n\nWhen `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.\n\n### Expenses\n| Tool | Notes |\n|------|-------|\n| `ofw_get_expense_totals` | Summary of owed/paid totals |\n| `ofw_list_expenses(page?, size?)` | Paginated (1-based page); default size 20; follow `nextPage` |\n| `ofw_list_expense_categories` | Category ids and split metadata (use the id in create/update) |\n| `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 |\n| `ofw_create_expense(title, amount, purchaseDate, categoryId, payerId, children, description?, privateExpense?, receiptFileId?)` | Log a new expense; confirm-gated; optionally private with one receipt |\n| `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 |\n\n### Journal\n| Tool | Notes |\n|------|-------|\n| `ofw_list_journal_entries(start?, max?)` | 1-based offset; default max 10 |\n| `ofw_create_journal_entry(title, body)` | Create a new entry |\n\n## Response shape (`view`)\n\n`ofw_list_messages`, `ofw_get_message` and `ofw_list_drafts` take\n`view: \"compact\" | \"full\"`, and **`compact` is the default** — you get the slim\nshape without asking. It measured 135.1 KB → 41.1 KB on a real 50-message page.\n\nWhat compact drops is `listData`, OFW's echo of the list payload. That blob was\n58% of the response and 78% of it duplicated fields the same object already\nemits at the top level — eleven pre-formatted renderings of one timestamp,\neight fields per recipient beside the three the row already normalised, and a\n`preview` that is a truncation of the `body` in the same object.\n\n**Two things about compact that will surprise you if you don't know them:**\n\n- **The sender is `from`, not `fromUser`.** `fromUser` is the empty string on\n  every row — inbox and sent alike — because OFW names the sender only inside\n  `listData.author`. So compact promotes it. Dropping `listData` without that\n  would have taken the sender off every message; this is the one field the fat\n  blob was carrying alone.\n- **On a draft, `ofw_get_message` returns `from: null`** — a draft is unsent, so\n  there is no sender, and `\"\"` would read as one we failed to find.\n\nPass `view: \"full\"` when you need a field compact dropped — `listData` and\neverything in it. There is deliberately **no `raw` rung**: a message here is\nassembled from a list item, a detail GET and derived fields, so there is no\nsingle upstream payload to hand back, and a rung that skipped normalisation\nwould put naive local times back beside UTC ones on exactly the rung you reach\nfor when something already looks wrong.\n\n`ofw_get_unread_sent` takes no `view`: it emits a verdict list\n(`{id, subject, sentAt, unreadBy}`) that is already narrower than the\nprojection.\n\n## Freshness, completeness, and lifecycle\n\nMessage 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:\n\n| Question | Signal |\n|---|---|\n| How old is this data? | `freshness` — `staleness` (`fresh`/`unverified`/`stale`), `asOf`, `ageSeconds`, a quotable `warning` |\n| Is this the WHOLE answer? | `complete` on `ofw_list_messages` / `ofw_list_drafts` / `ofw_get_unread_sent` / `ofw_status` |\n| 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 |\n| Is this entity still what I think it is? | `state` from `ofw_status` / `ofw_check_freshness` |\n\nRules:\n\n- **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.\n- **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.\n- **`existsOnServer` does not mean \"still a draft\".** A draft that was SENT still exists on the server. Only `state` distinguishes them.\n- **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`.\n- **`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.\n- **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.\n- **`state: \"unknown\"` is not \"fine\".** It means the question was not answered.\n- 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.\n- A missing folder count in `ofw_sync_messages` output means that folder was **not checked** — it is never \"no changes\". Check `notRefreshed`.\n\n### Draft identity (`draftKey`)\n\nEditing 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:\n\n- `ofw_status(draftKeys: [\"dk_…\"])` resolves the key to the chain's **current** id and state.\n- The key keeps resolving after the draft is sent: `state: \"sent\"` with `sentMessageId` and `sentAt`.\n- `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.\n\n\n## Workflows\n\n**Check inbox:**\n1. `ofw_list_message_folders` → find inbox folder ID\n2. `ofw_list_messages(folderId)` → list messages\n3. `ofw_get_message(messageId)` → read a specific message\n\n**Send a message:**\n1. `ofw_get_profile` → get co-parent's user ID\n2. `ofw_send_message(subject, body, [coParentId])`\n\n**Reply to a message (with thread history):**\n1. `ofw_get_message(messageId)` → read the message to reply to\n2. `ofw_send_message(subject, body, [coParentId], replyToId: messageId)` — original message is included in the thread\n\n**Draft before sending (sensitive messages):**\n1. `ofw_save_draft(subject, body)` → review with user; note the returned `draftKey` and `revision`\n2. `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\n\n**Check what's coming up:**\n- `ofw_get_notifications` for a quick summary\n- `ofw_list_events(today, +30days)` for calendar detail\n\n## Caution\n\n- **Always confirm before sending messages or deleting anything** — OFW is a legal co-parenting record.\n- **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 or, on clients that cannot (claude.ai, Claude Desktop), 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.\n- **`*_UNCONFIRMED` means it may have landed.** Never retry a `SEND_`/`EXPENSE_`/`EVENT_`/`JOURNAL_UNCONFIRMED` write until the matching list/sync shows it did not.\n- `ofw_get_notifications` updates last-seen status — avoid calling silently in the background.\n- `ofw_get_message` marks messages read — warn the user if they want to keep something unread.\n- **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.\n\nFile v2.20.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"ofw\",\n  \"version\": \"2.20.1\",\n  \"publishedAt\": 1790991902869\n}\n\nFile v2.20.1:skill-card.md\n\n## Description:\n\nHelps agents work with OurFamilyWizard co-parenting messages, calendar events, expenses, and journal entries through the OFW MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople managing co-parenting records use this skill to help an agent review OFW messages, events, expenses, and journal entries, and prepare or perform updates with their approval.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: OFW credentials and private co-parenting records could be exposed.\n\nMitigation: Store credentials in a protected, non-version-controlled location and verify the package source and version before installation.\n\nRisk: Sending, deleting, uploading, or changing shared records can affect a sensitive legal and family record.\n\nMitigation: Show the proposed action and obtain explicit approval before co-parent-visible writes or deletion.\n\nRisk: Reading an unread message or checking notifications can change visible read or last-seen status.\n\nMitigation: Warn the user before these reads and avoid them when the user wants unread or last-seen status preserved.\n\n## Reference(s):\n\n- [ofw-mcp npm package](https://www.npmjs.com/package/ofw-mcp)\n- [Project repository linked in skill](https://github.com/chrischall/ofw-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Text, API calls, Shell commands, Configuration instructions]\n\n**Output Format:** [Markdown with tool results, JSON examples, and shell commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May include sensitive co-parenting records; verify live status before reporting current state.]\n\n## Skill Version(s):\n\n2.20.1 (source: server-resolved release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.20.0: 3 files, 8843 bytes\n\nFiles: skill-card.md (2285b), SKILL.md (17079b), _meta.json (123b)\n\nFile v2.20.0:SKILL.md\n\n---\nname: ofw\ndescription: 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.\n---\n\n# ofw-mcp\n\nMCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.\n\n- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)\n- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)\n\n\n## Setup\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g ofw-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\n## Tools\n\n### User\n| Tool | Description |\n|------|-------------|\n| `ofw_get_profile` | Current user + co-parent info (IDs, contact details) |\n| `ofw_get_notifications` | Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status. |\n\n### Messages\n| Tool | Notes |\n|------|-------|\n| `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). |\n| `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. |\n| `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. |\n| `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. |\n| `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`. |\n| `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\". |\n| `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](#freshness). |\n| `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. |\n| `ofw_delete_draft(messageId)` | Delete a draft. |\n| `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`. |\n| `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. |\n| `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). |\n| `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. |\n\n### Calendar\n| Tool | Notes |\n|------|-------|\n| `ofw_list_events(startDate, endDate, detailed?)` | Dates as `YYYY-MM-DD` |\n| `ofw_create_event(title, startDate, endDate, ...)` | `startDate`/`endDate` as ISO datetime |\n| `ofw_update_event(eventId, ...)` | Partial update — only pass fields to change |\n| `ofw_delete_event(eventId)` | Permanent delete |\n\n### Restricted expense deployments\n\nWhen `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.\n\n### Expenses\n| Tool | Notes |\n|------|-------|\n| `ofw_get_expense_totals` | Summary of owed/paid totals |\n| `ofw_list_expenses(page?, size?)` | Paginated (1-based page); default size 20; follow `nextPage` |\n| `ofw_list_expense_categories` | Category ids and split metadata (use the id in create/update) |\n| `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 |\n| `ofw_create_expense(title, amount, purchaseDate, categoryId, payerId, children, description?, privateExpense?, receiptFileId?)` | Log a new expense; confirm-gated; optionally private with one receipt |\n| `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 |\n\n### Journal\n| Tool | Notes |\n|------|-------|\n| `ofw_list_journal_entries(start?, max?)` | 1-based offset; default max 10 |\n| `ofw_create_journal_entry(title, body)` | Create a new entry |\n\n## Response shape (`view`)\n\n`ofw_list_messages`, `ofw_get_message` and `ofw_list_drafts` take\n`view: \"compact\" | \"full\"`, and **`compact` is the default** — you get the slim\nshape without asking. It measured 135.1 KB → 41.1 KB on a real 50-message page.\n\nWhat compact drops is `listData`, OFW's echo of the list payload. That blob was\n58% of the response and 78% of it duplicated fields the same object already\nemits at the top level — eleven pre-formatted renderings of one timestamp,\neight fields per recipient beside the three the row already normalised, and a\n`preview` that is a truncation of the `body` in the same object.\n\n**Two things about compact that will surprise you if you don't know them:**\n\n- **The sender is `from`, not `fromUser`.** `fromUser` is the empty string on\n  every row — inbox and sent alike — because OFW names the sender only inside\n  `listData.author`. So compact promotes it. Dropping `listData` without that\n  would have taken the sender off every message; this is the one field the fat\n  blob was carrying alone.\n- **On a draft, `ofw_get_message` returns `from: null`** — a draft is unsent, so\n  there is no sender, and `\"\"` would read as one we failed to find.\n\nPass `view: \"full\"` when you need a field compact dropped — `listData` and\neverything in it. There is deliberately **no `raw` rung**: a message here is\nassembled from a list item, a detail GET and derived fields, so there is no\nsingle upstream payload to hand back, and a rung that skipped normalisation\nwould put naive local times back beside UTC ones on exactly the rung you reach\nfor when something already looks wrong.\n\n`ofw_get_unread_sent` takes no `view`: it emits a verdict list\n(`{id, subject, sentAt, unreadBy}`) that is already narrower than the\nprojection.\n\n## Freshness, completeness, and lifecycle\n\nMessage 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:\n\n| Question | Signal |\n|---|---|\n| How old is this data? | `freshness` — `staleness` (`fresh`/`unverified`/`stale`), `asOf`, `ageSeconds`, a quotable `warning` |\n| Is this the WHOLE answer? | `complete` on `ofw_list_messages` / `ofw_list_drafts` / `ofw_get_unread_sent` / `ofw_status` |\n| 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 |\n| Is this entity still what I think it is? | `state` from `ofw_status` / `ofw_check_freshness` |\n\nRules:\n\n- **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.\n- **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.\n- **`existsOnServer` does not mean \"still a draft\".** A draft that was SENT still exists on the server. Only `state` distinguishes them.\n- **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`.\n- **`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.\n- **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.\n- **`state: \"unknown\"` is not \"fine\".** It means the question was not answered.\n- 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.\n- A missing folder count in `ofw_sync_messages` output means that folder was **not checked** — it is never \"no changes\". Check `notRefreshed`.\n\n### Draft identity (`draftKey`)\n\nEditing 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:\n\n- `ofw_status(draftKeys: [\"dk_…\"])` resolves the key to the chain's **current** id and state.\n- The key keeps resolving after the draft is sent: `state: \"sent\"` with `sentMessageId` and `sentAt`.\n- `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.\n\n\n## Workflows\n\n**Check inbox:**\n1. `ofw_list_message_folders` → find inbox folder ID\n2. `ofw_list_messages(folderId)` → list messages\n3. `ofw_get_message(messageId)` → read a specific message\n\n**Send a message:**\n1. `ofw_get_profile` → get co-parent's user ID\n2. `ofw_send_message(subject, body, [coParentId])`\n\n**Reply to a message (with thread history):**\n1. `ofw_get_message(messageId)` → read the message to reply to\n2. `ofw_send_message(subject, body, [coParentId], replyToId: messageId)` — original message is included in the thread\n\n**Draft before sending (sensitive messages):**\n1. `ofw_save_draft(subject, body)` → review with user; note the returned `draftKey` and `revision`\n2. `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\n\n**Check what's coming up:**\n- `ofw_get_notifications` for a quick summary\n- `ofw_list_events(today, +30days)` for calendar detail\n\n## Caution\n\n- **Always confirm before sending messages or deleting anything** — OFW is a legal co-parenting record.\n- **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 or, on clients that cannot (claude.ai, Claude Desktop), 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.\n- **`*_UNCONFIRMED` means it may have landed.** Never retry a `SEND_`/`EXPENSE_`/`EVENT_`/`JOURNAL_UNCONFIRMED` write until the matching list/sync shows it did not.\n- `ofw_get_notifications` updates last-seen status — avoid calling silently in the background.\n- `ofw_get_message` marks messages read — warn the user if they want to keep something unread.\n- **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.\n\nFile v2.20.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"ofw\",\n  \"version\": \"2.20.0\",\n  \"publishedAt\": 1790642582503\n}\n\nFile v2.20.0:skill-card.md\n\n## Description:\n\nHelps an agent read and manage OurFamilyWizard co-parenting messages, calendar events, expenses, and journal entries.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople managing co-parenting arrangements use this skill to review OFW communications and schedules, track expenses, and prepare or make updates with appropriate confirmation.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: OFW credentials and locally cached co-parenting data are sensitive.\n\nMitigation: Use a protected secret manager or environment injection; keep credentials out of project files, repositories, and screenshots, and restrict local cache access.\n\nRisk: Reading an unread message or checking notifications can change visible account status.\n\nMitigation: Explain these side effects first; avoid opening unread messages or checking notifications without the user's intent.\n\nRisk: Sending, deleting, or changing shared records can be consequential or irreversible.\n\nMitigation: Require an explicit request and confirmation for sends, deletes, expenses, events, and journal writes; verify uncertain results before retrying.\n\nRisk: Cached messages and drafts may be stale or incomplete.\n\nMitigation: Check live status, freshness, completeness, and pagination before asserting current state or totals.\n\n## Reference(s):\n\n- [ofw ClawHub release](https://clawhub.ai/chrischall/skills/ofw)\n- [ofw-mcp npm package (linked in skill)](https://www.npmjs.com/package/ofw-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, JSON, Tool calls]\n\n**Output Format:** [Human-readable summaries and structured OFW tool responses]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Cached results may be stale or incomplete; check freshness and pagination before reporting current state.]\n\n## Skill Version(s):\n\n2.20.0 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.19.5: 3 files, 8361 bytes\n\nFiles: skill-card.md (2336b), SKILL.md (15864b), _meta.json (123b)\n\nFile v2.19.5:SKILL.md\n\n---\nname: ofw\ndescription: 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.\n---\n\n# ofw-mcp\n\nMCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.\n\n- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)\n- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)\n\n\n## Setup\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g ofw-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\n## Tools\n\n### User\n| Tool | Description |\n|------|-------------|\n| `ofw_get_profile` | Current user + co-parent info (IDs, contact details) |\n| `ofw_get_notifications` | Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status. |\n\n### Messages\n| Tool | Notes |\n|------|-------|\n| `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). |\n| `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. |\n| `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. |\n| `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. |\n| `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`. |\n| `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\". |\n| `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](#freshness). |\n| `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. |\n| `ofw_delete_draft(messageId)` | Delete a draft. |\n| `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`. |\n| `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. |\n| `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). |\n| `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. |\n\n### Calendar\n| Tool | Notes |\n|------|-------|\n| `ofw_list_events(startDate, endDate, detailed?)` | Dates as `YYYY-MM-DD` |\n| `ofw_create_event(title, startDate, endDate, ...)` | `startDate`/`endDate` as ISO datetime |\n| `ofw_update_event(eventId, ...)` | Partial update — only pass fields to change |\n| `ofw_delete_event(eventId)` | Permanent delete |\n\n### Expenses\n| Tool | Notes |\n|------|-------|\n| `ofw_get_expense_totals` | Summary of owed/paid totals |\n| `ofw_list_expenses(start?, max?)` | Paginated; default max 20 |\n| `ofw_create_expense(amount, description)` | Log a new expense |\n\n### Journal\n| Tool | Notes |\n|------|-------|\n| `ofw_list_journal_entries(start?, max?)` | 1-based offset; default max 10 |\n| `ofw_create_journal_entry(title, body)` | Create a new entry |\n\n## Response shape (`view`)\n\n`ofw_list_messages`, `ofw_get_message` and `ofw_list_drafts` take\n`view: \"compact\" | \"full\"`, and **`compact` is the default** — you get the slim\nshape without asking. It measured 135.1 KB → 41.1 KB on a real 50-message page.\n\nWhat compact drops is `listData`, OFW's echo of the list payload. That blob was\n58% of the response and 78% of it duplicated fields the same object already\nemits at the top level — eleven pre-formatted renderings of one timestamp,\neight fields per recipient beside the three the row already normalised, and a\n`preview` that is a truncation of the `body` in the same object.\n\n**Two things about compact that will surprise you if you don't know them:**\n\n- **The sender is `from`, not `fromUser`.** `fromUser` is the empty string on\n  every row — inbox and sent alike — because OFW names the sender only inside\n  `listData.author`. So compact promotes it. Dropping `listData` without that\n  would have taken the sender off every message; this is the one field the fat\n  blob was carrying alone.\n- **On a draft, `ofw_get_message` returns `from: null`** — a draft is unsent, so\n  there is no sender, and `\"\"` would read as one we failed to find.\n\nPass `view: \"full\"` when you need a field compact dropped — `listData` and\neverything in it. There is deliberately **no `raw` rung**: a message here is\nassembled from a list item, a detail GET and derived fields, so there is no\nsingle upstream payload to hand back, and a rung that skipped normalisation\nwould put naive local times back beside UTC ones on exactly the rung you reach\nfor when something already looks wrong.\n\n`ofw_get_unread_sent` takes no `view`: it emits a verdict list\n(`{id, subject, sentAt, unreadBy}`) that is already narrower than the\nprojection.\n\n## Freshness, completeness, and lifecycle\n\nMessage 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:\n\n| Question | Signal |\n|---|---|\n| How old is this data? | `freshness` — `staleness` (`fresh`/`unverified`/`stale`), `asOf`, `ageSeconds`, a quotable `warning` |\n| Is this the WHOLE answer? | `complete` on `ofw_list_messages` / `ofw_list_drafts` / `ofw_get_unread_sent` / `ofw_status` |\n| If not, how do I get the rest? | `nextPage` (message tools) or `nextStart` (`ofw_list_expenses` / `ofw_list_journal_entries`) — null means there is no more |\n| Is this entity still what I think it is? | `state` from `ofw_status` / `ofw_check_freshness` |\n\nRules:\n\n- **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.\n- **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.\n- **`existsOnServer` does not mean \"still a draft\".** A draft that was SENT still exists on the server. Only `state` distinguishes them.\n- **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`.\n- **`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.\n- **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.\n- **`state: \"unknown\"` is not \"fine\".** It means the question was not answered.\n- 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.\n- A missing folder count in `ofw_sync_messages` output means that folder was **not checked** — it is never \"no changes\". Check `notRefreshed`.\n\n### Draft identity (`draftKey`)\n\nEditing 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:\n\n- `ofw_status(draftKeys: [\"dk_…\"])` resolves the key to the chain's **current** id and state.\n- The key keeps resolving after the draft is sent: `state: \"sent\"` with `sentMessageId` and `sentAt`.\n- `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.\n\n\n## Workflows\n\n**Check inbox:**\n1. `ofw_list_message_folders` → find inbox folder ID\n2. `ofw_list_messages(folderId)` → list messages\n3. `ofw_get_message(messageId)` → read a specific message\n\n**Send a message:**\n1. `ofw_get_profile` → get co-parent's user ID\n2. `ofw_send_message(subject, body, [coParentId])`\n\n**Reply to a message (with thread history):**\n1. `ofw_get_message(messageId)` → read the message to reply to\n2. `ofw_send_message(subject, body, [coParentId], replyToId: messageId)` — original message is included in the thread\n\n**Draft before sending (sensitive messages):**\n1. `ofw_save_draft(subject, body)` → review with user; note the returned `draftKey` and `revision`\n2. `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\n\n**Check what's coming up:**\n- `ofw_get_notifications` for a quick summary\n- `ofw_list_events(today, +30days)` for calendar detail\n\n## Caution\n\n- **Always confirm before sending messages or deleting anything** — OFW is a legal co-parenting record.\n- **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 or, on clients that cannot (claude.ai, Claude Desktop), 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.\n- **`*_UNCONFIRMED` means it may have landed.** Never retry a `SEND_`/`EXPENSE_`/`EVENT_`/`JOURNAL_UNCONFIRMED` write until the matching list/sync shows it did not.\n- `ofw_get_notifications` updates last-seen status — avoid calling silently in the background.\n- `ofw_get_message` marks messages read — warn the user if they want to keep something unread.\n- **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.\n\nFile v2.19.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"ofw\",\n  \"version\": \"2.19.5\",\n  \"publishedAt\": 1790572314794\n}\n\nFile v2.19.5:skill-card.md\n\n## Description:\n\nHelps an agent access OurFamilyWizard co-parenting messages, calendars, expenses, and journal entries.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople managing co-parenting arrangements use this skill to review and manage their OurFamilyWizard messages, events, expenses, and journal entries through an agent.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Broad co-parenting requests may trigger access to sensitive legal records without an explicit request to open OFW.\n\nMitigation: Use only for explicit OFW tasks and confirm account access with the user before reading sensitive records.\n\nRisk: Sending messages, changing shared events or expenses, deleting records, and sharing attachments can affect a co-parenting record.\n\nMitigation: Show the proposed change and obtain explicit approval before each consequential write; verify uncertain outcomes before retrying.\n\nRisk: Reading unread messages or checking notifications can change viewed or last-seen status.\n\nMitigation: Warn the user before reads that change status and avoid background checks.\n\nRisk: Cached message and draft information may be incomplete or out of date.\n\nMitigation: Verify current status and completeness before reporting counts or claiming a draft remains unsent.\n\nRisk: Configuring OFW credentials gives the connected agent access to private account data.\n\nMitigation: Configure credentials carefully and restrict use to intended OFW tasks.\n\n## Reference(s):\n\n- [ClawHub ofw release](https://clawhub.ai/chrischall/skills/ofw)\n- [ofw-mcp npm package](https://www.npmjs.com/package/ofw-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Guidance]\n\n**Output Format:** [Markdown or plain-text responses]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May contain sensitive co-parenting records and proposed messages.]\n\n## Skill Version(s):\n\n2.19.5 (source: ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.19.4: 3 files, 8230 bytes\n\nFiles: skill-card.md (2131b), SKILL.md (15751b), _meta.json (123b)\n\nFile v2.19.4:SKILL.md\n\n---\nname: ofw\ndescription: 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.\n---\n\n# ofw-mcp\n\nMCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.\n\n- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)\n- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)\n\n\n## Setup\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g ofw-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\n## Tools\n\n### User\n| Tool | Description |\n|------|-------------|\n| `ofw_get_profile` | Current user + co-parent info (IDs, contact details) |\n| `ofw_get_notifications` | Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status. |\n\n### Messages\n| Tool | Notes |\n|------|-------|\n| `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). |\n| `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. |\n| `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. |\n| `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. |\n| `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`. |\n| `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\". |\n| `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](#freshness). |\n| `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. |\n| `ofw_delete_draft(messageId)` | Delete a draft. |\n| `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`. |\n| `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. |\n| `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). |\n| `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. |\n\n### Calendar\n| Tool | Notes |\n|------|-------|\n| `ofw_list_events(startDate, endDate, detailed?)` | Dates as `YYYY-MM-DD` |\n| `ofw_create_event(title, startDate, endDate, ...)` | `startDate`/`endDate` as ISO datetime |\n| `ofw_update_event(eventId, ...)` | Partial update — only pass fields to change |\n| `ofw_delete_event(eventId)` | Permanent delete |\n\n### Expenses\n| Tool | Notes |\n|------|-------|\n| `ofw_get_expense_totals` | Summary of owed/paid totals |\n| `ofw_list_expenses(start?, max?)` | Paginated; default max 20 |\n| `ofw_create_expense(amount, description)` | Log a new expense |\n\n### Journal\n| Tool | Notes |\n|------|-------|\n| `ofw_list_journal_entries(start?, max?)` | 1-based offset; default max 10 |\n| `ofw_create_journal_entry(title, body)` | Create a new entry |\n\n## Response shape (`view`)\n\n`ofw_list_messages`, `ofw_get_message` and `ofw_list_drafts` take\n`view: \"compact\" | \"full\"`, and **`compact` is the default** — you get the slim\nshape without asking. It measured 135.1 KB → 41.1 KB on a real 50-message page.\n\nWhat compact drops is `listData`, OFW's echo of the list payload. That blob was\n58% of the response and 78% of it duplicated fields the same object already\nemits at the top level — eleven pre-formatted renderings of one timestamp,\neight fields per recipient beside the three the row already normalised, and a\n`preview` that is a truncation of the `body` in the same object.\n\n**Two things about compact that will surprise you if you don't know them:**\n\n- **The sender is `from`, not `fromUser`.** `fromUser` is the empty string on\n  every row — inbox and sent alike — because OFW names the sender only inside\n  `listData.author`. So compact promotes it. Dropping `listData` without that\n  would have taken the sender off every message; this is the one field the fat\n  blob was carrying alone.\n- **On a draft, `ofw_get_message` returns `from: null`** — a draft is unsent, so\n  there is no sender, and `\"\"` would read as one we failed to find.\n\nPass `view: \"full\"` when you need a field compact dropped — `listData` and\neverything in it. There is deliberately **no `raw` rung**: a message here is\nassembled from a list item, a detail GET and derived fields, so there is no\nsingle upstream payload to hand back, and a rung that skipped normalisation\nwould put naive local times back beside UTC ones on exactly the rung you reach\nfor when something already looks wrong.\n\n`ofw_get_unread_sent` takes no `view`: it emits a verdict list\n(`{id, subject, sentAt, unreadBy}`) that is already narrower than the\nprojection.\n\n## Freshness, completeness, and lifecycle\n\nMessage 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:\n\n| Question | Signal |\n|---|---|\n| How old is this data? | `freshness` — `staleness` (`fresh`/`unverified`/`stale`), `asOf`, `ageSeconds`, a quotable `warning` |\n| Is this the WHOLE answer? | `complete` on `ofw_list_messages` / `ofw_list_drafts` / `ofw_get_unread_sent` / `ofw_status` |\n| If not, how do I get the rest? | `nextPage` (message tools) or `nextStart` (`ofw_list_expenses` / `ofw_list_journal_entries`) — null means there is no more |\n| Is this entity still what I think it is? | `state` from `ofw_status` / `ofw_check_freshness` |\n\nRules:\n\n- **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.\n- **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.\n- **`existsOnServer` does not mean \"still a draft\".** A draft that was SENT still exists on the server. Only `state` distinguishes them.\n- **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`.\n- **`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.\n- **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.\n- **`state: \"unknown\"` is not \"fine\".** It means the question was not answered.\n- 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.\n- A missing folder count in `ofw_sync_messages` output means that folder was **not checked** — it is never \"no changes\". Check `notRefreshed`.\n\n### Draft identity (`draftKey`)\n\nEditing 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:\n\n- `ofw_status(draftKeys: [\"dk_…\"])` resolves the key to the chain's **current** id and state.\n- The key keeps resolving after the draft is sent: `state: \"sent\"` with `sentMessageId` and `sentAt`.\n- `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.\n\n\n## Workflows\n\n**Check inbox:**\n1. `ofw_list_message_folders` → find inbox folder ID\n2. `ofw_list_messages(folderId)` → list messages\n3. `ofw_get_message(messageId)` → read a specific message\n\n**Send a message:**\n1. `ofw_get_profile` → get co-parent's user ID\n2. `ofw_send_message(subject, body, [coParentId])`\n\n**Reply to a message (with thread history):**\n1. `ofw_get_message(messageId)` → read the message to reply to\n2. `ofw_send_message(subject, body, [coParentId], replyToId: messageId)` — original message is included in the thread\n\n**Draft before sending (sensitive messages):**\n1. `ofw_save_draft(subject, body)` → review with user; note the returned `draftKey` and `revision`\n2. `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\n\n**Check what's coming up:**\n- `ofw_get_notifications` for a quick summary\n- `ofw_list_events(today, +30days)` for calendar detail\n\n## Caution\n\n- **Always confirm before sending messages or deleting anything** — OFW is a legal co-parenting record.\n- **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 or, on clients that cannot (claude.ai, Claude Desktop), 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.\n- **`*_UNCONFIRMED` means it may have landed.** Never retry a `SEND_`/`EXPENSE_`/`EVENT_`/`JOURNAL_UNCONFIRMED` write until the matching list/sync shows it did not.\n- `ofw_get_notifications` updates last-seen status — avoid calling silently in the background.\n- `ofw_get_message` marks messages read — warn the user if they want to keep something unread.\n- **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.\n\nFile v2.19.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"ofw\",\n  \"version\": \"2.19.4\",\n  \"publishedAt\": 1790350551052\n}\n\nFile v2.19.4:skill-card.md\n\n## Description:\n\nConnects an agent to OurFamilyWizard to review and manage co-parenting messages, calendar events, expenses, and journal entries.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople managing co-parenting arrangements use this skill to check OurFamilyWizard messages, calendars, expenses, and journal entries, and to prepare or perform account actions with approval.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Sensitive family and legal information, including the account password and downloaded data, may be exposed.\n\nMitigation: Protect credentials and downloaded data; keep configuration files out of repositories and backups where possible.\n\nRisk: Write actions can change a co-parenting record or share information with others.\n\nMitigation: Require explicit user approval before sending, deleting, recording expenses, changing events or journal entries, or sharing attachments.\n\nRisk: Reading unread messages can mark them as viewed, while cached results may be stale or incomplete.\n\nMitigation: Warn before fetching unread messages and verify live status, freshness, and completeness before stating current account facts.\n\n## Reference(s):\n\n- [ofw on ClawHub](https://clawhub.ai/chrischall/skills/ofw)\n- [ofw-mcp npm package](https://www.npmjs.com/package/ofw-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration guidance]\n\n**Output Format:** [Conversational text and Markdown with tool-backed results]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Account data may be cached or incomplete; verify freshness and completeness before reporting current status.]\n\n## Skill Version(s):\n\n2.19.4 (source: ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.19.3: 3 files, 7824 bytes\n\nFiles: skill-card.md (2029b), SKILL.md (14956b), _meta.json (123b)\n\nFile v2.19.3:SKILL.md\n\n---\nname: ofw\ndescription: 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.\n---\n\n# ofw-mcp\n\nMCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.\n\n- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)\n- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)\n\n\n## Setup\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g ofw-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\n## Tools\n\n### User\n| Tool | Description |\n|------|-------------|\n| `ofw_get_profile` | Current user + co-parent info (IDs, contact details) |\n| `ofw_get_notifications` | Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status. |\n\n### Messages\n| Tool | Notes |\n|------|-------|\n| `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). |\n| `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. |\n| `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. |\n| `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. |\n| `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`. |\n| `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\". |\n| `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](#freshness). |\n| `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. |\n| `ofw_delete_draft(messageId)` | Delete a draft. |\n| `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`. |\n| `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. |\n| `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 inb\n\nArchive v2.19.2: 3 files, 7986 bytes\n\nFiles: skill-card.md (2536b), SKILL.md (14778b), _meta.json (123b)\n\nArchive v2.19.1: 3 files, 7939 bytes\n\nFiles: skill-card.md (2498b), SKILL.md (14778b), _meta.json (123b)","readmeExcerpt":"Skill: ofw Owner: chrischall Summary: 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. Tags: la","codeSnippets":[],"executableExamples":[{"language":"json","snippet":"{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}"},{"language":"bash","snippet":"npm install -g ofw-mcp"},{"language":"bash","snippet":"git clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build"},{"language":"bash","snippet":"cp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD"},{"language":"bash","snippet":"mcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json"},{"language":"bash","snippet":"mcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: ofw\ndescription: 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.\n---\n\n# ofw-mcp\n\nMCP server for OurFamilyWizard — provides read/write access to messages, calendar, expenses, and journal.\n\n- **npm:** [npmjs.com/package/ofw-mcp](https://www.npmjs.com/package/ofw-mcp)\n- **Source:** [github.com/chrischall/ofw-mcp](https://github.com/chrischall/ofw-mcp)\n\n\n## Setup\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ofw\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ofw-mcp\"],\n      \"env\": {\n        \"OFW_USERNAME\": \"you@example.com\",\n        \"OFW_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g ofw-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/chrischall/ofw-mcp\ncd ofw-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set OFW_USERNAME and OFW_PASSWORD\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ofw \\\n  --command \"ofw-mcp\" \\\n  --env \"OFW_USERNAME=you@example.com\" \\\n  --env \"OFW_PASSWORD=yourpassword\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ofw.ofw_get_profile --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ofw.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\n## Tools\n\n### User\n| Tool | Description |\n|------|-------------|\n| `ofw_get_profile` | Current user + co-parent info (IDs, contact details) |\n| `ofw_get_notifications` | Dashboard summary: unread count, upcoming events, outstanding expenses. ⚠️ Updates last-seen status. |\n\n### Messages\n| Tool | Notes |\n|------|-------|\n| `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). |\n| `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. |\n| `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, b"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"ofw\",\n  \"version\": \"2.20.4\",\n  \"publishedAt\": 1791588417831\n}"},{"path":"skill-card.md","content":"## Description:\n\nHelps agents work with OurFamilyWizard co-parenting messages, calendars, expenses, and journal entries through an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nPeople using OurFamilyWizard and their assistants can check co-parenting records, draft or send messages, manage calendar events and expenses, and review journal entries.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The connected server can access sensitive co-parenting messages, calendar entries, expenses, journal entries, attachments, and profile details.\n\nMitigation: Review access before installation and connect only accounts whose records you are comfortable sharing with the server.\n\nRisk: Example setup stores the OFW password in project configuration or an .env file, where it could be exposed or committed.\n\nMitigation: Use a protected secret manager or tightly permissioned user-only configuration instead of project-local or committed credentials.\n\nRisk: Sending messages, deleting records, sharing uploads, and changing events or expenses can affect other people or permanently change records.\n\nMitigation: Show the intended action and obtain explicit user approval before each send, delete, shared upload, event change, or expense change.\n\nRisk: Some reads can mark messages as read or notifications as seen.\n\nMitigation: Warn users before reads that could change read or seen status, and avoid fetching them silently.\n\n## Reference(s):\n\n- [OFW skill on ClawHub](https://clawhub.ai/chrischall/skills/ofw)\n- [ofw-mcp package on npm](https://www.npmjs.com/package/ofw-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Guidance, Shell commands, Configuration instructions]\n\n**Output Format:** [Markdown with configuration and command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Can guide agent use of OFW tools to retrieve or change account records.]\n\n## Skill Version(s):\n\n2.20.4 (source: ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"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. Skill: ofw Owner: chrischall Summary: 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. Tags: la","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1189,"uniquenessScore":48,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T21:39:28.519Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T21:39:28.519Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T23:46:45.565Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}