@dizzlkheinz/ynab-mcpb 0.17.1 → 0.18.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 (56) hide show
  1. package/.github/workflows/ci-tests.yml +4 -4
  2. package/.github/workflows/full-integration.yml +2 -2
  3. package/.github/workflows/publish.yml +1 -1
  4. package/.github/workflows/release.yml +2 -2
  5. package/CHANGELOG.md +12 -1
  6. package/CLAUDE.md +10 -7
  7. package/README.md +6 -1
  8. package/dist/bundle/index.cjs +52 -52
  9. package/dist/server/YNABMCPServer.d.ts +7 -2
  10. package/dist/server/YNABMCPServer.js +42 -11
  11. package/dist/server/cacheManager.js +6 -5
  12. package/dist/server/completions.d.ts +25 -0
  13. package/dist/server/completions.js +160 -0
  14. package/dist/server/config.d.ts +2 -2
  15. package/dist/server/errorHandler.js +1 -0
  16. package/dist/server/rateLimiter.js +3 -1
  17. package/dist/server/resources.d.ts +1 -0
  18. package/dist/server/resources.js +33 -16
  19. package/dist/server/securityMiddleware.d.ts +2 -1
  20. package/dist/server/securityMiddleware.js +1 -0
  21. package/dist/server/toolRegistry.d.ts +9 -0
  22. package/dist/server/toolRegistry.js +11 -0
  23. package/dist/tools/adapters.d.ts +3 -1
  24. package/dist/tools/adapters.js +1 -0
  25. package/dist/tools/reconciliation/executor.d.ts +2 -0
  26. package/dist/tools/reconciliation/executor.js +26 -9
  27. package/dist/tools/reconciliation/index.d.ts +3 -2
  28. package/dist/tools/reconciliation/index.js +4 -3
  29. package/docs/reference/API.md +68 -27
  30. package/package.json +2 -2
  31. package/src/__tests__/comprehensive.integration.test.ts +4 -4
  32. package/src/__tests__/performance.test.ts +1 -2
  33. package/src/__tests__/smoke.e2e.test.ts +70 -0
  34. package/src/__tests__/testUtils.ts +2 -113
  35. package/src/server/YNABMCPServer.ts +64 -10
  36. package/src/server/__tests__/completions.integration.test.ts +117 -0
  37. package/src/server/__tests__/completions.test.ts +319 -0
  38. package/src/server/__tests__/resources.template.test.ts +3 -3
  39. package/src/server/__tests__/resources.test.ts +3 -3
  40. package/src/server/__tests__/toolRegistration.test.ts +1 -1
  41. package/src/server/cacheManager.ts +7 -6
  42. package/src/server/completions.ts +279 -0
  43. package/src/server/errorHandler.ts +1 -0
  44. package/src/server/rateLimiter.ts +4 -1
  45. package/src/server/resources.ts +49 -13
  46. package/src/server/securityMiddleware.ts +1 -0
  47. package/src/server/toolRegistry.ts +42 -0
  48. package/src/tools/adapters.ts +22 -1
  49. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +12 -26
  50. package/src/tools/reconciliation/__tests__/executor.progress.test.ts +462 -0
  51. package/src/tools/reconciliation/__tests__/executor.test.ts +36 -31
  52. package/src/tools/reconciliation/executor.ts +56 -27
  53. package/src/tools/reconciliation/index.ts +7 -3
  54. package/vitest.config.ts +2 -0
  55. package/src/__tests__/delta.performance.test.ts +0 -80
  56. package/src/__tests__/workflows.e2e.test.ts +0 -1658
@@ -1,4 +1,3 @@
1
- import { createHash } from 'crypto';
2
1
  import { YNABAPIError } from '../../server/errorHandler.js';
3
2
  import { toMilli, toMoneyValue, addMilli } from '../../utils/money.js';
4
3
  import { generateCorrelationKey, correlateResults, toCorrelationPayload, } from '../transactionTools.js';
@@ -29,12 +28,6 @@ function truncateMemo(memo) {
29
28
  return memo;
30
29
  return memo.substring(0, MAX_MEMO_LENGTH - 3) + '...';
31
30
  }
