@oliverames/ynab-mcp-server 1.2.0 → 1.2.1
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 +108 -59
- 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
|
@@ -31,6 +31,36 @@ function milliunits(dollars) {
|
|
|
31
31
|
return Math.round(dollars * 1000);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function dollarsMap(obj) {
|
|
35
|
+
return obj ? Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, dollars(v)])) : obj;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mapTransactionInput(t) {
|
|
39
|
+
const out = {
|
|
40
|
+
account_id: t.accountId,
|
|
41
|
+
date: t.date,
|
|
42
|
+
amount: milliunits(t.amount),
|
|
43
|
+
payee_id: t.payeeId,
|
|
44
|
+
payee_name: t.payeeName,
|
|
45
|
+
category_id: t.categoryId,
|
|
46
|
+
memo: t.memo,
|
|
47
|
+
cleared: t.cleared,
|
|
48
|
+
approved: t.approved,
|
|
49
|
+
flag_color: t.flagColor,
|
|
50
|
+
import_id: t.importId,
|
|
51
|
+
};
|
|
52
|
+
if (t.subtransactions) {
|
|
53
|
+
out.subtransactions = t.subtransactions.map((s) => ({
|
|
54
|
+
amount: milliunits(s.amount),
|
|
55
|
+
category_id: s.categoryId,
|
|
56
|
+
payee_id: s.payeeId,
|
|
57
|
+
payee_name: s.payeeName,
|
|
58
|
+
memo: s.memo,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
34
64
|
function ok(data) {
|
|
35
65
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
36
66
|
}
|
|
@@ -70,7 +100,7 @@ async function ynabFetch(path, { method = "GET", body } = {}) {
|
|
|
70
100
|
|
|
71
101
|
const server = new McpServer({
|
|
72
102
|
name: "ynab-mcp-server",
|
|
73
|
-
version: "1.2.
|
|
103
|
+
version: "1.2.1",
|
|
74
104
|
});
|
|
75
105
|
|
|
76
106
|
// ==================== User & Budgets ====================
|
|
@@ -85,7 +115,13 @@ server.tool("get_user", "Get the authenticated user", {}, () =>
|
|
|
85
115
|
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
116
|
run(async () => {
|
|
87
117
|
const { data } = await api.budgets.getBudgets();
|
|
88
|
-
|
|
118
|
+
const result = {
|
|
119
|
+
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 })),
|
|
120
|
+
};
|
|
121
|
+
if (data.default_budget) {
|
|
122
|
+
result.default_budget = { id: data.default_budget.id, name: data.default_budget.name };
|
|
123
|
+
}
|
|
124
|
+
return ok(result);
|
|
89
125
|
})
|
|
90
126
|
);
|
|
91
127
|
|
|
@@ -139,6 +175,10 @@ function formatAccount(a) {
|
|
|
139
175
|
direct_import_linked: a.direct_import_linked,
|
|
140
176
|
direct_import_in_error: a.direct_import_in_error,
|
|
141
177
|
last_reconciled_at: a.last_reconciled_at,
|
|
178
|
+
debt_original_balance: dollars(a.debt_original_balance),
|
|
179
|
+
debt_interest_rates: a.debt_interest_rates,
|
|
180
|
+
debt_minimum_payments: dollarsMap(a.debt_minimum_payments),
|
|
181
|
+
debt_escrow_amounts: dollarsMap(a.debt_escrow_amounts),
|
|
142
182
|
deleted: a.deleted,
|
|
143
183
|
};
|
|
144
184
|
if ("note" in a) out.note = a.note;
|
|
@@ -195,6 +235,7 @@ function formatCategory(c) {
|
|
|
195
235
|
id: c.id,
|
|
196
236
|
category_group_id: c.category_group_id,
|
|
197
237
|
category_group_name: c.category_group_name,
|
|
238
|
+
original_category_group_id: c.original_category_group_id,
|
|
198
239
|
name: c.name,
|
|
199
240
|
hidden: c.hidden,
|
|
200
241
|
note: c.note,
|
|
@@ -214,6 +255,7 @@ function formatCategory(c) {
|
|
|
214
255
|
goal_overall_funded: dollars(c.goal_overall_funded),
|
|
215
256
|
goal_overall_left: dollars(c.goal_overall_left),
|
|
216
257
|
goal_needs_whole_amount: c.goal_needs_whole_amount,
|
|
258
|
+
deleted: c.deleted,
|
|
217
259
|
};
|
|
218
260
|
}
|
|
219
261
|
|
|
@@ -229,6 +271,7 @@ server.tool(
|
|
|
229
271
|
id: g.id,
|
|
230
272
|
name: g.name,
|
|
231
273
|
hidden: g.hidden,
|
|
274
|
+
deleted: g.deleted,
|
|
232
275
|
categories: g.categories.map((c) => ({
|
|
233
276
|
id: c.id,
|
|
234
277
|
name: c.name,
|
|
@@ -237,6 +280,7 @@ server.tool(
|
|
|
237
280
|
activity: dollars(c.activity),
|
|
238
281
|
balance: dollars(c.balance),
|
|
239
282
|
goal_type: c.goal_type,
|
|
283
|
+
deleted: c.deleted,
|
|
240
284
|
})),
|
|
241
285
|
}))
|
|
242
286
|
);
|
|
@@ -300,14 +344,16 @@ server.tool(
|
|
|
300
344
|
note: z.string().nullable().optional().describe("Category note (null to clear)"),
|
|
301
345
|
categoryGroupId: z.string().optional().describe("Move to a different category group"),
|
|
302
346
|
goalTarget: z.number().nullable().optional().describe("Goal target amount in dollars (only if category already has a goal)"),
|
|
347
|
+
goalTargetDate: z.string().nullable().optional().describe("Goal target date in ISO format (e.g. 2026-12-01, null to clear)"),
|
|
303
348
|
},
|
|
304
|
-
({ budgetId, categoryId, name, note, categoryGroupId, goalTarget }) =>
|
|
349
|
+
({ budgetId, categoryId, name, note, categoryGroupId, goalTarget, goalTargetDate }) =>
|
|
305
350
|
run(async () => {
|
|
306
351
|
const cat = {};
|
|
307
352
|
if (name !== undefined) cat.name = name;
|
|
308
353
|
if (note !== undefined) cat.note = note;
|
|
309
354
|
if (categoryGroupId !== undefined) cat.category_group_id = categoryGroupId;
|
|
310
355
|
if (goalTarget !== undefined) cat.goal_target = goalTarget != null ? milliunits(goalTarget) : null;
|
|
356
|
+
if (goalTargetDate !== undefined) cat.goal_target_date = goalTargetDate;
|
|
311
357
|
|
|
312
358
|
const { data } = await api.categories.updateCategory(resolveBudgetId(budgetId), categoryId, {
|
|
313
359
|
category: cat,
|
|
@@ -386,7 +432,7 @@ server.tool(
|
|
|
386
432
|
({ budgetId }) =>
|
|
387
433
|
run(async () => {
|
|
388
434
|
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 })));
|
|
435
|
+
return ok(data.payees.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id, deleted: p.deleted })));
|
|
390
436
|
})
|
|
391
437
|
);
|
|
392
438
|
|
|
@@ -474,11 +520,13 @@ server.tool(
|
|
|
474
520
|
return ok(
|
|
475
521
|
data.months.map((m) => ({
|
|
476
522
|
month: m.month,
|
|
523
|
+
note: m.note,
|
|
477
524
|
income: dollars(m.income),
|
|
478
525
|
budgeted: dollars(m.budgeted),
|
|
479
526
|
activity: dollars(m.activity),
|
|
480
527
|
to_be_budgeted: dollars(m.to_be_budgeted),
|
|
481
528
|
age_of_money: m.age_of_money,
|
|
529
|
+
deleted: m.deleted,
|
|
482
530
|
}))
|
|
483
531
|
);
|
|
484
532
|
})
|
|
@@ -497,14 +545,17 @@ server.tool(
|
|
|
497
545
|
const m = data.month;
|
|
498
546
|
return ok({
|
|
499
547
|
month: m.month,
|
|
548
|
+
note: m.note,
|
|
500
549
|
income: dollars(m.income),
|
|
501
550
|
budgeted: dollars(m.budgeted),
|
|
502
551
|
activity: dollars(m.activity),
|
|
503
552
|
to_be_budgeted: dollars(m.to_be_budgeted),
|
|
504
553
|
age_of_money: m.age_of_money,
|
|
554
|
+
deleted: m.deleted,
|
|
505
555
|
categories: m.categories?.map((c) => ({
|
|
506
556
|
id: c.id,
|
|
507
557
|
name: c.name,
|
|
558
|
+
hidden: c.hidden,
|
|
508
559
|
category_group_name: c.category_group_name,
|
|
509
560
|
budgeted: dollars(c.budgeted),
|
|
510
561
|
activity: dollars(c.activity),
|
|
@@ -512,6 +563,7 @@ server.tool(
|
|
|
512
563
|
goal_type: c.goal_type,
|
|
513
564
|
goal_target: dollars(c.goal_target),
|
|
514
565
|
goal_under_funded: dollars(c.goal_under_funded),
|
|
566
|
+
deleted: c.deleted,
|
|
515
567
|
})),
|
|
516
568
|
});
|
|
517
569
|
})
|
|
@@ -530,6 +582,7 @@ function formatMoneyMovement(m) {
|
|
|
530
582
|
from_category_id: m.from_category_id,
|
|
531
583
|
to_category_id: m.to_category_id,
|
|
532
584
|
amount: dollars(m.amount),
|
|
585
|
+
deleted: m.deleted,
|
|
533
586
|
};
|
|
534
587
|
}
|
|
535
588
|
|
|
@@ -602,17 +655,25 @@ function formatTransaction(t) {
|
|
|
602
655
|
category_id: t.category_id,
|
|
603
656
|
category_name: t.category_name,
|
|
604
657
|
transfer_account_id: t.transfer_account_id,
|
|
658
|
+
transfer_transaction_id: t.transfer_transaction_id,
|
|
659
|
+
matched_transaction_id: t.matched_transaction_id,
|
|
605
660
|
import_id: t.import_id,
|
|
606
661
|
import_payee_name: t.import_payee_name,
|
|
662
|
+
import_payee_name_original: t.import_payee_name_original,
|
|
607
663
|
debt_transaction_type: t.debt_transaction_type,
|
|
664
|
+
deleted: t.deleted,
|
|
608
665
|
subtransactions: t.subtransactions?.map((s) => ({
|
|
609
666
|
id: s.id,
|
|
667
|
+
transaction_id: s.transaction_id,
|
|
610
668
|
amount: dollars(s.amount),
|
|
611
669
|
memo: s.memo,
|
|
612
670
|
payee_id: s.payee_id,
|
|
613
671
|
payee_name: s.payee_name,
|
|
614
672
|
category_id: s.category_id,
|
|
615
673
|
category_name: s.category_name,
|
|
674
|
+
transfer_account_id: s.transfer_account_id,
|
|
675
|
+
transfer_transaction_id: s.transfer_transaction_id,
|
|
676
|
+
deleted: s.deleted,
|
|
616
677
|
})),
|
|
617
678
|
};
|
|
618
679
|
}
|
|
@@ -693,32 +754,10 @@ server.tool(
|
|
|
693
754
|
memo: z.string().optional().describe("Memo"),
|
|
694
755
|
})).optional().describe("Split transaction into subtransactions. The subtransaction amounts must sum to the total transaction amount."),
|
|
695
756
|
},
|
|
696
|
-
({ budgetId,
|
|
757
|
+
({ budgetId, ...txnFields }) =>
|
|
697
758
|
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
759
|
const { data } = await api.transactions.createTransaction(resolveBudgetId(budgetId), {
|
|
721
|
-
transaction:
|
|
760
|
+
transaction: mapTransactionInput(txnFields),
|
|
722
761
|
});
|
|
723
762
|
return ok(formatTransaction(data.transaction));
|
|
724
763
|
})
|
|
@@ -741,25 +780,19 @@ server.tool(
|
|
|
741
780
|
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
742
781
|
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color"),
|
|
743
782
|
importId: z.string().optional().describe("Unique import ID for deduplication (max 36 chars)"),
|
|
783
|
+
subtransactions: z.array(z.object({
|
|
784
|
+
amount: z.number().describe("Subtransaction amount in dollars"),
|
|
785
|
+
categoryId: z.string().optional().describe("Category ID"),
|
|
786
|
+
payeeId: z.string().optional().describe("Payee ID"),
|
|
787
|
+
payeeName: z.string().optional().describe("Payee name"),
|
|
788
|
+
memo: z.string().optional().describe("Memo"),
|
|
789
|
+
})).optional().describe("Split transaction into subtransactions"),
|
|
744
790
|
})).describe("Array of transactions to create"),
|
|
745
791
|
},
|
|
746
792
|
({ budgetId, transactions: txns }) =>
|
|
747
793
|
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
794
|
const { data } = await api.transactions.createTransactions(resolveBudgetId(budgetId), {
|
|
762
|
-
transactions:
|
|
795
|
+
transactions: txns.map(mapTransactionInput),
|
|
763
796
|
});
|
|
764
797
|
return ok({
|
|
765
798
|
created: data.transactions?.map(formatTransaction),
|
|
@@ -777,10 +810,10 @@ server.tool(
|
|
|
777
810
|
accountId: z.string().optional().describe("Account ID"),
|
|
778
811
|
date: z.string().optional().describe("Transaction date (YYYY-MM-DD)"),
|
|
779
812
|
amount: z.number().optional().describe("Amount in dollars"),
|
|
780
|
-
payeeId: z.string().optional().describe("Payee ID"),
|
|
781
|
-
payeeName: z.string().optional().describe("Payee name"),
|
|
813
|
+
payeeId: z.string().nullable().optional().describe("Payee ID (null to remove)"),
|
|
814
|
+
payeeName: z.string().nullable().optional().describe("Payee name (null to clear)"),
|
|
782
815
|
categoryId: z.string().nullable().optional().describe("Category ID (null to uncategorize)"),
|
|
783
|
-
memo: z.string().optional().describe("Transaction memo"),
|
|
816
|
+
memo: z.string().nullable().optional().describe("Transaction memo (null to clear)"),
|
|
784
817
|
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status"),
|
|
785
818
|
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
786
819
|
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).nullable().optional().describe("Flag color (null to remove)"),
|
|
@@ -829,16 +862,16 @@ server.tool(
|
|
|
829
862
|
.array(
|
|
830
863
|
z.object({
|
|
831
864
|
id: z.string().describe("Transaction ID"),
|
|
832
|
-
|
|
833
|
-
date: z.string().optional(),
|
|
865
|
+
accountId: z.string().optional().describe("Account ID"),
|
|
866
|
+
date: z.string().optional().describe("Transaction date (YYYY-MM-DD)"),
|
|
834
867
|
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
|
-
|
|
868
|
+
payeeId: z.string().nullable().optional().describe("Payee ID (null to remove)"),
|
|
869
|
+
payeeName: z.string().nullable().optional().describe("Payee name (null to clear)"),
|
|
870
|
+
categoryId: z.string().nullable().optional().describe("Category ID (null to uncategorize)"),
|
|
871
|
+
memo: z.string().nullable().optional().describe("Transaction memo (null to clear)"),
|
|
872
|
+
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status"),
|
|
873
|
+
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
874
|
+
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).nullable().optional().describe("Flag color (null to remove)"),
|
|
842
875
|
})
|
|
843
876
|
)
|
|
844
877
|
.describe("Array of transaction updates"),
|
|
@@ -846,8 +879,17 @@ server.tool(
|
|
|
846
879
|
({ budgetId, transactions: txns }) =>
|
|
847
880
|
run(async () => {
|
|
848
881
|
const mapped = txns.map((t) => {
|
|
849
|
-
const out = {
|
|
850
|
-
if (
|
|
882
|
+
const out = { id: t.id };
|
|
883
|
+
if (t.accountId !== undefined) out.account_id = t.accountId;
|
|
884
|
+
if (t.date !== undefined) out.date = t.date;
|
|
885
|
+
if (t.amount !== undefined) out.amount = milliunits(t.amount);
|
|
886
|
+
if (t.payeeId !== undefined) out.payee_id = t.payeeId;
|
|
887
|
+
if (t.payeeName !== undefined) out.payee_name = t.payeeName;
|
|
888
|
+
if (t.categoryId !== undefined) out.category_id = t.categoryId;
|
|
889
|
+
if (t.memo !== undefined) out.memo = t.memo;
|
|
890
|
+
if (t.cleared !== undefined) out.cleared = t.cleared;
|
|
891
|
+
if (t.approved !== undefined) out.approved = t.approved;
|
|
892
|
+
if (t.flagColor !== undefined) out.flag_color = t.flagColor;
|
|
851
893
|
return out;
|
|
852
894
|
});
|
|
853
895
|
const { data } = await api.transactions.updateTransactions(resolveBudgetId(budgetId), {
|
|
@@ -890,14 +932,18 @@ function formatScheduledTransaction(t) {
|
|
|
890
932
|
category_id: t.category_id,
|
|
891
933
|
category_name: t.category_name,
|
|
892
934
|
transfer_account_id: t.transfer_account_id,
|
|
935
|
+
deleted: t.deleted,
|
|
893
936
|
subtransactions: t.subtransactions?.map((s) => ({
|
|
894
937
|
id: s.id,
|
|
938
|
+
scheduled_transaction_id: s.scheduled_transaction_id,
|
|
895
939
|
amount: dollars(s.amount),
|
|
896
940
|
memo: s.memo,
|
|
897
941
|
payee_id: s.payee_id,
|
|
898
942
|
payee_name: s.payee_name,
|
|
899
943
|
category_id: s.category_id,
|
|
900
944
|
category_name: s.category_name,
|
|
945
|
+
transfer_account_id: s.transfer_account_id,
|
|
946
|
+
deleted: s.deleted,
|
|
901
947
|
})),
|
|
902
948
|
};
|
|
903
949
|
}
|
|
@@ -1067,7 +1113,7 @@ server.tool(
|
|
|
1067
1113
|
const q = query.toLowerCase();
|
|
1068
1114
|
const matches = data.payees
|
|
1069
1115
|
.filter((p) => p.name.toLowerCase().includes(q))
|
|
1070
|
-
.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id }));
|
|
1116
|
+
.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id, deleted: p.deleted }));
|
|
1071
1117
|
if (matches.length === 0) return ok({ message: `No payees matching "${query}"` });
|
|
1072
1118
|
return ok(matches);
|
|
1073
1119
|
})
|
|
@@ -1081,8 +1127,11 @@ server.tool(
|
|
|
1081
1127
|
run(async () => {
|
|
1082
1128
|
const { data } = await api.transactions.getTransactions(resolveBudgetId(budgetId), undefined, "unapproved");
|
|
1083
1129
|
const txns = data.transactions.map(formatTransaction);
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1130
|
+
const isCategorized = (t) => (t.category_id && t.category_name !== "Uncategorized")
|
|
1131
|
+
|| (t.subtransactions && t.subtransactions.length > 0) // split transactions are categorized via subtransactions
|
|
1132
|
+
|| t.transfer_account_id; // transfers don't need categories
|
|
1133
|
+
const categorized = txns.filter(isCategorized);
|
|
1134
|
+
const uncategorized = txns.filter((t) => !isCategorized(t));
|
|
1086
1135
|
return ok({
|
|
1087
1136
|
total: txns.length,
|
|
1088
1137
|
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.1",
|
|
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",
|