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 +170 -0
- package/dist/client.js +96 -0
- package/dist/index.js +52 -0
- package/dist/tools/calendar.js +94 -0
- package/dist/tools/expenses.js +55 -0
- package/dist/tools/journal.js +46 -0
- package/dist/tools/messages.js +79 -0
- package/dist/tools/user.js +28 -0
- package/package.json +27 -0
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
|
+
}
|