@oliverames/ynab-mcp-server 1.1.0 → 1.2.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 +318 -0
- package/index.js +242 -4
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://api.ynab.com/papi/logo_api_meadow.svg" alt="YNAB API" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">YNAB MCP Server</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>The complete Model Context Protocol server for YNAB</strong><br>
|
|
9
|
+
<em>Give your AI assistant full access to your budget</em>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@oliverames/ynab-mcp-server"><img src="https://img.shields.io/npm/v/@oliverames/ynab-mcp-server" alt="npm version"></a>
|
|
14
|
+
<a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-compatible-blue" alt="MCP compatible"></a>
|
|
15
|
+
<a href="https://api.ynab.com"><img src="https://img.shields.io/badge/YNAB%20API-v1.79-green" alt="YNAB API v1.79"></a>
|
|
16
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen" alt="License: MIT"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
**43 tools. 100% API coverage. Zero configuration.**
|
|
22
|
+
|
|
23
|
+
Connect any MCP-compatible AI assistant — Claude, GPT, or your own agents — to your YNAB budget. Read transactions, categorize spending, manage accounts, review scheduled payments, track money movements, and more. All monetary values are automatically converted between dollars and YNAB's internal milliunits format so the AI never has to think about it.
|
|
24
|
+
|
|
25
|
+
Built on the [official YNAB JavaScript SDK](https://github.com/ynab/ynab-sdk-js) with direct API calls for the newest endpoints (category creation, category groups, money movements) that the SDK hasn't caught up with yet.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### 1. Get a YNAB Personal Access Token
|
|
32
|
+
|
|
33
|
+
Go to [YNAB Developer Settings](https://app.ynab.com/settings/developer) and create a new personal access token.
|
|
34
|
+
|
|
35
|
+
### 2. Configure your MCP client
|
|
36
|
+
|
|
37
|
+
**Claude Desktop** (`claude_desktop_config.json`):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"ynab": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["-y", "@oliverames/ynab-mcp-server"],
|
|
45
|
+
"env": {
|
|
46
|
+
"YNAB_API_TOKEN": "your-token-here"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Claude Code** (`.mcp.json`):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"ynab": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["-y", "@oliverames/ynab-mcp-server"],
|
|
61
|
+
"env": {
|
|
62
|
+
"YNAB_API_TOKEN": "your-token-here"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or install globally and point to the binary directly:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install -g @oliverames/ynab-mcp-server
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"ynab": {
|
|
79
|
+
"command": "ynab-mcp-server",
|
|
80
|
+
"env": {
|
|
81
|
+
"YNAB_API_TOKEN": "your-token-here",
|
|
82
|
+
"YNAB_BUDGET_ID": "optional-default-budget-id"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
That's it. Your AI can now talk to YNAB.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## What You Can Do
|
|
94
|
+
|
|
95
|
+
| Ask your AI... | What happens under the hood |
|
|
96
|
+
|---|---|
|
|
97
|
+
| "How much did I spend on groceries this month?" | `search_categories` → `get_month_category` |
|
|
98
|
+
| "Show me all unapproved transactions" | `review_unapproved` groups by readiness |
|
|
99
|
+
| "Log a $50 Costco trip under groceries" | `search_payees` → `search_categories` → `create_transaction` |
|
|
100
|
+
| "Set up monthly $1,500 rent on the 1st" | `create_scheduled_transaction` with `monthly` frequency |
|
|
101
|
+
| "Move $200 from emergency fund to dining" | `search_categories` → `update_month_category` (x2) |
|
|
102
|
+
| "Categorize all my Amazon orders from this week" | `get_transactions` (filtered) → `update_transactions` (batch) |
|
|
103
|
+
| "Create a 'Side Projects' spending category" | `search_categories` (find group) → `create_category` |
|
|
104
|
+
| "How has my budget been re-allocated this month?" | `get_money_movements_by_month` |
|
|
105
|
+
| "What recurring payments do I have?" | `list_scheduled_transactions` |
|
|
106
|
+
| "Import my latest bank transactions" | `import_transactions` triggers linked account sync |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Features
|
|
111
|
+
|
|
112
|
+
**Complete YNAB API v1.79 coverage** with 43 tools:
|
|
113
|
+
|
|
114
|
+
| Resource | Tools | Capabilities |
|
|
115
|
+
|----------|-------|-------------|
|
|
116
|
+
| **Budgets** | 4 | List, view details, settings |
|
|
117
|
+
| **Accounts** | 3 | List, view, create |
|
|
118
|
+
| **Categories** | 9 | Full CRUD, groups, search, goals, monthly budgets |
|
|
119
|
+
| **Payees** | 4 | List, view, rename, search |
|
|
120
|
+
| **Payee Locations** | 3 | GPS coordinates for mobile transactions |
|
|
121
|
+
| **Months** | 2 | Monthly summaries with per-category breakdown |
|
|
122
|
+
| **Money Movements** | 4 | Budget re-allocation tracking |
|
|
123
|
+
| **Transactions** | 8 | Full CRUD, bulk ops, split transactions, multi-filter |
|
|
124
|
+
| **Scheduled Transactions** | 5 | Full CRUD for recurring transactions |
|
|
125
|
+
| **Convenience** | 1 | Unapproved transaction review workflow |
|
|
126
|
+
|
|
127
|
+
### Design Decisions
|
|
128
|
+
|
|
129
|
+
- **Dollar amounts everywhere** — inputs and outputs are in dollars (`-12.34`), never milliunits (`-12340`). Conversion is automatic and transparent.
|
|
130
|
+
- **Smart budget resolution** — set `YNAB_BUDGET_ID` for a default, or omit it to auto-resolve to your last-used budget. Every tool accepts an optional `budgetId` override.
|
|
131
|
+
- **Split transactions** — first-class support for subtransactions in create, read, and format operations.
|
|
132
|
+
- **Bulk operations** — `create_transactions` and `update_transactions` handle arrays in a single API call.
|
|
133
|
+
- **Fetch-then-merge updates** — scheduled transaction updates (which use PUT semantics) automatically fetch the current state and merge your changes, so you only specify what changed.
|
|
134
|
+
- **Fuzzy search** — `search_categories` and `search_payees` do case-insensitive partial matching across all entries.
|
|
135
|
+
- **Approval workflow** — `review_unapproved` groups transactions into "ready to approve" (has category) and "needs attention" (uncategorized), with a built-in warning against approving uncategorized entries.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Tools Reference
|
|
140
|
+
|
|
141
|
+
### User & Budgets
|
|
142
|
+
|
|
143
|
+
| Tool | Description |
|
|
144
|
+
|------|-------------|
|
|
145
|
+
| `get_user` | Get the authenticated user |
|
|
146
|
+
| `list_budgets` | List all budgets with IDs, names, and date ranges |
|
|
147
|
+
| `get_budget` | Get budget summary (name, currency, account/category/payee counts) |
|
|
148
|
+
| `get_budget_settings` | Get currency and date format settings |
|
|
149
|
+
|
|
150
|
+
### Accounts
|
|
151
|
+
|
|
152
|
+
| Tool | Description |
|
|
153
|
+
|------|-------------|
|
|
154
|
+
| `list_accounts` | List all accounts with balances in dollars |
|
|
155
|
+
| `get_account` | Get details for a specific account |
|
|
156
|
+
| `create_account` | Create a new account (checking, savings, creditCard, mortgage, etc.) |
|
|
157
|
+
|
|
158
|
+
**Supported account types:** `checking`, `savings`, `cash`, `creditCard`, `lineOfCredit`, `otherAsset`, `otherLiability`, `mortgage`, `autoLoan`, `studentLoan`, `personalLoan`, `medicalDebt`, `otherDebt`
|
|
159
|
+
|
|
160
|
+
### Categories & Category Groups
|
|
161
|
+
|
|
162
|
+
| Tool | Description |
|
|
163
|
+
|------|-------------|
|
|
164
|
+
| `list_categories` | List all category groups and their categories with budgeted/activity/balance |
|
|
165
|
+
| `get_category` | Get full category details including goal progress and cadence |
|
|
166
|
+
| `get_month_category` | Get category budget for a specific month |
|
|
167
|
+
| `update_month_category` | Set the budgeted amount for a category in a month |
|
|
168
|
+
| `update_category` | Update name, note, goal target, or move to a different group |
|
|
169
|
+
| `create_category` | Create a new category in an existing group (with optional goal) |
|
|
170
|
+
| `create_category_group` | Create a new category group |
|
|
171
|
+
| `update_category_group` | Rename a category group |
|
|
172
|
+
| `search_categories` | Case-insensitive partial name search (e.g., "groc" finds "Groceries") |
|
|
173
|
+
|
|
174
|
+
### Payees
|
|
175
|
+
|
|
176
|
+
| Tool | Description |
|
|
177
|
+
|------|-------------|
|
|
178
|
+
| `list_payees` | List all payees with transfer account mappings |
|
|
179
|
+
| `get_payee` | Get payee details |
|
|
180
|
+
| `update_payee` | Rename a payee |
|
|
181
|
+
| `search_payees` | Case-insensitive partial name search |
|
|
182
|
+
|
|
183
|
+
### Payee Locations
|
|
184
|
+
|
|
185
|
+
| Tool | Description |
|
|
186
|
+
|------|-------------|
|
|
187
|
+
| `list_payee_locations` | List all payee locations (GPS coordinates from mobile app) |
|
|
188
|
+
| `get_payee_location` | Get a specific payee location |
|
|
189
|
+
| `get_payee_locations_by_payee` | Get all locations for a specific payee |
|
|
190
|
+
|
|
191
|
+
### Months
|
|
192
|
+
|
|
193
|
+
| Tool | Description |
|
|
194
|
+
|------|-------------|
|
|
195
|
+
| `list_months` | List budget months with income, budgeted, activity, to-be-budgeted, age of money |
|
|
196
|
+
| `get_month` | Get month detail with per-category budget/activity/balance breakdown |
|
|
197
|
+
|
|
198
|
+
### Money Movements
|
|
199
|
+
|
|
200
|
+
| Tool | Description |
|
|
201
|
+
|------|-------------|
|
|
202
|
+
| `list_money_movements` | List all money movements (budget re-allocations between categories) |
|
|
203
|
+
| `get_money_movements_by_month` | Get money movements for a specific month |
|
|
204
|
+
| `list_money_movement_groups` | List all money movement groups (batched re-allocations) |
|
|
205
|
+
| `get_money_movement_groups_by_month` | Get money movement groups for a specific month |
|
|
206
|
+
|
|
207
|
+
### Transactions
|
|
208
|
+
|
|
209
|
+
| Tool | Description |
|
|
210
|
+
|------|-------------|
|
|
211
|
+
| `get_transactions` | Get transactions with filters: by account, category, payee, month, or status (`unapproved`/`uncategorized`) |
|
|
212
|
+
| `get_transaction` | Get a single transaction by ID (includes subtransactions) |
|
|
213
|
+
| `create_transaction` | Create a transaction with optional split (subtransactions must sum to total) |
|
|
214
|
+
| `create_transactions` | Bulk create multiple transactions in a single API call |
|
|
215
|
+
| `update_transaction` | Partial update — only specified fields change |
|
|
216
|
+
| `update_transactions` | Batch update multiple transactions at once |
|
|
217
|
+
| `delete_transaction` | Delete a transaction |
|
|
218
|
+
| `import_transactions` | Trigger import from linked bank accounts |
|
|
219
|
+
|
|
220
|
+
### Scheduled Transactions
|
|
221
|
+
|
|
222
|
+
| Tool | Description |
|
|
223
|
+
|------|-------------|
|
|
224
|
+
| `list_scheduled_transactions` | List all recurring transactions |
|
|
225
|
+
| `get_scheduled_transaction` | Get a specific scheduled transaction |
|
|
226
|
+
| `create_scheduled_transaction` | Create a recurring transaction with frequency |
|
|
227
|
+
| `update_scheduled_transaction` | Update (fetch-then-merge preserves unchanged fields) |
|
|
228
|
+
| `delete_scheduled_transaction` | Delete a scheduled transaction |
|
|
229
|
+
|
|
230
|
+
**Supported frequencies:** `never`, `daily`, `weekly`, `everyOtherWeek`, `twiceAMonth`, `every4Weeks`, `monthly`, `everyOtherMonth`, `every3Months`, `every4Months`, `twiceAYear`, `yearly`, `everyOtherYear`
|
|
231
|
+
|
|
232
|
+
### Convenience
|
|
233
|
+
|
|
234
|
+
| Tool | Description |
|
|
235
|
+
|------|-------------|
|
|
236
|
+
| `review_unapproved` | Get unapproved transactions grouped by readiness: "ready to approve" (categorized) vs. "needs category first" (uncategorized). Includes a warning against blind approval. |
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Environment Variables
|
|
241
|
+
|
|
242
|
+
| Variable | Required | Description |
|
|
243
|
+
|----------|----------|-------------|
|
|
244
|
+
| `YNAB_API_TOKEN` | Yes | [Personal access token](https://app.ynab.com/settings/developer) from YNAB Developer Settings |
|
|
245
|
+
| `YNAB_BUDGET_ID` | No | Default budget ID. If omitted, uses `"last-used"` (your most recently accessed budget). Run `list_budgets` to find IDs. |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Amount Handling
|
|
250
|
+
|
|
251
|
+
All amounts in tool inputs and outputs are in **dollars** (e.g., `-12.34` for a $12.34 outflow). The server converts to/from YNAB's internal milliunits format automatically.
|
|
252
|
+
|
|
253
|
+
| Direction | Sign | Example |
|
|
254
|
+
|-----------|------|---------|
|
|
255
|
+
| Outflow (spending) | Negative | `-50.00` |
|
|
256
|
+
| Inflow (income) | Positive | `2500.00` |
|
|
257
|
+
| Transfer out | Negative | `-1000.00` |
|
|
258
|
+
| Transfer in | Positive | `1000.00` |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Rate Limiting
|
|
263
|
+
|
|
264
|
+
The YNAB API allows **200 requests per hour** per access token, enforced on a rolling window. Each tool call typically uses one API request (except `update_scheduled_transaction` which uses two — a GET to fetch current state, then a PUT to merge changes). The server surfaces rate limit errors as standard MCP error responses.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Architecture
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
┌─────────────────────┐ ┌──────────────────┐ ┌──────────────┐
|
|
272
|
+
│ AI Assistant │────▶│ YNAB MCP Server │────▶│ YNAB API │
|
|
273
|
+
│ (Claude, GPT, etc) │◀────│ (this package) │◀────│ api.ynab.com│
|
|
274
|
+
└─────────────────────┘ └──────────────────┘ └──────────────┘
|
|
275
|
+
MCP stdio transport HTTPS/REST
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
- **Transport:** stdio (standard MCP server pattern)
|
|
279
|
+
- **Auth:** Bearer token via `YNAB_API_TOKEN` environment variable
|
|
280
|
+
- **SDK:** Official [`ynab`](https://www.npmjs.com/package/ynab) v2.5+ for core endpoints, direct `fetch` for newer API features
|
|
281
|
+
- **Validation:** All parameters validated with [Zod](https://zod.dev) schemas
|
|
282
|
+
- **Error handling:** API errors are caught, formatted, and returned as MCP error responses with detail messages
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Testing
|
|
287
|
+
|
|
288
|
+
The test suite (43 tests) runs against a live YNAB budget. It creates test data and cleans up after itself:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm test
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Tests cover all tool categories: reads, writes, bulk operations, search, split transactions, scheduled transaction CRUD with fetch-then-merge verification, category/group creation, money movements, and payee locations.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Development
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
git clone https://github.com/oliverames/ynab-mcp-server.git
|
|
302
|
+
cd ynab-mcp-server
|
|
303
|
+
npm install
|
|
304
|
+
YNAB_API_TOKEN=your-token npm start
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Dependencies
|
|
308
|
+
|
|
309
|
+
- [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk) — MCP server framework
|
|
310
|
+
- [`ynab`](https://www.npmjs.com/package/ynab) — Official YNAB JavaScript client
|
|
311
|
+
|
|
312
|
+
Zero additional dependencies. No build step. Pure ESM.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT
|
package/index.js
CHANGED
|
@@ -48,11 +48,29 @@ async function run(fn) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Direct API helper for endpoints not yet in the ynab SDK
|
|
52
|
+
const BASE_URL = "https://api.ynab.com/v1";
|
|
53
|
+
async function ynabFetch(path, { method = "GET", body } = {}) {
|
|
54
|
+
const opts = {
|
|
55
|
+
method,
|
|
56
|
+
headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
|
|
57
|
+
};
|
|
58
|
+
if (body) opts.body = JSON.stringify(body);
|
|
59
|
+
const res = await fetch(`${BASE_URL}${path}`, opts);
|
|
60
|
+
const json = await res.json();
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const err = new Error(json?.error?.detail || `HTTP ${res.status}`);
|
|
63
|
+
err.error = json?.error;
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
return json.data;
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
// --- Server ---
|
|
52
70
|
|
|
53
71
|
const server = new McpServer({
|
|
54
72
|
name: "ynab-mcp-server",
|
|
55
|
-
version: "1.
|
|
73
|
+
version: "1.2.0",
|
|
56
74
|
});
|
|
57
75
|
|
|
58
76
|
// ==================== User & Budgets ====================
|
|
@@ -67,13 +85,13 @@ server.tool("get_user", "Get the authenticated user", {}, () =>
|
|
|
67
85
|
server.tool("list_budgets", "List all budgets. Use a budget ID from the results in other tools, or omit budgetId to use the last-used budget.", {}, () =>
|
|
68
86
|
run(async () => {
|
|
69
87
|
const { data } = await api.budgets.getBudgets();
|
|
70
|
-
return ok(data.budgets.map((b) => ({ id: b.id, name: b.name, last_modified_on: b.last_modified_on })));
|
|
88
|
+
return ok(data.budgets.map((b) => ({ id: b.id, name: b.name, last_modified_on: b.last_modified_on, first_month: b.first_month, last_month: b.last_month })));
|
|
71
89
|
})
|
|
72
90
|
);
|
|
73
91
|
|
|
74
92
|
server.tool(
|
|
75
93
|
"get_budget",
|
|
76
|
-
"Get
|
|
94
|
+
"Get a budget summary including name, currency format, and account/category/payee counts",
|
|
77
95
|
{ budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") },
|
|
78
96
|
({ budgetId }) =>
|
|
79
97
|
run(async () => {
|
|
@@ -82,6 +100,10 @@ server.tool(
|
|
|
82
100
|
return ok({
|
|
83
101
|
id: b.id,
|
|
84
102
|
name: b.name,
|
|
103
|
+
last_modified_on: b.last_modified_on,
|
|
104
|
+
first_month: b.first_month,
|
|
105
|
+
last_month: b.last_month,
|
|
106
|
+
date_format: b.date_format,
|
|
85
107
|
currency_format: b.currency_format,
|
|
86
108
|
accounts: b.accounts?.length,
|
|
87
109
|
categories: b.categories?.length,
|
|
@@ -180,6 +202,10 @@ function formatCategory(c) {
|
|
|
180
202
|
activity: dollars(c.activity),
|
|
181
203
|
balance: dollars(c.balance),
|
|
182
204
|
goal_type: c.goal_type,
|
|
205
|
+
goal_day: c.goal_day,
|
|
206
|
+
goal_cadence: c.goal_cadence,
|
|
207
|
+
goal_cadence_frequency: c.goal_cadence_frequency,
|
|
208
|
+
goal_creation_month: c.goal_creation_month,
|
|
183
209
|
goal_target: dollars(c.goal_target),
|
|
184
210
|
goal_target_date: c.goal_target_date,
|
|
185
211
|
goal_percentage_complete: c.goal_percentage_complete,
|
|
@@ -290,6 +316,67 @@ server.tool(
|
|
|
290
316
|
})
|
|
291
317
|
);
|
|
292
318
|
|
|
319
|
+
server.tool(
|
|
320
|
+
"create_category",
|
|
321
|
+
"Create a new category in a category group",
|
|
322
|
+
{
|
|
323
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
324
|
+
categoryGroupId: z.string().describe("Category group ID to create the category in"),
|
|
325
|
+
name: z.string().describe("Category name"),
|
|
326
|
+
note: z.string().optional().describe("Category note"),
|
|
327
|
+
goalTarget: z.number().optional().describe("Goal target amount in dollars (creates a 'Needed for Spending' goal)"),
|
|
328
|
+
goalTargetDate: z.string().optional().describe("Goal target date in ISO format (e.g. 2026-12-01)"),
|
|
329
|
+
},
|
|
330
|
+
({ budgetId, categoryGroupId, name, note, goalTarget, goalTargetDate }) =>
|
|
331
|
+
run(async () => {
|
|
332
|
+
const bid = resolveBudgetId(budgetId);
|
|
333
|
+
const cat = { category_group_id: categoryGroupId, name };
|
|
334
|
+
if (note !== undefined) cat.note = note;
|
|
335
|
+
if (goalTarget !== undefined) cat.goal_target = milliunits(goalTarget);
|
|
336
|
+
if (goalTargetDate !== undefined) cat.goal_target_date = goalTargetDate;
|
|
337
|
+
const data = await ynabFetch(`/budgets/${bid}/categories`, {
|
|
338
|
+
method: "POST",
|
|
339
|
+
body: { category: cat },
|
|
340
|
+
});
|
|
341
|
+
return ok(formatCategory(data.category));
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
server.tool(
|
|
346
|
+
"create_category_group",
|
|
347
|
+
"Create a new category group",
|
|
348
|
+
{
|
|
349
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
350
|
+
name: z.string().describe("Category group name (max 50 characters)"),
|
|
351
|
+
},
|
|
352
|
+
({ budgetId, name }) =>
|
|
353
|
+
run(async () => {
|
|
354
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/category_groups`, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
body: { category_group: { name } },
|
|
357
|
+
});
|
|
358
|
+
return ok(data.category_group);
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
server.tool(
|
|
363
|
+
"update_category_group",
|
|
364
|
+
"Rename a category group",
|
|
365
|
+
{
|
|
366
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
367
|
+
categoryGroupId: z.string().describe("Category group ID"),
|
|
368
|
+
name: z.string().describe("New category group name (max 50 characters)"),
|
|
369
|
+
},
|
|
370
|
+
({ budgetId, categoryGroupId, name }) =>
|
|
371
|
+
run(async () => {
|
|
372
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/category_groups/${categoryGroupId}`, {
|
|
373
|
+
method: "PATCH",
|
|
374
|
+
body: { category_group: { name } },
|
|
375
|
+
});
|
|
376
|
+
return ok(data.category_group);
|
|
377
|
+
})
|
|
378
|
+
);
|
|
379
|
+
|
|
293
380
|
// ==================== Payees ====================
|
|
294
381
|
|
|
295
382
|
server.tool(
|
|
@@ -334,6 +421,47 @@ server.tool(
|
|
|
334
421
|
})
|
|
335
422
|
);
|
|
336
423
|
|
|
424
|
+
// ==================== Payee Locations ====================
|
|
425
|
+
|
|
426
|
+
server.tool(
|
|
427
|
+
"list_payee_locations",
|
|
428
|
+
"List all payee locations (GPS coordinates where transactions occurred)",
|
|
429
|
+
{ budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") },
|
|
430
|
+
({ budgetId }) =>
|
|
431
|
+
run(async () => {
|
|
432
|
+
const { data } = await api.payeeLocations.getPayeeLocations(resolveBudgetId(budgetId));
|
|
433
|
+
return ok(data.payee_locations);
|
|
434
|
+
})
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
server.tool(
|
|
438
|
+
"get_payee_location",
|
|
439
|
+
"Get a specific payee location",
|
|
440
|
+
{
|
|
441
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
442
|
+
payeeLocationId: z.string().describe("Payee location ID"),
|
|
443
|
+
},
|
|
444
|
+
({ budgetId, payeeLocationId }) =>
|
|
445
|
+
run(async () => {
|
|
446
|
+
const { data } = await api.payeeLocations.getPayeeLocationById(resolveBudgetId(budgetId), payeeLocationId);
|
|
447
|
+
return ok(data.payee_location);
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
server.tool(
|
|
452
|
+
"get_payee_locations_by_payee",
|
|
453
|
+
"Get all locations for a specific payee",
|
|
454
|
+
{
|
|
455
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
456
|
+
payeeId: z.string().describe("Payee ID"),
|
|
457
|
+
},
|
|
458
|
+
({ budgetId, payeeId }) =>
|
|
459
|
+
run(async () => {
|
|
460
|
+
const { data } = await api.payeeLocations.getPayeeLocationsByPayee(resolveBudgetId(budgetId), payeeId);
|
|
461
|
+
return ok(data.payee_locations);
|
|
462
|
+
})
|
|
463
|
+
);
|
|
464
|
+
|
|
337
465
|
// ==================== Months ====================
|
|
338
466
|
|
|
339
467
|
server.tool(
|
|
@@ -389,6 +517,72 @@ server.tool(
|
|
|
389
517
|
})
|
|
390
518
|
);
|
|
391
519
|
|
|
520
|
+
// ==================== Money Movements ====================
|
|
521
|
+
|
|
522
|
+
function formatMoneyMovement(m) {
|
|
523
|
+
return {
|
|
524
|
+
id: m.id,
|
|
525
|
+
month: m.month,
|
|
526
|
+
moved_at: m.moved_at,
|
|
527
|
+
note: m.note,
|
|
528
|
+
money_movement_group_id: m.money_movement_group_id,
|
|
529
|
+
performed_by_user_id: m.performed_by_user_id,
|
|
530
|
+
from_category_id: m.from_category_id,
|
|
531
|
+
to_category_id: m.to_category_id,
|
|
532
|
+
amount: dollars(m.amount),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
server.tool(
|
|
537
|
+
"list_money_movements",
|
|
538
|
+
"List all money movements (budget re-allocations between categories)",
|
|
539
|
+
{ budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") },
|
|
540
|
+
({ budgetId }) =>
|
|
541
|
+
run(async () => {
|
|
542
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/money_movements`);
|
|
543
|
+
return ok(data.money_movements.map(formatMoneyMovement));
|
|
544
|
+
})
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
server.tool(
|
|
548
|
+
"get_money_movements_by_month",
|
|
549
|
+
"Get money movements for a specific month",
|
|
550
|
+
{
|
|
551
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
552
|
+
month: z.string().describe("Month in YYYY-MM-DD format (first of month), or 'current'"),
|
|
553
|
+
},
|
|
554
|
+
({ budgetId, month }) =>
|
|
555
|
+
run(async () => {
|
|
556
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/months/${month}/money_movements`);
|
|
557
|
+
return ok(data.money_movements.map(formatMoneyMovement));
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
server.tool(
|
|
562
|
+
"list_money_movement_groups",
|
|
563
|
+
"List all money movement groups (batches of related money movements)",
|
|
564
|
+
{ budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") },
|
|
565
|
+
({ budgetId }) =>
|
|
566
|
+
run(async () => {
|
|
567
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/money_movement_groups`);
|
|
568
|
+
return ok(data.money_movement_groups);
|
|
569
|
+
})
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
server.tool(
|
|
573
|
+
"get_money_movement_groups_by_month",
|
|
574
|
+
"Get money movement groups for a specific month",
|
|
575
|
+
{
|
|
576
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
577
|
+
month: z.string().describe("Month in YYYY-MM-DD format (first of month), or 'current'"),
|
|
578
|
+
},
|
|
579
|
+
({ budgetId, month }) =>
|
|
580
|
+
run(async () => {
|
|
581
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/months/${month}/money_movement_groups`);
|
|
582
|
+
return ok(data.money_movement_groups);
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
|
|
392
586
|
// ==================== Transactions ====================
|
|
393
587
|
|
|
394
588
|
function formatTransaction(t) {
|
|
@@ -477,7 +671,7 @@ server.tool(
|
|
|
477
671
|
|
|
478
672
|
server.tool(
|
|
479
673
|
"create_transaction",
|
|
480
|
-
"Create a new transaction. Amounts are in dollars (positive for inflows, negative for outflows).",
|
|
674
|
+
"Create a new transaction. Amounts are in dollars (positive for inflows, negative for outflows). Note: future-dated transactions cannot be created here — use create_scheduled_transaction instead.",
|
|
481
675
|
{
|
|
482
676
|
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
483
677
|
accountId: z.string().describe("Account ID"),
|
|
@@ -530,6 +724,50 @@ server.tool(
|
|
|
530
724
|
})
|
|
531
725
|
);
|
|
532
726
|
|
|
727
|
+
server.tool(
|
|
728
|
+
"create_transactions",
|
|
729
|
+
"Create multiple transactions at once. Amounts are in dollars. Returns created transactions and any duplicate import IDs. Future-dated transactions are not supported — use create_scheduled_transaction instead.",
|
|
730
|
+
{
|
|
731
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
732
|
+
transactions: z.array(z.object({
|
|
733
|
+
accountId: z.string().describe("Account ID"),
|
|
734
|
+
date: z.string().describe("Transaction date (YYYY-MM-DD)"),
|
|
735
|
+
amount: z.number().describe("Amount in dollars (negative for outflows, positive for inflows)"),
|
|
736
|
+
payeeId: z.string().optional().describe("Payee ID"),
|
|
737
|
+
payeeName: z.string().optional().describe("Payee name (creates new payee if no payeeId)"),
|
|
738
|
+
categoryId: z.string().optional().describe("Category ID"),
|
|
739
|
+
memo: z.string().optional().describe("Transaction memo"),
|
|
740
|
+
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status"),
|
|
741
|
+
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
742
|
+
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color"),
|
|
743
|
+
importId: z.string().optional().describe("Unique import ID for deduplication (max 36 chars)"),
|
|
744
|
+
})).describe("Array of transactions to create"),
|
|
745
|
+
},
|
|
746
|
+
({ budgetId, transactions: txns }) =>
|
|
747
|
+
run(async () => {
|
|
748
|
+
const mapped = txns.map((t) => ({
|
|
749
|
+
account_id: t.accountId,
|
|
750
|
+
date: t.date,
|
|
751
|
+
amount: milliunits(t.amount),
|
|
752
|
+
payee_id: t.payeeId,
|
|
753
|
+
payee_name: t.payeeName,
|
|
754
|
+
category_id: t.categoryId,
|
|
755
|
+
memo: t.memo,
|
|
756
|
+
cleared: t.cleared,
|
|
757
|
+
approved: t.approved,
|
|
758
|
+
flag_color: t.flagColor,
|
|
759
|
+
import_id: t.importId,
|
|
760
|
+
}));
|
|
761
|
+
const { data } = await api.transactions.createTransactions(resolveBudgetId(budgetId), {
|
|
762
|
+
transactions: mapped,
|
|
763
|
+
});
|
|
764
|
+
return ok({
|
|
765
|
+
created: data.transactions?.map(formatTransaction),
|
|
766
|
+
duplicate_import_ids: data.duplicate_import_ids,
|
|
767
|
+
});
|
|
768
|
+
})
|
|
769
|
+
);
|
|
770
|
+
|
|
533
771
|
server.tool(
|
|
534
772
|
"update_transaction",
|
|
535
773
|
"Update an existing transaction. Only provided fields are changed. Amounts in dollars.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oliverames/ynab-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "YNAB MCP server with full API coverage",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"index.js"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "node index.js"
|
|
14
|
+
"start": "node index.js",
|
|
15
|
+
"test": "node test.js"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
18
|
"@modelcontextprotocol/sdk": "^1.12.1",
|