@oliverames/ynab-mcp-server 1.2.0 → 1.2.2
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/LICENSE +21 -0
- package/README.md +47 -16
- package/index.js +121 -61
- package/package.json +3 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oliver Ames
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -10,19 +10,31 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
|
|
13
|
+
<code>43 tools</code> •
|
|
14
|
+
<code>100% API coverage</code> •
|
|
15
|
+
<code>YNAB API v1.79</code>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="https://www.npmjs.com/package/@oliverames/ynab-mcp-server"><img src="https://img.shields.io/npm/v/%40oliverames%2Fynab-mcp-server?style=flat-square&color=f5a542" alt="npm"></a>
|
|
20
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-f5a542?style=flat-square" alt="License"></a>
|
|
21
|
+
<a href="https://www.buymeacoffee.com/oliverames"><img src="https://img.shields.io/badge/Buy_Me_a_Coffee-support-f5a542?style=flat-square&logo=buy-me-a-coffee&logoColor=white" alt="Buy Me a Coffee"></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<a href="#quick-start">Quick Start</a> •
|
|
26
|
+
<a href="#what-you-can-do">What You Can Do</a> •
|
|
27
|
+
<a href="#tools-reference">All 43 Tools</a> •
|
|
28
|
+
<a href="#environment-variables">Configuration</a>
|
|
17
29
|
</p>
|
|
18
30
|
|
|
19
31
|
---
|
|
20
32
|
|
|
21
|
-
|
|
33
|
+
## Why This Exists
|
|
22
34
|
|
|
23
|
-
|
|
35
|
+
YNAB's budgeting philosophy works best when you interact with your budget frequently — but the app interface isn't designed for quick queries or bulk operations. "How much did I spend on groceries this month?" shouldn't require navigating three screens. "Categorize all my Amazon orders from this week" shouldn't be a manual, one-by-one process.
|
|
24
36
|
|
|
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.
|
|
37
|
+
This server gives your AI assistant full access to YNAB's API, turning natural language into budget operations. All monetary values are automatically converted between dollars and YNAB's internal milliunits format so the AI never has to think about it. 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
38
|
|
|
27
39
|
---
|
|
28
40
|
|
|
@@ -132,7 +144,9 @@ That's it. Your AI can now talk to YNAB.
|
|
|
132
144
|
- **Bulk operations** — `create_transactions` and `update_transactions` handle arrays in a single API call.
|
|
133
145
|
- **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
146
|
- **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" (
|
|
147
|
+
- **Approval workflow** — `review_unapproved` groups transactions into "ready to approve" (categorized, split, or transfer) and "needs attention" (uncategorized), with a built-in warning against approving uncategorized entries.
|
|
148
|
+
- **Nullable updates** — update tools accept `null` for clearable fields (`memo`, `payeeName`, `categoryId`, `flagColor`) to distinguish "don't change" (omit) from "clear this field" (`null`).
|
|
149
|
+
- **Debt account support** — loan and debt accounts include `debt_original_balance`, `debt_interest_rates`, `debt_minimum_payments`, and `debt_escrow_amounts` with correct unit conversion (rates stay as percentages, payments convert from milliunits).
|
|
136
150
|
|
|
137
151
|
---
|
|
138
152
|
|
|
@@ -143,7 +157,7 @@ That's it. Your AI can now talk to YNAB.
|
|
|
143
157
|
| Tool | Description |
|
|
144
158
|
|------|-------------|
|
|
145
159
|
| `get_user` | Get the authenticated user |
|
|
146
|
-
| `list_budgets` | List all budgets with IDs, names,
|
|
160
|
+
| `list_budgets` | List all budgets with IDs, names, date ranges, format settings, and default budget |
|
|
147
161
|
| `get_budget` | Get budget summary (name, currency, account/category/payee counts) |
|
|
148
162
|
| `get_budget_settings` | Get currency and date format settings |
|
|
149
163
|
|
|
@@ -151,8 +165,8 @@ That's it. Your AI can now talk to YNAB.
|
|
|
151
165
|
|
|
152
166
|
| Tool | Description |
|
|
153
167
|
|------|-------------|
|
|
154
|
-
| `list_accounts` | List all accounts with balances
|
|
155
|
-
| `get_account` | Get details
|
|
168
|
+
| `list_accounts` | List all accounts with balances, debt details, and import status |
|
|
169
|
+
| `get_account` | Get full account details including notes and debt fields |
|
|
156
170
|
| `create_account` | Create a new account (checking, savings, creditCard, mortgage, etc.) |
|
|
157
171
|
|
|
158
172
|
**Supported account types:** `checking`, `savings`, `cash`, `creditCard`, `lineOfCredit`, `otherAsset`, `otherLiability`, `mortgage`, `autoLoan`, `studentLoan`, `personalLoan`, `medicalDebt`, `otherDebt`
|
|
@@ -165,7 +179,7 @@ That's it. Your AI can now talk to YNAB.
|
|
|
165
179
|
| `get_category` | Get full category details including goal progress and cadence |
|
|
166
180
|
| `get_month_category` | Get category budget for a specific month |
|
|
167
181
|
| `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 |
|
|
182
|
+
| `update_category` | Update name, note, goal target, goal target date, or move to a different group |
|
|
169
183
|
| `create_category` | Create a new category in an existing group (with optional goal) |
|
|
170
184
|
| `create_category_group` | Create a new category group |
|
|
171
185
|
| `update_category_group` | Rename a category group |
|
|
@@ -192,8 +206,8 @@ That's it. Your AI can now talk to YNAB.
|
|
|
192
206
|
|
|
193
207
|
| Tool | Description |
|
|
194
208
|
|------|-------------|
|
|
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 |
|
|
209
|
+
| `list_months` | List budget months with income, budgeted, activity, to-be-budgeted, age of money, and notes |
|
|
210
|
+
| `get_month` | Get month detail with per-category budget/activity/balance/goal breakdown |
|
|
197
211
|
|
|
198
212
|
### Money Movements
|
|
199
213
|
|
|
@@ -211,7 +225,7 @@ That's it. Your AI can now talk to YNAB.
|
|
|
211
225
|
| `get_transactions` | Get transactions with filters: by account, category, payee, month, or status (`unapproved`/`uncategorized`) |
|
|
212
226
|
| `get_transaction` | Get a single transaction by ID (includes subtransactions) |
|
|
213
227
|
| `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 |
|
|
228
|
+
| `create_transactions` | Bulk create multiple transactions in a single API call (supports split transactions) |
|
|
215
229
|
| `update_transaction` | Partial update — only specified fields change |
|
|
216
230
|
| `update_transactions` | Batch update multiple transactions at once |
|
|
217
231
|
| `delete_transaction` | Delete a transaction |
|
|
@@ -233,7 +247,7 @@ That's it. Your AI can now talk to YNAB.
|
|
|
233
247
|
|
|
234
248
|
| Tool | Description |
|
|
235
249
|
|------|-------------|
|
|
236
|
-
| `review_unapproved` | Get unapproved transactions grouped by readiness: "ready to approve" (categorized) vs. "needs category first" (uncategorized). Includes a warning against blind approval. |
|
|
250
|
+
| `review_unapproved` | Get unapproved transactions grouped by readiness: "ready to approve" (categorized, split, or transfer) vs. "needs category first" (uncategorized). Includes a warning against blind approval. |
|
|
237
251
|
|
|
238
252
|
---
|
|
239
253
|
|
|
@@ -316,3 +330,20 @@ Zero additional dependencies. No build step. Pure ESM.
|
|
|
316
330
|
## License
|
|
317
331
|
|
|
318
332
|
MIT
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
<p align="center">
|
|
337
|
+
<a href="https://www.buymeacoffee.com/oliverames">
|
|
338
|
+
<img src="https://img.shields.io/badge/Buy_Me_a_Coffee-support-f5a542?style=for-the-badge&logo=buy-me-a-coffee&logoColor=white" alt="Buy Me a Coffee">
|
|
339
|
+
</a>
|
|
340
|
+
</p>
|
|
341
|
+
|
|
342
|
+
<p align="center">
|
|
343
|
+
<sub>
|
|
344
|
+
Built by <a href="https://ames.consulting">Oliver Ames</a> in Vermont
|
|
345
|
+
• <a href="https://github.com/oliverames">GitHub</a>
|
|
346
|
+
• <a href="https://linkedin.com/in/oliverames">LinkedIn</a>
|
|
347
|
+
• <a href="https://bsky.app/profile/oliverames.bsky.social">Bluesky</a>
|
|
348
|
+
</sub>
|
|
349
|
+
</p>
|
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
6
|
import { z } from "zod";
|
|
@@ -7,9 +8,19 @@ import * as ynab from "ynab";
|
|
|
7
8
|
|
|
8
9
|
// --- Init ---
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
let API_TOKEN = process.env.YNAB_API_TOKEN;
|
|
11
12
|
if (!API_TOKEN) {
|
|
12
|
-
|
|
13
|
+
try {
|
|
14
|
+
API_TOKEN = execFileSync(
|
|
15
|
+
"op", ["read", "op://Development/YNAB API Token/credential"],
|
|
16
|
+
{ encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] }
|
|
17
|
+
).trim();
|
|
18
|
+
} catch {
|
|
19
|
+
// 1Password CLI unavailable or item not found
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!API_TOKEN) {
|
|
23
|
+
console.error("YNAB_API_TOKEN environment variable is required (1Password fallback also failed)");
|
|
13
24
|
process.exit(1);
|
|
14
25
|
}
|
|
15
26
|
|
|
@@ -31,6 +42,36 @@ function milliunits(dollars) {
|
|
|
31
42
|
return Math.round(dollars * 1000);
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
function dollarsMap(obj) {
|
|
46
|
+
return obj ? Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, dollars(v)])) : obj;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mapTransactionInput(t) {
|
|
50
|
+
const out = {
|
|
51
|
+
account_id: t.accountId,
|
|
52
|
+
date: t.date,
|
|
53
|
+
amount: milliunits(t.amount),
|
|
54
|
+
payee_id: t.payeeId,
|
|
55
|
+
payee_name: t.payeeName,
|
|
56
|
+
category_id: t.categoryId,
|
|
57
|
+
memo: t.memo,
|
|
58
|
+
cleared: t.cleared,
|
|
59
|
+
approved: t.approved,
|
|
60
|
+
flag_color: t.flagColor,
|
|
61
|
+
import_id: t.importId,
|
|
62
|
+
};
|
|
63
|
+
if (t.subtransactions) {
|
|
64
|
+
out.subtransactions = t.subtransactions.map((s) => ({
|
|
65
|
+
amount: milliunits(s.amount),
|
|
66
|
+
category_id: s.categoryId,
|
|
67
|
+
payee_id: s.payeeId,
|
|
68
|
+
payee_name: s.payeeName,
|
|
69
|
+
memo: s.memo,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
function ok(data) {
|
|
35
76
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
36
77
|
}
|
|
@@ -70,7 +111,7 @@ async function ynabFetch(path, { method = "GET", body } = {}) {
|
|
|
70
111
|
|
|
71
112
|
const server = new McpServer({
|
|
72
113
|
name: "ynab-mcp-server",
|
|
73
|
-
version: "1.2.
|
|
114
|
+
version: "1.2.1",
|
|
74
115
|
});
|
|
75
116
|
|
|
76
117
|
// ==================== User & Budgets ====================
|
|
@@ -85,7 +126,13 @@ server.tool("get_user", "Get the authenticated user", {}, () =>
|
|
|
85
126
|
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.", {}, () =>
|
|
86
127
|
run(async () => {
|
|
87
128
|
const { data } = await api.budgets.getBudgets();
|
|
88
|
-
|
|
129
|
+
const result = {
|
|
130
|
+
budgets: 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, date_format: b.date_format, currency_format: b.currency_format })),
|
|
131
|
+
};
|
|
132
|
+
if (data.default_budget) {
|
|
133
|
+
result.default_budget = { id: data.default_budget.id, name: data.default_budget.name };
|
|
134
|
+
}
|
|
135
|
+
return ok(result);
|
|
89
136
|
})
|
|
90
137
|
);
|
|
91
138
|
|
|
@@ -139,6 +186,10 @@ function formatAccount(a) {
|
|
|
139
186
|
direct_import_linked: a.direct_import_linked,
|
|
140
187
|
direct_import_in_error: a.direct_import_in_error,
|
|
141
188
|
last_reconciled_at: a.last_reconciled_at,
|
|
189
|
+
debt_original_balance: dollars(a.debt_original_balance),
|
|
190
|
+
debt_interest_rates: a.debt_interest_rates,
|
|
191
|
+
debt_minimum_payments: dollarsMap(a.debt_minimum_payments),
|
|
192
|
+
debt_escrow_amounts: dollarsMap(a.debt_escrow_amounts),
|
|
142
193
|
deleted: a.deleted,
|
|
143
194
|
};
|
|
144
195
|
if ("note" in a) out.note = a.note;
|
|
@@ -195,6 +246,7 @@ function formatCategory(c) {
|
|
|
195
246
|
id: c.id,
|
|
196
247
|
category_group_id: c.category_group_id,
|
|
197
248
|
category_group_name: c.category_group_name,
|
|
249
|
+
original_category_group_id: c.original_category_group_id,
|
|
198
250
|
name: c.name,
|
|
199
251
|
hidden: c.hidden,
|
|
200
252
|
note: c.note,
|
|
@@ -214,6 +266,7 @@ function formatCategory(c) {
|
|
|
214
266
|
goal_overall_funded: dollars(c.goal_overall_funded),
|
|
215
267
|
goal_overall_left: dollars(c.goal_overall_left),
|
|
216
268
|
goal_needs_whole_amount: c.goal_needs_whole_amount,
|
|
269
|
+
deleted: c.deleted,
|
|
217
270
|
};
|
|
218
271
|
}
|
|
219
272
|
|
|
@@ -229,6 +282,7 @@ server.tool(
|
|
|
229
282
|
id: g.id,
|
|
230
283
|
name: g.name,
|
|
231
284
|
hidden: g.hidden,
|
|
285
|
+
deleted: g.deleted,
|
|
232
286
|
categories: g.categories.map((c) => ({
|
|
233
287
|
id: c.id,
|
|
234
288
|
name: c.name,
|
|
@@ -237,6 +291,7 @@ server.tool(
|
|
|
237
291
|
activity: dollars(c.activity),
|
|
238
292
|
balance: dollars(c.balance),
|
|
239
293
|
goal_type: c.goal_type,
|
|
294
|
+
deleted: c.deleted,
|
|
240
295
|
})),
|
|
241
296
|
}))
|
|
242
297
|
);
|
|
@@ -300,14 +355,16 @@ server.tool(
|
|
|
300
355
|
note: z.string().nullable().optional().describe("Category note (null to clear)"),
|
|
301
356
|
categoryGroupId: z.string().optional().describe("Move to a different category group"),
|
|
302
357
|
goalTarget: z.number().nullable().optional().describe("Goal target amount in dollars (only if category already has a goal)"),
|
|
358
|
+
goalTargetDate: z.string().nullable().optional().describe("Goal target date in ISO format (e.g. 2026-12-01, null to clear)"),
|
|
303
359
|
},
|
|
304
|
-
({ budgetId, categoryId, name, note, categoryGroupId, goalTarget }) =>
|
|
360
|
+
({ budgetId, categoryId, name, note, categoryGroupId, goalTarget, goalTargetDate }) =>
|
|
305
361
|
run(async () => {
|
|
306
362
|
const cat = {};
|
|
307
363
|
if (name !== undefined) cat.name = name;
|
|
308
364
|
if (note !== undefined) cat.note = note;
|
|
309
365
|
if (categoryGroupId !== undefined) cat.category_group_id = categoryGroupId;
|
|
310
366
|
if (goalTarget !== undefined) cat.goal_target = goalTarget != null ? milliunits(goalTarget) : null;
|
|
367
|
+
if (goalTargetDate !== undefined) cat.goal_target_date = goalTargetDate;
|
|
311
368
|
|
|
312
369
|
const { data } = await api.categories.updateCategory(resolveBudgetId(budgetId), categoryId, {
|
|
313
370
|
category: cat,
|
|
@@ -386,7 +443,7 @@ server.tool(
|
|
|
386
443
|
({ budgetId }) =>
|
|
387
444
|
run(async () => {
|
|
388
445
|
const { data } = await api.payees.getPayees(resolveBudgetId(budgetId));
|
|
389
|
-
return ok(data.payees.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id })));
|
|
446
|
+
return ok(data.payees.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id, deleted: p.deleted })));
|
|
390
447
|
})
|
|
391
448
|
);
|
|
392
449
|
|
|
@@ -474,11 +531,13 @@ server.tool(
|
|
|
474
531
|
return ok(
|
|
475
532
|
data.months.map((m) => ({
|
|
476
533
|
month: m.month,
|
|
534
|
+
note: m.note,
|
|
477
535
|
income: dollars(m.income),
|
|
478
536
|
budgeted: dollars(m.budgeted),
|
|
479
537
|
activity: dollars(m.activity),
|
|
480
538
|
to_be_budgeted: dollars(m.to_be_budgeted),
|
|
481
539
|
age_of_money: m.age_of_money,
|
|
540
|
+
deleted: m.deleted,
|
|
482
541
|
}))
|
|
483
542
|
);
|
|
484
543
|
})
|
|
@@ -497,14 +556,17 @@ server.tool(
|
|
|
497
556
|
const m = data.month;
|
|
498
557
|
return ok({
|
|
499
558
|
month: m.month,
|
|
559
|
+
note: m.note,
|
|
500
560
|
income: dollars(m.income),
|
|
501
561
|
budgeted: dollars(m.budgeted),
|
|
502
562
|
activity: dollars(m.activity),
|
|
503
563
|
to_be_budgeted: dollars(m.to_be_budgeted),
|
|
504
564
|
age_of_money: m.age_of_money,
|
|
565
|
+
deleted: m.deleted,
|
|
505
566
|
categories: m.categories?.map((c) => ({
|
|
506
567
|
id: c.id,
|
|
507
568
|
name: c.name,
|
|
569
|
+
hidden: c.hidden,
|
|
508
570
|
category_group_name: c.category_group_name,
|
|
509
571
|
budgeted: dollars(c.budgeted),
|
|
510
572
|
activity: dollars(c.activity),
|
|
@@ -512,6 +574,7 @@ server.tool(
|
|
|
512
574
|
goal_type: c.goal_type,
|
|
513
575
|
goal_target: dollars(c.goal_target),
|
|
514
576
|
goal_under_funded: dollars(c.goal_under_funded),
|
|
577
|
+
deleted: c.deleted,
|
|
515
578
|
})),
|
|
516
579
|
});
|
|
517
580
|
})
|
|
@@ -530,6 +593,7 @@ function formatMoneyMovement(m) {
|
|
|
530
593
|
from_category_id: m.from_category_id,
|
|
531
594
|
to_category_id: m.to_category_id,
|
|
532
595
|
amount: dollars(m.amount),
|
|
596
|
+
deleted: m.deleted,
|
|
533
597
|
};
|
|
534
598
|
}
|
|
535
599
|
|
|
@@ -602,17 +666,25 @@ function formatTransaction(t) {
|
|
|
602
666
|
category_id: t.category_id,
|
|
603
667
|
category_name: t.category_name,
|
|
604
668
|
transfer_account_id: t.transfer_account_id,
|
|
669
|
+
transfer_transaction_id: t.transfer_transaction_id,
|
|
670
|
+
matched_transaction_id: t.matched_transaction_id,
|
|
605
671
|
import_id: t.import_id,
|
|
606
672
|
import_payee_name: t.import_payee_name,
|
|
673
|
+
import_payee_name_original: t.import_payee_name_original,
|
|
607
674
|
debt_transaction_type: t.debt_transaction_type,
|
|
675
|
+
deleted: t.deleted,
|
|
608
676
|
subtransactions: t.subtransactions?.map((s) => ({
|
|
609
677
|
id: s.id,
|
|
678
|
+
transaction_id: s.transaction_id,
|
|
610
679
|
amount: dollars(s.amount),
|
|
611
680
|
memo: s.memo,
|
|
612
681
|
payee_id: s.payee_id,
|
|
613
682
|
payee_name: s.payee_name,
|
|
614
683
|
category_id: s.category_id,
|
|
615
684
|
category_name: s.category_name,
|
|
685
|
+
transfer_account_id: s.transfer_account_id,
|
|
686
|
+
transfer_transaction_id: s.transfer_transaction_id,
|
|
687
|
+
deleted: s.deleted,
|
|
616
688
|
})),
|
|
617
689
|
};
|
|
618
690
|
}
|
|
@@ -693,32 +765,10 @@ server.tool(
|
|
|
693
765
|
memo: z.string().optional().describe("Memo"),
|
|
694
766
|
})).optional().describe("Split transaction into subtransactions. The subtransaction amounts must sum to the total transaction amount."),
|
|
695
767
|
},
|
|
696
|
-
({ budgetId,
|
|
768
|
+
({ budgetId, ...txnFields }) =>
|
|
697
769
|
run(async () => {
|
|
698
|
-
const txn = {
|
|
699
|
-
account_id: accountId,
|
|
700
|
-
date,
|
|
701
|
-
amount: milliunits(amount),
|
|
702
|
-
payee_id: payeeId,
|
|
703
|
-
payee_name: payeeName,
|
|
704
|
-
category_id: categoryId,
|
|
705
|
-
memo,
|
|
706
|
-
cleared,
|
|
707
|
-
approved,
|
|
708
|
-
flag_color: flagColor,
|
|
709
|
-
import_id: importId,
|
|
710
|
-
};
|
|
711
|
-
if (subtransactions) {
|
|
712
|
-
txn.subtransactions = subtransactions.map((s) => ({
|
|
713
|
-
amount: milliunits(s.amount),
|
|
714
|
-
category_id: s.categoryId,
|
|
715
|
-
payee_id: s.payeeId,
|
|
716
|
-
payee_name: s.payeeName,
|
|
717
|
-
memo: s.memo,
|
|
718
|
-
}));
|
|
719
|
-
}
|
|
720
770
|
const { data } = await api.transactions.createTransaction(resolveBudgetId(budgetId), {
|
|
721
|
-
transaction:
|
|
771
|
+
transaction: mapTransactionInput(txnFields),
|
|
722
772
|
});
|
|
723
773
|
return ok(formatTransaction(data.transaction));
|
|
724
774
|
})
|
|
@@ -741,25 +791,19 @@ server.tool(
|
|
|
741
791
|
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
742
792
|
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color"),
|
|
743
793
|
importId: z.string().optional().describe("Unique import ID for deduplication (max 36 chars)"),
|
|
794
|
+
subtransactions: z.array(z.object({
|
|
795
|
+
amount: z.number().describe("Subtransaction amount in dollars"),
|
|
796
|
+
categoryId: z.string().optional().describe("Category ID"),
|
|
797
|
+
payeeId: z.string().optional().describe("Payee ID"),
|
|
798
|
+
payeeName: z.string().optional().describe("Payee name"),
|
|
799
|
+
memo: z.string().optional().describe("Memo"),
|
|
800
|
+
})).optional().describe("Split transaction into subtransactions"),
|
|
744
801
|
})).describe("Array of transactions to create"),
|
|
745
802
|
},
|
|
746
803
|
({ budgetId, transactions: txns }) =>
|
|
747
804
|
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
805
|
const { data } = await api.transactions.createTransactions(resolveBudgetId(budgetId), {
|
|
762
|
-
transactions:
|
|
806
|
+
transactions: txns.map(mapTransactionInput),
|
|
763
807
|
});
|
|
764
808
|
return ok({
|
|
765
809
|
created: data.transactions?.map(formatTransaction),
|
|
@@ -777,10 +821,10 @@ server.tool(
|
|
|
777
821
|
accountId: z.string().optional().describe("Account ID"),
|
|
778
822
|
date: z.string().optional().describe("Transaction date (YYYY-MM-DD)"),
|
|
779
823
|
amount: z.number().optional().describe("Amount in dollars"),
|
|
780
|
-
payeeId: z.string().optional().describe("Payee ID"),
|
|
781
|
-
payeeName: z.string().optional().describe("Payee name"),
|
|
824
|
+
payeeId: z.string().nullable().optional().describe("Payee ID (null to remove)"),
|
|
825
|
+
payeeName: z.string().nullable().optional().describe("Payee name (null to clear)"),
|
|
782
826
|
categoryId: z.string().nullable().optional().describe("Category ID (null to uncategorize)"),
|
|
783
|
-
memo: z.string().optional().describe("Transaction memo"),
|
|
827
|
+
memo: z.string().nullable().optional().describe("Transaction memo (null to clear)"),
|
|
784
828
|
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status"),
|
|
785
829
|
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
786
830
|
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).nullable().optional().describe("Flag color (null to remove)"),
|
|
@@ -829,16 +873,16 @@ server.tool(
|
|
|
829
873
|
.array(
|
|
830
874
|
z.object({
|
|
831
875
|
id: z.string().describe("Transaction ID"),
|
|
832
|
-
|
|
833
|
-
date: z.string().optional(),
|
|
876
|
+
accountId: z.string().optional().describe("Account ID"),
|
|
877
|
+
date: z.string().optional().describe("Transaction date (YYYY-MM-DD)"),
|
|
834
878
|
amount: z.number().optional().describe("Amount in dollars"),
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
memo: z.string().optional(),
|
|
839
|
-
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional(),
|
|
840
|
-
approved: z.boolean().optional(),
|
|
841
|
-
|
|
879
|
+
payeeId: z.string().nullable().optional().describe("Payee ID (null to remove)"),
|
|
880
|
+
payeeName: z.string().nullable().optional().describe("Payee name (null to clear)"),
|
|
881
|
+
categoryId: z.string().nullable().optional().describe("Category ID (null to uncategorize)"),
|
|
882
|
+
memo: z.string().nullable().optional().describe("Transaction memo (null to clear)"),
|
|
883
|
+
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status"),
|
|
884
|
+
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
885
|
+
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).nullable().optional().describe("Flag color (null to remove)"),
|
|
842
886
|
})
|
|
843
887
|
)
|
|
844
888
|
.describe("Array of transaction updates"),
|
|
@@ -846,8 +890,17 @@ server.tool(
|
|
|
846
890
|
({ budgetId, transactions: txns }) =>
|
|
847
891
|
run(async () => {
|
|
848
892
|
const mapped = txns.map((t) => {
|
|
849
|
-
const out = {
|
|
850
|
-
if (
|
|
893
|
+
const out = { id: t.id };
|
|
894
|
+
if (t.accountId !== undefined) out.account_id = t.accountId;
|
|
895
|
+
if (t.date !== undefined) out.date = t.date;
|
|
896
|
+
if (t.amount !== undefined) out.amount = milliunits(t.amount);
|
|
897
|
+
if (t.payeeId !== undefined) out.payee_id = t.payeeId;
|
|
898
|
+
if (t.payeeName !== undefined) out.payee_name = t.payeeName;
|
|
899
|
+
if (t.categoryId !== undefined) out.category_id = t.categoryId;
|
|
900
|
+
if (t.memo !== undefined) out.memo = t.memo;
|
|
901
|
+
if (t.cleared !== undefined) out.cleared = t.cleared;
|
|
902
|
+
if (t.approved !== undefined) out.approved = t.approved;
|
|
903
|
+
if (t.flagColor !== undefined) out.flag_color = t.flagColor;
|
|
851
904
|
return out;
|
|
852
905
|
});
|
|
853
906
|
const { data } = await api.transactions.updateTransactions(resolveBudgetId(budgetId), {
|
|
@@ -890,14 +943,18 @@ function formatScheduledTransaction(t) {
|
|
|
890
943
|
category_id: t.category_id,
|
|
891
944
|
category_name: t.category_name,
|
|
892
945
|
transfer_account_id: t.transfer_account_id,
|
|
946
|
+
deleted: t.deleted,
|
|
893
947
|
subtransactions: t.subtransactions?.map((s) => ({
|
|
894
948
|
id: s.id,
|
|
949
|
+
scheduled_transaction_id: s.scheduled_transaction_id,
|
|
895
950
|
amount: dollars(s.amount),
|
|
896
951
|
memo: s.memo,
|
|
897
952
|
payee_id: s.payee_id,
|
|
898
953
|
payee_name: s.payee_name,
|
|
899
954
|
category_id: s.category_id,
|
|
900
955
|
category_name: s.category_name,
|
|
956
|
+
transfer_account_id: s.transfer_account_id,
|
|
957
|
+
deleted: s.deleted,
|
|
901
958
|
})),
|
|
902
959
|
};
|
|
903
960
|
}
|
|
@@ -1067,7 +1124,7 @@ server.tool(
|
|
|
1067
1124
|
const q = query.toLowerCase();
|
|
1068
1125
|
const matches = data.payees
|
|
1069
1126
|
.filter((p) => p.name.toLowerCase().includes(q))
|
|
1070
|
-
.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id }));
|
|
1127
|
+
.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id, deleted: p.deleted }));
|
|
1071
1128
|
if (matches.length === 0) return ok({ message: `No payees matching "${query}"` });
|
|
1072
1129
|
return ok(matches);
|
|
1073
1130
|
})
|
|
@@ -1081,8 +1138,11 @@ server.tool(
|
|
|
1081
1138
|
run(async () => {
|
|
1082
1139
|
const { data } = await api.transactions.getTransactions(resolveBudgetId(budgetId), undefined, "unapproved");
|
|
1083
1140
|
const txns = data.transactions.map(formatTransaction);
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1141
|
+
const isCategorized = (t) => (t.category_id && t.category_name !== "Uncategorized")
|
|
1142
|
+
|| (t.subtransactions && t.subtransactions.length > 0) // split transactions are categorized via subtransactions
|
|
1143
|
+
|| t.transfer_account_id; // transfers don't need categories
|
|
1144
|
+
const categorized = txns.filter(isCategorized);
|
|
1145
|
+
const uncategorized = txns.filter((t) => !isCategorized(t));
|
|
1086
1146
|
return ok({
|
|
1087
1147
|
total: txns.length,
|
|
1088
1148
|
ready_to_approve: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oliverames/ynab-mcp-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "YNAB MCP server with full API coverage",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "node index.js",
|
|
15
|
-
"test": "node test.js"
|
|
15
|
+
"test": "node test.js",
|
|
16
|
+
"postpublish": "$HOME/Developer/projects/ames-claude/bump-and-sync"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
19
|
"@modelcontextprotocol/sdk": "^1.12.1",
|