32
- function generateBulkImportId(accountId, date, amountMilli, payee) {
33
- const normalizedPayee = (payee ?? '').trim().toLowerCase();
34
- const raw = `${accountId}|${date}|${amountMilli}|${normalizedPayee}`;
35
- const digest = createHash('sha256').update(raw).digest('hex').slice(0, 24);
36
- return `YNAB:bulk:${digest}`;
37
- }
38
31
  function parseISODate(dateStr) {
39
32
  if (!dateStr)
40
33
  return undefined;
@@ -79,7 +72,7 @@ function isWithinStatementWindow(dateStr, window) {
79
72
  return true;
80
73
  }
81
74
  export async function executeReconciliation(options) {
82
- const { analysis, params, ynabAPI, budgetId, accountId, initialAccount, currencyCode } = options;
75
+ const { analysis, params, ynabAPI, budgetId, accountId, initialAccount, currencyCode, sendProgress, } = options;
83
76
  const actions_taken = [];
84
77
  const summary = {
85
78
  bank_transactions_count: analysis.summary.bank_transactions_count,
@@ -92,6 +85,23 @@ export async function executeReconciliation(options) {
92
85
  dates_adjusted: 0,
93
86
  dry_run: params.dry_run,
94
87
  };
88
+ const matchesNeedingUpdate = analysis.auto_matches.filter((match) => {
89
+ const flags = computeUpdateFlags(match, params);
90
+ return flags.needsClearedUpdate || flags.needsDateUpdate;
91
+ });
92
+ const totalOperations = (params.auto_create_transactions ? analysis.unmatched_bank.length : 0) +
93
+ matchesNeedingUpdate.length +
94
+ (params.auto_unclear_missing ? analysis.unmatched_ynab.length : 0);
95
+ let completedOperations = 0;
96
+ const reportProgress = async (message) => {
97
+ if (sendProgress && totalOperations > 0) {
98
+ await sendProgress({
99
+ progress: completedOperations,
100
+ total: totalOperations,
101
+ message,
102
+ });
103
+ }
104
+ };
95
105
  let afterAccount = { ...initialAccount };
96
106
  let accountSnapshotDirty = false;
97
107
  const statementTargetMilli = resolveStatementBalanceMilli(analysis.balance_info, params.statement_balance);
@@ -143,7 +153,6 @@ export async function executeReconciliation(options) {
143
153
  memo: truncateMemo(bankTxn.memo),
144
154
  cleared: 'cleared',
145
155
  approved: true,
146
- import_id: generateBulkImportId(accountId, bankTxn.date, amountMilli, bankTxn.payee),
147
156
  };
148
157
  const correlationKey = generateCorrelationKey(toCorrelationPayload(saveTransaction));
149
158
  return {
@@ -193,6 +202,8 @@ export async function executeReconciliation(options) {
193
202
  recordCreateAction(recordArgs);
194
203
  accountSnapshotDirty = true;
195
204
  applyClearedDelta(entry.amountMilli);
205
+ completedOperations += 1;
206
+ await reportProgress(`Created ${completedOperations} of ${totalOperations} transactions`);
196
207
  const trigger = options.chunkIndex
197
208
  ? `creating ${entry.bankTransaction.payee ?? 'missing transaction'} (chunk ${options.chunkIndex})`
198
209
  : `creating ${entry.bankTransaction.payee ?? 'missing transaction'}`;
@@ -343,6 +354,8 @@ export async function executeReconciliation(options) {
343
354
  try {
344
355
  await processBulkChunk(chunk, chunkIndex);
345
356
  bulkOperationDetails.bulk_successes += 1;
357
+ completedOperations += chunk.length;
358
+ await reportProgress(`Created ${completedOperations} of ${totalOperations} transactions`);
346
359
  }
347
360
  catch (error) {
348
361
  const ynabError = normalizeYnabError(error);
@@ -445,6 +458,8 @@ export async function executeReconciliation(options) {
445
458
  });
446
459
  }
447
460
  accountSnapshotDirty = true;
461
+ completedOperations += updatedTransactions.length;
462
+ await reportProgress(`Updated ${completedOperations} of ${totalOperations} transactions`);
448
463
  }
449
464
  catch (error) {
450
465
  const ynabError = normalizeYnabError(error);
@@ -533,6 +548,8 @@ export async function executeReconciliation(options) {
533
548
  });
534
549
  }
535
550
  accountSnapshotDirty = true;
551
+ completedOperations += updatedTransactions.length;
552
+ await reportProgress(`Marked ${completedOperations} of ${totalOperations} transactions uncleared`);
536
553
  }
537
554
  catch (error) {
538
555
  const ynabError = normalizeYnabError(error);
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import type * as ynab from 'ynab';
3
- import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
4
+ import type { ProgressCallback } from '../../server/toolRegistry.js';
4
5
  import type { ToolFactory } from '../../types/toolRegistration.js';
5
6
  import type { DeltaFetcher } from '../deltaFetcher.js';
6
7
  export type * from './types.js';
@@ -51,6 +52,6 @@ export declare const ReconcileAccountSchema: z.ZodObject<{
51
52
  force_full_refresh: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
52
53
  }, z.core.$strip>;
53
54
  export type ReconcileAccountRequest = z.infer<typeof ReconcileAccountSchema>;
54
- export declare function handleReconcileAccount(ynabAPI: ynab.API, deltaFetcher: DeltaFetcher, params: ReconcileAccountRequest): Promise<CallToolResult>;
55
+ export declare function handleReconcileAccount(ynabAPI: ynab.API, deltaFetcher: DeltaFetcher, params: ReconcileAccountRequest, sendProgress?: ProgressCallback): Promise<CallToolResult>;
55
56
  export declare function handleReconcileAccount(ynabAPI: ynab.API, params: ReconcileAccountRequest): Promise<CallToolResult>;
56
57
  export declare const registerReconciliationTools: ToolFactory;
@@ -88,7 +88,7 @@ export const ReconcileAccountSchema = z
88
88
  message: 'Either csv_file_path or csv_data must be provided',
89
89
  path: ['csv_data'],
90
90
  });
91
- export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, maybeParams) {
91
+ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, maybeParams, sendProgress) {
92
92
  const { deltaFetcher, params } = resolveDeltaFetcherArgs(ynabAPI, deltaFetcherOrParams, maybeParams);
93
93
  const forceFullRefresh = params.force_full_refresh ?? true;
94
94
  return await withToolErrorHandling(async () => {
@@ -274,6 +274,7 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
274
274
  accountId: params.account_id,
275
275
  initialAccount,
276
276
  currencyCode,
277
+ ...(sendProgress !== undefined && { sendProgress }),
277
278
  });
278
279
  }
