@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.
- package/CHANGELOG.md +36 -0
- package/dist/bundle/index.cjs +53 -52
- package/dist/server/YNABMCPServer.d.ts +2 -6
- package/dist/server/YNABMCPServer.js +5 -1
- package/dist/server/resources.d.ts +17 -13
- package/dist/server/resources.js +237 -48
- package/dist/tools/reconcileAdapter.d.ts +1 -0
- package/dist/tools/reconcileAdapter.js +1 -0
- package/dist/tools/reconciliation/csvParser.d.ts +3 -0
- package/dist/tools/reconciliation/csvParser.js +58 -19
- package/dist/tools/reconciliation/executor.js +47 -1
- package/dist/tools/reconciliation/index.js +82 -42
- package/dist/tools/reconciliation/reportFormatter.d.ts +1 -0
- package/dist/tools/reconciliation/reportFormatter.js +49 -36
- package/docs/reference/API.md +144 -0
- package/docs/technical/reconciliation-system-architecture.md +2251 -0
- package/package.json +1 -1
- package/src/server/YNABMCPServer.ts +7 -0
- package/src/server/__tests__/resources.template.test.ts +198 -0
- package/src/server/__tests__/resources.test.ts +10 -2
- package/src/server/resources.ts +307 -62
- package/src/tools/reconcileAdapter.ts +2 -0
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +23 -23
- package/src/tools/reconciliation/csvParser.ts +84 -18
- package/src/tools/reconciliation/executor.ts +58 -1
- package/src/tools/reconciliation/index.ts +105 -55
- package/src/tools/reconciliation/reportFormatter.ts +55 -37
|
@@ -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 ??
|
|
98
|
+
dateToleranceDays: params.date_tolerance_days ?? 7,
|
|
99
99
|
amountToleranceMilliunits: (params.amount_tolerance_cents ?? 1) * 10,
|
|
100
|
-
autoMatchThreshold: params.auto_match_threshold ??
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 &&
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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(
|
|
19
|
-
lines.push(
|
|
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('
|
|
26
|
-
lines.push(
|
|
27
|
-
lines.push(
|
|
28
|
-
lines.push(
|
|
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('
|
|
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(
|
|
40
|
-
lines.push(`
|
|
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('
|
|
47
|
-
lines.push(
|
|
59
|
+
lines.push('Transaction Analysis');
|
|
60
|
+
lines.push(SECTION_DIVIDER);
|
|
48
61
|
const summary = analysis.summary;
|
|
49
|
-
lines.push(
|
|
50
|
-
lines.push(
|
|
51
|
-
lines.push(
|
|
52
|
-
lines.push(
|
|
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('
|
|
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('
|
|
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('
|
|
98
|
-
lines.push(
|
|
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('
|
|
145
|
-
lines.push(
|
|
157
|
+
lines.push('Execution Summary');
|
|
158
|
+
lines.push(SECTION_DIVIDER);
|
|
146
159
|
const summary = execution.summary;
|
|
147
|
-
lines.push(
|
|
148
|
-
lines.push(
|
|
149
|
-
lines.push(
|
|
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(`
|
|
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('
|
|
177
|
+
lines.push('NOTE: Dry run only - no YNAB changes were applied.');
|
|
165
178
|
}
|
|
166
179
|
else {
|
|
167
|
-
lines.push('
|
|
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('
|
|
174
|
-
lines.push(
|
|
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(
|
|
194
|
+
lines.push(`- ${step}`);
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
else {
|
|
185
|
-
lines.push('
|
|
186
|
-
lines.push('
|
|
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
|
}
|
package/docs/reference/API.md
CHANGED
|
@@ -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
|