@dizzlkheinz/ynab-mcpb 0.15.1 → 0.16.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.
@@ -95,9 +95,9 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
95
95
  date: 0.15,
96
96
  payee: 0.35,
97
97
  },
98
- dateToleranceDays: params.date_tolerance_days ?? 5,
98
+ dateToleranceDays: params.date_tolerance_days ?? 7,
99
99
  amountToleranceMilliunits: (params.amount_tolerance_cents ?? 1) * 10,
100
- autoMatchThreshold: params.auto_match_threshold ?? 90,
100
+ autoMatchThreshold: params.auto_match_threshold ?? 85,
101
101
  suggestedMatchThreshold: params.suggestion_threshold ?? 60,
102
102
  minimumCandidateScore: 40,
103
103
  exactAmountBonus: 10,
@@ -128,6 +128,7 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
128
128
  : params.statement_balance;
129
129
  const budgetResponse = await ynabAPI.budgets.getBudgetById(params.budget_id);
130
130
  const currencyCode = budgetResponse.data.budget?.currency_format?.iso_code ?? 'USD';
131
+ const narrativeNotes = [];
131
132
  const dateFormat = mapCsvDateFormatToHint(params.csv_format?.date_format);
132
133
  const csvOptions = {
133
134
  columns: {
@@ -151,6 +152,9 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
151
152
  ...(params.csv_format?.has_header !== undefined && {
152
153
  header: params.csv_format.has_header,
153
154
  }),
155
+ ...(params.csv_format?.delimiter !== undefined && {
156
+ delimiter: params.csv_format.delimiter,
157
+ }),
154
158
  };
155
159
  let csvContent = params.csv_data ?? '';
156
160
  if (!csvContent && params.csv_file_path) {
@@ -164,59 +168,58 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
164
168
  throw new Error(`Failed to read CSV file at path ${params.csv_file_path}: ${message}`);
165
169
  }
166
170
  }
171
+ if (!csvContent.trim()) {
172
+ throw new Error('CSV content is empty after reading the provided source.');
173
+ }
174
+ let rawCsvResult;
175
+ try {
176
+ rawCsvResult = parseCSV(csvContent, {
177
+ ...csvOptions,
178
+ invertAmounts: false,
179
+ });
180
+ }
181
+ catch (error) {
182
+ const message = error instanceof Error && error.message
183
+ ? error.message
184
+ : 'Unknown error while parsing CSV';
185
+ throw new Error(`Failed to parse CSV data: ${message}`);
186
+ }
167
187
  let sinceDate;
168
- let parseResult;
188
+ let dateWindowSource;
169
189
  if (params.statement_start_date) {
170
190
  sinceDate = new Date(params.statement_start_date);
191
+ dateWindowSource = 'statement_start_date';
192
+ }
193
+ else if (rawCsvResult.transactions.length > 0) {
194
+ sinceDate = inferSinceDateFromTransactions(rawCsvResult.transactions);
195
+ dateWindowSource = 'csv_min_date_with_buffer';
171
196
  }
172
197
  else {
173
- try {
174
- parseResult = parseCSV(csvContent, {
175
- ...csvOptions,
176
- invertAmounts: shouldInvertBankAmounts,
177
- });
178
- if (parseResult.transactions.length > 0) {
179
- const dates = parseResult.transactions
180
- .map((t) => new Date(t.date).getTime())
181
- .filter((t) => !isNaN(t));
182
- if (dates.length > 0) {
183
- const minTime = Math.min(...dates);
184
- const minDateObj = new Date(minTime);
185
- minDateObj.setDate(minDateObj.getDate() - 7);
186
- sinceDate = minDateObj;
187
- }
188
- else {
189
- sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
190
- }
191
- }
192
- else {
193
- sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
194
- }
195
- }
196
- catch {
197
- sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
198
- }
198
+ sinceDate = fallbackSinceDate();
199
+ dateWindowSource = 'fallback_90_days';
200
+ narrativeNotes.push('CSV contained no parsable transactions for date detection; fetched the last 90 days from YNAB.');
199
201
  }
200
202
  const sinceDateString = sinceDate.toISOString().split('T')[0];
201
203
  const transactionsResult = forceFullRefresh
202
204
  ? await deltaFetcher.fetchTransactionsByAccountFull(params.budget_id, params.account_id, sinceDateString)
