ofw-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # OurFamilyWizard MCP
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that connects Claude to [OurFamilyWizard](https://www.ourfamilywizard.com), giving you natural-language access to your co-parenting messages, calendar, expenses, and journal.
4
+
5
+ ## What you can do
6
+
7
+ Ask Claude things like:
8
+
9
+ - *"Show me my recent OFW messages"*
10
+ - *"What's on the kids' calendar next week?"*
11
+ - *"List recent expenses and tell me what I owe"*
12
+ - *"Add a journal entry about today's pickup"*
13
+ - *"Draft a reply to the last message from my co-parent"*
14
+
15
+ ## Requirements
16
+
17
+ - [Claude Desktop](https://claude.ai/download)
18
+ - [Node.js](https://nodejs.org) 18 or later
19
+ - An active OurFamilyWizard account
20
+
21
+ ## Installation
22
+
23
+ ### 1. Clone and build
24
+
25
+ ```bash
26
+ git clone https://github.com/chrischall/ofw-mcp.git
27
+ cd ofw-mcp
28
+ npm install
29
+ npm run build
30
+ ```
31
+
32
+ ### 2. Add to Claude Desktop
33
+
34
+ Edit your Claude Desktop config file:
35
+
36
+ - **Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`
37
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
38
+
39
+ Add the `ofw` entry inside `"mcpServers"` (create the key if it doesn't exist):
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "ofw": {
45
+ "command": "node",
46
+ "args": ["/absolute/path/to/ofw-mcp/dist/index.js"],
47
+ "env": {
48
+ "OFW_USERNAME": "your-email@example.com",
49
+ "OFW_PASSWORD": "your-ofw-password"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ Replace `/absolute/path/to/ofw-mcp` with the actual path where you cloned the repo. On Mac, run `pwd` inside the cloned directory to get it.
57
+
58
+ ### 3. Restart Claude Desktop
59
+
60
+ Quit completely (Cmd+Q on Mac, not just close the window) and relaunch.
61
+
62
+ ### 4. Verify
63
+
64
+ Ask Claude: *"What does my OFW dashboard look like?"* — it should show your unread message count, upcoming events, and outstanding expenses.
65
+
66
+ ## Credentials
67
+
68
+ Credentials are read from environment variables, with two ways to provide them:
69
+
70
+ **Option A — env block in Claude Desktop config** (shown above, recommended):
71
+
72
+ ```json
73
+ "env": {
74
+ "OFW_USERNAME": "your-email@example.com",
75
+ "OFW_PASSWORD": "your-ofw-password"
76
+ }
77
+ ```
78
+
79
+ **Option B — `.env` file** in the project directory:
80
+
81
+ ```bash
82
+ cp .env.example .env
83
+ # edit .env and fill in your credentials
84
+ ```
85
+
86
+ Environment variables always take priority over the `.env` file. You can also pass them directly on the command line:
87
+
88
+ ```bash
89
+ OFW_USERNAME=you@example.com OFW_PASSWORD=yourpass node dist/index.js
90
+ ```
91
+
92
+ ## Available tools
93
+
94
+ Read-only tools run automatically. Write tools ask for your confirmation first.
95
+
96
+ | Tool | What it does | Permission |
97
+ |------|-------------|------------|
98
+ | `ofw_get_profile` | Your profile and co-parent info | Auto |
99
+ | `ofw_get_notifications` | Dashboard counts (unread messages, upcoming events, outstanding expenses) | Auto |
100
+ | `ofw_list_message_folders` | Folders with unread counts — **get folder IDs here before listing messages** | Auto |
101
+ | `ofw_list_messages` | Messages in a folder | Auto |
102
+ | `ofw_get_message` | Full content of a single message | Auto |
103
+ | `ofw_send_message` | Send a message | Confirm |
104
+ | `ofw_list_events` | Calendar events in a date range | Auto |
105
+ | `ofw_create_event` | Create a calendar event | Confirm |
106
+ | `ofw_update_event` | Update a calendar event | Confirm |
107
+ | `ofw_delete_event` | Delete a calendar event | Confirm |
108
+ | `ofw_get_expense_totals` | Expense summary totals | Auto |
109
+ | `ofw_list_expenses` | Expense history | Auto |
110
+ | `ofw_create_expense` | Log a new expense | Confirm |
111
+ | `ofw_list_journal_entries` | Journal entries | Auto |
112
+ | `ofw_create_journal_entry` | Create a journal entry | Confirm |
113
+
114
+ ## Troubleshooting
115
+
116
+ **"0 messages"** — Claude may have read the notification counts rather than the actual messages. Ask explicitly: *"List the messages in my OFW inbox"* or *"Use ofw_list_message_folders then ofw_list_messages"*.
117
+
118
+ **"OFW_USERNAME and OFW_PASSWORD must be set"** — credentials are missing. Check the `env` block in your Claude Desktop config or your `.env` file.
119
+
120
+ **403 Forbidden** — wrong credentials. Verify your username/password at [ofw.ourfamilywizard.com](https://ofw.ourfamilywizard.com).
121
+
122
+ **Tools not appearing in Claude** — go to **Claude Desktop → Settings → Developer** to see connected servers and any error output. Make sure you fully quit and relaunched after editing the config.
123
+
124
+ **Can't find the config file on Mac** — in Finder press Cmd+Shift+G and paste `~/Library/Application Support/Claude/`.
125
+
126
+ ## Security
127
+
128
+ - Credentials live only in your local config file or `.env`
129
+ - They are passed to the server as environment variables and never logged
130
+ - The server authenticates with OFW using the same login flow as the web app
131
+ - Use a strong, unique OFW password
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ npm test # run the test suite
137
+ npm run build # compile TypeScript → dist/
138
+ ```
139
+
140
+ ### Project structure
141
+
142
+ ```
143
+ src/
144
+ client.ts OFW auth and HTTP client
145
+ index.ts MCP server entry point
146
+ tools/
147
+ user.ts ofw_get_profile, ofw_get_notifications
148
+ messages.ts folders, list, get, send
149
+ calendar.ts list, create, update, delete events
150
+ expenses.ts totals, list, create
151
+ journal.ts list, create entries
152
+ tests/
153
+ client.test.ts
154
+ tools/
155
+ ```
156
+
157
+ ### Auth flow
158
+
159
+ OFW uses Spring Security form login:
160
+
161
+ 1. `GET /ofw/login.form` — establishes a session cookie
162
+ 2. `POST /ofw/login` — submits credentials, returns `{ auth: "<token>" }`
163
+ 3. All API calls use `Authorization: Bearer <token>`
164
+ 4. On 401, re-authenticates automatically and retries once
165
+
166
+ Tokens are cached for 6 hours.
167
+
168
+ ## License
169
+
170
+ MIT
package/dist/client.js ADDED
@@ -0,0 +1,96 @@
1
+ import 'dotenv/config';
2
+ const BASE_URL = 'https://ofw.ourfamilywizard.com';
3
+ const STATIC_HEADERS = {
4
+ 'ofw-client': 'WebApplication',
5
+ 'ofw-version': '1.0.0',
6
+ Accept: 'application/json',
7
+ 'Content-Type': 'application/json',
8
+ };
9
+ export class OFWClient {
10
+ token = null;
11
+ tokenExpiry = null;
12
+ async request(method, path, body) {
13
+ await this.ensureAuthenticated();
14
+ return this.doRequest(method, path, body, false);
15
+ }
16
+ async doRequest(method, path, body, isRetry) {
17
+ const response = await fetch(`${BASE_URL}${path}`, {
18
+ method,
19
+ headers: {
20
+ ...STATIC_HEADERS,
21
+ ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
22
+ },
23
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
24
+ });
25
+ if (response.status === 401 && !isRetry) {
26
+ this.token = null;
27
+ this.tokenExpiry = null;
28
+ await this.ensureAuthenticated();
29
+ return this.doRequest(method, path, body, true);
30
+ }
31
+ if (response.status === 429) {
32
+ if (!isRetry) {
33
+ await new Promise((r) => setTimeout(r, 2000));
34
+ return this.doRequest(method, path, body, true);
35
+ }
36
+ throw new Error('Rate limited by OFW API');
37
+ }
38
+ if (!response.ok) {
39
+ throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
40
+ }
41
+ return response.json();
42
+ }
43
+ async ensureAuthenticated() {
44
+ if (!this.isTokenExpiredSoon())
45
+ return;
46
+ await this.login();
47
+ }
48
+ async login() {
49
+ const username = process.env.OFW_USERNAME;
50
+ const password = process.env.OFW_PASSWORD;
51
+ if (!username || !password) {
52
+ throw new Error('OFW_USERNAME and OFW_PASSWORD must be set');
53
+ }
54
+ // Spring Security requires a SESSION cookie before accepting the login POST.
55
+ // GET /ofw/login.form with redirect:manual to capture the Set-Cookie from the 303 response.
56
+ const initResponse = await fetch(`${BASE_URL}/ofw/login.form`, {
57
+ headers: { 'ofw-client': 'WebApplication', 'ofw-version': '1.0.0' },
58
+ redirect: 'manual',
59
+ });
60
+ // Extract just the SESSION=value part (strip attributes like Path, Secure, etc.)
61
+ const setCookie = initResponse.headers.get('set-cookie') ?? '';
62
+ const sessionCookie = setCookie.split(';')[0] ?? null;
63
+ const response = await fetch(`${BASE_URL}/ofw/login`, {
64
+ method: 'POST',
65
+ headers: {
66
+ ...STATIC_HEADERS,
67
+ 'Content-Type': 'application/x-www-form-urlencoded',
68
+ ...(sessionCookie ? { Cookie: sessionCookie } : {}),
69
+ },
70
+ body: new URLSearchParams({
71
+ submit: 'Sign In',
72
+ _eventId: 'submit',
73
+ username: username,
74
+ password,
75
+ }).toString(),
76
+ });
77
+ if (!response.ok) {
78
+ throw new Error(`OFW login failed: ${response.status} ${response.statusText}`);
79
+ }
80
+ const contentType = response.headers.get('content-type') ?? '';
81
+ if (!contentType.includes('application/json')) {
82
+ const body = await response.text();
83
+ throw new Error(`OFW login returned unexpected response (${contentType}): ${body.substring(0, 200)}`);
84
+ }
85
+ const data = (await response.json());
86
+ this.token = data.auth;
87
+ // Token expiry not returned by login endpoint; use 6h as a safe default
88
+ this.tokenExpiry = new Date(Date.now() + 6 * 60 * 60 * 1000);
89
+ }
90
+ isTokenExpiredSoon() {
91
+ if (!this.token || !this.tokenExpiry)
92
+ return true;
93
+ return this.tokenExpiry.getTime() - Date.now() < 5 * 60 * 1000;
94
+ }
95
+ }
96
+ export const client = new OFWClient();
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { client } from './client.js';
6
+ import { toolDefinitions as userTools, handleTool as handleUser } from './tools/user.js';
7
+ import { toolDefinitions as messageTools, handleTool as handleMessages } from './tools/messages.js';
8
+ import { toolDefinitions as calendarTools, handleTool as handleCalendar } from './tools/calendar.js';
9
+ import { toolDefinitions as expenseTools, handleTool as handleExpenses } from './tools/expenses.js';
10
+ import { toolDefinitions as journalTools, handleTool as handleJournal } from './tools/journal.js';
11
+ const allTools = [
12
+ ...userTools,
13
+ ...messageTools,
14
+ ...calendarTools,
15
+ ...expenseTools,
16
+ ...journalTools,
17
+ ];
18
+ const handlers = {};
19
+ for (const tool of userTools)
20
+ handlers[tool.name] = (n, a) => handleUser(n, a, client);
21
+ for (const tool of messageTools)
22
+ handlers[tool.name] = (n, a) => handleMessages(n, a, client);
23
+ for (const tool of calendarTools)
24
+ handlers[tool.name] = (n, a) => handleCalendar(n, a, client);
25
+ for (const tool of expenseTools)
26
+ handlers[tool.name] = (n, a) => handleExpenses(n, a, client);
27
+ for (const tool of journalTools)
28
+ handlers[tool.name] = (n, a) => handleJournal(n, a, client);
29
+ const server = new Server({ name: 'ofw', version: '1.0.0' }, { capabilities: { tools: {} } });
30
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: allTools }));
31
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
32
+ const { name, arguments: args = {} } = request.params;
33
+ const handler = handlers[name];
34
+ if (!handler) {
35
+ return {
36
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
37
+ isError: true,
38
+ };
39
+ }
40
+ try {
41
+ return await handler(name, args);
42
+ }
43
+ catch (err) {
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ return {
46
+ content: [{ type: 'text', text: `Error: ${message}` }],
47
+ isError: true,
48
+ };
49
+ }
50
+ });
51
+ const transport = new StdioServerTransport();
52
+ await server.connect(transport);
@@ -0,0 +1,94 @@
1
+ export const toolDefinitions = [
2
+ {
3
+ name: 'ofw_list_events',
4
+ description: 'List OurFamilyWizard calendar events in a date range',
5
+ annotations: { readOnlyHint: true },
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ startDate: { type: 'string', description: 'Start date YYYY-MM-DD' },
10
+ endDate: { type: 'string', description: 'End date YYYY-MM-DD' },
11
+ detailed: { type: 'boolean', description: 'Return full event details (default false)' },
12
+ },
13
+ required: ['startDate', 'endDate'],
14
+ },
15
+ },
16
+ {
17
+ name: 'ofw_create_event',
18
+ description: 'Create a calendar event in OurFamilyWizard',
19
+ annotations: { destructiveHint: false },
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ title: { type: 'string' },
24
+ startDate: { type: 'string', description: 'ISO datetime string' },
25
+ endDate: { type: 'string', description: 'ISO datetime string' },
26
+ allDay: { type: 'boolean' },
27
+ location: { type: 'string' },
28
+ reminder: { type: 'string', description: 'Reminder setting (e.g. "1 hour before")' },
29
+ privateEvent: { type: 'boolean' },
30
+ eventFor: { type: 'string', description: 'neither | parent1 | parent2' },
31
+ dropOffParent: { type: 'string' },
32
+ pickUpParent: { type: 'string' },
33
+ children: { type: 'array', items: { type: 'number' }, description: 'Array of child IDs' },
34
+ },
35
+ required: ['title', 'startDate', 'endDate'],
36
+ },
37
+ },
38
+ {
39
+ name: 'ofw_update_event',
40
+ description: 'Update an existing OurFamilyWizard calendar event',
41
+ annotations: { destructiveHint: false },
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ eventId: { type: 'string' },
46
+ title: { type: 'string' },
47
+ startDate: { type: 'string' },
48
+ endDate: { type: 'string' },
49
+ allDay: { type: 'boolean' },
50
+ location: { type: 'string' },
51
+ reminder: { type: 'string' },
52
+ privateEvent: { type: 'boolean' },
53
+ },
54
+ required: ['eventId'],
55
+ },
56
+ },
57
+ {
58
+ name: 'ofw_delete_event',
59
+ description: 'Delete an OurFamilyWizard calendar event',
60
+ annotations: { destructiveHint: true },
61
+ inputSchema: {
62
+ type: 'object',
63
+ properties: { eventId: { type: 'string', description: 'Event ID to delete' } },
64
+ required: ['eventId'],
65
+ },
66
+ },
67
+ ];
68
+ export async function handleTool(name, args, client) {
69
+ switch (name) {
70
+ case 'ofw_list_events': {
71
+ const { startDate, endDate, detailed = false } = args;
72
+ const variant = detailed ? 'detailed' : 'basic';
73
+ const data = await client.request('GET', `/pub/v1/calendar/${variant}?startDate=${encodeURIComponent(startDate)}&endDate=${encodeURIComponent(endDate)}`);
74
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
75
+ }
76
+ case 'ofw_create_event': {
77
+ // Field names are best-guess; confirm via DevTools capture and update if needed (see pre-task note)
78
+ const data = await client.request('POST', '/pub/v1/calendar/events', args);
79
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
80
+ }
81
+ case 'ofw_update_event': {
82
+ const { eventId, ...updateData } = args;
83
+ const data = await client.request('PUT', `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
84
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
85
+ }
86
+ case 'ofw_delete_event': {
87
+ const { eventId } = args;
88
+ await client.request('DELETE', `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`);
89
+ return { content: [{ type: 'text', text: `Event ${eventId} deleted` }] };
90
+ }
91
+ default:
92
+ throw new Error(`Unknown tool: ${name}`);
93
+ }
94
+ }
@@ -0,0 +1,55 @@
1
+ export const toolDefinitions = [
2
+ {
3
+ name: 'ofw_get_expense_totals',
4
+ description: 'Get OurFamilyWizard expense summary totals (owed/paid)',
5
+ annotations: { readOnlyHint: true },
6
+ inputSchema: { type: 'object', properties: {}, required: [] },
7
+ },
8
+ {
9
+ name: 'ofw_list_expenses',
10
+ description: 'List OurFamilyWizard expenses with pagination',
11
+ annotations: { readOnlyHint: true },
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ start: { type: 'number', description: 'Start offset (default 0)' },
16
+ max: { type: 'number', description: 'Max results (default 20)' },
17
+ },
18
+ required: [],
19
+ },
20
+ },
21
+ {
22
+ name: 'ofw_create_expense',
23
+ description: 'Log a new expense in OurFamilyWizard',
24
+ annotations: { destructiveHint: false },
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ amount: { type: 'number', description: 'Expense amount' },
29
+ description: { type: 'string', description: 'Expense description' },
30
+ // Additional fields TBD — add after DevTools capture (see pre-task note)
31
+ },
32
+ required: ['amount', 'description'],
33
+ },
34
+ },
35
+ ];
36
+ export async function handleTool(name, args, client) {
37
+ switch (name) {
38
+ case 'ofw_get_expense_totals': {
39
+ const data = await client.request('GET', '/pub/v2/expense/expenses/totals');
40
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
41
+ }
42
+ case 'ofw_list_expenses': {
43
+ const { start = 0, max = 20 } = args;
44
+ const data = await client.request('GET', `/pub/v2/expense/expenses?start=${start}&max=${max}`);
45
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
46
+ }
47
+ case 'ofw_create_expense': {
48
+ // Field names are best-guess; confirm via DevTools capture and update if needed (see pre-task note)
49
+ const data = await client.request('POST', '/pub/v2/expense/expenses', args);
50
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
51
+ }
52
+ default:
53
+ throw new Error(`Unknown tool: ${name}`);
54
+ }
55
+ }
@@ -0,0 +1,46 @@
1
+ export const toolDefinitions = [
2
+ {
3
+ name: 'ofw_list_journal_entries',
4
+ description: 'List OurFamilyWizard journal entries',
5
+ annotations: { readOnlyHint: true },
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ start: { type: 'number', description: 'Start offset (default 1)' },
10
+ max: { type: 'number', description: 'Max results (default 10)' },
11
+ },
12
+ required: [],
13
+ },
14
+ },
15
+ {
16
+ name: 'ofw_create_journal_entry',
17
+ description: 'Create a new journal entry in OurFamilyWizard',
18
+ annotations: { destructiveHint: false },
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ title: { type: 'string', description: 'Entry title' },
23
+ body: { type: 'string', description: 'Entry text content' },
24
+ // Additional fields TBD — add after DevTools capture (see pre-task note)
25
+ },
26
+ required: ['title', 'body'],
27
+ },
28
+ },
29
+ ];
30
+ export async function handleTool(name, args, client) {
31
+ switch (name) {
32
+ case 'ofw_list_journal_entries': {
33
+ // Journal API uses 1-based offset (unlike expenses which start at 0)
34
+ const { start = 1, max = 10 } = args;
35
+ const data = await client.request('GET', `/pub/v1/journals?start=${start}&max=${max}`);
36
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
37
+ }
38
+ case 'ofw_create_journal_entry': {
39
+ // Field names are best-guess; confirm via DevTools capture and update if needed (see pre-task note)
40
+ const data = await client.request('POST', '/pub/v1/journals', args);
41
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
42
+ }
43
+ default:
44
+ throw new Error(`Unknown tool: ${name}`);
45
+ }
46
+ }
@@ -0,0 +1,79 @@
1
+ export const toolDefinitions = [
2
+ {
3
+ name: 'ofw_list_message_folders',
4
+ description: 'List OurFamilyWizard message folders (inbox, sent, etc.) and their unread counts. Returns folder IDs needed to call ofw_list_messages. Does NOT return message content.',
5
+ annotations: { readOnlyHint: true },
6
+ inputSchema: { type: 'object', properties: {}, required: [] },
7
+ },
8
+ {
9
+ name: 'ofw_list_messages',
10
+ description: 'List messages in an OurFamilyWizard folder. Call ofw_list_message_folders first to get folder IDs. Returns actual message content.',
11
+ annotations: { readOnlyHint: true },
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ folderId: { type: 'string', description: 'Folder ID (get from ofw_list_message_folders)' },
16
+ page: { type: 'number', description: 'Page number (default 1)' },
17
+ size: { type: 'number', description: 'Messages per page (default 50)' },
18
+ },
19
+ required: ['folderId'],
20
+ },
21
+ },
22
+ {
23
+ name: 'ofw_get_message',
24
+ description: 'Get a single OurFamilyWizard message by ID. Note: reading an unread message marks it as read.',
25
+ annotations: { readOnlyHint: false },
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ messageId: { type: 'string', description: 'Message ID' },
30
+ },
31
+ required: ['messageId'],
32
+ },
33
+ },
34
+ {
35
+ name: 'ofw_send_message',
36
+ description: 'Send a message via OurFamilyWizard',
37
+ annotations: { destructiveHint: true },
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ subject: { type: 'string', description: 'Message subject' },
42
+ body: { type: 'string', description: 'Message body text' },
43
+ recipients: {
44
+ type: 'array',
45
+ items: { type: 'number' },
46
+ description: 'Array of recipient contact IDs (get from ofw_get_profile)',
47
+ },
48
+ },
49
+ required: ['subject', 'body', 'recipients'],
50
+ },
51
+ },
52
+ ];
53
+ export async function handleTool(name, args, client) {
54
+ switch (name) {
55
+ case 'ofw_list_message_folders': {
56
+ const data = await client.request('GET', '/pub/v1/messageFolders?includeFolderCounts=true');
57
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
58
+ }
59
+ case 'ofw_list_messages': {
60
+ const { folderId, page = 1, size = 50 } = args;
61
+ const path = `/pub/v3/messages?folders=${encodeURIComponent(folderId)}&page=${page}&size=${size}&sort=date&sortDirection=desc`;
62
+ const data = await client.request('GET', path);
63
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
64
+ }
65
+ case 'ofw_get_message': {
66
+ const { messageId } = args;
67
+ const data = await client.request('GET', `/pub/v3/messages/${encodeURIComponent(messageId)}`);
68
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
69
+ }
70
+ case 'ofw_send_message': {
71
+ const { subject, body, recipients } = args;
72
+ // Field names are best-guess; confirm via DevTools capture and update if needed (see pre-task note)
73
+ const data = await client.request('POST', '/pub/v3/messages', { subject, body, recipients });
74
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
75
+ }
76
+ default:
77
+ throw new Error(`Unknown tool: ${name}`);
78
+ }
79
+ }
@@ -0,0 +1,28 @@
1
+ export const toolDefinitions = [
2
+ {
3
+ name: 'ofw_get_profile',
4
+ description: 'Get current user and co-parent profile information from OurFamilyWizard',
5
+ annotations: { readOnlyHint: true },
6
+ inputSchema: { type: 'object', properties: {}, required: [] },
7
+ },
8
+ {
9
+ name: 'ofw_get_notifications',
10
+ description: 'Get OurFamilyWizard dashboard summary: unread message count, upcoming events, outstanding expenses. Note: updates your last-seen status.',
11
+ annotations: { readOnlyHint: false },
12
+ inputSchema: { type: 'object', properties: {}, required: [] },
13
+ },
14
+ ];
15
+ export async function handleTool(name, _args, client) {
16
+ switch (name) {
17
+ case 'ofw_get_profile': {
18
+ const data = await client.request('GET', '/pub/v2/profiles');
19
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
20
+ }
21
+ case 'ofw_get_notifications': {
22
+ const data = await client.request('GET', '/pub/v1/users/useraccountstatus');
23
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
24
+ }
25
+ default:
26
+ throw new Error(`Unknown tool: ${name}`);
27
+ }
28
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "ofw-mcp",
3
+ "version": "1.0.0",
4
+ "description": "OurFamilyWizard MCP server for Claude",
5
+ "type": "module",
6
+ "bin": {
7
+ "ofw-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "node --env-file=.env dist/index.js",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "dotenv": "^16.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^20.0.0",
24
+ "typescript": "^5.0.0",
25
+ "vitest": "^2.0.0"
26
+ }
27
+ }