@dizzlkheinz/ynab-mcpb 0.16.0 → 0.17.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.
Files changed (114) hide show
  1. package/.code/agents/0098661e-0fa3-4990-beb9-c0cbf3f123aa/status.txt +1 -0
  2. package/.code/agents/1324/exec-call_tIpx9uV1TpARbAMZonRQm8AO.txt +757 -0
  3. package/.code/agents/1572/exec-call_GjVFBFOWcY7lE0idc5nWlLNh.txt +781 -0
  4. package/.code/agents/1846/exec-call_1YNAVD18RjrMN7JnfkkQhUP3.txt +766 -0
  5. package/.code/agents/1846/exec-call_lh3lDzE4WJAh1lFiomiiZ73D.txt +766 -0
  6. package/.code/agents/2038/exec-call_DYwOukaYsL8VCONWmV2rUW5u.txt +766 -0
  7. package/.code/agents/2038/exec-call_c7fOQ7UrpVcTtvdfGBRM146V.txt +652 -0
  8. package/.code/agents/2038/exec-call_ySNyq9Mm55jWE480s54r5QcA.txt +766 -0
  9. package/.code/agents/2256/exec-call_AtPcRWPmFPMcmX6qOFm1fCEY.txt +766 -0
  10. package/.code/agents/2454/exec-call_aFJpupwjfZeOBm7ixI5Vc8z2.txt +766 -0
  11. package/.code/agents/2454/exec-call_wogZ4HfXTodTEXvdgXlVUBpv.txt +766 -0
  12. package/.code/agents/2e905864-aa07-4314-bcf9-c5b32277e4ac/result.txt +36 -0
  13. package/.code/agents/3073/exec-call_Peeagc9DxGYLgE6pNdMZhqIE.txt +766 -0
  14. package/.code/agents/3073/exec-call_d2YSE3hXF08KRSoUM3qd8Z3x.txt +766 -0
  15. package/.code/agents/335aa031-466d-4fb7-925f-3cd864e264d0/result.txt +191 -0
  16. package/.code/agents/3364/exec-call_NbhIrsM5HhyDZDmJZG5CuCYL.txt +766 -0
  17. package/.code/agents/3364/exec-call_cKtJg0NrXiwXEFwlsE3uPZRA.txt +766 -0
  18. package/.code/agents/36d98414-5cde-4d9d-9a67-a240a18c1f07/result.txt +189 -0
  19. package/.code/agents/4604e866-b7b8-44f5-992f-2f683b0a523b/status.txt +1 -0
  20. package/.code/agents/5f8dc01c-47b3-4163-b0b3-aa31be89fcdc/status.txt +1 -0
  21. package/.code/agents/7/exec-call_HltHpkDox0Zm1vGEjdksUgpE.txt +1120 -0
  22. package/.code/agents/7/exec-call_LCATrOPPAgbxW9Q1z0XaVi2E.txt +2646 -0
  23. package/.code/agents/7/exec-call_W8DeRfNG9hvbgVFvf0clBf6R.txt +2646 -0
  24. package/.code/agents/94a0ddf3-a304-4ec3-913e-3cceef509948/error.txt +1 -0
  25. package/.code/agents/e2c752b7-711d-423a-af57-f53c809deb84/result.txt +160 -0
  26. package/.code/agents/e6601719-c31f-4a0e-8c71-d70787d0ab71/status.txt +1 -0
  27. package/.code/agents/f250b7ed-5bd5-4036-aa8c-ce63caee7d61/result.txt +20 -0
  28. package/AGENTS.md +1 -36
  29. package/CLAUDE.md +131 -51
  30. package/NUL +0 -1
  31. package/README.md +27 -14
  32. package/dist/bundle/index.cjs +41 -41
  33. package/dist/server/YNABMCPServer.js +28 -381
  34. package/dist/server/config.d.ts +2 -0
  35. package/dist/server/config.js +1 -0
  36. package/dist/tools/accountTools.d.ts +2 -0
  37. package/dist/tools/accountTools.js +45 -0
  38. package/dist/tools/adapters.d.ts +12 -0
  39. package/dist/tools/adapters.js +25 -0
  40. package/dist/tools/budgetTools.d.ts +2 -0
  41. package/dist/tools/budgetTools.js +30 -0
  42. package/dist/tools/categoryTools.d.ts +2 -0
  43. package/dist/tools/categoryTools.js +45 -0
  44. package/dist/tools/monthTools.d.ts +2 -0
  45. package/dist/tools/monthTools.js +32 -0
  46. package/dist/tools/payeeTools.d.ts +2 -0
  47. package/dist/tools/payeeTools.js +32 -0
  48. package/dist/tools/reconciliation/index.d.ts +2 -0
  49. package/dist/tools/reconciliation/index.js +33 -0
  50. package/dist/tools/schemas/common.d.ts +3 -0
  51. package/dist/tools/schemas/common.js +3 -0
  52. package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
  53. package/dist/tools/transactionTools.d.ts +2 -0
  54. package/dist/tools/transactionTools.js +129 -0
  55. package/dist/tools/utilityTools.d.ts +3 -1
  56. package/dist/tools/utilityTools.js +32 -2
  57. package/dist/types/index.d.ts +1 -0
  58. package/dist/types/toolRegistration.d.ts +27 -0
  59. package/dist/types/toolRegistration.js +1 -0
  60. package/package.json +2 -2
  61. package/scripts/run-domain-integration-tests.js +4 -1
  62. package/src/__tests__/workflows.e2e.test.ts +1 -7
  63. package/src/server/YNABMCPServer.ts +33 -519
  64. package/src/server/__tests__/toolRegistration.test.ts +236 -0
  65. package/src/server/config.ts +1 -0
  66. package/src/tools/__tests__/adapters.test.ts +113 -0
  67. package/src/tools/__tests__/transactionTools.test.ts +90 -17
  68. package/src/tools/__tests__/utilityTools.test.ts +7 -7
  69. package/src/tools/accountTools.ts +53 -0
  70. package/src/tools/adapters.ts +74 -0
  71. package/src/tools/budgetTools.ts +37 -0
  72. package/src/tools/categoryTools.ts +53 -0
  73. package/src/tools/monthTools.ts +39 -0
  74. package/src/tools/payeeTools.ts +39 -0
  75. package/src/tools/reconciliation/index.ts +45 -0
  76. package/src/tools/schemas/common.ts +18 -0
  77. package/src/tools/transactionTools.ts +150 -0
  78. package/src/tools/utilityTools.ts +42 -2
  79. package/src/types/index.ts +3 -0
  80. package/src/types/toolRegistration.ts +88 -0
  81. package/.dxtignore +0 -57
  82. package/.github/workflows/pr-description-check.yml +0 -88
  83. package/CODEREVIEW_RESPONSE.md +0 -128
  84. package/SCHEMA_IMPROVEMENT_SUMMARY.md +0 -120
  85. package/TESTING_NOTES.md +0 -217
  86. package/accountactivity-merged.csv +0 -149
  87. package/bundle-analysis.html +0 -13110
  88. package/docs/README.md +0 -72
  89. package/docs/getting-started/CONFIGURATION.md +0 -175
  90. package/docs/getting-started/INSTALLATION.md +0 -333
  91. package/docs/getting-started/QUICKSTART.md +0 -282
  92. package/docs/guides/ARCHITECTURE.md +0 -533
  93. package/docs/guides/DEPLOYMENT.md +0 -189
  94. package/docs/guides/INTEGRATION_TESTING.md +0 -730
  95. package/docs/guides/TESTING.md +0 -591
  96. package/docs/plans/2025-11-20-reloadable-config-token-validation.md +0 -93
  97. package/docs/plans/2025-11-21-fix-transaction-cached-property.md +0 -362
  98. package/docs/plans/2025-11-21-reconciliation-error-handling.md +0 -90
  99. package/docs/plans/2025-11-21-v014-hardening.md +0 -153
  100. package/docs/plans/reconciliation-v2-redesign.md +0 -1571
  101. package/docs/reconciliation-flow.md +0 -83
  102. package/docs/reference/EXAMPLES.md +0 -946
  103. package/docs/reference/TOOLS.md +0 -348
  104. package/docs/reference/TROUBLESHOOTING.md +0 -481
  105. package/fix-types.sh +0 -17
  106. package/test-csv-sample.csv +0 -28
  107. package/test-exports/sample_bank_statement.csv +0 -7
  108. package/test-reconcile-autodetect.js +0 -40
  109. package/test-reconcile-tool.js +0 -152
  110. package/test-reconcile-with-csv.cjs +0 -89
  111. package/test-statement.csv +0 -8
  112. package/test_debug.js +0 -47
  113. package/test_mcp_tools.mjs +0 -75
  114. package/test_simple.mjs +0 -16