203
205
  : await deltaFetcher.fetchTransactionsByAccount(params.budget_id, params.account_id, sinceDateString);
204
206
  const ynabTransactions = transactionsResult.data;
207
+ const normalizedYNAB = normalizeYNABTransactions(ynabTransactions);
205
208
  let finalInvertAmounts = shouldInvertBankAmounts;
206
- if (params.invert_bank_amounts === undefined && csvContent) {
207
- const rawParseResult = parseCSV(csvContent, {
208
- ...csvOptions,
209
- invertAmounts: false,
210
- });
211
- if (rawParseResult.transactions.length > 0 && ynabTransactions.length > 0) {
212
- const normalizedYNAB = normalizeYNABTransactions(ynabTransactions);
213
- const needsInversion = detectSignInversion(rawParseResult.transactions, normalizedYNAB);
214
- finalInvertAmounts = needsInversion;
215
- if (needsInversion !== shouldInvertBankAmounts && parseResult) {
216
- parseResult = undefined;
217
- }
209
+ if (params.invert_bank_amounts === undefined &&
210
+ rawCsvResult.transactions.length > 0 &&
211
+ normalizedYNAB.length > 0) {
212
+ const needsInversion = detectSignInversion(rawCsvResult.transactions, normalizedYNAB);
213
+ if (needsInversion !== finalInvertAmounts) {
214
+ narrativeNotes.push(needsInversion
215
+ ? 'Detected bank CSV amounts opposite YNAB; inverting bank amounts for matching.'
216
+ : 'Detected bank CSV amounts already align with YNAB; using CSV amounts as-is.');
218
217
  }
218
+ finalInvertAmounts = needsInversion;
219
219
  }
220
+ const parseResult = finalInvertAmounts === false
221
+ ? rawCsvResult
222
+ : parseCSV(csvContent, { ...csvOptions, invertAmounts: finalInvertAmounts });
220
223
  const auditMetadata = {
221
224
  data_freshness: getDataFreshness(transactionsResult, forceFullRefresh),
222
225
  data_source: getAuditDataSource(transactionsResult, forceFullRefresh),
@@ -229,13 +232,28 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
229
232
  transactions_cached: transactionsResult.wasCached,
230
233
  delta_merge_applied: transactionsResult.usedDelta,
231
234
  },
235
+ csv: {
236
+ rows: parseResult.meta.totalRows,
237
+ transactions: parseResult.transactions.length,
238
+ errors: parseResult.errors.length,
239
+ warnings: parseResult.warnings.length,
240
+ delimiter: parseResult.meta.detectedDelimiter,
241
+ },
242
+ date_window: {
243
+ since_date: sinceDateString,
244
+ source: dateWindowSource,
245
+ },
246
+ sign_detection: {
247
+ default_invert: shouldInvertBankAmounts,
248
+ final_invert: finalInvertAmounts,
249
+ },
232
250
  };
233
251
  const initialAccount = {
234
252
  balance: accountData.balance,
235
253
  cleared_balance: accountData.cleared_balance,
236
254
  uncleared_balance: accountData.uncleared_balance,
237
255
  };
238
- const analysis = analyzeReconciliation(parseResult ?? csvContent, params.csv_file_path, ynabTransactions, adjustedStatementBalance, config, currencyCode, params.account_id, params.budget_id, finalInvertAmounts, csvOptions, initialAccount);
256
+ const analysis = analyzeReconciliation(parseResult, params.csv_file_path, ynabTransactions, adjustedStatementBalance, config, currencyCode, params.account_id, params.budget_id, finalInvertAmounts, csvOptions, initialAccount);
239
257
  let executionData;
240
258
  const wantsBalanceVerification = Boolean(params.statement_date);
241
259
  const shouldExecute = params.auto_create_transactions ||
@@ -265,6 +283,9 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
265
283
  if (csvFormatForPayload !== undefined) {
266
284
  adapterOptions.csvFormat = csvFormatForPayload;
267
285
  }
286
+ if (narrativeNotes.length > 0) {
287
+ adapterOptions.notes = narrativeNotes;
288
+ }
268
289
  const payload = buildReconciliationPayload(analysis, adapterOptions, executionData);
269
290
  const responseData = {
270
291
  human: payload.human,
@@ -323,3 +344,22 @@ function mapCsvFormatForPayload(format) {
323
344
  payee_column: coerceString(format.description_column, '') ?? null,
324
345
  };
325
346
  }
347
+ function fallbackSinceDate() {
348
+ const date = new Date();
349
+ date.setDate(date.getDate() - 90);
350
+ return date;
351
+ }
352
+ function inferSinceDateFromTransactions(transactions) {
353
+ if (transactions.length === 0) {
354
+ return fallbackSinceDate();
355
+ }
356
+ const timestamps = transactions
357
+ .map((t) => new Date(t.date).getTime())
358
+ .filter((time) => !Number.isNaN(time));
359
+ if (timestamps.length === 0) {
360
+ return fallbackSinceDate();
361
+ }
362
+ const minDate = new Date(Math.min(...timestamps));
363
+ minDate.setDate(minDate.getDate() - 7);
364
+ return minDate;
365
+ }
@@ -7,6 +7,7 @@ export interface ReportFormatterOptions {
7
7
  includeDetailedMatches?: boolean | undefined;
8
8
  maxUnmatchedToShow?: number | undefined;
9
9
  maxInsightsToShow?: number | undefined;
10
+ notes?: string[] | undefined;
10
11
  }
11
12
  export declare function formatHumanReadableReport(analysis: ReconciliationAnalysis, options?: ReportFormatterOptions, execution?: LegacyReconciliationResult): string;
12
13
  export declare function formatBalanceInfo(balance: BalanceInfo): string;
@@ -1,7 +1,11 @@
1
+ const SECTION_DIVIDER = '-'.repeat(60);
1
2
  export function formatHumanReadableReport(analysis, options = {}, execution) {
2
3
  const accountLabel = options.accountName ?? 'Account';
3
4
  const sections = [];
4
5
  sections.push(formatHeader(accountLabel, analysis));
6
+ if (options.notes && options.notes.length > 0) {
7
+ sections.push(formatNotesSection(options.notes));
8
+ }
5
9
  sections.push(formatBalanceSection(analysis.balance_info, analysis.summary));
6
10
  sections.push(formatTransactionAnalysisSection(analysis, options));
7
11
  if (analysis.insights.length > 0) {
@@ -15,44 +19,53 @@ export function formatHumanReadableReport(analysis, options = {}, execution) {
15
19
  }
16
20
  function formatHeader(accountName, analysis) {
17
21
  const lines = [];
18
- lines.push(`📊 ${accountName} Reconciliation Report`);
19
- lines.push('═'.repeat(60));
22
+ lines.push(`${accountName} Reconciliation Report`);
23
+ lines.push(SECTION_DIVIDER);
20
24
  lines.push(`Statement Period: ${analysis.summary.statement_date_range}`);
21
25
  return lines.join('\n');
22
26
  }
27
+ function formatNotesSection(notes) {
28
+ const lines = [];
29
+ lines.push('Notes');
30
+ lines.push(SECTION_DIVIDER);
31
+ for (const note of notes) {
32
+ lines.push(`- ${note}`);
33
+ }
34
+ return lines.join('\n');
35
+ }
23
36
  function formatBalanceSection(balanceInfo, summary) {
24
37
  const lines = [];
25
- lines.push('BALANCE CHECK');
26
- lines.push('═'.repeat(60));
27
- lines.push(`✓ YNAB Cleared Balance: ${summary.current_cleared_balance.value_display}`);
28
- lines.push(`✓ Statement Balance: ${summary.target_statement_balance.value_display}`);
38
+ lines.push('Balance Check');
39
+ lines.push(SECTION_DIVIDER);
40
+ lines.push(`- YNAB Cleared Balance: ${summary.current_cleared_balance.value_display}`);
41
+ lines.push(`- Statement Balance: ${summary.target_statement_balance.value_display}`);
29
42
  lines.push('');
30
43
  const discrepancyMilli = balanceInfo.discrepancy.value_milliunits;
31
44
  if (discrepancyMilli === 0) {
32
- lines.push(' BALANCES MATCH PERFECTLY');
45
+ lines.push('Balances match perfectly.');
33
46
  }
34
47
  else {
35
48
  const direction = discrepancyMilli > 0 ? 'ynab_higher' : 'bank_higher';
36
49
  const directionLabel = direction === 'ynab_higher'
37
50
  ? 'YNAB shows MORE than statement'
38
51
  : 'Statement shows MORE than YNAB';
39
- lines.push(`❌ DISCREPANCY: ${balanceInfo.discrepancy.value_display}`);
40
- lines.push(` Direction: ${directionLabel}`);
52
+ lines.push(`Discrepancy: ${balanceInfo.discrepancy.value_display}`);
53
+ lines.push(`Direction: ${directionLabel}`);
41
54
  }
42
55
  return lines.join('\n');
43
56
  }
44
57
  function formatTransactionAnalysisSection(analysis, options) {
45
58
  const lines = [];
46
- lines.push('TRANSACTION ANALYSIS');
47
- lines.push('═'.repeat(60));
59
+ lines.push('Transaction Analysis');
60
+ lines.push(SECTION_DIVIDER);
48
61
  const summary = analysis.summary;
49
- lines.push(`✓ Automatically matched: ${summary.auto_matched} of ${summary.bank_transactions_count} transactions`);
50
- lines.push(`✓ Suggested matches: ${summary.suggested_matches}`);
51
- lines.push(`✓ Unmatched bank: ${summary.unmatched_bank}`);
52
- lines.push(`✓ Unmatched YNAB: ${summary.unmatched_ynab}`);
62
+ lines.push(`- Automatically matched: ${summary.auto_matched} of ${summary.bank_transactions_count} transactions`);
63
+ lines.push(`- Suggested matches: ${summary.suggested_matches}`);
64
+ lines.push(`- Unmatched bank: ${summary.unmatched_bank}`);
65
+ lines.push(`- Unmatched YNAB: ${summary.unmatched_ynab}`);
53
66
  if (analysis.unmatched_bank.length > 0) {
54
67
  lines.push('');
55
- lines.push(' UNMATCHED BANK TRANSACTIONS:');
68
+ lines.push('Unmatched bank transactions:');
56
69
  const maxToShow = options.maxUnmatchedToShow ?? 5;
57
70
  const toShow = analysis.unmatched_bank.slice(0, maxToShow);
58
71
  for (const txn of toShow) {
@@ -64,7 +77,7 @@ function formatTransactionAnalysisSection(analysis, options) {
64
77
  }
65
78
  if (analysis.suggested_matches.length > 0) {
66
79
  lines.push('');
67
- lines.push('💡 SUGGESTED MATCHES:');
80
+ lines.push('Suggested matches:');
68
81
  const maxToShow = options.maxUnmatchedToShow ?? 3;
69
82
  const toShow = analysis.suggested_matches.slice(0, maxToShow);
70
83
  for (const match of toShow) {
@@ -94,8 +107,8 @@ function formatAmount(amountMilli) {
94
107
  }
95
108
  function formatInsightsSection(insights, maxToShow = 3) {
96
109
  const lines = [];
97
- lines.push('KEY INSIGHTS');
98
- lines.push('═'.repeat(60));
110
+ lines.push('Key Insights');
111
+ lines.push(SECTION_DIVIDER);
99
112
  const toShow = insights.slice(0, maxToShow);
100
113
  for (const insight of toShow) {
101
114
  const severityIcon = getSeverityIcon(insight.severity);
@@ -117,13 +130,13 @@ function formatInsightsSection(insights, maxToShow = 3) {
117
130
  function getSeverityIcon(severity) {
118
131
  switch (severity) {
119
132
  case 'critical':
120
- return '🚨';
133
+ return '[CRITICAL]';
121
134
  case 'warning':
122
- return '⚠️';
135
+ return '[WARN]';
123
136
  case 'info':
124
- return 'ℹ️';
137
+ return '[INFO]';
125
138
  default:
126
- return '';
139
+ return '[NOTE]';
127
140
  }
128
141
  }
129
142
  function formatEvidenceSummary(evidence) {
@@ -141,19 +154,19 @@ function formatEvidenceSummary(evidence) {
141
154
  }
142
155
  function formatExecutionSection(execution) {
143
156
  const lines = [];
144
- lines.push('EXECUTION SUMMARY');
145
- lines.push('═'.repeat(60));
157
+ lines.push('Execution Summary');
158
+ lines.push(SECTION_DIVIDER);
146
159
  const summary = execution.summary;
147
- lines.push(`• Transactions created: ${summary.transactions_created}`);
148
- lines.push(`• Transactions updated: ${summary.transactions_updated}`);
149
- lines.push(`• Date adjustments: ${summary.dates_adjusted}`);
160
+ lines.push(`Transactions created: ${summary.transactions_created}`);
161
+ lines.push(`Transactions updated: ${summary.transactions_updated}`);
162
+ lines.push(`Date adjustments: ${summary.dates_adjusted}`);
150
163
  if (execution.recommendations.length > 0) {
151
164
  lines.push('');
152
165
  lines.push('Recommendations:');
153
166
  const maxRecs = 3;
154
167
  const toShow = execution.recommendations.slice(0, maxRecs);
155
168
  for (const rec of toShow) {
156
- lines.push(` ${rec}`);
169
+ lines.push(` - ${rec}`);
157
170
  }
158
171
  if (execution.recommendations.length > maxRecs) {
159
172
  lines.push(` ... and ${execution.recommendations.length - maxRecs} more`);
@@ -161,29 +174,29 @@ function formatExecutionSection(execution) {
161
174
  }
162
175
  lines.push('');
163
176
  if (summary.dry_run) {
164
- lines.push('⚠️ Dry run only no YNAB changes were applied.');
177
+ lines.push('NOTE: Dry run only - no YNAB changes were applied.');
165
178
  }
166
179
  else {
167
- lines.push('Changes applied to YNAB. Review structured output for action details.');
180
+ lines.push('Changes applied to YNAB. Review structured output for action details.');
168
181
  }
169
182
  return lines.join('\n');
170
183
  }
171
184
  function formatRecommendationsSection(analysis, execution) {
172
185
  const lines = [];
173
- lines.push('RECOMMENDED ACTIONS');
174
- lines.push('═'.repeat(60));
186
+ lines.push('Recommended Actions');
187
+ lines.push(SECTION_DIVIDER);
175
188
  if (execution && !execution.summary.dry_run) {
176
189
  lines.push('All recommended actions have been applied.');
177
190
  return lines.join('\n');
178
191
  }
179
192
  if (analysis.next_steps.length > 0) {
180
193
  for (const step of analysis.next_steps) {
181
- lines.push(`• ${step}`);
194
+ lines.push(`- ${step}`);
182
195
  }
183
196
  }
184
197
  else {
185
- lines.push('No specific actions recommended.');
186
- lines.push('Review the structured output for detailed match information.');
198
+ lines.push('No specific actions recommended.');
199
+ lines.push('Review the structured output for detailed match information.');
187
200
  }
188
201
  return lines.join('\n');
189
202
  }
@@ -7,6 +7,7 @@ This document provides comprehensive documentation for all tools available in th
7
7
  - [Overview](#overview)
8
8
  - [Authentication](#authentication)
9
9
  - [Data Formats](#data-formats)
10
+ - [MCP Resources](#mcp-resources)
10
11
  - [Budget Management Tools](#budget-management-tools)
11
12
  - [Account Management Tools](#account-management-tools)
12
13
  - [Transaction Management Tools](#transaction-management-tools)
@@ -65,6 +66,149 @@ All YNAB IDs are UUID strings:
65
66
  - Budget ID: `12345678-1234-1234-1234-123456789012`
66
67
  - Account ID: `87654321-4321-4321-4321-210987654321`
67
68
 
69
+ ## MCP Resources
70
+
71
+ **📢 New in v0.16.0**: The YNAB MCP Server now supports MCP resource templates, enabling AI assistants to discover and access YNAB data through standardized URI patterns.
72
+
73
+ ### What are MCP Resources?
74
+
75
+ MCP resources provide a standardized way for AI assistants to access structured data using URI patterns. Unlike tools (which perform actions), resources are read-only data endpoints that can be discovered and accessed dynamically.
76
+
77
+ ### Available Resources
78
+
79
+ #### Static Resources
80
+
81
+ | URI | Description | Returns |
82
+ |-----|-------------|---------|
83
+ | `ynab://budgets` | List all available budgets | Array of budget summaries with IDs, names, and metadata |
84
+ | `ynab://user` | Current user information | User ID and basic profile data |
85
+
86
+ #### Resource Templates (Dynamic URIs)
87
+
88
+ | URI Template | Parameters | Description | Returns |
89
+ |--------------|------------|-------------|---------|
90
+ | `ynab://budgets/{budget_id}` | `budget_id` (UUID) | Detailed budget information | Complete budget object with accounts, categories, months, and settings |
91
+ | `ynab://budgets/{budget_id}/accounts` | `budget_id` (UUID) | List accounts for a budget | Array of all accounts in the specified budget |
92
+ | `ynab://budgets/{budget_id}/accounts/{account_id}` | `budget_id` (UUID)<br>`account_id` (UUID) | Detailed account information | Complete account object with balance, type, and metadata |
93
+
94
+ ### Usage Examples
95
+
96
+ #### Listing Budgets
97
+ ```
98
+ URI: ynab://budgets
99
+ ```
100
+
101
+ **Response:**
102
+ ```json
103
+ {
104
+ "budgets": [
105
+ {
106
+ "id": "12345678-1234-1234-1234-123456789012",
107
+ "name": "My Budget 2025",
108
+ "last_modified_on": "2025-12-01T10:30:00Z",
109
+ "first_month": "2025-01-01",
110
+ "last_month": "2025-12-01",
111
+ "currency_format": {
112
+ "iso_code": "USD",
113
+ "example_format": "$123.45",
114
+ "decimal_digits": 2,
115
+ "decimal_separator": ".",
116
+ "symbol_first": true,
117
+ "group_separator": ",",
118
+ "currency_symbol": "$",
119
+ "display_symbol": true
120
+ }
121
+ }
122
+ ]
123
+ }
124
+ ```
125
+
126
+ #### Getting Budget Details
127
+ ```
128
+ URI: ynab://budgets/12345678-1234-1234-1234-123456789012
129
+ ```
130
+
131
+ **Response:**
132
+ ```json
133
+ {
134
+ "id": "12345678-1234-1234-1234-123456789012",
135
+ "name": "My Budget 2025",
136
+ "accounts": [...],
137
+ "categories": [...],
138
+ "months": [...],
139
+ "date_format": {"format": "DD/MM/YYYY"},
140
+ "currency_format": {...}
141
+ }
142
+ ```
143
+
144
+ #### Listing Budget Accounts
145
+ ```
146
+ URI: ynab://budgets/12345678-1234-1234-1234-123456789012/accounts
147
+ ```
148
+
149
+ **Response:**
150
+ ```json
151
+ [
152
+ {
153
+ "id": "87654321-4321-4321-4321-210987654321",
154
+ "name": "Checking Account",
155
+ "type": "checking",
156
+ "balance": -1924.37,
157
+ "cleared_balance": -1850.00,
158
+ "uncleared_balance": -74.37,
159
+ "on_budget": true,
160
+ "closed": false
161
+ }
162
+ ]
163
+ ```
164
+
165
+ #### Getting Account Details
166
+ ```
167
+ URI: ynab://budgets/12345678-1234-1234-1234-123456789012/accounts/87654321-4321-4321-4321-210987654321
168
+ ```
169
+
170
+ **Response:**
171
+ ```json
172
+ {
173
+ "id": "87654321-4321-4321-4321-210987654321",
174
+ "name": "Checking Account",
175
+ "type": "checking",
176
+ "balance": -1924.37,
177
+ "cleared_balance": -1850.00,
178
+ "uncleared_balance": -74.37,
179
+ "on_budget": true,
180
+ "closed": false,
181
+ "note": null,
182
+ "transfer_payee_id": "..."
183
+ }
184
+ ```
185
+
186
+ ### Caching
187
+
188
+ 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)
192
+
193
+ ### Resource vs Tool: When to Use What
194
+
195
+ **Use MCP Resources when:**
196
+ - You need to **read** structured data
197
+ - You want to **discover** available budgets or accounts
198
+ - You're building a UI that needs to **list** items
199
+ - You need **cached** data for performance
200
+
201
+ **Use Tools when:**
202
+ - You need to **create**, **update**, or **delete** data
203
+ - You need advanced filtering or querying (e.g., transactions since date)
204
+ - You need to perform **actions** (e.g., reconcile, export)
205
+ - You need the latest **real-time** data
206
+
207
+ **Example:**
208
+ - Get list of budgets: Use `ynab://budgets` resource ✅
209
+ - Get transactions for an account: Use `list_transactions` tool ✅
210
+ - Create a new transaction: Use `create_transaction` tool ✅
211
+
68
212
  ## Budget Management Tools
69
213
 
70
214
  ### list_budgets