@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -16
  3. package/index.js +108 -59
  4. 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
- <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>
13
+ <code>43 tools</code> &bull;
14
+ <code>100% API coverage</code> &bull;
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> &bull;
26
+ <a href="#what-you-can-do">What You Can Do</a> &bull;
27
+ <a href="#tools-reference">All 43 Tools</a> &bull;
28
+ <a href="#environment-variables">Configuration</a>
17
29
  </p>
18
30
 
19
31
  ---
20
32
 
21
- **43 tools. 100% API coverage. Zero configuration.**
33
+ ## Why This Exists
22
34
 
23
- Connect any MCP-compatible AI assistant Claude, GPT, or your own agentsto 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.
35
+ YNAB's budgeting philosophy works best when you interact with your budget frequentlybut 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" (has category) and "needs attention" (uncategorized), with a built-in warning against approving uncategorized entries.
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, and date ranges |
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 in dollars |
155
- | `get_account` | Get details for a specific account |
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
+ &bull; <a href="https://github.com/oliverames">GitHub</a>
346
+ &bull; <a href="https://linkedin.com/in/oliverames">LinkedIn</a>
347
+ &bull; <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.0",
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
- 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 })));
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, accountId, date, amount, payeeId, payeeName, categoryId, memo, cleared, approved, flagColor, importId, subtransactions }) =>
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: txn,
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: mapped,
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
- account_id: z.string().optional(),
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
- payee_id: z.string().optional(),
836
- payee_name: z.string().optional(),
837
- category_id: z.string().optional(),
838
- memo: z.string().optional(),
839
- cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional(),
840
- approved: z.boolean().optional(),
841
- flag_color: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).nullable().optional(),
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 = { ...t };
850
- if (out.amount !== undefined) out.amount = milliunits(out.amount);
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 categorized = txns.filter((t) => t.category_id && t.category_name !== "Uncategorized");
1085
- const uncategorized = txns.filter((t) => !t.category_id || t.category_name === "Uncategorized");
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.0",
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",