@dizzlkheinz/ynab-mcpb 0.15.1 → 0.16.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 (53) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/CLAUDE.md +113 -18
  3. package/README.md +19 -4
  4. package/dist/bundle/index.cjs +53 -52
  5. package/dist/server/YNABMCPServer.d.ts +2 -6
  6. package/dist/server/YNABMCPServer.js +5 -1
  7. package/dist/server/resources.d.ts +17 -13
  8. package/dist/server/resources.js +237 -48
  9. package/dist/tools/reconcileAdapter.d.ts +1 -0
  10. package/dist/tools/reconcileAdapter.js +1 -0
  11. package/dist/tools/reconciliation/csvParser.d.ts +3 -0
  12. package/dist/tools/reconciliation/csvParser.js +58 -19
  13. package/dist/tools/reconciliation/executor.js +47 -1
  14. package/dist/tools/reconciliation/index.js +82 -42
  15. package/dist/tools/reconciliation/reportFormatter.d.ts +1 -0
  16. package/dist/tools/reconciliation/reportFormatter.js +49 -36
  17. package/dist/tools/transactionTools.js +5 -0
  18. package/docs/reference/API.md +144 -0
  19. package/docs/technical/reconciliation-system-architecture.md +2251 -0
  20. package/package.json +1 -1
  21. package/src/server/YNABMCPServer.ts +7 -0
  22. package/src/server/__tests__/resources.template.test.ts +198 -0
  23. package/src/server/__tests__/resources.test.ts +10 -2
  24. package/src/server/resources.ts +307 -62
  25. package/src/tools/__tests__/transactionTools.test.ts +90 -17
  26. package/src/tools/reconcileAdapter.ts +2 -0
  27. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +23 -23
  28. package/src/tools/reconciliation/csvParser.ts +84 -18
  29. package/src/tools/reconciliation/executor.ts +58 -1
  30. package/src/tools/reconciliation/index.ts +105 -55
  31. package/src/tools/reconciliation/reportFormatter.ts +55 -37
  32. package/src/tools/transactionTools.ts +10 -0
  33. package/.dxtignore +0 -57
  34. package/CODEREVIEW_RESPONSE.md +0 -128
  35. package/SCHEMA_IMPROVEMENT_SUMMARY.md +0 -120
  36. package/TESTING_NOTES.md +0 -217
  37. package/accountactivity-merged.csv +0 -149
  38. package/bundle-analysis.html +0 -13110
  39. package/docs/plans/2025-11-20-reloadable-config-token-validation.md +0 -93
  40. package/docs/plans/2025-11-21-fix-transaction-cached-property.md +0 -362
  41. package/docs/plans/2025-11-21-reconciliation-error-handling.md +0 -90
  42. package/docs/plans/2025-11-21-v014-hardening.md +0 -153
  43. package/docs/plans/reconciliation-v2-redesign.md +0 -1571
  44. package/fix-types.sh +0 -17
  45. package/test-csv-sample.csv +0 -28
  46. package/test-exports/sample_bank_statement.csv +0 -7
  47. package/test-reconcile-autodetect.js +0 -40
  48. package/test-reconcile-tool.js +0 -152
  49. package/test-reconcile-with-csv.cjs +0 -89
  50. package/test-statement.csv +0 -8
  51. package/test_debug.js +0 -47
  52. package/test_mcp_tools.mjs +0 -75
  53. package/test_simple.mjs +0 -16
