@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.
- package/.github/workflows/ci-tests.yml +4 -4
- package/.github/workflows/full-integration.yml +2 -2
- package/.github/workflows/publish.yml +1 -1
- package/.github/workflows/release.yml +2 -2
- package/CHANGELOG.md +12 -1
- package/CLAUDE.md +10 -7
- package/README.md +6 -1
- package/dist/bundle/index.cjs +52 -52
- package/dist/server/YNABMCPServer.d.ts +7 -2
- package/dist/server/YNABMCPServer.js +42 -11
- package/dist/server/cacheManager.js +6 -5
- package/dist/server/completions.d.ts +25 -0
- package/dist/server/completions.js +160 -0
- package/dist/server/config.d.ts +2 -2
- package/dist/server/errorHandler.js +1 -0
- package/dist/server/rateLimiter.js +3 -1
- package/dist/server/resources.d.ts +1 -0
- package/dist/server/resources.js +33 -16
- package/dist/server/securityMiddleware.d.ts +2 -1
- package/dist/server/securityMiddleware.js +1 -0
- package/dist/server/toolRegistry.d.ts +9 -0
- package/dist/server/toolRegistry.js +11 -0
- package/dist/tools/adapters.d.ts +3 -1
- package/dist/tools/adapters.js +1 -0
- package/dist/tools/reconciliation/executor.d.ts +2 -0
- package/dist/tools/reconciliation/executor.js +26 -9
- package/dist/tools/reconciliation/index.d.ts +3 -2
- package/dist/tools/reconciliation/index.js +4 -3
- package/docs/reference/API.md +68 -27
- package/package.json +2 -2
- package/src/__tests__/comprehensive.integration.test.ts +4 -4
- package/src/__tests__/performance.test.ts +1 -2
- package/src/__tests__/smoke.e2e.test.ts +70 -0
- package/src/__tests__/testUtils.ts +2 -113
- package/src/server/YNABMCPServer.ts +64 -10
- package/src/server/__tests__/completions.integration.test.ts +117 -0
- package/src/server/__tests__/completions.test.ts +319 -0
- package/src/server/__tests__/resources.template.test.ts +3 -3
- package/src/server/__tests__/resources.test.ts +3 -3
- package/src/server/__tests__/toolRegistration.test.ts +1 -1
- package/src/server/cacheManager.ts +7 -6
- package/src/server/completions.ts +279 -0
- package/src/server/errorHandler.ts +1 -0
- package/src/server/rateLimiter.ts +4 -1
- package/src/server/resources.ts +49 -13
- package/src/server/securityMiddleware.ts +1 -0
- package/src/server/toolRegistry.ts +42 -0
- package/src/tools/adapters.ts +22 -1
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +12 -26
- package/src/tools/reconciliation/__tests__/executor.progress.test.ts +462 -0
- package/src/tools/reconciliation/__tests__/executor.test.ts +36 -31
- package/src/tools/reconciliation/executor.ts +56 -27
- package/src/tools/reconciliation/index.ts +7 -3
- package/vitest.config.ts +2 -0
- package/src/__tests__/delta.performance.test.ts +0 -80
- 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,
|
|
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:
|
|
330
|
+
handler: adaptWithDeltaAndProgress(handleReconcileAccount),
|
|
330
331
|
defaultArgumentResolver: budgetResolver(),
|
|
331
332
|
metadata: {
|
|
332
333
|
annotations: {
|
package/docs/reference/API.md
CHANGED
|
@@ -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
|
|
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**:
|
|
190
|
-
- **Accounts**:
|
|
191
|
-
- **User info**:
|
|
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\":
|
|
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
|
|
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":
|
|
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\":
|
|
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\": -
|
|
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\": -
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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` (
|
|
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": -
|
|
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": -
|
|
949
|
+
"amount": -125.0,
|
|
923
950
|
"date": "2024-02-01",
|
|
924
951
|
"memo": "Rent and utilities",
|
|
925
952
|
"subtransactions": [
|
|
926
|
-
{ "amount": -
|
|
927
|
-
{ "amount": -
|
|
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
|
|
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": -
|
|
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\":
|
|
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
|
|
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":
|
|
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
|
|
1475
|
-
if (!Number.
|
|
1476
|
-
throw new Error('Amount must be
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
});
|