279
280
  const csvFormatForPayload = mapCsvFormatForPayload(params.csv_format);
@@ -307,7 +308,7 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
307
308
  }, 'ynab:reconcile_account', 'analyzing account reconciliation');
308
309
  }
309
310
  export const registerReconciliationTools = (registry, context) => {
310
- const { adapt, adaptWithDelta } = createAdapters(context);
311
+ const { adapt, adaptWithDeltaAndProgress } = createAdapters(context);
311
312
  const budgetResolver = createBudgetResolver(context);
312
313
  registry.register({
313
314
  name: 'compare_transactions',
@@ -326,7 +327,7 @@ export const registerReconciliationTools = (registry, context) => {
326
327
  name: 'reconcile_account',
327
328
  description: 'Guided reconciliation workflow with human narrative, insight detection, and optional execution (create/update/unclear). Set include_structured_data=true to also get full JSON output (large).',
328
329
  inputSchema: ReconcileAccountSchema,
329
- handler: adaptWithDelta(handleReconcileAccount),
330
+ handler: adaptWithDeltaAndProgress(handleReconcileAccount),
330
331
  defaultArgumentResolver: budgetResolver(),
331
332
  metadata: {
332
333
  annotations: {
@@ -8,6 +8,7 @@ This document provides comprehensive documentation for all tools available in th
8
8
  - [Authentication](#authentication)
9
9
  - [Data Formats](#data-formats)
10
10
  - [MCP Resources](#mcp-resources)
11
+ - [MCP Client Features](#mcp-client-features)
11
12
  - [Budget Management Tools](#budget-management-tools)
12
13
  - [Account Management Tools](#account-management-tools)
13
14
  - [Transaction Management Tools](#transaction-management-tools)
@@ -20,7 +21,7 @@ This document provides comprehensive documentation for all tools available in th
20
21
 
21
22
  ## Overview
22
23
 
23
- The YNAB MCP Server provides 30 tools that enable AI assistants to interact with YNAB data. All tools follow consistent patterns for parameters, responses, and error handling.
24
+ The YNAB MCP Server provides 29 tools that enable AI assistants to interact with YNAB data. All tools follow consistent patterns for parameters, responses, and error handling.
24
25
 
25
26
  ### Tool Naming Convention
26
27
 
@@ -54,6 +55,10 @@ The server automatically converts YNAB's internal milliunits to dollars in all r
54
55
 
55
56
  **Note**: YNAB's internal representation uses milliunits (1/1000th of currency unit), but this is now transparent to users - all inputs and outputs use standard dollar amounts
56
57
 
58
+ ### Output Formatting Overrides
59
+
60
+ All tool calls accept optional boolean hints `minify`, `_minify`, or `__minify` in their arguments to override JSON formatting for that call. These keys are removed before validation and do not affect tool behavior.
61
+
57
62
  ### Dates
58
63
 
59
64
  All dates use ISO 8601 format: `YYYY-MM-DD`
@@ -186,9 +191,9 @@ URI: ynab://budgets/12345678-1234-1234-1234-123456789012/accounts/87654321-4321-
186
191
  ### Caching
187
192
 
188
193
  All MCP resources are cached for optimal performance:
189
- - **Budgets**: 1 hour TTL (rarely change)
190
- - **Accounts**: 30 minutes TTL (balances update periodically)
191
- - **User info**: 1 hour TTL (static data)
194
+ - **Budgets**: 10 minutes TTL (rarely change)
195
+ - **Accounts**: 5 minutes TTL (balances update periodically)
196
+ - **User info**: 30 minutes TTL (static data)
192
197
 
193
198
  ### Resource vs Tool: When to Use What
194
199
 
@@ -209,6 +214,27 @@ All MCP resources are cached for optimal performance:
209
214
  - Get transactions for an account: Use `list_transactions` tool ✅
210
215
  - Create a new transaction: Use `create_transaction` tool ✅
211
216
 
217
+ ## MCP Client Features
218
+
219
+ These features depend on MCP client support. If your client doesn't surface them, tool calls still work normally.
220
+
221
+ ### Completions (Autocomplete)
222
+
223
+ The server provides MCP completions for common arguments:
224
+ - `budget_id`
225
+ - `account_id`
226
+ - `account_name`
227
+ - `category`
228
+ - `category_id`
229
+ - `payee`
230
+ - `payee_id`
231
+
232
+ Clients can use these completions to suggest IDs or names while composing tool calls.
233
+
234
+ ### Progress Notifications
235
+
236
+ When a client provides a `progressToken` in tool requests, the server emits `notifications/progress` events. Progress updates are most useful for long-running operations such as `reconcile_account`, which can create, update, or unclear large batches of transactions.
237
+
212
238
  ## Budget Management Tools
213
239
 
214
240
  ### list_budgets
@@ -291,7 +317,7 @@ Lists all accounts for a specific budget.
291
317
  "content": [
292
318
  {
293
319
  "type": "text",
294
- "text": "{\n \"accounts\": [\n {\n \"id\": \"87654321-4321-4321-4321-210987654321\",\n \"name\": \"Checking Account\",\n \"type\": \"checking\",\n \"on_budget\": true,\n \"closed\": false,\n \"note\": null,\n \"balance\": 150000,\n \"cleared_balance\": 145000,\n \"uncleared_balance\": 5000,\n \"transfer_payee_id\": \"transfer-payee-id\",\n \"direct_import_linked\": false,\n \"direct_import_in_error\": false,\n \"last_reconciled_at\": null,\n \"debt_original_balance\": null,\n \"debt_interest_rates\": {},\n \"debt_minimum_payments\": {},\n \"debt_escrow_amounts\": {}\n }\n ]\n}"
320
+ "text": "{\n \"accounts\": [\n {\n \"id\": \"87654321-4321-4321-4321-210987654321\",\n \"name\": \"Checking Account\",\n \"type\": \"checking\",\n \"on_budget\": true,\n \"closed\": false,\n \"note\": null,\n \"balance\": 150.0,\n \"cleared_balance\": 145.0,\n \"uncleared_balance\": 5.0,\n \"transfer_payee_id\": \"transfer-payee-id\",\n \"direct_import_linked\": false,\n \"direct_import_in_error\": false,\n \"last_reconciled_at\": null,\n \"debt_original_balance\": null,\n \"debt_interest_rates\": {},\n \"debt_minimum_payments\": {},\n \"debt_escrow_amounts\": {}\n }\n ]\n}"
295
321
  }
296
322
  ]
297
323
  }
@@ -331,7 +357,7 @@ Creates a new account in the specified budget.
331
357
  - `lineOfCredit` - Line of credit
332
358
  - `otherAsset` - Other asset account
333
359
  - `otherLiability` - Other liability account
334
- - `balance` (number, optional): Initial balance in milliunits
360
+ - `balance` (number, optional): Initial balance in dollars
335
361
  - `dry_run` (boolean, optional): Validate and return simulated result; no API call
336
362
 
337
363
  **Example Request:**
@@ -342,7 +368,7 @@ Creates a new account in the specified budget.
342
368
  "budget_id": "12345678-1234-1234-1234-123456789012",
343
369
  "name": "New Savings Account",
344
370
  "type": "savings",
345
- "balance": 100000
371
+ "balance": 100.0
346
372
  }
347
373
  }
348
374
  ```
@@ -353,7 +379,7 @@ Creates a new account in the specified budget.
353
379
  "content": [
354
380
  {
355
381
  "type": "text",
356
- "text": "{\n \"account\": {\n \"id\": \"new-account-id\",\n \"name\": \"New Savings Account\",\n \"type\": \"savings\",\n \"on_budget\": true,\n \"closed\": false,\n \"balance\": 100000,\n \"cleared_balance\": 100000,\n \"uncleared_balance\": 0\n }\n}"
382
+ "text": "{\n \"account\": {\n \"id\": \"new-account-id\",\n \"name\": \"New Savings Account\",\n \"type\": \"savings\",\n \"on_budget\": true,\n \"closed\": false,\n \"balance\": 100.0,\n \"cleared_balance\": 100.0,\n \"uncleared_balance\": 0\n }\n}"
357
383
  }
358
384
  ]
359
385
  }
@@ -390,7 +416,7 @@ Lists transactions for a budget with optional filtering.
390
416
  "content": [
391
417
  {
392
418
  "type": "text",
393
- "text": "{\n \"transactions\": [\n {\n \"id\": \"transaction-id\",\n \"date\": \"2024-01-15\",\n \"amount\": -5000,\n \"memo\": \"Coffee shop\",\n \"cleared\": \"cleared\",\n \"approved\": true,\n \"flag_color\": null,\n \"account_id\": \"87654321-4321-4321-4321-210987654321\",\n \"payee_id\": \"payee-id\",\n \"category_id\": \"category-id\",\n \"transfer_account_id\": null,\n \"transfer_transaction_id\": null,\n \"matched_transaction_id\": null,\n \"import_id\": null,\n \"import_payee_name\": null,\n \"import_payee_name_original\": null,\n \"debt_transaction_type\": null,\n \"deleted\": false\n }\n ]\n}"
419
+ "text": "{\n \"transactions\": [\n {\n \"id\": \"transaction-id\",\n \"date\": \"2024-01-15\",\n \"amount\": -5.0,\n \"memo\": \"Coffee shop\",\n \"cleared\": \"cleared\",\n \"approved\": true,\n \"flag_color\": null,\n \"account_id\": \"87654321-4321-4321-4321-210987654321\",\n \"payee_id\": \"payee-id\",\n \"category_id\": \"category-id\",\n \"transfer_account_id\": null,\n \"transfer_transaction_id\": null,\n \"matched_transaction_id\": null,\n \"import_id\": null,\n \"import_payee_name\": null,\n \"import_payee_name_original\": null,\n \"debt_transaction_type\": null,\n \"deleted\": false\n }\n ]\n}"
394
420
  }
395
421
  ]
396
422
  }
@@ -426,7 +452,7 @@ Exports all transactions to a JSON file with descriptive filename and platform-s
426
452
  "content": [
427
453
  {
428
454
  "type": "text",
429
- "text": "{\n \"message\": \"Successfully exported 1247 transactions\",\n \"filename\": \"ynab_since_2024-01-01_1247items_2024-09-10_14-30-15.json\",\n \"full_path\": \"C:\\\\Users\\\\YourName\\\\Downloads\\\\ynab_since_2024-01-01_1247items_2024-09-10_14-30-15.json\",\n \"export_directory\": \"C:\\\\Users\\\\YourName\\\\Downloads\",\n \"filename_explanation\": \"Filename format: ynab_{filters}_{count}items_{timestamp}.json - identifies what data was exported, when, and how many transactions\",\n \"preview_count\": 10,\n \"total_count\": 1247,\n \"preview_transactions\": [\n {\n \"id\": \"transaction-id\",\n \"date\": \"2024-01-15\",\n \"amount\": -5000,\n \"memo\": \"Coffee shop\",\n \"payee_name\": \"Starbucks\",\n \"category_name\": \"Dining Out\"\n }\n ]\n}"
455
+ "text": "{\n \"message\": \"Successfully exported 1247 transactions\",\n \"filename\": \"ynab_since_2024-01-01_1247items_2024-09-10_14-30-15.json\",\n \"full_path\": \"C:\\\\Users\\\\YourName\\\\Downloads\\\\ynab_since_2024-01-01_1247items_2024-09-10_14-30-15.json\",\n \"export_directory\": \"C:\\\\Users\\\\YourName\\\\Downloads\",\n \"filename_explanation\": \"Filename format: ynab_{filters}_{count}items_{timestamp}.json - identifies what data was exported, when, and how many transactions\",\n \"preview_count\": 10,\n \"total_count\": 1247,\n \"preview_transactions\": [\n {\n \"id\": \"transaction-id\",\n \"date\": \"2024-01-15\",\n \"amount\": -5.0,\n \"memo\": \"Coffee shop\",\n \"payee_name\": \"Starbucks\",\n \"category_name\": \"Dining Out\"\n }\n ]\n}"
430
456
  }
431
457
  ]
432
458
  }
@@ -678,16 +704,17 @@ The `reconcile_account_v2` tool now includes an optional `recommendations` array
678
704
 
679
705
  Recommendations include complete parameters for YNAB MCP tool calls:
680
706
 
681
- **CRITICAL**: Recommendation `parameters.amount` values are in **milliunits** (YNAB's internal format where 1 dollar = 1000 milliunits). These values are ready to pass directly to `create_transaction` without conversion. However, `estimated_impact.value` remains in decimal dollars for human readability.
707
+ **CRITICAL**: Recommendation `parameters.amount` values are in **milliunits** (YNAB's internal format where 1 dollar = 1000 milliunits). These values are intended for reconciliation execution; convert to dollars before calling `create_transaction`. `estimated_impact.value` remains in decimal dollars for human readability.
682
708
 
683
709
  ```typescript
684
710
  // For create_transaction recommendations:
685
- // Note: Recommendation amounts are already in milliunits, ready to use directly
711
+ // Note: Recommendation amounts are already in milliunits; convert to dollars before tool calls
686
712
  const rec = recommendations.find(r => r.action_type === 'create_transaction');
687
713
  if (rec) {
688
714
  await create_transaction({
689
715
  budget_id: 'your-budget-id',
690
- ...rec.parameters // Parameters already contain amounts in milliunits
716
+ ...rec.parameters,
717
+ amount: rec.parameters.amount / 1000 // Convert milliunits to dollars
691
718
  });
692
719
  }
693
720
 
@@ -880,7 +907,7 @@ Creates a new transaction in the specified budget and account.
880
907
  **Parameters:**
881
908
  - `budget_id` (string, required): The ID of the budget
882
909
  - `account_id` (string, required): The ID of the account
883
- - `amount` (number, required): Transaction amount in milliunits (negative for outflows)
910
+ - `amount` (number, required): Transaction amount in dollars (negative for outflows)
884
911
  - `date` (string, required): Transaction date in ISO format (YYYY-MM-DD)
885
912
  - `payee_name` (string, optional): The payee name
886
913
  - `payee_id` (string, optional): The payee ID
@@ -890,7 +917,7 @@ Creates a new transaction in the specified budget and account.
890
917
  - `approved` (boolean, optional): Whether the transaction is approved
891
918
  - `flag_color` (string, optional): Transaction flag color (`red`, `orange`, `yellow`, `green`, `blue`, `purple`)
892
919
  - `dry_run` (boolean, optional): Validate and return simulated result; no API call
893
- - `subtransactions` (array, optional): Split line items; each entry accepts `amount` (milliunits), plus optional `memo`, `category_id`, `payee_id`, and `payee_name`
920
+ - `subtransactions` (array, optional): Split line items; each entry accepts `amount` (dollars), plus optional `memo`, `category_id`, `payee_id`, and `payee_name`
894
921
 
895
922
  When `subtransactions` are supplied, their `amount` values must sum to the parent `amount`, matching YNAB API requirements.
896
923
 
@@ -901,7 +928,7 @@ When `subtransactions` are supplied, their `amount` values must sum to the paren
901
928
  "arguments": {
902
929
  "budget_id": "12345678-1234-1234-1234-123456789012",
903
930
  "account_id": "87654321-4321-4321-4321-210987654321",
904
- "amount": -5000,
931
+ "amount": -5.0,
905
932
  "date": "2024-01-15",
906
933
  "payee_name": "Coffee Shop",
907
934
  "category_id": "category-id",
@@ -919,12 +946,12 @@ When `subtransactions` are supplied, their `amount` values must sum to the paren
919
946
  "arguments": {
920
947
  "budget_id": "12345678-1234-1234-1234-123456789012",
921
948
  "account_id": "87654321-4321-4321-4321-210987654321",
922
- "amount": -125000,
949
+ "amount": -125.0,
923
950
  "date": "2024-02-01",
924
951
  "memo": "Rent and utilities",
925
952
  "subtransactions": [
926
- { "amount": -100000, "category_id": "rent-category", "memo": "Rent" },
927
- { "amount": -25000, "category_id": "utilities-category", "memo": "Utilities" }
953
+ { "amount": -100.0, "category_id": "rent-category", "memo": "Rent" },
954
+ { "amount": -25.0, "category_id": "utilities-category", "memo": "Utilities" }
928
955
  ]
929
956
  }
930
957
  }
@@ -1038,7 +1065,7 @@ Updates an existing transaction.
1038
1065
  - `budget_id` (string, required): The ID of the budget
1039
1066
  - `transaction_id` (string, required): The ID of the transaction to update
1040
1067
  - `account_id` (string, optional): Update the account ID
1041
- - `amount` (number, optional): Update the amount in milliunits
1068
+ - `amount` (number, optional): Update the amount in dollars
1042
1069
  - `date` (string, optional): Update the date (YYYY-MM-DD)
1043
1070
  - `payee_name` (string, optional): Update the payee name
1044
1071
  - `payee_id` (string, optional): Update the payee ID
@@ -1056,7 +1083,7 @@ Updates an existing transaction.
1056
1083
  "arguments": {
1057
1084
  "budget_id": "12345678-1234-1234-1234-123456789012",
1058
1085
  "transaction_id": "transaction-id",
1059
- "amount": -6000,
1086
+ "amount": -6.0,
1060
1087
  "memo": "Updated memo",
1061
1088
  "flag_color": "red"
1062
1089
  }
@@ -1108,7 +1135,7 @@ Lists all categories for a specific budget.
1108
1135
  "content": [
1109
1136
  {
1110
1137
  "type": "text",
1111
- "text": "{\n \"category_groups\": [\n {\n \"id\": \"group-id\",\n \"name\": \"Monthly Bills\",\n \"hidden\": false,\n \"deleted\": false,\n \"categories\": [\n {\n \"id\": \"category-id\",\n \"category_group_id\": \"group-id\",\n \"name\": \"Rent/Mortgage\",\n \"hidden\": false,\n \"original_category_group_id\": null,\n \"note\": null,\n \"budgeted\": 150000,\n \"activity\": -150000,\n \"balance\": 0,\n \"goal_type\": null,\n \"goal_creation_month\": null,\n \"goal_target\": null,\n \"goal_target_month\": null,\n \"goal_percentage_complete\": null,\n \"goal_months_to_budget\": null,\n \"goal_under_funded\": null,\n \"goal_overall_funded\": null,\n \"goal_overall_left\": null,\n \"deleted\": false\n }\n ]\n }\n ]\n}"
1138
+ "text": "{\n \"category_groups\": [\n {\n \"id\": \"group-id\",\n \"name\": \"Monthly Bills\",\n \"hidden\": false,\n \"deleted\": false,\n \"categories\": [\n {\n \"id\": \"category-id\",\n \"category_group_id\": \"group-id\",\n \"name\": \"Rent/Mortgage\",\n \"hidden\": false,\n \"original_category_group_id\": null,\n \"note\": null,\n \"budgeted\": 150.0,\n \"activity\": -150.0,\n \"balance\": 0,\n \"goal_type\": null,\n \"goal_creation_month\": null,\n \"goal_target\": null,\n \"goal_target_month\": null,\n \"goal_percentage_complete\": null,\n \"goal_months_to_budget\": null,\n \"goal_under_funded\": null,\n \"goal_overall_funded\": null,\n \"goal_overall_left\": null,\n \"deleted\": false\n }\n ]\n }\n ]\n}"
1112
1139
  }
1113
1140
  ]
1114
1141
  }
@@ -1129,7 +1156,7 @@ Updates the budgeted amount for a category in the current month.
1129
1156
  **Parameters:**
1130
1157
  - `budget_id` (string, required): The ID of the budget
1131
1158
  - `category_id` (string, required): The ID of the category
1132
- - `budgeted` (number, required): The budgeted amount in milliunits
1159
+ - `budgeted` (number, required): The budgeted amount in dollars
1133
1160
  - `dry_run` (boolean, optional): Validate and return simulated result; no API call
1134
1161
 
1135
1162
  **Example Request:**
@@ -1139,7 +1166,7 @@ Updates the budgeted amount for a category in the current month.
1139
1166
  "arguments": {
1140
1167
  "budget_id": "12345678-1234-1234-1234-123456789012",
1141
1168
  "category_id": "category-id",
1142
- "budgeted": 50000
1169
+ "budgeted": 50.0
1143
1170
  }
1144
1171
  }
1145
1172
  ```
@@ -1471,9 +1498,9 @@ if (!dateRegex.test(date)) {
1471
1498
  throw new Error('Date must be in YYYY-MM-DD format');
1472
1499
  }
1473
1500
 
1474
- // Validate amount is in milliunits
1475
- if (!Number.isInteger(amount)) {
1476
- throw new Error('Amount must be an integer in milliunits');
1501
+ // Validate amount is in dollars
1502
+ if (!Number.isFinite(amount)) {
1503
+ throw new Error('Amount must be a number in dollars');
1477
1504
  }
1478
1505
  ```
1479
1506
 
@@ -1537,3 +1564,17 @@ const transactions = await mcpClient.callTool('list_transactions', {
1537
1564
  ```
1538
1565
 
1539
1566
  This API reference provides comprehensive documentation for all available tools. For additional information, see the [Developer Guide](DEVELOPER.md) for best practices and common usage patterns.
1567
+
1568
+
1569
+
1570
+
1571
+
1572
+
1573
+
1574
+
1575
+
1576
+
1577
+
1578
+
1579
+
1580
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dizzlkheinz/ynab-mcpb",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "description": "Model Context Protocol server for YNAB (You Need A Budget) integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -66,7 +66,7 @@
66
66
  "author": "",
67
67
  "license": "AGPL-3.0",
68
68
  "dependencies": {
69
- "@modelcontextprotocol/sdk": "^1.24.3",
69
+ "@modelcontextprotocol/sdk": "^1.25.1",
70
70
  "chrono-node": "^2.9.0",
71
71
  "csv-parse": "^6.1.0",
72
72
  "d3-array": "^3.2.4",
@@ -351,7 +351,7 @@ describe('YNAB MCP Server - Comprehensive Integration Tests', () => {
351
351
  });
352
352
 
353
353
  describe('Complete Transaction Management Integration', () => {
354
- // TODO: Re-enable after DeltaFetcher cache integration alignment (see docs/plans/2025-11-15-cache-test-alignment.md)
354
+ // TODO: Re-enable after DeltaFetcher cache integration alignment.
355
355
  it.skip(
356
356
  'should handle complete transaction workflow',
357
357
  { meta: { tier: 'domain', domain: 'workflows' } },
@@ -851,7 +851,7 @@ describe('YNAB MCP Server - Comprehensive Integration Tests', () => {
851
851
  }
852
852
  });
853
853
 
854
- // TODO: Re-enable after DeltaFetcher cache integration alignment (see docs/plans/2025-11-15-cache-test-alignment.md)
854
+ // TODO: Re-enable after DeltaFetcher cache integration alignment.
855
855
  it.skip(
856
856
  'should cache budget list requests and improve performance on subsequent calls',
857
857
  { meta: { tier: 'domain', domain: 'workflows' } },
@@ -982,7 +982,7 @@ describe('YNAB MCP Server - Comprehensive Integration Tests', () => {
982
982
  },
983
983
  );
984
984
 
985
- // TODO: Re-enable after DeltaFetcher cache integration alignment (see docs/plans/2025-11-15-cache-test-alignment.md)
985
+ // TODO: Re-enable after DeltaFetcher cache integration alignment.
986
986
  it.skip(
987
987
  'should not cache filtered transaction requests',
988
988
  { meta: { tier: 'domain', domain: 'workflows' } },
@@ -1115,7 +1115,7 @@ describe('YNAB MCP Server - Comprehensive Integration Tests', () => {
1115
1115
  },
1116
1116
  );
1117
1117
 
1118
- // TODO: Re-enable after DeltaFetcher cache integration alignment (see docs/plans/2025-11-15-cache-test-alignment.md)
1118
+ // TODO: Re-enable after DeltaFetcher cache integration alignment.
1119
1119
  it.skip(
1120
1120
  'should respect cache TTL and return fresh data after expiration',
1121
1121
  { meta: { tier: 'domain', domain: 'workflows' } },
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect, beforeEach, vi } from 'vitest';
6
- import { YNABMCPServer } from '../server/YNABMCPServer.js';
7
6
  import { executeToolCall, parseToolResult } from './testUtils.js';
8
7
  import { executeReconciliation, type AccountSnapshot } from '../tools/reconciliation/executor.js';
9
8
  import type { ReconciliationAnalysis } from '../tools/reconciliation/types.js';
@@ -373,7 +372,7 @@ async function measurePerformanceScenario(options: {
373
372
  }
374
373
 
375
374
  describe('YNAB MCP Server - Performance Tests', () => {
376
- let server: YNABMCPServer;
375
+ let server: InstanceType<typeof import('../server/YNABMCPServer.js').YNABMCPServer>;
377
376
  let mockYnabAPI: any;
378
377
 
379
378
  beforeEach(async () => {
@@ -0,0 +1,70 @@
1
+ /**
2
+ * End-to-end smoke tests for YNAB MCP Server
3
+ *
4
+ * These tests require a real YNAB API key but only perform read operations
5
+ * to verify connectivity and basic functionality without hitting rate limits.
6
+ *
7
+ * NOTE: This file was intentionally reduced from comprehensive CRUD workflow tests
8
+ * to lightweight smoke tests. The full E2E test suite was removed because:
9
+ * 1. YNAB API rate limits (200 requests/hour) made full E2E runs unreliable in CI
10
+ * 2. Comprehensive integration tests with mocked APIs provide better coverage
11
+ * 3. Smoke tests validate real API connectivity without rate limit pressure
12
+ *
13
+ * For full workflow testing, see integration tests in src/tools/__tests__/
14
+ */
15
+
16
+ import { describe, it, expect, beforeAll } from 'vitest';
17
+ import { YNABMCPServer } from '../server/YNABMCPServer.js';
18
+ import {
19
+ getTestConfig,
20
+ createTestServer,
21
+ executeToolCall,
22
+ parseToolResult,
23
+ validateOutputSchema,
24
+ } from './testUtils.js';
25
+
26
+ const runE2ETests = process.env['SKIP_E2E_TESTS'] !== 'true';
27
+ const describeE2E = runE2ETests ? describe : describe.skip;
28
+
29
+ describeE2E('YNAB MCP Server - Smoke Tests', () => {
30
+ let server: YNABMCPServer;
31
+ let testConfig: ReturnType<typeof getTestConfig>;
32
+
33
+ beforeAll(async () => {
34
+ testConfig = getTestConfig();
35
+
36
+ if (testConfig.skipE2ETests) {
37
+ console.warn('Skipping E2E smoke tests - no real API key or SKIP_E2E_TESTS=true');
38
+ return;
39
+ }
40
+
41
+ server = await createTestServer();
42
+ });
43
+
44
+ it('should authenticate and retrieve user information', async () => {
45
+ if (testConfig.skipE2ETests) return;
46
+
47
+ const result = await executeToolCall(server, 'ynab:get_user');
48
+
49
+ // Validate output schema
50
+ const validation = validateOutputSchema(server, 'get_user', result);
51
+ expect(validation.valid).toBe(true);
52
+
53
+ const data = parseToolResult(result);
54
+ expect(data.data.user).toBeDefined();
55
+ expect(data.data.user.id).toBeDefined();
56
+ });
57
+
58
+ it('should list budgets', async () => {
59
+ if (testConfig.skipE2ETests) return;
60
+
61
+ const result = await executeToolCall(server, 'ynab:list_budgets');
62
+
63
+ // Validate output schema
64
+ const validation = validateOutputSchema(server, 'list_budgets', result);
65
+ expect(validation.valid).toBe(true);
66
+
67
+ const data = parseToolResult(result);
68
+ expect(Array.isArray(data.data.budgets)).toBe(true);
69
+ });
70
+ });