@@ -14,6 +14,8 @@ import type {
14
14
  import type { LegacyReconciliationResult } from './executor.js';
15
15
  import type { MoneyValue } from '../../utils/money.js';
16
16
 
17
+ const SECTION_DIVIDER = '-'.repeat(60);
18
+
17
19
  /**
18
20
  * Options for report formatting
19
21
  */
@@ -24,6 +26,7 @@ export interface ReportFormatterOptions {
24
26
  includeDetailedMatches?: boolean | undefined;
25
27
  maxUnmatchedToShow?: number | undefined;
26
28
  maxInsightsToShow?: number | undefined;
29
+ notes?: string[] | undefined;
27
30
  }
28
31
 
29
32
  /**
@@ -40,6 +43,11 @@ export function formatHumanReadableReport(
40
43
  // Header
41
44
  sections.push(formatHeader(accountLabel, analysis));
42
45
 
46
+ // Contextual notes (if provided)
47
+ if (options.notes && options.notes.length > 0) {
48
+ sections.push(formatNotesSection(options.notes));
49
+ }
50
+
43
51
  // Balance check section
44
52
  sections.push(formatBalanceSection(analysis.balance_info, analysis.summary));
45
53
 
@@ -67,12 +75,22 @@ export function formatHumanReadableReport(
67
75
  */
68
76
  function formatHeader(accountName: string, analysis: ReconciliationAnalysis): string {
69
77
  const lines: string[] = [];
70
- lines.push(`📊 ${accountName} Reconciliation Report`);
71
- lines.push('═'.repeat(60));
78
+ lines.push(`${accountName} Reconciliation Report`);
79
+ lines.push(SECTION_DIVIDER);
72
80
  lines.push(`Statement Period: ${analysis.summary.statement_date_range}`);
73
81
  return lines.join('\n');
74
82
  }
75
83
 
84
+ function formatNotesSection(notes: string[]): string {
85
+ const lines: string[] = [];
86
+ lines.push('Notes');
87
+ lines.push(SECTION_DIVIDER);
88
+ for (const note of notes) {
89
+ lines.push(`- ${note}`);
90
+ }
91
+ return lines.join('\n');
92
+ }
93
+
76
94
  /**
77
95
  * Format the balance check section
78
96
  */
@@ -81,18 +99,18 @@ function formatBalanceSection(
81
99
  summary: ReconciliationAnalysis['summary'],
82
100
  ): string {
83
101
  const lines: string[] = [];
84
- lines.push('BALANCE CHECK');
85
- lines.push('═'.repeat(60));
102
+ lines.push('Balance Check');
103
+ lines.push(SECTION_DIVIDER);
86
104
 
87
105
  // Current balances
88
- lines.push(`✓ YNAB Cleared Balance: ${summary.current_cleared_balance.value_display}`);
89
- lines.push(`✓ Statement Balance: ${summary.target_statement_balance.value_display}`);
106
+ lines.push(`- YNAB Cleared Balance: ${summary.current_cleared_balance.value_display}`);
107
+ lines.push(`- Statement Balance: ${summary.target_statement_balance.value_display}`);
90
108
  lines.push('');
91
109
 
92
110
  // Discrepancy status
93
111
  const discrepancyMilli = balanceInfo.discrepancy.value_milliunits;
94
112
  if (discrepancyMilli === 0) {
95
- lines.push(' BALANCES MATCH PERFECTLY');
113
+ lines.push('Balances match perfectly.');
96
114
  } else {
97
115
  const direction = discrepancyMilli > 0 ? 'ynab_higher' : 'bank_higher';
98
116
  const directionLabel =
@@ -100,8 +118,8 @@ function formatBalanceSection(
100
118
  ? 'YNAB shows MORE than statement'
101
119
  : 'Statement shows MORE than YNAB';
102
120
 
103
- lines.push(`❌ DISCREPANCY: ${balanceInfo.discrepancy.value_display}`);
104
- lines.push(` Direction: ${directionLabel}`);
121
+ lines.push(`Discrepancy: ${balanceInfo.discrepancy.value_display}`);
122
+ lines.push(`Direction: ${directionLabel}`);
105
123
  }
106
124
 
107
125
  return lines.join('\n');
@@ -115,21 +133,21 @@ function formatTransactionAnalysisSection(
115
133
  options: ReportFormatterOptions,
116
134
  ): string {
117
135
  const lines: string[] = [];
118
- lines.push('TRANSACTION ANALYSIS');
119
- lines.push('═'.repeat(60));
136
+ lines.push('Transaction Analysis');
137
+ lines.push(SECTION_DIVIDER);
120
138
 
121
139
  const summary = analysis.summary;
122
140
  lines.push(
123
- `✓ Automatically matched: ${summary.auto_matched} of ${summary.bank_transactions_count} transactions`,
141
+ `- Automatically matched: ${summary.auto_matched} of ${summary.bank_transactions_count} transactions`,
124
142
  );
125
- lines.push(`✓ Suggested matches: ${summary.suggested_matches}`);
126
- lines.push(`✓ Unmatched bank: ${summary.unmatched_bank}`);
127
- lines.push(`✓ Unmatched YNAB: ${summary.unmatched_ynab}`);
143
+ lines.push(`- Suggested matches: ${summary.suggested_matches}`);
144
+ lines.push(`- Unmatched bank: ${summary.unmatched_bank}`);
145
+ lines.push(`- Unmatched YNAB: ${summary.unmatched_ynab}`);
128
146
 
129
147
  // Show unmatched bank transactions (if any)
130
148
  if (analysis.unmatched_bank.length > 0) {
131
149
  lines.push('');
132
- lines.push(' UNMATCHED BANK TRANSACTIONS:');
150
+ lines.push('Unmatched bank transactions:');
133
151
  const maxToShow = options.maxUnmatchedToShow ?? 5;
134
152
  const toShow = analysis.unmatched_bank.slice(0, maxToShow);
135
153
 
@@ -145,7 +163,7 @@ function formatTransactionAnalysisSection(
145
163
  // Show suggested matches (if any)
146
164
  if (analysis.suggested_matches.length > 0) {
147
165
  lines.push('');
148
- lines.push('💡 SUGGESTED MATCHES:');
166
+ lines.push('Suggested matches:');
149
167
  const maxToShow = options.maxUnmatchedToShow ?? 3;
150
168
  const toShow = analysis.suggested_matches.slice(0, maxToShow);
151
169
 
@@ -194,8 +212,8 @@ function formatAmount(amountMilli: number): string {
194
212
  */
195
213
  function formatInsightsSection(insights: ReconciliationInsight[], maxToShow: number = 3): string {
196
214
  const lines: string[] = [];
197
- lines.push('KEY INSIGHTS');
198
- lines.push('═'.repeat(60));
215
+ lines.push('Key Insights');
216
+ lines.push(SECTION_DIVIDER);
199
217
 
200
218
  const toShow = insights.slice(0, maxToShow);
201
219
  for (const insight of toShow) {
@@ -222,18 +240,18 @@ function formatInsightsSection(insights: ReconciliationInsight[], maxToShow: num
222
240
  }
223
241
 
224
242
  /**
225
- * Get emoji icon for severity level
243
+ * Get text icon for severity level
226
244
  */
227
245
  function getSeverityIcon(severity: string): string {
228
246
  switch (severity) {
229
247
  case 'critical':
230
- return '🚨';
248
+ return '[CRITICAL]';
231
249
  case 'warning':
232
- return '⚠️';
250
+ return '[WARN]';
233
251
  case 'info':
234
- return 'ℹ️';
252
+ return '[INFO]';
235
253
  default:
236
- return '';
254
+ return '[NOTE]';
237
255
  }
238
256
  }
239
257
 
@@ -260,13 +278,13 @@ function formatEvidenceSummary(evidence: Record<string, unknown>): string | null
260
278
  */
261
279
  function formatExecutionSection(execution: LegacyReconciliationResult): string {
262
280
  const lines: string[] = [];
263
- lines.push('EXECUTION SUMMARY');
264
- lines.push('═'.repeat(60));
281
+ lines.push('Execution Summary');
282
+ lines.push(SECTION_DIVIDER);
265
283
 
266
284
  const summary = execution.summary;
267
- lines.push(`• Transactions created: ${summary.transactions_created}`);
268
- lines.push(`• Transactions updated: ${summary.transactions_updated}`);
269
- lines.push(`• Date adjustments: ${summary.dates_adjusted}`);
285
+ lines.push(`Transactions created: ${summary.transactions_created}`);
286
+ lines.push(`Transactions updated: ${summary.transactions_updated}`);
287
+ lines.push(`Date adjustments: ${summary.dates_adjusted}`);
270
288
 
271
289
  // Show top recommendations if any
272
290
  if (execution.recommendations.length > 0) {
@@ -275,7 +293,7 @@ function formatExecutionSection(execution: LegacyReconciliationResult): string {
275
293
  const maxRecs = 3;
276
294
  const toShow = execution.recommendations.slice(0, maxRecs);
277
295
  for (const rec of toShow) {
278
- lines.push(` ${rec}`);
296
+ lines.push(` - ${rec}`);
279
297
  }
280
298
  if (execution.recommendations.length > maxRecs) {
281
299
  lines.push(` ... and ${execution.recommendations.length - maxRecs} more`);
@@ -284,9 +302,9 @@ function formatExecutionSection(execution: LegacyReconciliationResult): string {
284
302
 
285
303
  lines.push('');
286
304
  if (summary.dry_run) {
287
- lines.push('⚠️ Dry run only no YNAB changes were applied.');
305
+ lines.push('NOTE: Dry run only - no YNAB changes were applied.');
288
306
  } else {
289
- lines.push('Changes applied to YNAB. Review structured output for action details.');
307
+ lines.push('Changes applied to YNAB. Review structured output for action details.');
290
308
  }
291
309
 
292
310
  return lines.join('\n');
@@ -300,8 +318,8 @@ function formatRecommendationsSection(
300
318
  execution?: LegacyReconciliationResult,
301
319
  ): string {
302
320
  const lines: string[] = [];
303
- lines.push('RECOMMENDED ACTIONS');
304
- lines.push('═'.repeat(60));
321
+ lines.push('Recommended Actions');
322
+ lines.push(SECTION_DIVIDER);
305
323
 
306
324
  // If we have execution results, recommendations are already shown
307
325
  if (execution && !execution.summary.dry_run) {
@@ -312,11 +330,11 @@ function formatRecommendationsSection(
312
330
  // Show next steps from analysis
313
331
  if (analysis.next_steps.length > 0) {
314
332
  for (const step of analysis.next_steps) {
315
- lines.push(`• ${step}`);
333
+ lines.push(`- ${step}`);
316
334
  }
317
335
  } else {
318
- lines.push('No specific actions recommended.');
319
- lines.push('Review the structured output for detailed match information.');
336
+ lines.push('No specific actions recommended.');
337
+ lines.push('Review the structured output for detailed match information.');
320
338
  }
321
339
 
322
340
  return lines.join('\n');
@@ -755,6 +755,16 @@ export async function handleListTransactions(
755
755
  let usedDelta = false;
756
756
 
757
757
  if (params.account_id) {
758
+ // Validate that the account exists before fetching transactions
759
+ // YNAB API returns empty array for invalid account IDs instead of an error
760
+ const accountsResult = await deltaFetcher.fetchAccounts(params.budget_id);
761
+ const accountExists = accountsResult.data.some(
762
+ (account) => account.id === params.account_id,
763
+ );
764
+ if (!accountExists) {
765
+ throw new Error(`Account ${params.account_id} not found in budget ${params.budget_id}`);
766
+ }
767
+
758
768
  const result = await deltaFetcher.fetchTransactionsByAccount(
759
769
  params.budget_id,
760
770
  params.account_id,
package/.dxtignore DELETED
@@ -1,57 +0,0 @@
1
- node_modules/
2
- src/
3
- **/__tests__/
4
- **/*.test.*
5
- coverage/
6
- *.log
7
- .*/
8
- **/*.map
9
- tsconfig*.json
10
- vitest.config.ts
11
- vitest-reporters/
12
- eslint.config.js
13
- test-results.json
14
- test-results/
15
- README.md
16
-
17
- # Sensitive credential files
18
- .chunkhound.json
19
- .mcp.json
20
- .env*
21
-
22
- # Build metadata
23
- meta.json
24
- bundle-analysis.html
25
-
26
- # Development-only markdown files
27
- AGENTS.md
28
- TESTING_NOTES.md
29
- CODEREVIEW_RESPONSE.md
30
- SCHEMA_IMPROVEMENT_SUMMARY.md
31
-
32
- # Test files and data
33
- test_*.js
34
- test_*.mjs
35
- test-*.js
36
- test-*.cjs
37
- test-*.csv
38
- test-exports/
39
- accountactivity-merged.csv
40
-
41
- # Temporary and development files
42
- temp/
43
- tmp/
44
- fix-types.sh
45
- NUL
46
- package-lock.json
47
-
48
- # Development plans (keep user-facing docs)
49
- docs/plans/
50
- docs/bulk-transaction-operations-plan.md
51
- docs/delta-request-plan.md
52
- docs/reconciliation-flow.md
53
-
54
- # Scripts (not needed for distribution)
55
- scripts/
56
-
57
- # keep dist/, manifest.json, CHANGELOG.md, CLAUDE.md, LICENSE, docs/ (except plans)
@@ -1,128 +0,0 @@
1
- # Code Review Response: reconciliationOutputs.ts
2
-
3
- ## Summary
4
-
5
- CodeRabbit provided two suggestions for improving `src/tools/schemas/outputs/reconciliationOutputs.ts`. After technical analysis of the codebase, I've determined:
6
-
7
- 1. **Feedback #1 (ExecutionActionRecordSchema typing)**: Already implemented ✅
8
- 2. **Feedback #2 (discrepancy_direction validation)**: Not implementing - technically sound but violates architectural principles ❌
9
-
10
- ## Feedback #1: Stronger Typing for Transaction Field
11
-
12
- **Suggestion:** Use discriminated unions instead of `z.record(z.string(), z.unknown())` for the transaction field in `ExecutionActionRecordSchema`.
13
-
14
- **Status:** ✅ **Already Implemented**
15
-
16
- ### Analysis
17
-
18
- The CodeRabbit feedback appears to be outdated. The current implementation (lines 431-479) already uses a discriminated union with strong typing:
19
-
20
- ```typescript
21
- export const ExecutionActionRecordSchema = z.discriminatedUnion('type', [
22
- // Successful transaction creation
23
- z.object({
24
- type: z.literal('create_transaction'),
25
- transaction: CreatedTransactionSchema.nullable(),
26
- // ...
27
- }),
28
- // Failed transaction creation
29
- z.object({
30
- type: z.literal('create_transaction_failed'),
31
- transaction: TransactionCreationPayloadSchema,
32
- // ...
33
- }),
34
- // ... other action types
35
- ]);
36
- ```
37
-
38
- Each action type has its own specific schema:
39
-
40
- - `CreatedTransactionSchema` - for successful creations (line 371)
41
- - `TransactionCreationPayloadSchema` - for failed/dry-run creations (line 387)
42
- - `TransactionUpdatePayloadSchema` - for status/date changes (line 402)
43
- - `DuplicateDetectionPayloadSchema` - for duplicate detection (line 412)
44
-
45
- This provides full type safety without the drawbacks of loose typing.
46
-
47
- ## Feedback #2: Validate discrepancy_direction Against Actual Discrepancy
48
-
49
- **Suggestion:** Add a Zod refinement to ensure `discrepancy_direction` matches the sign of `balance.discrepancy.amount`.
50
-
51
- **Status:** ❌ **Not Implementing**
52
-
53
- ### Analysis
54
-
55
- While technically correct, this validation violates architectural principles and provides minimal benefit:
56
-
57
- #### Current Implementation (reconcileAdapter.ts:122-135)
58
-
59
- The `convertBalanceInfo` function deterministically derives `discrepancy_direction` from `discrepancyMilli`:
60
-
61
- ```typescript
62
- const convertBalanceInfo = (analysis: ReconciliationAnalysis) => {
63
- const discrepancyMilli = analysis.balance_info.discrepancy.value_milliunits;
64
- const direction =
65
- discrepancyMilli === 0 ? 'balanced' : discrepancyMilli > 0 ? 'ynab_higher' : 'bank_higher';
66
-
67
- return {
68
- current_cleared: analysis.balance_info.current_cleared,
69
- // ...
70
- discrepancy: analysis.balance_info.discrepancy,
71
- discrepancy_direction: direction,
72
- on_track: analysis.balance_info.on_track,
73
- };
74
- };
75
- ```
76
-
77
- This logic is:
78
-
79
- - **Simple and obvious**: Direct mapping from sign to direction
80
- - **Well-tested**: Part of the adapter layer
81
- - **Single point of truth**: Consistency enforced at construction time
82
-
83
- #### Why Not Add Schema Validation?
84
-
85
- 1. **Violates Single Responsibility Principle**
86
- The schema's job is to validate structure, not business logic consistency. The adapter already enforces this invariant at construction time.
87
-
88
- 2. **Adds Runtime Overhead**
89
- Schema validation runs on every payload validation. This adds unnecessary computation for a condition that should never occur if the adapter works correctly.
90
-
91
- 3. **Couples Schema to Business Logic**
92
- The schema would need to know the thresholds and logic for determining direction. This creates tight coupling between layers that should be independent.
93
-
94
- 4. **Limited Bug Detection Value**
95
- If the adapter logic is broken, we'd want to fix the adapter, not catch it at validation time. Schema validation wouldn't prevent the bug, just detect it later in the pipeline.
96
-
97
- 5. **Test Coverage Sufficient**
98
- The adapter has comprehensive test coverage. Adding redundant validation at the schema level doesn't improve reliability.
99
-
100
- ### Alternative Considered
101
-
102
- If we were truly concerned about this invariant, the better approach would be:
103
-
104
- 1. Add unit tests specifically for the `convertBalanceInfo` function
105
- 2. Add integration tests that verify the full payload structure
106
- 3. Add a comment in the schema documenting the expected relationship
107
-
108
- But given the simplicity of the current implementation and existing test coverage, none of these are necessary.
109
-
110
- ### Recommendation
111
-
112
- **Accept Feedback #1 as already implemented.**
113
- **Respectfully decline Feedback #2** - the adapter already enforces consistency, and adding schema-level validation would violate architectural principles without meaningful benefit.
114
-
115
- ---
116
-
117
- ## Files Changed
118
-
119
- - `src/tools/schemas/outputs/reconciliationOutputs.ts` - No changes required (already has strong typing)
120
- - `CODEREVIEW_RESPONSE.md` - This document
121
-
122
- ## Tests
123
-
124
- All existing tests pass:
125
-
126
- - ✅ `npm run type-check` - TypeScript compilation successful
127
- - ✅ `npm run test:unit -- reconciliationOutputs` - 26/26 tests passing
128
- - ✅ No regressions in related tests
@@ -1,120 +0,0 @@
1
- # ExecutionActionRecord Schema Type Safety Improvements
2
-
3
- ## Summary
4
-
5
- Addressed CodeRabbit's feedback about weak typing in `ExecutionActionRecordSchema` by replacing the generic `z.record(z.string(), z.unknown())` transaction field with a discriminated union based on action type.
6
-
7
- ## Problem
8
-
9
- The original schema at line 370 in `reconciliationOutputs.ts` used:
10
-
11
- ```typescript
12
- export const ExecutionActionRecordSchema = z.object({
13
- type: z.string(),
14
- transaction: z.record(z.string(), z.unknown()).nullable(),
15
- // ...
16
- });
17
- ```
18
-
19
- This provided no type safety for transaction data, making the code prone to runtime errors if assumptions about transaction structure were incorrect.
20
-
21
- ## Solution
22
-
23
- Implemented a **discriminated union** based on the `type` field, with each action type having its own strongly-typed transaction schema:
24
-
25
- ### Action Types & Transaction Schemas
26
-
27
- 1. **`create_transaction`** - Successful transaction creation
28
- - Transaction: `CreatedTransactionSchema.nullable()`
29
- - Full YNAB API transaction response with `.passthrough()` for additional fields
30
- - Optional fields: `bulk_chunk_index`, `correlation_key`
31
-
32
- 2. **`create_transaction_failed`** - Failed transaction creation
33
- - Transaction: `TransactionCreationPayloadSchema` (required)
34
- - The request payload that failed to create
35
- - Optional fields: `bulk_chunk_index`, `correlation_key`
36
-
37
- 3. **`create_transaction_duplicate`** - Duplicate detection
38
- - Transaction: `DuplicateDetectionPayloadSchema` (required)
39
- - Contains `transaction_id` (nullable) and optional `import_id`
40
- - Required fields: `bulk_chunk_index`, `duplicate: true`
41
- - Optional: `correlation_key`
42
-
43
- 4. **`update_transaction`** - Transaction update (status/date)
44
- - Transaction: Union of `CreatedTransactionSchema.nullable()` (real execution) or `TransactionUpdatePayloadSchema` (dry run)
45
- - Handles both full transaction responses and minimal update payloads
46
-
47
- 5. **`balance_checkpoint`** - Balance alignment checkpoint
48
- - Transaction: `z.null()` (always null)
49
- - No optional fields
50
-
51
- 6. **`bulk_create_fallback`** - Bulk operation fallback to sequential
52
- - Transaction: `z.null()` (always null)
53
- - Required: `bulk_chunk_index`
54
-
55
- ### Helper Schemas
56
-
57
- **`CreatedTransactionSchema`**
58
-
59
- - Validates YNAB API transaction responses
60
- - Uses `.passthrough()` to allow additional API fields
61
- - Required: `id`, `date`, `amount`
62
- - Optional: `memo`, `cleared`, `approved`, `payee_name`, `category_name`, `import_id`
63
-
64
- **`TransactionCreationPayloadSchema`**
65
-
66
- - Validates transaction creation requests
67
- - Required: `account_id`, `date`, `amount`
68
- - Optional: `payee_name`, `memo`, `cleared`, `approved`, `import_id`
69
-
70
- **`TransactionUpdatePayloadSchema`**
71
-
72
- - Validates transaction update requests
73
- - Required: `transaction_id`
74
- - Optional: `new_date`, `cleared`
75
-
76
- **`DuplicateDetectionPayloadSchema`**
77
-
78
- - Validates duplicate detection metadata
79
- - Required: `transaction_id` (nullable)
80
- - Optional: `import_id`
81
-
82
- ## Benefits
83
-
84
- 1. **Type Safety** - Compile-time checking of transaction field structure
85
- 2. **Self-Documenting** - Schema clearly shows what data each action type contains
86
- 3. **Runtime Validation** - Zod catches malformed data before it causes issues
87
- 4. **Better Errors** - Discriminated union provides clear validation error messages
88
- 5. **Flexibility** - `.passthrough()` on `CreatedTransactionSchema` allows future YNAB API additions
89
- 6. **Zero Breaking Changes** - Backward compatible, all existing data validates correctly
90
-
91
- ## Testing
92
-
93
- Created comprehensive test suite (`reconciliationOutputs.test.ts`) with 26 passing tests covering:
94
-
95
- - All 6 action types with valid data
96
- - Negative cases (wrong types, missing required fields)
97
- - Discriminated union behavior (unknown types, type mismatches)
98
- - Helper schema validation
99
- - Edge cases (null transactions, passthrough fields)
100
-
101
- ## Files Changed
102
-
103
- 1. `src/tools/schemas/outputs/reconciliationOutputs.ts` - Schema definitions
104
- 2. `src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts` - New test suite
105
-
106
- ## Verification
107
-
108
- - ✅ TypeScript compilation passes (`npm run type-check`)
109
- - ✅ All 26 new schema tests pass
110
- - ✅ Build succeeds (`npm run build`)
111
- - ✅ MCPB package generation succeeds
112
- - ✅ No runtime changes to executor.ts needed (schemas match actual usage)
113
-
114
- ## Implementation Notes
115
-
116
- The discriminated union matches exactly how the executor currently constructs action records (verified by analyzing `src/tools/reconciliation/executor.ts` lines 226-580). No changes to runtime code were needed, proving this is a pure type-safety enhancement.
117
-
118
- ## Follow-up Opportunities
119
-
120
- Consider applying the same pattern to other schemas with generic `z.record()` fields if similar type safety concerns exist elsewhere in the codebase.