@@ -1,946 +0,0 @@
1
- # YNAB MCP Server Usage Examples
2
-
3
- This document provides practical examples of using the YNAB MCP Server tools in real-world scenarios. Each example includes complete code with error handling and best practices.
4
-
5
- ## Table of Contents
6
-
7
- - [Basic Examples](#basic-examples)
8
- - [Budget Management Examples](#budget-management-examples)
9
- - [Account Management Examples](#account-management-examples)
10
- - [Transaction Management Examples](#transaction-management-examples)
11
- - [Category Management Examples](#category-management-examples)
12
- - [Financial Analysis Examples](#financial-analysis-examples)
13
- - [Advanced Workflows](#advanced-workflows)
14
- - [Integration Examples](#integration-examples)
15
-
16
- ## Basic Examples
17
-
18
- ### Getting Started - User Information
19
-
20
- ```javascript
21
- // Get authenticated user information
22
- async function getUserInfo() {
23
- try {
24
- const result = await client.callTool('get_user', {});
25
- const data = JSON.parse(result.content[0].text);
26
-
27
- console.log('User Information:');
28
- console.log(`Email: ${data.user.email}`);
29
- console.log(`Subscription: ${data.user.subscription.frequency}`);
30
-
31
- return data.user;
32
- } catch (error) {
33
- console.error('Failed to get user info:', error.message);
34
- throw error;
35
- }
36
- }
37
- ```
38
-
39
- ### Amount Conversion Examples
40
-
41
- ```javascript
42
- // Convert dollars to milliunits for API calls
43
- async function convertToMilliunits(dollarAmount) {
44
- const result = await client.callTool('convert_amount', {
45
- amount: dollarAmount,
46
- to_milliunits: true
47
- });
48
- const data = JSON.parse(result.content[0].text);
49
- return data.converted_amount;
50
- }
51
-
52
- // Convert milliunits to dollars for display
53
- async function convertToDollars(milliunits) {
54
- const result = await client.callTool('convert_amount', {
55
- amount: milliunits,
56
- to_milliunits: false
57
- });
58
- const data = JSON.parse(result.content[0].text);
59
- return data.converted_amount;
60
- }
61
-
62
- // Example usage
63
- const userInput = 25.50; // User enters $25.50
64
- const milliunits = await convertToMilliunits(userInput);
65
- console.log(`$${userInput} = ${milliunits} milliunits`);
66
-
67
- const displayAmount = await convertToDollars(milliunits);
68
- console.log(`${milliunits} milliunits = $${displayAmount}`);
69
- ```
70
-
71
- ## Budget Management Examples
72
-
73
- ### List All Budgets
74
-
75
- ```javascript
76
- async function listAllBudgets() {
77
- try {
78
- const result = await client.callTool('list_budgets', {});
79
- const data = JSON.parse(result.content[0].text);
80
-
81
- console.log('Available Budgets:');
82
- data.budgets.forEach((budget, index) => {
83
- console.log(`${index + 1}. ${budget.name} (${budget.id})`);
84
- console.log(` Last modified: ${new Date(budget.last_modified_on).toLocaleDateString()}`);
85
- console.log(` Currency: ${budget.currency_format.currency_symbol}`);
86
- });
87
-
88
- return data.budgets;
89
- } catch (error) {
90
- console.error('Failed to list budgets:', error.message);
91
- throw error;
92
- }
93
- }
94
- ```
95
-
96
- ### Get Detailed Budget Information
97
-
98
- ```javascript
99
- async function getBudgetDetails(budgetId) {
100
- try {
101
- const result = await client.callTool('get_budget', {
102
- budget_id: budgetId
103
- });
104
- const data = JSON.parse(result.content[0].text);
105
-
106
- const budget = data.budget;
107
- console.log(`Budget: ${budget.name}`);
108
- console.log(`Accounts: ${budget.accounts.length}`);
109
- console.log(`Categories: ${budget.categories.length}`);
110
- console.log(`Transactions: ${budget.transactions.length}`);
111
-
112
- return budget;
113
- } catch (error) {
114
- console.error('Failed to get budget details:', error.message);
115
- throw error;
116
- }
117
- }
118
- ```
119
-
120
- ## Account Management Examples
121
-
122
- ### List Accounts by Type
123
-
124
- ```javascript
125
- async function listAccountsByType(budgetId, accountType = null) {
126
- try {
127
- const result = await client.callTool('list_accounts', {
128
- budget_id: budgetId
129
- });
130
- const data = JSON.parse(result.content[0].text);
131
-
132
- let accounts = data.accounts.filter(account => !account.closed);
133
-
134
- if (accountType) {
135
- accounts = accounts.filter(account => account.type === accountType);
136
- }
137
-
138
- console.log(`${accountType || 'All'} Accounts:`);
139
- accounts.forEach(account => {
140
- const balance = (account.balance / 1000).toFixed(2);
141
- console.log(`- ${account.name}: $${balance} (${account.type})`);
142
- });
143
-
144
- return accounts;
145
- } catch (error) {
146
- console.error('Failed to list accounts:', error.message);
147
- throw error;
148
- }
149
- }
150
-
151
- // Usage examples
152
- await listAccountsByType(budgetId); // All accounts
153
- await listAccountsByType(budgetId, 'checking'); // Only checking accounts
154
- await listAccountsByType(budgetId, 'creditCard'); // Only credit cards
155
- ```
156
-
157
- ### Create New Account
158
-
159
- ```javascript
160
- async function createNewAccount(budgetId, accountName, accountType, initialBalance = 0) {
161
- try {
162
- // Convert initial balance to milliunits
163
- const balanceMilliunits = await convertToMilliunits(initialBalance);
164
-
165
- const result = await client.callTool('create_account', {
166
- budget_id: budgetId,
167
- name: accountName,
168
- type: accountType,
169
- balance: balanceMilliunits
170
- });
171
- const data = JSON.parse(result.content[0].text);
172
-
173
- const account = data.account;
174
- console.log(`Created account: ${account.name}`);
175
- console.log(`Type: ${account.type}`);
176
- console.log(`Initial balance: $${(account.balance / 1000).toFixed(2)}`);
177
-
178
- return account;
179
- } catch (error) {
180
- console.error('Failed to create account:', error.message);
181
- throw error;
182
- }
183
- }
184
-
185
- // Example: Create a new savings account with $1000 initial balance
186
- const newAccount = await createNewAccount(
187
- budgetId,
188
- 'Emergency Fund',
189
- 'savings',
190
- 1000.00
191
- );
192
- ```
193
-
194
- ### Get Account Balance Summary
195
-
196
- ```javascript
197
- async function getAccountBalanceSummary(budgetId) {
198
- try {
199
- const result = await client.callTool('list_accounts', {
200
- budget_id: budgetId
201
- });
202
- const data = JSON.parse(result.content[0].text);
203
-
204
- const summary = {
205
- totalAssets: 0,
206
- totalLiabilities: 0,
207
- netWorth: 0,
208
- accountsByType: {}
209
- };
210
-
211
- data.accounts.forEach(account => {
212
- if (account.closed) return;
213
-
214
- const balance = account.balance / 1000;
215
-
216
- // Group by account type
217
- if (!summary.accountsByType[account.type]) {
218
- summary.accountsByType[account.type] = {
219
- count: 0,
220
- totalBalance: 0,
221
- accounts: []
222
- };
223
- }
224
-
225
- summary.accountsByType[account.type].count++;
226
- summary.accountsByType[account.type].totalBalance += balance;
227
- summary.accountsByType[account.type].accounts.push({
228
- name: account.name,
229
- balance: balance
230
- });
231
-
232
- // Calculate assets vs liabilities
233
- if (['checking', 'savings', 'cash', 'otherAsset'].includes(account.type)) {
234
- summary.totalAssets += balance;
235
- } else if (['creditCard', 'lineOfCredit', 'otherLiability'].includes(account.type)) {
236
- summary.totalLiabilities += Math.abs(balance);
237
- }
238
- });
239
-
240
- summary.netWorth = summary.totalAssets - summary.totalLiabilities;
241
-
242
- console.log('Account Balance Summary:');
243
- console.log(`Total Assets: $${summary.totalAssets.toFixed(2)}`);
244
- console.log(`Total Liabilities: $${summary.totalLiabilities.toFixed(2)}`);
245
- console.log(`Net Worth: $${summary.netWorth.toFixed(2)}`);
246
-
247
- return summary;
248
- } catch (error) {
249
- console.error('Failed to get balance summary:', error.message);
250
- throw error;
251
- }
252
- }
253
- ```
254
-
255
- ## Transaction Management Examples
256
-
257
- ### Create Simple Transaction
258
-
259
- ```javascript
260
- async function createSimpleTransaction(budgetId, accountId, amount, payeeName, memo = '') {
261
- try {
262
- // Convert amount to milliunits
263
- const milliunits = await convertToMilliunits(Math.abs(amount));
264
- const transactionAmount = amount < 0 ? -milliunits : milliunits;
265
-
266
- // Use today's date
267
- const today = new Date().toISOString().split('T')[0];
268
-
269
- const result = await client.callTool('create_transaction', {
270
- budget_id: budgetId,
271
- account_id: accountId,
272
- amount: transactionAmount,
273
- date: today,
274
- payee_name: payeeName,
275
- memo: memo,
276
- cleared: 'uncleared',
277
- approved: true
278
- });
279
- const data = JSON.parse(result.content[0].text);
280
-
281
- const transaction = data.transaction;
282
- console.log(`Created transaction: ${payeeName} for $${Math.abs(amount)}`);
283
-
284
- return transaction;
285
- } catch (error) {
286
- console.error('Failed to create transaction:', error.message);
287
- throw error;
288
- }
289
- }
290
-
291
- // Example: Record a $25.50 coffee purchase
292
- const transaction = await createSimpleTransaction(
293
- budgetId,
294
- checkingAccountId,
295
- -25.50,
296
- 'Coffee Shop',
297
- 'Morning coffee'
298
- );
299
- ```
300
-
301
- ### Get Recent Transactions
302
-
303
- ```javascript
304
- async function getRecentTransactions(budgetId, days = 30, accountId = null) {
305
- try {
306
- // Calculate date filter
307
- const sinceDate = new Date();
308
- sinceDate.setDate(sinceDate.getDate() - days);
309
- const sinceDateStr = sinceDate.toISOString().split('T')[0];
310
-
311
- const params = {
312
- budget_id: budgetId,
313
- since_date: sinceDateStr
314
- };
315
-
316
- if (accountId) {
317
- params.account_id = accountId;
318
- }
319
-
320
- const result = await client.callTool('list_transactions', params);
321
- const data = JSON.parse(result.content[0].text);
322
-
323
- console.log(`Recent Transactions (last ${days} days):`);
324
- data.transactions.forEach(transaction => {
325
- const amount = (transaction.amount / 1000).toFixed(2);
326
- const sign = transaction.amount < 0 ? '-' : '+';
327
- console.log(`${transaction.date}: ${transaction.payee_name} ${sign}$${Math.abs(amount)}`);
328
- if (transaction.memo) {
329
- console.log(` Memo: ${transaction.memo}`);
330
- }
331
- });
332
-
333
- return data.transactions;
334
- } catch (error) {
335
- console.error('Failed to get recent transactions:', error.message);
336
- throw error;
337
- }
338
- }
339
- ```
340
-
341
- ### Update Transaction
342
-
343
- ```javascript
344
- async function updateTransaction(budgetId, transactionId, updates) {
345
- try {
346
- const params = {
347
- budget_id: budgetId,
348
- transaction_id: transactionId
349
- };
350
-
351
- // Convert amount if provided
352
- if (updates.amount !== undefined) {
353
- const milliunits = await convertToMilliunits(Math.abs(updates.amount));
354
- params.amount = updates.amount < 0 ? -milliunits : milliunits;
355
- }
356
-
357
- // Add other updates
358
- Object.keys(updates).forEach(key => {
359
- if (key !== 'amount') {
360
- params[key] = updates[key];
361
- }
362
- });
363
-
364
- const result = await client.callTool('update_transaction', params);
365
- const data = JSON.parse(result.content[0].text);
366
-
367
- console.log('Transaction updated successfully');
368
- return data.transaction;
369
- } catch (error) {
370
- console.error('Failed to update transaction:', error.message);
371
- throw error;
372
- }
373
- }
374
-
375
- // Example: Update transaction amount and add flag
376
- const updatedTransaction = await updateTransaction(budgetId, transactionId, {
377
- amount: -30.00,
378
- flag_color: 'red',
379
- memo: 'Updated amount'
380
- });
381
- ```
382
-
383
- ### Bulk Transaction Import
384
-
385
- ```javascript
386
- async function importTransactions(budgetId, transactions) {
387
- const results = [];
388
- const errors = [];
389
-
390
- console.log(`Importing ${transactions.length} transactions...`);
391
-
392
- for (let i = 0; i < transactions.length; i++) {
393
- const transaction = transactions[i];
394
-
395
- try {
396
- // Validate transaction data
397
- if (!transaction.accountId || !transaction.amount || !transaction.date) {
398
- throw new Error('Missing required fields: accountId, amount, date');
399
- }
400
-
401
- // Convert amount
402
- const milliunits = await convertToMilliunits(Math.abs(transaction.amount));
403
- const amount = transaction.amount < 0 ? -milliunits : milliunits;
404
-
405
- const result = await client.callTool('create_transaction', {
406
- budget_id: budgetId,
407
- account_id: transaction.accountId,
408
- amount: amount,
409
- date: transaction.date,
410
- payee_name: transaction.payeeName || 'Unknown',
411
- category_id: transaction.categoryId,
412
- memo: transaction.memo || '',
413
- cleared: transaction.cleared || 'uncleared',
414
- approved: transaction.approved !== false
415
- });
416
-
417
- const data = JSON.parse(result.content[0].text);
418
- results.push({
419
- index: i,
420
- success: true,
421
- transaction: data.transaction
422
- });
423
-
424
- console.log(`✓ Imported: ${transaction.payeeName} ($${Math.abs(transaction.amount)})`);
425
-
426
- } catch (error) {
427
- errors.push({
428
- index: i,
429
- transaction: transaction,
430
- error: error.message
431
- });
432
-
433
- console.log(`✗ Failed: ${transaction.payeeName} - ${error.message}`);
434
- }
435
-
436
- // Add delay to respect rate limits
437
- if (i < transactions.length - 1) {
438
- await new Promise(resolve => setTimeout(resolve, 200));
439
- }
440
- }
441
-
442
- console.log(`Import complete: ${results.length} successful, ${errors.length} failed`);
443
-
444
- return { results, errors };
445
- }
446
-
447
- // Example usage
448
- const transactionsToImport = [
449
- {
450
- accountId: 'checking-account-id',
451
- amount: -25.50,
452
- date: '2024-01-15',
453
- payeeName: 'Coffee Shop',
454
- memo: 'Morning coffee'
455
- },
456
- {
457
- accountId: 'checking-account-id',
458
- amount: -45.00,
459
- date: '2024-01-15',
460
- payeeName: 'Gas Station',
461
- memo: 'Fill up'
462
- }
463
- ];
464
-
465
- const importResults = await importTransactions(budgetId, transactionsToImport);
466
- ```
467
-
468
- ## Category Management Examples
469
-
470
- ### List Categories with Budget Analysis
471
-
472
- ```javascript
473
- async function analyzeCategoryBudgets(budgetId) {
474
- try {
475
- const result = await client.callTool('list_categories', {
476
- budget_id: budgetId
477
- });
478
- const data = JSON.parse(result.content[0].text);
479
-
480
- const analysis = {
481
- totalBudgeted: 0,
482
- totalActivity: 0,
483
- overspentCategories: [],
484
- underspentCategories: []
485
- };
486
-
487
- console.log('Category Budget Analysis:');
488
- console.log('========================');
489
-
490
- data.category_groups.forEach(group => {
491
- if (group.hidden || group.deleted) return;
492
-
493
- console.log(`\n${group.name}:`);
494
-
495
- group.categories.forEach(category => {
496
- if (category.hidden || category.deleted) return;
497
-
498
- const budgeted = category.budgeted / 1000;
499
- const activity = category.activity / 1000;
500
- const balance = category.balance / 1000;
501
-
502
- analysis.totalBudgeted += budgeted;
503
- analysis.totalActivity += Math.abs(activity);
504
-
505
- const percentUsed = budgeted !== 0 ? (Math.abs(activity) / budgeted) * 100 : 0;
506
-
507
- console.log(` ${category.name}:`);
508
- console.log(` Budgeted: $${budgeted.toFixed(2)}`);
509
- console.log(` Activity: $${activity.toFixed(2)}`);
510
- console.log(` Balance: $${balance.toFixed(2)}`);
511
- console.log(` Used: ${percentUsed.toFixed(1)}%`);
512
-
513
- // Track overspent categories
514
- if (balance < 0) {
515
- analysis.overspentCategories.push({
516
- name: category.name,
517
- overspent: Math.abs(balance)
518
- });
519
- }
520
-
521
- // Track significantly underspent categories
522
- if (budgeted > 0 && percentUsed < 50) {
523
- analysis.underspentCategories.push({
524
- name: category.name,
525
- budgeted: budgeted,
526
- used: percentUsed
527
- });
528
- }
529
- });
530
- });
531
-
532
- console.log('\n=== SUMMARY ===');
533
- console.log(`Total Budgeted: $${analysis.totalBudgeted.toFixed(2)}`);
534
- console.log(`Total Activity: $${analysis.totalActivity.toFixed(2)}`);
535
-
536
- if (analysis.overspentCategories.length > 0) {
537
- console.log('\nOverspent Categories:');
538
- analysis.overspentCategories.forEach(cat => {
539
- console.log(` ${cat.name}: -$${cat.overspent.toFixed(2)}`);
540
- });
541
- }
542
-
543
- if (analysis.underspentCategories.length > 0) {
544
- console.log('\nUnderspent Categories (< 50% used):');
545
- analysis.underspentCategories.forEach(cat => {
546
- console.log(` ${cat.name}: ${cat.used.toFixed(1)}% of $${cat.budgeted.toFixed(2)}`);
547
- });
548
- }
549
-
550
- return analysis;
551
- } catch (error) {
552
- console.error('Failed to analyze categories:', error.message);
553
- throw error;
554
- }
555
- }
556
- ```
557
-
558
- ### Update Category Budget
559
-
560
- ```javascript
561
- async function updateCategoryBudget(budgetId, categoryId, newBudgetAmount) {
562
- try {
563
- // Convert to milliunits
564
- const milliunits = await convertToMilliunits(newBudgetAmount);
565
-
566
- const result = await client.callTool('update_category', {
567
- budget_id: budgetId,
568
- category_id: categoryId,
569
- budgeted: milliunits
570
- });
571
- const data = JSON.parse(result.content[0].text);
572
-
573
- console.log(`Updated category budget to $${newBudgetAmount}`);
574
- return data.category;
575
- } catch (error) {
576
- console.error('Failed to update category budget:', error.message);
577
- throw error;
578
- }
579
- }
580
- ```
581
-
582
- ## Financial Analysis Examples
583
-
584
- ### Comprehensive Financial Overview
585
-
586
- ### Advanced Workflows
587
-
588
-
589
- ### Monthly Budget Review
590
-
591
- ```javascript
592
- async function monthlyBudgetReview(budgetId, month = null) {
593
- try {
594
- // Use current month if not specified
595
- if (!month) {
596
- const now = new Date();
597
- month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
598
- }
599
-
600
- console.log(`Monthly Budget Review for ${month}`);
601
- console.log('=====================================');
602
-
603
- // Get monthly data
604
- const monthResult = await client.callTool('get_month', {
605
- budget_id: budgetId,
606
- month: month
607
- });
608
- const monthData = JSON.parse(monthResult.content[0].text);
609
-
610
- // Get category structure for names
611
- const categoriesResult = await client.callTool('list_categories', {
612
- budget_id: budgetId
613
- });
614
- const categoriesData = JSON.parse(categoriesResult.content[0].text);
615
-
616
- // Create category lookup
617
- const categoryLookup = {};
618
- categoriesData.category_groups.forEach(group => {
619
- group.categories.forEach(category => {
620
- categoryLookup[category.id] = {
621
- name: category.name,
622
- groupName: group.name
623
- };
624
- });
625
- });
626
-
627
- const review = {
628
- month: month,
629
- income: monthData.month.income / 1000,
630
- budgeted: monthData.month.budgeted / 1000,
631
- activity: monthData.month.activity / 1000,
632
- toBeBudgeted: monthData.month.to_be_budgeted / 1000,
633
- categoryPerformance: []
634
- };
635
-
636
- console.log(`Income: $${review.income.toFixed(2)}`);
637
- console.log(`Budgeted: $${review.budgeted.toFixed(2)}`);
638
- console.log(`Activity: $${review.activity.toFixed(2)}`);
639
- console.log(`To Be Budgeted: $${review.toBeBudgeted.toFixed(2)}`);
640
-
641
- console.log('\nCategory Performance:');
642
- monthData.month.categories.forEach(category => {
643
- const categoryInfo = categoryLookup[category.id];
644
- if (!categoryInfo) return;
645
-
646
- const budgeted = category.budgeted / 1000;
647
- const activity = category.activity / 1000;
648
- const balance = category.balance / 1000;
649
-
650
- if (budgeted === 0 && activity === 0) return; // Skip inactive categories
651
-
652
- const performance = {
653
- name: categoryInfo.name,
654
- group: categoryInfo.groupName,
655
- budgeted: budgeted,
656
- activity: activity,
657
- balance: balance,
658
- percentUsed: budgeted !== 0 ? (Math.abs(activity) / budgeted) * 100 : 0,
659
- status: balance < 0 ? 'OVERSPENT' : balance > budgeted * 0.8 ? 'UNDERUSED' : 'GOOD'
660
- };
661
-
662
- review.categoryPerformance.push(performance);
663
-
664
- const status = performance.status === 'OVERSPENT' ? '🔴' :
665
- performance.status === 'UNDERUSED' ? '🟡' : '🟢';
666
-
667
- console.log(`${status} ${categoryInfo.name}: $${activity.toFixed(2)} of $${budgeted.toFixed(2)} (${performance.percentUsed.toFixed(1)}%)`);
668
- });
669
-
670
- return review;
671
- } catch (error) {
672
- console.error('Failed to generate monthly review:', error.message);
673
- throw error;
674
- }
675
- }
676
- ```
677
-
678
- ### Account Reconciliation Helper
679
-
680
- ```javascript
681
- async function reconcileAccount(budgetId, accountId, statementBalance, statementDate) {
682
- try {
683
- console.log('Account Reconciliation Helper');
684
- console.log('============================');
685
-
686
- // Get account details
687
- const accountResult = await client.callTool('get_account', {
688
- budget_id: budgetId,
689
- account_id: accountId
690
- });
691
- const accountData = JSON.parse(accountResult.content[0].text);
692
- const account = accountData.account;
693
-
694
- console.log(`Account: ${account.name}`);
695
- console.log(`YNAB Balance: $${(account.balance / 1000).toFixed(2)}`);
696
- console.log(`Statement Balance: $${statementBalance.toFixed(2)}`);
697
- console.log(`Statement Date: ${statementDate}`);
698
-
699
- // Get uncleared transactions
700
- const transactionsResult = await client.callTool('list_transactions', {
701
- budget_id: budgetId,
702
- account_id: accountId,
703
- since_date: statementDate
704
- });
705
- const transactionsData = JSON.parse(transactionsResult.content[0].text);
706
-
707
- const unclearedTransactions = transactionsData.transactions.filter(
708
- transaction => transaction.cleared === 'uncleared'
709
- );
710
-
711
- const clearedBalance = account.cleared_balance / 1000;
712
- const unclearedAmount = unclearedTransactions.reduce(
713
- (sum, transaction) => sum + (transaction.amount / 1000), 0
714
- );
715
-
716
- console.log(`\nCleared Balance: $${clearedBalance.toFixed(2)}`);
717
- console.log(`Uncleared Amount: $${unclearedAmount.toFixed(2)}`);
718
- console.log(`Difference from Statement: $${(clearedBalance - statementBalance).toFixed(2)}`);
719
-
720
- if (Math.abs(clearedBalance - statementBalance) > 0.01) {
721
- console.log('\n⚠️ Reconciliation needed!');
722
- console.log('Uncleared transactions:');
723
- unclearedTransactions.forEach(transaction => {
724
- const amount = (transaction.amount / 1000).toFixed(2);
725
- const sign = transaction.amount < 0 ? '-' : '+';
726
- console.log(` ${transaction.date}: ${transaction.payee_name} ${sign}$${Math.abs(amount)}`);
727
- });
728
- } else {
729
- console.log('\n✅ Account is reconciled!');
730
- }
731
-
732
- return {
733
- account: account,
734
- clearedBalance: clearedBalance,
735
- statementBalance: statementBalance,
736
- difference: clearedBalance - statementBalance,
737
- unclearedTransactions: unclearedTransactions,
738
- isReconciled: Math.abs(clearedBalance - statementBalance) <= 0.01
739
- };
740
- } catch (error) {
741
- console.error('Failed to reconcile account:', error.message);
742
- throw error;
743
- }
744
- }
745
- ```
746
-
747
- ## Integration Examples
748
-
749
- ### Express.js API Integration
750
-
751
- ```javascript
752
- const express = require('express');
753
- const app = express();
754
-
755
- app.use(express.json());
756
-
757
- // Middleware to validate YNAB connection
758
- async function validateYNABConnection(req, res, next) {
759
- try {
760
- await client.callTool('get_user', {});
761
- next();
762
- } catch (error) {
763
- res.status(401).json({ error: 'YNAB connection failed' });
764
- }
765
- }
766
-
767
- // Get budgets endpoint
768
- app.get('/api/budgets', validateYNABConnection, async (req, res) => {
769
- try {
770
- const result = await client.callTool('list_budgets', {});
771
- const data = JSON.parse(result.content[0].text);
772
- res.json(data.budgets);
773
- } catch (error) {
774
- res.status(500).json({ error: error.message });
775
- }
776
- });
777
-
778
- // Create transaction endpoint
779
- app.post('/api/budgets/:budgetId/transactions', validateYNABConnection, async (req, res) => {
780
- try {
781
- const { budgetId } = req.params;
782
- const { accountId, amount, payeeName, memo, date } = req.body;
783
-
784
- // Validate required fields
785
- if (!accountId || !amount || !payeeName || !date) {
786
- return res.status(400).json({
787
- error: 'Missing required fields: accountId, amount, payeeName, date'
788
- });
789
- }
790
-
791
- // Convert amount to milliunits
792
- const milliunits = await convertToMilliunits(Math.abs(amount));
793
- const transactionAmount = amount < 0 ? -milliunits : milliunits;
794
-
795
- const result = await client.callTool('create_transaction', {
796
- budget_id: budgetId,
797
- account_id: accountId,
798
- amount: transactionAmount,
799
- date: date,
800
- payee_name: payeeName,
801
- memo: memo || '',
802
- cleared: 'uncleared',
803
- approved: true
804
- });
805
-
806
- const data = JSON.parse(result.content[0].text);
807
- res.status(201).json(data.transaction);
808
- } catch (error) {
809
- res.status(500).json({ error: error.message });
810
- }
811
- });
812
-
813
- app.listen(3000, () => {
814
- console.log('YNAB API server running on port 3000');
815
- });
816
- ```
817
-
818
- ### Slack Bot Integration
819
-
820
- ```javascript
821
- const { App } = require('@slack/bolt');
822
-
823
- const app = new App({
824
- token: process.env.SLACK_BOT_TOKEN,
825
- signingSecret: process.env.SLACK_SIGNING_SECRET
826
- });
827
-
828
- // Command to add expense
829
- app.command('/expense', async ({ command, ack, respond }) => {
830
- await ack();
831
-
832
- try {
833
- // Parse command text: "/expense 25.50 Coffee Shop Starbucks"
834
- const parts = command.text.split(' ');
835
- const amount = parseFloat(parts[0]);
836
- const payeeName = parts.slice(1).join(' ');
837
-
838
- if (isNaN(amount) || !payeeName) {
839
- await respond('Usage: /expense <amount> <payee name>');
840
- return;
841
- }
842
-
843
- // Get user's default budget and account (you'd store this per user)
844
- const budgetId = 'user-default-budget-id';
845
- const accountId = 'user-default-account-id';
846
-
847
- // Create transaction
848
- const milliunits = await convertToMilliunits(amount);
849
- const result = await client.callTool('create_transaction', {
850
- budget_id: budgetId,
851
- account_id: accountId,
852
- amount: -milliunits, // Negative for expense
853
- date: new Date().toISOString().split('T')[0],
854
- payee_name: payeeName,
855
- cleared: 'uncleared',
856
- approved: true
857
- });
858
-
859
- await respond(`✅ Added expense: ${payeeName} for $${amount.toFixed(2)}`);
860
- } catch (error) {
861
- await respond(`❌ Failed to add expense: ${error.message}`);
862
- }
863
- });
864
-
865
- // Command to check balance
866
- app.command('/balance', async ({ command, ack, respond }) => {
867
- await ack();
868
-
869
- try {
870
- const budgetId = 'user-default-budget-id';
871
-
872
- const result = await client.callTool('list_accounts', {
873
- budget_id: budgetId
874
- });
875
- const data = JSON.parse(result.content[0].text);
876
-
877
- const balanceText = data.accounts
878
- .filter(account => !account.closed && account.on_budget)
879
- .map(account => {
880
- const balance = (account.balance / 1000).toFixed(2);
881
- return `${account.name}: $${balance}`;
882
- })
883
- .join('\n');
884
-
885
- await respond(`💰 Account Balances:\n${balanceText}`);
886
- } catch (error) {
887
- await respond(`❌ Failed to get balances: ${error.message}`);
888
- }
889
- });
890
-
891
- (async () => {
892
- await app.start(process.env.PORT || 3000);
893
- console.log('⚡️ Slack bot is running!');
894
- })();
895
- ```
896
-
897
- ### CSV Import Script
898
-
899
- ```javascript
900
- const fs = require('fs');
901
- const csv = require('csv-parser');
902
-
903
- async function importCSV(budgetId, accountId, csvFilePath) {
904
- const transactions = [];
905
-
906
- return new Promise((resolve, reject) => {
907
- fs.createReadStream(csvFilePath)
908
- .pipe(csv())
909
- .on('data', (row) => {
910
- // Assuming CSV format: Date,Payee,Amount,Memo
911
- transactions.push({
912
- date: row.Date,
913
- payeeName: row.Payee,
914
- amount: parseFloat(row.Amount),
915
- memo: row.Memo || ''
916
- });
917
- })
918
- .on('end', async () => {
919
- try {
920
- console.log(`Importing ${transactions.length} transactions from CSV...`);
921
-
922
- const results = await importTransactions(budgetId, transactions.map(t => ({
923
- ...t,
924
- accountId: accountId
925
- })));
926
-
927
- resolve(results);
928
- } catch (error) {
929
- reject(error);
930
- }
931
- })
932
- .on('error', reject);
933
- });
934
- }
935
-
936
- // Usage
937
- const importResults = await importCSV(
938
- 'budget-id',
939
- 'account-id',
940
- './transactions.csv'
941
- );
942
-
943
- console.log(`Import completed: ${importResults.results.length} successful, ${importResults.errors.length} failed`);
944
- ```
945
-
946
- These examples demonstrate practical usage patterns for the YNAB MCP Server. Each example includes proper error handling, data validation, and follows best practices for working with the YNAB API through the MCP server.