@dizzlkheinz/ynab-mcpb 0.18.3 → 0.19.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 +17 -0
- package/CLAUDE.md +87 -8
- package/bin/ynab-mcp-server.cjs +2 -2
- package/bin/ynab-mcp-server.js +3 -3
- package/biome.json +39 -0
- package/dist/bundle/index.cjs +67 -67
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -27
- package/dist/server/YNABMCPServer.d.ts +3 -4
- package/dist/server/YNABMCPServer.js +111 -116
- package/dist/server/budgetResolver.d.ts +6 -5
- package/dist/server/budgetResolver.js +46 -36
- package/dist/server/cacheKeys.js +6 -6
- package/dist/server/cacheManager.js +14 -11
- package/dist/server/completions.d.ts +2 -2
- package/dist/server/completions.js +20 -15
- package/dist/server/config.d.ts +10 -5
- package/dist/server/config.js +24 -7
- package/dist/server/deltaCache.d.ts +2 -2
- package/dist/server/deltaCache.js +22 -16
- package/dist/server/deltaCache.merge.d.ts +2 -2
- package/dist/server/diagnostics.d.ts +4 -4
- package/dist/server/diagnostics.js +38 -32
- package/dist/server/errorHandler.d.ts +5 -12
- package/dist/server/errorHandler.js +219 -217
- package/dist/server/prompts.d.ts +2 -2
- package/dist/server/prompts.js +45 -45
- package/dist/server/rateLimiter.js +4 -4
- package/dist/server/requestLogger.d.ts +1 -1
- package/dist/server/requestLogger.js +40 -35
- package/dist/server/resources.d.ts +3 -3
- package/dist/server/resources.js +55 -52
- package/dist/server/responseFormatter.js +6 -6
- package/dist/server/securityMiddleware.d.ts +2 -2
- package/dist/server/securityMiddleware.js +22 -20
- package/dist/server/serverKnowledgeStore.js +1 -1
- package/dist/server/toolRegistry.d.ts +3 -3
- package/dist/server/toolRegistry.js +47 -40
- package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
- package/dist/tools/__tests__/deltaTestUtils.js +2 -2
- package/dist/tools/accountTools.d.ts +9 -8
- package/dist/tools/accountTools.js +47 -47
- package/dist/tools/adapters.d.ts +13 -8
- package/dist/tools/adapters.js +21 -11
- package/dist/tools/budgetTools.d.ts +8 -7
- package/dist/tools/budgetTools.js +22 -22
- package/dist/tools/categoryTools.d.ts +9 -8
- package/dist/tools/categoryTools.js +68 -59
- package/dist/tools/compareTransactions/formatter.d.ts +3 -3
- package/dist/tools/compareTransactions/formatter.js +9 -9
- package/dist/tools/compareTransactions/index.d.ts +6 -6
- package/dist/tools/compareTransactions/index.js +58 -43
- package/dist/tools/compareTransactions/matcher.d.ts +1 -1
- package/dist/tools/compareTransactions/matcher.js +28 -15
- package/dist/tools/compareTransactions/parser.d.ts +2 -2
- package/dist/tools/compareTransactions/parser.js +144 -138
- package/dist/tools/compareTransactions/types.d.ts +4 -4
- package/dist/tools/compareTransactions.d.ts +1 -1
- package/dist/tools/compareTransactions.js +1 -1
- package/dist/tools/deltaFetcher.d.ts +2 -2
- package/dist/tools/deltaFetcher.js +16 -15
- package/dist/tools/deltaSupport.d.ts +4 -4
- package/dist/tools/deltaSupport.js +35 -41
- package/dist/tools/exportTransactions.d.ts +5 -4
- package/dist/tools/exportTransactions.js +61 -59
- package/dist/tools/monthTools.d.ts +7 -6
- package/dist/tools/monthTools.js +31 -29
- package/dist/tools/payeeTools.d.ts +7 -6
- package/dist/tools/payeeTools.js +28 -28
- package/dist/tools/reconcileAdapter.d.ts +2 -2
- package/dist/tools/reconcileAdapter.js +21 -11
- package/dist/tools/reconciliation/analyzer.d.ts +4 -4
- package/dist/tools/reconciliation/analyzer.js +136 -57
- package/dist/tools/reconciliation/csvParser.d.ts +3 -3
- package/dist/tools/reconciliation/csvParser.js +128 -104
- package/dist/tools/reconciliation/executor.d.ts +4 -4
- package/dist/tools/reconciliation/executor.js +148 -109
- package/dist/tools/reconciliation/index.d.ts +10 -10
- package/dist/tools/reconciliation/index.js +96 -83
- package/dist/tools/reconciliation/matcher.d.ts +3 -3
- package/dist/tools/reconciliation/matcher.js +17 -16
- package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
- package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
- package/dist/tools/reconciliation/recommendationEngine.js +40 -40
- package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
- package/dist/tools/reconciliation/reportFormatter.js +79 -54
- package/dist/tools/reconciliation/signDetector.d.ts +1 -1
- package/dist/tools/reconciliation/types.d.ts +19 -16
- package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
- package/dist/tools/schemas/common.d.ts +1 -1
- package/dist/tools/schemas/common.js +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
- package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
- package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
- package/dist/tools/schemas/outputs/index.d.ts +14 -14
- package/dist/tools/schemas/outputs/index.js +14 -14
- package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
- package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
- package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
- package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
- package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
- package/dist/tools/schemas/shared/commonOutputs.js +15 -9
- package/dist/tools/transactionReadTools.d.ts +11 -0
- package/dist/tools/transactionReadTools.js +202 -0
- package/dist/tools/transactionSchemas.d.ts +309 -0
- package/dist/tools/transactionSchemas.js +235 -0
- package/dist/tools/transactionTools.d.ts +6 -302
- package/dist/tools/transactionTools.js +7 -2054
- package/dist/tools/transactionUtils.d.ts +31 -0
- package/dist/tools/transactionUtils.js +364 -0
- package/dist/tools/transactionWriteTools.d.ts +20 -0
- package/dist/tools/transactionWriteTools.js +1342 -0
- package/dist/tools/utilityTools.d.ts +5 -4
- package/dist/tools/utilityTools.js +11 -11
- package/dist/types/index.d.ts +7 -7
- package/dist/types/index.js +6 -6
- package/dist/types/reconciliation.d.ts +1 -1
- package/dist/types/toolRegistration.d.ts +14 -12
- package/dist/utils/amountUtils.js +1 -1
- package/dist/utils/dateUtils.js +4 -4
- package/dist/utils/errors.d.ts +3 -3
- package/dist/utils/errors.js +4 -4
- package/dist/utils/money.d.ts +2 -2
- package/dist/utils/money.js +8 -8
- package/dist/utils/validationError.d.ts +1 -1
- package/dist/utils/validationError.js +1 -1
- package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
- package/docs/assets/schemas/reconciliation-v2.json +360 -336
- package/docs/plans/2025-12-25-transaction-tools-refactor-design.md +211 -0
- package/docs/plans/2025-12-25-transaction-tools-refactor.md +905 -0
- package/esbuild.config.mjs +53 -50
- package/meta.json +12548 -12548
- package/package.json +98 -109
- package/scripts/analyze-bundle.mjs +33 -30
- package/scripts/create-pr-description.js +169 -120
- package/scripts/run-all-tests.js +205 -0
- package/scripts/run-domain-integration-tests.js +28 -18
- package/scripts/run-generate-mcpb.js +19 -17
- package/scripts/run-throttled-integration-tests.js +92 -83
- package/scripts/test-delta-params.mjs +149 -120
- package/scripts/test-recommendations.ts +36 -32
- package/scripts/tmpTransaction.ts +80 -43
- package/scripts/validate-env.js +98 -91
- package/scripts/verify-build.js +78 -76
- package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
- package/src/__tests__/performance.test.ts +723 -671
- package/src/__tests__/setup.ts +442 -395
- package/src/__tests__/smoke.e2e.test.ts +41 -39
- package/src/__tests__/testRunner.ts +314 -295
- package/src/__tests__/testUtils.ts +456 -364
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
- package/src/index.ts +68 -59
- package/src/server/CLAUDE.md +480 -0
- package/src/server/YNABMCPServer.ts +821 -794
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
- package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
- package/src/server/__tests__/budgetResolver.test.ts +466 -423
- package/src/server/__tests__/cacheManager.test.ts +891 -874
- package/src/server/__tests__/completions.integration.test.ts +115 -106
- package/src/server/__tests__/completions.test.ts +334 -313
- package/src/server/__tests__/config.test.ts +98 -86
- package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
- package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
- package/src/server/__tests__/deltaCache.test.ts +946 -759
- package/src/server/__tests__/diagnostics.test.ts +825 -792
- package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
- package/src/server/__tests__/errorHandler.test.ts +402 -397
- package/src/server/__tests__/prompts.test.ts +424 -347
- package/src/server/__tests__/rateLimiter.test.ts +313 -309
- package/src/server/__tests__/requestLogger.test.ts +443 -403
- package/src/server/__tests__/resources.template.test.ts +196 -185
- package/src/server/__tests__/resources.test.ts +294 -288
- package/src/server/__tests__/security.integration.test.ts +487 -421
- package/src/server/__tests__/securityMiddleware.test.ts +519 -444
- package/src/server/__tests__/server-startup.integration.test.ts +509 -490
- package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
- package/src/server/__tests__/toolRegistration.test.ts +239 -210
- package/src/server/__tests__/toolRegistry.test.ts +907 -845
- package/src/server/budgetResolver.ts +221 -181
- package/src/server/cacheKeys.ts +6 -6
- package/src/server/cacheManager.ts +498 -484
- package/src/server/completions.ts +267 -243
- package/src/server/config.ts +35 -14
- package/src/server/deltaCache.merge.ts +146 -128
- package/src/server/deltaCache.ts +352 -309
- package/src/server/diagnostics.ts +257 -242
- package/src/server/errorHandler.ts +747 -744
- package/src/server/prompts.ts +181 -176
- package/src/server/rateLimiter.ts +131 -129
- package/src/server/requestLogger.ts +350 -322
- package/src/server/resources.ts +442 -374
- package/src/server/responseFormatter.ts +41 -37
- package/src/server/securityMiddleware.ts +223 -205
- package/src/server/serverKnowledgeStore.ts +67 -67
- package/src/server/toolRegistry.ts +508 -474
- package/src/tools/CLAUDE.md +604 -0
- package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
- package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
- package/src/tools/__tests__/accountTools.test.ts +685 -638
- package/src/tools/__tests__/adapters.test.ts +142 -108
- package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
- package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
- package/src/tools/__tests__/budgetTools.test.ts +442 -413
- package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
- package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
- package/src/tools/__tests__/categoryTools.test.ts +656 -625
- package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
- package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
- package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
- package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
- package/src/tools/__tests__/compareTransactions.test.ts +352 -332
- package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
- package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
- package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
- package/src/tools/__tests__/deltaSupport.test.ts +211 -184
- package/src/tools/__tests__/deltaTestUtils.ts +37 -33
- package/src/tools/__tests__/exportTransactions.test.ts +205 -200
- package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
- package/src/tools/__tests__/monthTools.test.ts +561 -512
- package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
- package/src/tools/__tests__/payeeTools.test.ts +486 -434
- package/src/tools/__tests__/transactionSchemas.test.ts +1204 -0
- package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
- package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
- package/src/tools/__tests__/transactionUtils.test.ts +1016 -0
- package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
- package/src/tools/__tests__/utilityTools.test.ts +68 -58
- package/src/tools/accountTools.ts +293 -271
- package/src/tools/adapters.ts +120 -63
- package/src/tools/budgetTools.ts +121 -116
- package/src/tools/categoryTools.ts +379 -339
- package/src/tools/compareTransactions/formatter.ts +131 -119
- package/src/tools/compareTransactions/index.ts +249 -214
- package/src/tools/compareTransactions/matcher.ts +259 -209
- package/src/tools/compareTransactions/parser.ts +517 -487
- package/src/tools/compareTransactions/types.ts +38 -38
- package/src/tools/compareTransactions.ts +1 -1
- package/src/tools/deltaFetcher.ts +281 -260
- package/src/tools/deltaSupport.ts +264 -259
- package/src/tools/exportTransactions.ts +230 -218
- package/src/tools/monthTools.ts +180 -165
- package/src/tools/payeeTools.ts +152 -140
- package/src/tools/reconcileAdapter.ts +297 -246
- package/src/tools/reconciliation/CLAUDE.md +506 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +135 -112
- package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -227
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -335
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
- package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
- package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
- package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
- package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
- package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -986
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -530
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -71
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -58
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +58 -43
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
- package/src/tools/reconciliation/analyzer.ts +582 -406
- package/src/tools/reconciliation/csvParser.ts +656 -609
- package/src/tools/reconciliation/executor.ts +1290 -1128
- package/src/tools/reconciliation/index.ts +580 -528
- package/src/tools/reconciliation/matcher.ts +256 -240
- package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
- package/src/tools/reconciliation/recommendationEngine.ts +357 -345
- package/src/tools/reconciliation/reportFormatter.ts +349 -276
- package/src/tools/reconciliation/signDetector.ts +89 -83
- package/src/tools/reconciliation/types.ts +164 -153
- package/src/tools/reconciliation/ynabAdapter.ts +17 -15
- package/src/tools/schemas/CLAUDE.md +546 -0
- package/src/tools/schemas/common.ts +1 -1
- package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
- package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
- package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
- package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
- package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
- package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
- package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
- package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
- package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
- package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
- package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
- package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
- package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
- package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
- package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
- package/src/tools/schemas/outputs/index.ts +163 -163
- package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
- package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
- package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
- package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
- package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
- package/src/tools/schemas/shared/commonOutputs.ts +27 -19
- package/src/tools/toolCategories.ts +114 -114
- package/src/tools/transactionReadTools.ts +327 -0
- package/src/tools/transactionSchemas.ts +484 -0
- package/src/tools/transactionTools.ts +107 -2990
- package/src/tools/transactionUtils.ts +621 -0
- package/src/tools/transactionWriteTools.ts +2110 -0
- package/src/tools/utilityTools.ts +46 -41
- package/src/types/CLAUDE.md +477 -0
- package/src/types/__tests__/index.test.ts +51 -51
- package/src/types/index.ts +43 -39
- package/src/types/integration-tests.d.ts +26 -26
- package/src/types/reconciliation.ts +29 -29
- package/src/types/toolAnnotations.ts +30 -30
- package/src/types/toolRegistration.ts +43 -32
- package/src/utils/CLAUDE.md +508 -0
- package/src/utils/__tests__/dateUtils.test.ts +174 -168
- package/src/utils/__tests__/money.test.ts +193 -187
- package/src/utils/amountUtils.ts +5 -5
- package/src/utils/baseError.ts +5 -5
- package/src/utils/dateUtils.ts +29 -26
- package/src/utils/errors.ts +14 -14
- package/src/utils/money.ts +66 -52
- package/src/utils/validationError.ts +1 -1
- package/tsconfig.json +29 -29
- package/tsconfig.prod.json +16 -16
- package/vitest-reporters/split-json-reporter.ts +247 -204
- package/vitest.config.ts +99 -95
- package/.prettierignore +0 -10
- package/.prettierrc.json +0 -10
- package/eslint.config.js +0 -49
|
@@ -1,901 +1,937 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { skipOnRateLimit } from "../../__tests__/testUtils.js";
|
|
3
|
+
import { cacheManager } from "../../server/cacheManager.js";
|
|
4
|
+
import { responseFormatter } from "../../server/responseFormatter.js";
|
|
5
|
+
import { ValidationError } from "../../types/index.js";
|
|
6
|
+
import { YNABMCPServer } from "../YNABMCPServer.js";
|
|
7
|
+
import type { ToolRegistry } from "../toolRegistry.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Real YNAB API tests using token from .env (YNAB_ACCESS_TOKEN)
|
|
11
11
|
* Skips if YNAB_ACCESS_TOKEN is not set or if SKIP_E2E_TESTS is true
|
|
12
12
|
*/
|
|
13
|
-
const hasToken = !!process.env
|
|
14
|
-
const shouldSkip = process.env
|
|
13
|
+
const hasToken = !!process.env.YNAB_ACCESS_TOKEN;
|
|
14
|
+
const shouldSkip = process.env.SKIP_E2E_TESTS === "true" || !hasToken;
|
|
15
15
|
const describeIntegration = shouldSkip ? describe.skip : describe;
|
|
16
16
|
|
|
17
|
-
describeIntegration(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
17
|
+
describeIntegration("YNABMCPServer", () => {
|
|
18
|
+
const originalEnv = process.env;
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// Don't restore env completely, keep the API key loaded
|
|
22
|
+
Object.keys(process.env).forEach((key) => {
|
|
23
|
+
if (key !== "YNAB_ACCESS_TOKEN" && key !== "YNAB_BUDGET_ID") {
|
|
24
|
+
if (originalEnv[key] !== undefined) {
|
|
25
|
+
process.env[key] = originalEnv[key];
|
|
26
|
+
} else {
|
|
27
|
+
process.env[key] = undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("Constructor and Environment Validation", () => {
|
|
34
|
+
it(
|
|
35
|
+
"should create server instance with valid access token",
|
|
36
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
37
|
+
() => {
|
|
38
|
+
const server = new YNABMCPServer();
|
|
39
|
+
expect(server).toBeInstanceOf(YNABMCPServer);
|
|
40
|
+
expect(server.getYNABAPI()).toBeDefined();
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
it(
|
|
45
|
+
"should throw ValidationError when YNAB_ACCESS_TOKEN is missing",
|
|
46
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
47
|
+
() => {
|
|
48
|
+
const originalToken = process.env.YNAB_ACCESS_TOKEN;
|
|
49
|
+
process.env.YNAB_ACCESS_TOKEN = undefined;
|
|
50
|
+
|
|
51
|
+
expect(() => new YNABMCPServer()).toThrow(/YNAB_ACCESS_TOKEN/i);
|
|
52
|
+
|
|
53
|
+
// Restore token
|
|
54
|
+
process.env.YNAB_ACCESS_TOKEN = originalToken;
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
it(
|
|
59
|
+
"should throw ValidationError when YNAB_ACCESS_TOKEN is empty string",
|
|
60
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
61
|
+
() => {
|
|
62
|
+
const originalToken = process.env.YNAB_ACCESS_TOKEN;
|
|
63
|
+
process.env.YNAB_ACCESS_TOKEN = "";
|
|
64
|
+
|
|
65
|
+
expect(() => new YNABMCPServer()).toThrow(
|
|
66
|
+
"YNAB_ACCESS_TOKEN must be a non-empty string",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Restore token
|
|
70
|
+
process.env.YNAB_ACCESS_TOKEN = originalToken;
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
it(
|
|
75
|
+
"should throw ValidationError when YNAB_ACCESS_TOKEN is only whitespace",
|
|
76
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
77
|
+
() => {
|
|
78
|
+
const originalToken = process.env.YNAB_ACCESS_TOKEN;
|
|
79
|
+
process.env.YNAB_ACCESS_TOKEN = " ";
|
|
80
|
+
|
|
81
|
+
expect(() => new YNABMCPServer()).toThrow(
|
|
82
|
+
"YNAB_ACCESS_TOKEN must be a non-empty string",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Restore token
|
|
86
|
+
process.env.YNAB_ACCESS_TOKEN = originalToken;
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
it(
|
|
91
|
+
"should trim whitespace from access token",
|
|
92
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
93
|
+
() => {
|
|
94
|
+
const originalToken = process.env.YNAB_ACCESS_TOKEN;
|
|
95
|
+
process.env.YNAB_ACCESS_TOKEN = ` ${originalToken} `;
|
|
96
|
+
|
|
97
|
+
const server = new YNABMCPServer();
|
|
98
|
+
expect(server).toBeInstanceOf(YNABMCPServer);
|
|
99
|
+
|
|
100
|
+
// Restore token
|
|
101
|
+
process.env.YNAB_ACCESS_TOKEN = originalToken;
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("Real YNAB API Integration", () => {
|
|
107
|
+
let server: YNABMCPServer;
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
server = new YNABMCPServer(false); // Don't exit on error in tests
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it(
|
|
114
|
+
"should successfully validate real YNAB token",
|
|
115
|
+
{ meta: { tier: "core", domain: "server" } },
|
|
116
|
+
async (ctx) => {
|
|
117
|
+
await skipOnRateLimit(async () => {
|
|
118
|
+
const isValid = await server.validateToken();
|
|
119
|
+
expect(isValid).toBe(true);
|
|
120
|
+
}, ctx);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
it(
|
|
125
|
+
"should successfully get user information",
|
|
126
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
127
|
+
async (ctx) => {
|
|
128
|
+
await skipOnRateLimit(async () => {
|
|
129
|
+
// Verify we can get user info
|
|
130
|
+
const ynabAPI = server.getYNABAPI();
|
|
131
|
+
const userResponse = await ynabAPI.user.getUser();
|
|
132
|
+
|
|
133
|
+
expect(userResponse.data.user).toBeDefined();
|
|
134
|
+
expect(userResponse.data.user.id).toBeDefined();
|
|
135
|
+
console.warn(
|
|
136
|
+
`✅ Connected to YNAB user: ${userResponse.data.user.id}`,
|
|
137
|
+
);
|
|
138
|
+
}, ctx);
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
it(
|
|
143
|
+
"should successfully get budgets",
|
|
144
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
145
|
+
async (ctx) => {
|
|
146
|
+
await skipOnRateLimit(async () => {
|
|
147
|
+
const ynabAPI = server.getYNABAPI();
|
|
148
|
+
const budgetsResponse = await ynabAPI.budgets.getBudgets();
|
|
149
|
+
|
|
150
|
+
expect(budgetsResponse.data.budgets).toBeDefined();
|
|
151
|
+
expect(Array.isArray(budgetsResponse.data.budgets)).toBe(true);
|
|
152
|
+
expect(budgetsResponse.data.budgets.length).toBeGreaterThan(0);
|
|
153
|
+
|
|
154
|
+
console.warn(
|
|
155
|
+
`✅ Found ${budgetsResponse.data.budgets.length} budget(s)`,
|
|
156
|
+
);
|
|
157
|
+
budgetsResponse.data.budgets.forEach((budget) => {
|
|
158
|
+
console.warn(` - ${budget.name} (${budget.id})`);
|
|
159
|
+
});
|
|
160
|
+
}, ctx);
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
it(
|
|
165
|
+
"should handle invalid token gracefully",
|
|
166
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
167
|
+
async () => {
|
|
168
|
+
const originalToken = process.env.YNAB_ACCESS_TOKEN;
|
|
169
|
+
process.env.YNAB_ACCESS_TOKEN = "invalid-token-format";
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const invalidServer = new YNABMCPServer(false);
|
|
173
|
+
await expect(invalidServer.validateToken()).rejects.toHaveProperty(
|
|
174
|
+
"name",
|
|
175
|
+
"AuthenticationError",
|
|
176
|
+
);
|
|
177
|
+
} finally {
|
|
178
|
+
// Restore original token
|
|
179
|
+
process.env.YNAB_ACCESS_TOKEN = originalToken;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
it(
|
|
185
|
+
"should successfully start and connect MCP server",
|
|
186
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
187
|
+
async (ctx) => {
|
|
188
|
+
await skipOnRateLimit(async () => {
|
|
189
|
+
// This test verifies the full server startup process
|
|
190
|
+
// Note: We can't fully test the stdio connection in a test environment,
|
|
191
|
+
// but we can verify the server initializes without errors
|
|
192
|
+
|
|
193
|
+
// Validate token first (this may skip if rate limited)
|
|
194
|
+
const isValid = await server.validateToken();
|
|
195
|
+
expect(isValid).toBe(true);
|
|
196
|
+
|
|
197
|
+
// If we get here, token is valid - now test transport connection
|
|
198
|
+
const consoleSpy = vi
|
|
199
|
+
.spyOn(console, "error")
|
|
200
|
+
.mockImplementation(() => {
|
|
201
|
+
// Mock implementation for testing
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
// The run method will attempt to connect
|
|
206
|
+
// In a test environment, the stdio connection will fail, but that's expected
|
|
207
|
+
await server.run();
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// Expected to fail on stdio connection in test environment
|
|
210
|
+
// Token was already validated above, so this error should be transport-related
|
|
211
|
+
expect(error).not.toBeInstanceOf(ValidationError);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
consoleSpy.mockRestore();
|
|
215
|
+
}, ctx);
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
it(
|
|
220
|
+
"should handle multiple rapid API calls without rate limiting issues",
|
|
221
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
222
|
+
async (ctx) => {
|
|
223
|
+
await skipOnRateLimit(async () => {
|
|
224
|
+
// Make multiple validation calls to test rate limiting behavior
|
|
225
|
+
const promises = Array(3)
|
|
226
|
+
.fill(null)
|
|
227
|
+
.map(() => server.validateToken());
|
|
228
|
+
|
|
229
|
+
// All should succeed (YNAB API is generally permissive for user info calls)
|
|
230
|
+
const results = await Promise.all(promises);
|
|
231
|
+
results.forEach((result) => expect(result).toBe(true));
|
|
232
|
+
}, ctx);
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("MCP Server Functionality", () => {
|
|
238
|
+
let server: YNABMCPServer;
|
|
239
|
+
let registry: ToolRegistry;
|
|
240
|
+
|
|
241
|
+
const accessToken = () => {
|
|
242
|
+
const token = process.env.YNAB_ACCESS_TOKEN;
|
|
243
|
+
if (!token) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
"YNAB_ACCESS_TOKEN must be defined for integration tests",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return token;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
beforeEach(() => {
|
|
252
|
+
server = new YNABMCPServer(false);
|
|
253
|
+
registry = (server as unknown as { toolRegistry: ToolRegistry })
|
|
254
|
+
.toolRegistry;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it(
|
|
258
|
+
"should expose registered tools via the registry",
|
|
259
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
260
|
+
() => {
|
|
261
|
+
const tools = registry.listTools();
|
|
262
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
263
|
+
const names = tools.map((tool) => tool.name);
|
|
264
|
+
expect(names).toContain("list_budgets");
|
|
265
|
+
expect(names).toContain("diagnostic_info");
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
it(
|
|
270
|
+
"should execute get_user tool via the registry",
|
|
271
|
+
{ meta: { tier: "core", domain: "server" } },
|
|
272
|
+
async (ctx) => {
|
|
273
|
+
await skipOnRateLimit(async () => {
|
|
274
|
+
const result = await registry.executeTool({
|
|
275
|
+
name: "get_user",
|
|
276
|
+
accessToken: accessToken(),
|
|
277
|
+
arguments: {},
|
|
278
|
+
});
|
|
279
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
280
|
+
|
|
281
|
+
// If response contains an error, throw it so skipOnRateLimit can catch it
|
|
282
|
+
if (payload.error) {
|
|
283
|
+
throw new Error(JSON.stringify(payload.error));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
expect(payload.user?.id).toBeDefined();
|
|
287
|
+
}, ctx);
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
it(
|
|
292
|
+
"should set and retrieve default budget using tools",
|
|
293
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
294
|
+
async (ctx) => {
|
|
295
|
+
await skipOnRateLimit(async () => {
|
|
296
|
+
const budgetsResult = await registry.executeTool({
|
|
297
|
+
name: "list_budgets",
|
|
298
|
+
accessToken: accessToken(),
|
|
299
|
+
arguments: {},
|
|
300
|
+
});
|
|
301
|
+
const budgetsPayload = JSON.parse(
|
|
302
|
+
budgetsResult.content?.[0]?.text ?? "{}",
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// If response contains an error, throw it so skipOnRateLimit can catch it
|
|
306
|
+
if (budgetsPayload.error) {
|
|
307
|
+
throw new Error(JSON.stringify(budgetsPayload.error));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const firstBudget = budgetsPayload.budgets?.[0];
|
|
311
|
+
expect(firstBudget).toBeDefined();
|
|
312
|
+
|
|
313
|
+
await registry.executeTool({
|
|
314
|
+
name: "set_default_budget",
|
|
315
|
+
accessToken: accessToken(),
|
|
316
|
+
arguments: { budget_id: firstBudget.id },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const defaultResult = await registry.executeTool({
|
|
320
|
+
name: "get_default_budget",
|
|
321
|
+
accessToken: accessToken(),
|
|
322
|
+
arguments: {},
|
|
323
|
+
});
|
|
324
|
+
const defaultPayload = JSON.parse(
|
|
325
|
+
defaultResult.content?.[0]?.text ?? "{}",
|
|
326
|
+
);
|
|
327
|
+
expect(defaultPayload.default_budget_id).toBe(firstBudget.id);
|
|
328
|
+
expect(defaultPayload.has_default).toBe(true);
|
|
329
|
+
}, ctx);
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
it(
|
|
334
|
+
"should provide diagnostic info with requested sections",
|
|
335
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
336
|
+
async () => {
|
|
337
|
+
const diagResult = await registry.executeTool({
|
|
338
|
+
name: "diagnostic_info",
|
|
339
|
+
accessToken: accessToken(),
|
|
340
|
+
arguments: {
|
|
341
|
+
include_server: true,
|
|
342
|
+
include_security: true,
|
|
343
|
+
include_cache: true,
|
|
344
|
+
include_memory: false,
|
|
345
|
+
include_environment: false,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
const diagnostics = JSON.parse(diagResult.content?.[0]?.text ?? "{}");
|
|
349
|
+
expect(diagnostics.timestamp).toBeDefined();
|
|
350
|
+
expect(diagnostics.server).toBeDefined();
|
|
351
|
+
expect(diagnostics.security).toBeDefined();
|
|
352
|
+
expect(diagnostics.cache).toBeDefined();
|
|
353
|
+
expect(diagnostics.memory).toBeUndefined();
|
|
354
|
+
expect(diagnostics.environment).toBeUndefined();
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
it(
|
|
359
|
+
"should clear cache using the clear_cache tool",
|
|
360
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
361
|
+
async () => {
|
|
362
|
+
cacheManager.set("test:key", { value: 1 }, 1000);
|
|
363
|
+
const statsBeforeClear = cacheManager.getStats();
|
|
364
|
+
expect(statsBeforeClear.size).toBeGreaterThan(0);
|
|
365
|
+
|
|
366
|
+
await registry.executeTool({
|
|
367
|
+
name: "clear_cache",
|
|
368
|
+
accessToken: accessToken(),
|
|
369
|
+
arguments: {},
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const statsAfterClear = cacheManager.getStats();
|
|
373
|
+
expect(statsAfterClear.size).toBe(0);
|
|
374
|
+
expect(statsAfterClear.hits).toBe(0);
|
|
375
|
+
expect(statsAfterClear.misses).toBe(0);
|
|
376
|
+
expect(statsAfterClear.evictions).toBe(0);
|
|
377
|
+
expect(statsAfterClear.lastCleanup).toBe(null);
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
it(
|
|
382
|
+
"should track cache performance metrics during real tool execution",
|
|
383
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
384
|
+
async () => {
|
|
385
|
+
// Clear cache and capture initial state
|
|
386
|
+
cacheManager.clear();
|
|
387
|
+
|
|
388
|
+
// Manually simulate cache usage that would occur during API calls
|
|
389
|
+
const mockApiResult = { budgets: [{ id: "123", name: "Test Budget" }] };
|
|
390
|
+
cacheManager.set("budgets:list", mockApiResult, 60000);
|
|
391
|
+
|
|
392
|
+
// Test cache hit
|
|
393
|
+
const cachedResult = cacheManager.get("budgets:list");
|
|
394
|
+
expect(cachedResult).toEqual(mockApiResult);
|
|
395
|
+
|
|
396
|
+
// Test cache miss
|
|
397
|
+
const missResult = cacheManager.get("nonexistent:key");
|
|
398
|
+
expect(missResult).toBeNull();
|
|
399
|
+
|
|
400
|
+
const stats = cacheManager.getStats();
|
|
401
|
+
expect(stats.size).toBeGreaterThan(0);
|
|
402
|
+
expect(stats.hits).toBeGreaterThan(0);
|
|
403
|
+
expect(stats.misses).toBeGreaterThan(0);
|
|
404
|
+
expect(stats.hitRate).toBeGreaterThan(0);
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
it(
|
|
409
|
+
"should demonstrate LRU eviction with real cache operations",
|
|
410
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
411
|
+
async () => {
|
|
412
|
+
// This test demonstrates the LRU eviction functionality
|
|
413
|
+
// by creating a temporary cache with a low maxEntries limit
|
|
414
|
+
const originalEnvValue = process.env.YNAB_MCP_CACHE_MAX_ENTRIES;
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
// Set low limit and create a new cache manager instance
|
|
418
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = "2";
|
|
419
|
+
const tempCache = new (
|
|
420
|
+
await import("../cacheManager.js")
|
|
421
|
+
).CacheManager();
|
|
422
|
+
|
|
423
|
+
// Add entries that should trigger eviction
|
|
424
|
+
tempCache.set("test:entry1", { data: "value1" }, 60000);
|
|
425
|
+
tempCache.set("test:entry2", { data: "value2" }, 60000);
|
|
426
|
+
|
|
427
|
+
// This should trigger eviction of entry1 due to LRU policy
|
|
428
|
+
tempCache.set("test:entry3", { data: "value3" }, 60000);
|
|
429
|
+
|
|
430
|
+
const stats = tempCache.getStats();
|
|
431
|
+
// Should have some evictions due to LRU policy
|
|
432
|
+
expect(stats.evictions).toBeGreaterThan(0);
|
|
433
|
+
expect(stats.size).toBeLessThanOrEqual(2);
|
|
434
|
+
} finally {
|
|
435
|
+
// Restore original environment
|
|
436
|
+
if (originalEnvValue !== undefined) {
|
|
437
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = originalEnvValue;
|
|
438
|
+
} else {
|
|
439
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = undefined;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
it(
|
|
446
|
+
"should show cache hit rate improvement with repeated operations",
|
|
447
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
448
|
+
async () => {
|
|
449
|
+
cacheManager.clear();
|
|
450
|
+
|
|
451
|
+
// Manually demonstrate cache hit rate improvement
|
|
452
|
+
cacheManager.set("test:operation1", { data: "result1" }, 60000);
|
|
453
|
+
cacheManager.get("test:operation1"); // Hit
|
|
454
|
+
cacheManager.get("test:nonexistent"); // Miss
|
|
455
|
+
cacheManager.get("test:operation1"); // Hit
|
|
456
|
+
|
|
457
|
+
const finalStats = cacheManager.getStats();
|
|
458
|
+
expect(finalStats.hits).toBeGreaterThan(0);
|
|
459
|
+
expect(finalStats.misses).toBeGreaterThan(0);
|
|
460
|
+
expect(finalStats.hitRate).toBeGreaterThan(0);
|
|
461
|
+
expect(finalStats.hitRate).toBeGreaterThan(0.5); // Should have more hits than misses
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
it(
|
|
466
|
+
"should handle concurrent cache operations correctly",
|
|
467
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
468
|
+
async () => {
|
|
469
|
+
cacheManager.clear();
|
|
470
|
+
|
|
471
|
+
// Simulate concurrent cache operations manually
|
|
472
|
+
cacheManager.set("test:concurrent1", { data: "value1" }, 60000);
|
|
473
|
+
cacheManager.set("test:concurrent2", { data: "value2" }, 60000);
|
|
474
|
+
|
|
475
|
+
// Simulate concurrent reads
|
|
476
|
+
const value1 = cacheManager.get("test:concurrent1");
|
|
477
|
+
const value2 = cacheManager.get("test:concurrent2");
|
|
478
|
+
const nonexistent = cacheManager.get("test:nonexistent");
|
|
479
|
+
|
|
480
|
+
expect(value1).toBeTruthy();
|
|
481
|
+
expect(value2).toBeTruthy();
|
|
482
|
+
expect(nonexistent).toBeNull();
|
|
483
|
+
|
|
484
|
+
// Cache should have handled concurrent requests properly
|
|
485
|
+
const stats = cacheManager.getStats();
|
|
486
|
+
expect(stats.size).toBeGreaterThan(0);
|
|
487
|
+
expect(stats.hits + stats.misses).toBeGreaterThan(0);
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
it(
|
|
492
|
+
"should include enhanced cache metrics in real diagnostic collection",
|
|
493
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
494
|
+
async (ctx) => {
|
|
495
|
+
await skipOnRateLimit(async () => {
|
|
496
|
+
// Generate some real cache activity
|
|
497
|
+
await registry.executeTool({
|
|
498
|
+
name: "list_budgets",
|
|
499
|
+
accessToken: accessToken(),
|
|
500
|
+
arguments: {},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
await registry.executeTool({
|
|
504
|
+
name: "get_user",
|
|
505
|
+
accessToken: accessToken(),
|
|
506
|
+
arguments: {},
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Call diagnostics tool with cache enabled
|
|
510
|
+
const result = await registry.executeTool({
|
|
511
|
+
name: "diagnostic_info",
|
|
512
|
+
accessToken: accessToken(),
|
|
513
|
+
arguments: {
|
|
514
|
+
include_server: false,
|
|
515
|
+
include_memory: false,
|
|
516
|
+
include_environment: false,
|
|
517
|
+
include_security: false,
|
|
518
|
+
include_cache: true,
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const diagnostics = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
523
|
+
|
|
524
|
+
// If response contains an error, throw it so skipOnRateLimit can catch it
|
|
525
|
+
if (diagnostics.error) {
|
|
526
|
+
throw new Error(JSON.stringify(diagnostics.error));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
expect(diagnostics.cache).toBeDefined();
|
|
530
|
+
expect(diagnostics.cache.entries).toEqual(expect.any(Number));
|
|
531
|
+
expect(diagnostics.cache.estimated_size_kb).toEqual(
|
|
532
|
+
expect.any(Number),
|
|
533
|
+
);
|
|
534
|
+
expect(diagnostics.cache.keys).toEqual(expect.any(Array));
|
|
535
|
+
|
|
536
|
+
// Enhanced metrics should be present
|
|
537
|
+
expect(diagnostics.cache.hits).toEqual(expect.any(Number));
|
|
538
|
+
expect(diagnostics.cache.misses).toEqual(expect.any(Number));
|
|
539
|
+
expect(diagnostics.cache.evictions).toEqual(expect.any(Number));
|
|
540
|
+
expect(diagnostics.cache.maxEntries).toEqual(expect.any(Number));
|
|
541
|
+
expect(diagnostics.cache.hitRate).toEqual(
|
|
542
|
+
expect.stringMatching(/^\d+\.\d{2}%$/),
|
|
543
|
+
);
|
|
544
|
+
expect(diagnostics.cache.performance_summary).toEqual(
|
|
545
|
+
expect.stringContaining("Hit rate"),
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
// lastCleanup can be null or a timestamp
|
|
549
|
+
expect(
|
|
550
|
+
diagnostics.cache.lastCleanup === null ||
|
|
551
|
+
typeof diagnostics.cache.lastCleanup === "string",
|
|
552
|
+
).toBe(true);
|
|
553
|
+
}, ctx);
|
|
554
|
+
},
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
it(
|
|
558
|
+
"should configure output formatter via set_output_format tool",
|
|
559
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
560
|
+
async () => {
|
|
561
|
+
const baseline = responseFormatter.format({ probe: true });
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
await registry.executeTool({
|
|
565
|
+
name: "set_output_format",
|
|
566
|
+
accessToken: accessToken(),
|
|
567
|
+
arguments: { default_minify: false, pretty_spaces: 4 },
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const formatted = responseFormatter.format({ probe: true });
|
|
571
|
+
expect(formatted).not.toBe(baseline);
|
|
572
|
+
expect(formatted).toContain("\n");
|
|
573
|
+
} finally {
|
|
574
|
+
await registry.executeTool({
|
|
575
|
+
name: "set_output_format",
|
|
576
|
+
accessToken: accessToken(),
|
|
577
|
+
arguments: { default_minify: true, pretty_spaces: 2 },
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
it(
|
|
584
|
+
"should surface validation errors for invalid inputs",
|
|
585
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
586
|
+
async () => {
|
|
587
|
+
const result = await registry.executeTool({
|
|
588
|
+
name: "get_budget",
|
|
589
|
+
accessToken: accessToken(),
|
|
590
|
+
arguments: {} as Record<string, unknown>,
|
|
591
|
+
});
|
|
592
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
593
|
+
expect(payload.error).toBeDefined();
|
|
594
|
+
expect(payload.error.code).toBe("VALIDATION_ERROR");
|
|
595
|
+
},
|
|
596
|
+
);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
describe("Modular Architecture Integration with Real API", () => {
|
|
600
|
+
let server: YNABMCPServer;
|
|
601
|
+
let registry: ToolRegistry;
|
|
602
|
+
|
|
603
|
+
const accessToken = () => {
|
|
604
|
+
const token = process.env.YNAB_ACCESS_TOKEN;
|
|
605
|
+
if (!token) {
|
|
606
|
+
throw new Error(
|
|
607
|
+
"YNAB_ACCESS_TOKEN must be defined for integration tests",
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return token;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
beforeEach(() => {
|
|
614
|
+
server = new YNABMCPServer(false);
|
|
615
|
+
registry = (server as unknown as { toolRegistry: ToolRegistry })
|
|
616
|
+
.toolRegistry;
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it(
|
|
620
|
+
"should maintain real API functionality after modular refactoring",
|
|
621
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
622
|
+
async (ctx) => {
|
|
623
|
+
await skipOnRateLimit(async () => {
|
|
624
|
+
// Test that the key integration points work with real API calls
|
|
625
|
+
// This verifies that resource manager, diagnostic manager, and other modules
|
|
626
|
+
// properly integrate with the real YNAB API
|
|
627
|
+
|
|
628
|
+
// Test 1: User info via API (tests core YNAB integration)
|
|
629
|
+
const userResult = await registry.executeTool({
|
|
630
|
+
name: "get_user",
|
|
631
|
+
accessToken: accessToken(),
|
|
632
|
+
arguments: {},
|
|
633
|
+
});
|
|
634
|
+
const userPayload = JSON.parse(userResult.content?.[0]?.text ?? "{}");
|
|
635
|
+
|
|
636
|
+
// If response contains an error, throw it so skipOnRateLimit can catch it
|
|
637
|
+
if (userPayload.error) {
|
|
638
|
+
throw new Error(JSON.stringify(userPayload.error));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
expect(userPayload.user).toBeDefined();
|
|
642
|
+
expect(userPayload.user.id).toBeDefined();
|
|
643
|
+
|
|
644
|
+
// Test 2: Budget listing (tests resource-like functionality)
|
|
645
|
+
const budgetsResult = await registry.executeTool({
|
|
646
|
+
name: "list_budgets",
|
|
647
|
+
accessToken: accessToken(),
|
|
648
|
+
arguments: {},
|
|
649
|
+
});
|
|
650
|
+
const budgetsPayload = JSON.parse(
|
|
651
|
+
budgetsResult.content?.[0]?.text ?? "{}",
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
// If response contains an error, throw it so skipOnRateLimit can catch it
|
|
655
|
+
if (budgetsPayload.error) {
|
|
656
|
+
throw new Error(JSON.stringify(budgetsPayload.error));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
expect(budgetsPayload.budgets).toBeDefined();
|
|
660
|
+
expect(Array.isArray(budgetsPayload.budgets)).toBe(true);
|
|
661
|
+
|
|
662
|
+
// Test 3: Diagnostic info (tests diagnostic manager integration)
|
|
663
|
+
const diagResult = await registry.executeTool({
|
|
664
|
+
name: "diagnostic_info",
|
|
665
|
+
accessToken: accessToken(),
|
|
666
|
+
arguments: {
|
|
667
|
+
include_server: true,
|
|
668
|
+
include_memory: false,
|
|
669
|
+
include_environment: false,
|
|
670
|
+
include_security: true,
|
|
671
|
+
include_cache: true,
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
const diagnostics = JSON.parse(diagResult.content?.[0]?.text ?? "{}");
|
|
675
|
+
|
|
676
|
+
// If response contains an error, throw it so skipOnRateLimit can catch it
|
|
677
|
+
if (diagnostics.error) {
|
|
678
|
+
throw new Error(JSON.stringify(diagnostics.error));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
expect(diagnostics.timestamp).toBeDefined();
|
|
682
|
+
expect(diagnostics.server).toBeDefined();
|
|
683
|
+
expect(diagnostics.server.name).toBe("ynab-mcp-server");
|
|
684
|
+
expect(diagnostics.security).toBeDefined();
|
|
685
|
+
expect(diagnostics.cache).toBeDefined();
|
|
686
|
+
}, ctx);
|
|
687
|
+
},
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
it(
|
|
691
|
+
"should handle modular service errors gracefully in integration",
|
|
692
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
693
|
+
async () => {
|
|
694
|
+
// Test error handling through the modules with real API
|
|
695
|
+
const result = await registry.executeTool({
|
|
696
|
+
name: "get_budget",
|
|
697
|
+
accessToken: accessToken(),
|
|
698
|
+
arguments: {} as Record<string, unknown>, // Missing required budget_id
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Should return an error result, not throw an exception
|
|
702
|
+
expect(result.content).toBeDefined();
|
|
703
|
+
expect(result.content[0]).toBeDefined();
|
|
704
|
+
expect(result.content[0].type).toBe("text");
|
|
705
|
+
// Should contain validation error about missing budget_id
|
|
706
|
+
expect(result.content[0].text).toContain("VALIDATION_ERROR");
|
|
707
|
+
expect(result.content[0].text).toContain("budget_id");
|
|
708
|
+
},
|
|
709
|
+
);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
describe("Budget Resolution Integration Tests", () => {
|
|
713
|
+
let server: YNABMCPServer;
|
|
714
|
+
let registry: ToolRegistry;
|
|
715
|
+
|
|
716
|
+
const accessToken = () => {
|
|
717
|
+
const token = process.env.YNAB_ACCESS_TOKEN;
|
|
718
|
+
if (!token) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
"YNAB_ACCESS_TOKEN must be defined for integration tests",
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
return token;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const getFirstAvailableBudgetId = async (): Promise<string> => {
|
|
727
|
+
const result = await registry.executeTool({
|
|
728
|
+
name: "list_budgets",
|
|
729
|
+
accessToken: accessToken(),
|
|
730
|
+
arguments: {},
|
|
731
|
+
});
|
|
732
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
733
|
+
|
|
734
|
+
// If response contains an error, throw it so skipOnRateLimit can catch it
|
|
735
|
+
if (payload.error) {
|
|
736
|
+
throw new Error(JSON.stringify(payload.error));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const firstBudget = payload.budgets?.[0];
|
|
740
|
+
expect(firstBudget?.id).toBeDefined();
|
|
741
|
+
return firstBudget.id as string;
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
beforeEach(() => {
|
|
745
|
+
server = new YNABMCPServer(false);
|
|
746
|
+
registry = (server as unknown as { toolRegistry: ToolRegistry })
|
|
747
|
+
.toolRegistry;
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it(
|
|
751
|
+
"should handle real YNAB API calls with budget resolution errors",
|
|
752
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
753
|
+
async () => {
|
|
754
|
+
// Test with no default budget set - should get standardized error
|
|
755
|
+
const result = await registry.executeTool({
|
|
756
|
+
name: "list_accounts",
|
|
757
|
+
accessToken: accessToken(),
|
|
758
|
+
arguments: {},
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
762
|
+
expect(payload.error).toBeDefined();
|
|
763
|
+
expect(payload.error.code).toBe("VALIDATION_ERROR");
|
|
764
|
+
expect(payload.error.message).toContain(
|
|
765
|
+
"No budget ID provided and no default budget set",
|
|
766
|
+
);
|
|
767
|
+
expect(payload.error.suggestions).toBeDefined();
|
|
768
|
+
},
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
it(
|
|
772
|
+
"should handle real YNAB API calls with invalid budget ID",
|
|
773
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
774
|
+
async () => {
|
|
775
|
+
const invalidBudgetId = "invalid-uuid-format";
|
|
776
|
+
const result = await registry.executeTool({
|
|
777
|
+
name: "list_accounts",
|
|
778
|
+
accessToken: accessToken(),
|
|
779
|
+
arguments: { budget_id: invalidBudgetId },
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
783
|
+
expect(payload.error).toBeDefined();
|
|
784
|
+
expect(payload.error.code).toBe("VALIDATION_ERROR");
|
|
785
|
+
expect(payload.error.message).toContain("Invalid budget ID format");
|
|
786
|
+
expect(payload.error.suggestions).toBeDefined();
|
|
787
|
+
expect(
|
|
788
|
+
payload.error.suggestions.some((s: string) =>
|
|
789
|
+
s.includes("UUID v4 format"),
|
|
790
|
+
),
|
|
791
|
+
).toBe(true);
|
|
792
|
+
},
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
it(
|
|
796
|
+
"should complete end-to-end workflow with real YNAB API after setting default budget",
|
|
797
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
798
|
+
async (ctx) => {
|
|
799
|
+
await skipOnRateLimit(async () => {
|
|
800
|
+
// Step 1: Verify error with no default budget for a tool that requires budget_id
|
|
801
|
+
let result = await registry.executeTool({
|
|
802
|
+
name: "list_accounts",
|
|
803
|
+
accessToken: accessToken(),
|
|
804
|
+
arguments: {}, // No budget_id provided, should use default budget
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
let payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
808
|
+
expect(payload.error).toBeDefined();
|
|
809
|
+
expect(payload.error.code).toBe("VALIDATION_ERROR");
|
|
810
|
+
|
|
811
|
+
// Step 2: Get a valid budget ID and set as default
|
|
812
|
+
const budgetId = await getFirstAvailableBudgetId();
|
|
813
|
+
await registry.executeTool({
|
|
814
|
+
name: "set_default_budget",
|
|
815
|
+
accessToken: accessToken(),
|
|
816
|
+
arguments: { budget_id: budgetId },
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// Step 3: Verify list_accounts now works with real API using default budget
|
|
820
|
+
result = await registry.executeTool({
|
|
821
|
+
name: "list_accounts",
|
|
822
|
+
accessToken: accessToken(),
|
|
823
|
+
arguments: {}, // No budget_id provided, should use default budget now
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
827
|
+
|
|
828
|
+
// If response contains an error, throw it so skipOnRateLimit can catch it
|
|
829
|
+
if (payload.error) {
|
|
830
|
+
throw new Error(JSON.stringify(payload.error));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
expect(payload.error).toBeUndefined();
|
|
834
|
+
expect(payload).toHaveProperty("accounts");
|
|
835
|
+
expect(Array.isArray(payload.accounts)).toBe(true);
|
|
836
|
+
}, ctx);
|
|
837
|
+
},
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
it(
|
|
841
|
+
"should handle real API errors properly with budget resolution",
|
|
842
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
843
|
+
async (ctx) => {
|
|
844
|
+
await skipOnRateLimit(async () => {
|
|
845
|
+
// Use a UUID that is valid format but doesn't exist in YNAB
|
|
846
|
+
const nonExistentButValidUuid =
|
|
847
|
+
"123e4567-e89b-12d3-a456-426614174000";
|
|
848
|
+
|
|
849
|
+
const result = await registry.executeTool({
|
|
850
|
+
name: "list_accounts",
|
|
851
|
+
accessToken: accessToken(),
|
|
852
|
+
arguments: { budget_id: nonExistentButValidUuid },
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
856
|
+
// Should get a YNAB API error (404) not a validation error
|
|
857
|
+
expect(payload.error).toBeDefined();
|
|
858
|
+
expect(payload.error.code).toBe(404); // YNAB NOT_FOUND error
|
|
859
|
+
}, ctx);
|
|
860
|
+
},
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
it(
|
|
864
|
+
"should maintain performance with real API calls and budget resolution",
|
|
865
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
866
|
+
async (ctx) => {
|
|
867
|
+
await skipOnRateLimit(async () => {
|
|
868
|
+
const budgetId = await getFirstAvailableBudgetId();
|
|
869
|
+
await registry.executeTool({
|
|
870
|
+
name: "set_default_budget",
|
|
871
|
+
accessToken: accessToken(),
|
|
872
|
+
arguments: { budget_id: budgetId },
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
const startTime = Date.now();
|
|
876
|
+
|
|
877
|
+
// Make multiple concurrent calls that use budget resolution
|
|
878
|
+
const promises = [
|
|
879
|
+
registry.executeTool({
|
|
880
|
+
name: "list_accounts",
|
|
881
|
+
accessToken: accessToken(),
|
|
882
|
+
arguments: {},
|
|
883
|
+
}),
|
|
884
|
+
registry.executeTool({
|
|
885
|
+
name: "list_categories",
|
|
886
|
+
accessToken: accessToken(),
|
|
887
|
+
arguments: {},
|
|
888
|
+
}),
|
|
889
|
+
registry.executeTool({
|
|
890
|
+
name: "list_payees",
|
|
891
|
+
accessToken: accessToken(),
|
|
892
|
+
arguments: {},
|
|
893
|
+
}),
|
|
894
|
+
];
|
|
895
|
+
|
|
896
|
+
const results = await Promise.all(promises);
|
|
897
|
+
const endTime = Date.now();
|
|
898
|
+
|
|
899
|
+
// All should succeed
|
|
900
|
+
results.forEach((result) => {
|
|
901
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
902
|
+
|
|
903
|
+
// If response contains an error, throw it so skipOnRateLimit can catch it
|
|
904
|
+
if (payload.error) {
|
|
905
|
+
throw new Error(JSON.stringify(payload.error));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
expect(payload.error).toBeUndefined();
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// Should complete reasonably quickly (accounting for network latency)
|
|
912
|
+
expect(endTime - startTime).toBeLessThan(10000); // 10 seconds max for 3 API calls
|
|
913
|
+
}, ctx);
|
|
914
|
+
},
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
it(
|
|
918
|
+
"should handle security middleware with budget resolution errors",
|
|
919
|
+
{ meta: { tier: "domain", domain: "server" } },
|
|
920
|
+
async (ctx) => {
|
|
921
|
+
await skipOnRateLimit(async () => {
|
|
922
|
+
// Test that security middleware still works with budget resolution
|
|
923
|
+
const result = await registry.executeTool({
|
|
924
|
+
name: "list_accounts",
|
|
925
|
+
accessToken: "invalid-token",
|
|
926
|
+
arguments: {},
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
|
|
930
|
+
expect(payload.error).toBeDefined();
|
|
931
|
+
// Should get authentication error, not budget resolution error
|
|
932
|
+
expect(payload.error.code).toBe(401);
|
|
933
|
+
}, ctx);
|
|
934
|
+
},
|
|
935
|
+
);
|
|
936
|
+
});
|
|
901
937
|
});
|