@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
|
@@ -2,46 +2,77 @@
|
|
|
2
2
|
* Test utilities for comprehensive testing suite
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { expect } from "vitest";
|
|
7
|
+
import type { z } from "zod";
|
|
8
|
+
import type { YNABMCPServer } from "../server/YNABMCPServer.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Test environment configuration
|
|
12
12
|
*/
|
|
13
13
|
export interface TestConfig {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
hasRealApiKey: boolean;
|
|
15
|
+
testBudgetId: string | undefined;
|
|
16
|
+
testAccountId: string | undefined;
|
|
17
|
+
skipE2ETests: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const normalizeAccessToken = (
|
|
21
|
+
token: string | undefined,
|
|
22
|
+
): string | undefined => {
|
|
23
|
+
if (typeof token !== "string") {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const trimmed = token.trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const lowered = trimmed.toLowerCase();
|
|
32
|
+
if (lowered === "undefined" || lowered === "null") {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (lowered === "your_ynab_personal_access_token_here") {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (trimmed === "test-token-for-mocked-tests") {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return trimmed;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const hasRealAccessToken = (token?: string): boolean =>
|
|
48
|
+
!!normalizeAccessToken(token);
|
|
49
|
+
|
|
20
50
|
/**
|
|
21
51
|
* Get test configuration from environment
|
|
22
52
|
*/
|
|
23
53
|
export function getTestConfig(): TestConfig {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
const hasRealApiKey = hasRealAccessToken(process.env["YNAB_ACCESS_TOKEN"]);
|
|
55
|
+
const skipE2ETests =
|
|
56
|
+
process.env["SKIP_E2E_TESTS"] === "true" || !hasRealApiKey;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
hasRealApiKey,
|
|
60
|
+
testBudgetId: process.env["TEST_BUDGET_ID"],
|
|
61
|
+
testAccountId: process.env["TEST_ACCOUNT_ID"],
|
|
62
|
+
skipE2ETests,
|
|
63
|
+
};
|
|
33
64
|
}
|
|
34
65
|
|
|
35
66
|
/**
|
|
36
67
|
* Create a test server instance
|
|
37
68
|
*/
|
|
38
69
|
export async function createTestServer(): Promise<YNABMCPServer> {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
70
|
+
if (!hasRealAccessToken(process.env["YNAB_ACCESS_TOKEN"])) {
|
|
71
|
+
throw new Error("YNAB_ACCESS_TOKEN is required for testing");
|
|
72
|
+
}
|
|
42
73
|
|
|
43
|
-
|
|
44
|
-
|
|
74
|
+
const { YNABMCPServer } = await import("../server/YNABMCPServer.js");
|
|
75
|
+
return new YNABMCPServer();
|
|
45
76
|
}
|
|
46
77
|
|
|
47
78
|
/**
|
|
@@ -53,25 +84,25 @@ export async function createTestServer(): Promise<YNABMCPServer> {
|
|
|
53
84
|
* @throws Error if the `YNAB_ACCESS_TOKEN` environment variable is not set.
|
|
54
85
|
*/
|
|
55
86
|
export async function executeToolCall(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
server: YNABMCPServer,
|
|
88
|
+
toolName: string,
|
|
89
|
+
args: Record<string, any> = {},
|
|
59
90
|
): Promise<CallToolResult> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
const accessToken = normalizeAccessToken(process.env["YNAB_ACCESS_TOKEN"]);
|
|
92
|
+
if (!accessToken) {
|
|
93
|
+
throw new Error("YNAB_ACCESS_TOKEN is required for tool execution");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const registry = server.getToolRegistry();
|
|
97
|
+
const normalizedName = toolName.startsWith("ynab:")
|
|
98
|
+
? toolName.slice(toolName.indexOf(":") + 1)
|
|
99
|
+
: toolName;
|
|
100
|
+
|
|
101
|
+
return await registry.executeTool({
|
|
102
|
+
name: normalizedName,
|
|
103
|
+
accessToken,
|
|
104
|
+
arguments: args,
|
|
105
|
+
});
|
|
75
106
|
}
|
|
76
107
|
|
|
77
108
|
/**
|
|
@@ -83,16 +114,16 @@ export async function executeToolCall(
|
|
|
83
114
|
* @param result - The CallToolResult to validate
|
|
84
115
|
*/
|
|
85
116
|
export function validateToolResult(result: CallToolResult): void {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
expect(result).toBeDefined();
|
|
118
|
+
expect(result.content).toBeDefined();
|
|
119
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
120
|
+
expect(result.content.length).toBeGreaterThan(0);
|
|
121
|
+
|
|
122
|
+
for (const content of result.content) {
|
|
123
|
+
if (content.type === "text") {
|
|
124
|
+
expect(typeof content.text).toBe("string");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
96
127
|
}
|
|
97
128
|
|
|
98
129
|
/**
|
|
@@ -104,21 +135,21 @@ export function validateToolResult(result: CallToolResult): void {
|
|
|
104
135
|
* @returns `true` if the first text content parses as a JSON object with an `error` field, `false` otherwise.
|
|
105
136
|
*/
|
|
106
137
|
export function isErrorResult(result: CallToolResult): boolean {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
if (!result.content || result.content.length === 0) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const content = result.content[0];
|
|
143
|
+
if (!content || content.type !== "text") {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(content.text);
|
|
149
|
+
return parsed && typeof parsed === "object" && "error" in parsed;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
122
153
|
}
|
|
123
154
|
|
|
124
155
|
/**
|
|
@@ -127,55 +158,56 @@ export function isErrorResult(result: CallToolResult): boolean {
|
|
|
127
158
|
* @returns A human-readable error message extracted from `result` (falls back to the raw text), or an empty string if no error message is available.
|
|
128
159
|
*/
|
|
129
160
|
export function getErrorMessage(result: CallToolResult): string {
|
|
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
|
-
|
|
161
|
+
if (!isErrorResult(result)) {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const content = result.content[0];
|
|
166
|
+
if (!content || content.type !== "text") {
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(content.text);
|
|
172
|
+
const error = parsed?.error;
|
|
173
|
+
if (typeof error === "string" && error.length > 0) {
|
|
174
|
+
return error;
|
|
175
|
+
}
|
|
176
|
+
if (error && typeof error === "object") {
|
|
177
|
+
const { message, userMessage, details, suggestions, name } =
|
|
178
|
+
error as Record<string, unknown>;
|
|
179
|
+
|
|
180
|
+
let errorMessage = "";
|
|
181
|
+
if (typeof message === "string" && message.length > 0) {
|
|
182
|
+
errorMessage = message;
|
|
183
|
+
} else if (typeof userMessage === "string" && userMessage.length > 0) {
|
|
184
|
+
errorMessage = userMessage;
|
|
185
|
+
} else if (typeof name === "string" && name.length > 0) {
|
|
186
|
+
errorMessage = name;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Include details if available
|
|
190
|
+
if (typeof details === "string" && details.length > 0) {
|
|
191
|
+
errorMessage += `\n\n${details}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Include suggestions if available
|
|
195
|
+
if (Array.isArray(suggestions) && suggestions.length > 0) {
|
|
196
|
+
const suggestionsText = suggestions
|
|
197
|
+
.filter((s) => typeof s === "string")
|
|
198
|
+
.map((s, i) => `${i + 1}. ${s}`)
|
|
199
|
+
.join("\n");
|
|
200
|
+
if (suggestionsText) {
|
|
201
|
+
errorMessage += `\n\nSuggestions:\n${suggestionsText}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (errorMessage) return errorMessage;
|
|
206
|
+
}
|
|
207
|
+
return content.text;
|
|
208
|
+
} catch {
|
|
209
|
+
return content.text;
|
|
210
|
+
}
|
|
179
211
|
}
|
|
180
212
|
|
|
181
213
|
/**
|
|
@@ -186,38 +218,38 @@ export function getErrorMessage(result: CallToolResult): string {
|
|
|
186
218
|
* @throws If the result has no text content, the text is not a string, or the text cannot be parsed as JSON.
|
|
187
219
|
*/
|
|
188
220
|
export function parseToolResult<T = any>(result: CallToolResult): T {
|
|
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
|
+
validateToolResult(result);
|
|
222
|
+
const content = result.content[0];
|
|
223
|
+
if (!content || content.type !== "text") {
|
|
224
|
+
throw new Error("No text content in tool result");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const text = content.text;
|
|
228
|
+
if (typeof text !== "string") {
|
|
229
|
+
throw new Error("Tool result text is not a string");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(text) as Record<string, unknown> | T;
|
|
234
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
235
|
+
const record = parsed as Record<string, unknown>;
|
|
236
|
+
|
|
237
|
+
// Handle backward compatibility - ensure both success and data properties exist
|
|
238
|
+
if ("data" in record) {
|
|
239
|
+
// Response already has data property, add success if missing
|
|
240
|
+
if (!("success" in record)) {
|
|
241
|
+
return { success: true, ...record } as T;
|
|
242
|
+
}
|
|
243
|
+
return parsed as T;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Response doesn't have data property, wrap it and add success
|
|
247
|
+
return { success: true, data: parsed } as T;
|
|
248
|
+
}
|
|
249
|
+
return parsed as T;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
throw new Error(`Failed to parse tool result as JSON: ${error}`);
|
|
252
|
+
}
|
|
221
253
|
}
|
|
222
254
|
|
|
223
255
|
/**
|
|
@@ -243,128 +275,136 @@ export function parseToolResult<T = any>(result: CallToolResult): T {
|
|
|
243
275
|
* ```
|
|
244
276
|
*/
|
|
245
277
|
export function validateOutputSchema(
|
|
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
|
-
|
|
278
|
+
server: YNABMCPServer,
|
|
279
|
+
toolName: string,
|
|
280
|
+
result: CallToolResult,
|
|
281
|
+
): {
|
|
282
|
+
valid: boolean;
|
|
283
|
+
hasSchema: boolean;
|
|
284
|
+
errors?: string[];
|
|
285
|
+
data?: unknown;
|
|
286
|
+
note?: string;
|
|
287
|
+
} {
|
|
288
|
+
// Get tool definitions from registry
|
|
289
|
+
const registry = server.getToolRegistry();
|
|
290
|
+
const toolDefinitions = registry.getToolDefinitions();
|
|
291
|
+
const toolDef = toolDefinitions.find((t) => t.name === toolName);
|
|
292
|
+
|
|
293
|
+
if (!toolDef) {
|
|
294
|
+
return {
|
|
295
|
+
valid: false,
|
|
296
|
+
hasSchema: false,
|
|
297
|
+
errors: [
|
|
298
|
+
`Tool '${toolName}' not found in registry for schema validation`,
|
|
299
|
+
],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!toolDef.outputSchema) {
|
|
304
|
+
return {
|
|
305
|
+
valid: true,
|
|
306
|
+
hasSchema: false,
|
|
307
|
+
note: `Tool '${toolName}' does not define an outputSchema (schemas are optional)`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Parse JSON response from result's text content
|
|
312
|
+
let parsedData: unknown;
|
|
313
|
+
try {
|
|
314
|
+
const textContent = result.content.find((c) => c.type === "text");
|
|
315
|
+
if (!textContent || textContent.type !== "text") {
|
|
316
|
+
return {
|
|
317
|
+
valid: false,
|
|
318
|
+
hasSchema: true,
|
|
319
|
+
errors: ["Result does not contain text content"],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
parsedData = JSON.parse(textContent.text);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return {
|
|
325
|
+
valid: false,
|
|
326
|
+
hasSchema: true,
|
|
327
|
+
errors: [`Failed to parse result as JSON: ${error}`],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Validate against output schema
|
|
332
|
+
const validationResult = toolDef.outputSchema.safeParse(parsedData);
|
|
333
|
+
|
|
334
|
+
if (!validationResult.success) {
|
|
335
|
+
// Extract detailed error messages from Zod errors
|
|
336
|
+
const zodError = validationResult.error as z.ZodError;
|
|
337
|
+
const errors = zodError.issues.map((err: z.ZodIssue) => {
|
|
338
|
+
const path = err.path.join(".");
|
|
339
|
+
return `${path ? `${path}: ` : ""}${err.message}`;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
valid: false,
|
|
344
|
+
hasSchema: true,
|
|
345
|
+
errors,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
valid: true,
|
|
351
|
+
hasSchema: true,
|
|
352
|
+
data: validationResult.data,
|
|
353
|
+
};
|
|
314
354
|
}
|
|
315
355
|
|
|
316
356
|
/**
|
|
317
357
|
* Wait for a condition to be true
|
|
318
358
|
*/
|
|
319
359
|
export async function waitFor(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
360
|
+
condition: () => boolean | Promise<boolean>,
|
|
361
|
+
timeout = 5000,
|
|
362
|
+
interval = 100,
|
|
323
363
|
): Promise<void> {
|
|
324
|
-
|
|
364
|
+
const start = Date.now();
|
|
325
365
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
366
|
+
while (Date.now() - start < timeout) {
|
|
367
|
+
if (await condition()) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
371
|
+
}
|
|
332
372
|
|
|
333
|
-
|
|
373
|
+
throw new Error(`Condition not met within ${timeout}ms`);
|
|
334
374
|
}
|
|
335
375
|
|
|
336
376
|
/**
|
|
337
377
|
* Generate test data
|
|
338
378
|
*/
|
|
339
379
|
export const TestData = {
|
|
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
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Generate a unique test account name
|
|
382
|
+
*/
|
|
383
|
+
generateAccountName(): string {
|
|
384
|
+
return `Test Account ${Date.now()}`;
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Generate a test transaction
|
|
389
|
+
*/
|
|
390
|
+
generateTransaction(accountId: string, categoryId?: string) {
|
|
391
|
+
return {
|
|
392
|
+
account_id: accountId,
|
|
393
|
+
category_id: categoryId,
|
|
394
|
+
payee_name: `Test Payee ${Date.now()}`,
|
|
395
|
+
amount: -5000, // $5.00 outflow
|
|
396
|
+
memo: `Test transaction ${Date.now()}`,
|
|
397
|
+
date: new Date().toISOString().split("T")[0], // Today's date
|
|
398
|
+
cleared: "uncleared" as const,
|
|
399
|
+
};
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Generate test amounts in milliunits
|
|
404
|
+
*/
|
|
405
|
+
generateAmount(dollars: number): number {
|
|
406
|
+
return Math.round(dollars * 1000);
|
|
407
|
+
},
|
|
368
408
|
};
|
|
369
409
|
|
|
370
410
|
/**
|
|
@@ -376,55 +416,99 @@ export const TestData = {
|
|
|
376
416
|
* @returns `true` if the provided value represents a rate limit error, `false` otherwise.
|
|
377
417
|
*/
|
|
378
418
|
export function isRateLimitError(error: any): boolean {
|
|
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
|
-
|
|
419
|
+
if (!error) return false;
|
|
420
|
+
|
|
421
|
+
// Check various ways rate limit errors can appear
|
|
422
|
+
const errorString = error.toString
|
|
423
|
+
? error.toString().toLowerCase()
|
|
424
|
+
: String(error).toLowerCase();
|
|
425
|
+
const hasRateLimitMessage =
|
|
426
|
+
errorString.includes("rate limit") ||
|
|
427
|
+
errorString.includes("too many requests") ||
|
|
428
|
+
errorString.includes("429");
|
|
429
|
+
|
|
430
|
+
// Check for HTML responses (YNAB API returns HTML when rate limited or down)
|
|
431
|
+
// This manifests as JSON parsing errors with messages like:
|
|
432
|
+
// "SyntaxError: Unexpected token '<', "<style>..." is not valid JSON"
|
|
433
|
+
const looksLikeHTML =
|
|
434
|
+
errorString.includes("<html") ||
|
|
435
|
+
errorString.includes("<head") ||
|
|
436
|
+
errorString.includes("<body") ||
|
|
437
|
+
errorString.includes("<!doctype html");
|
|
438
|
+
|
|
439
|
+
const isHTMLResponse =
|
|
440
|
+
looksLikeHTML ||
|
|
441
|
+
((errorString.includes("syntaxerror") ||
|
|
442
|
+
errorString.includes("unexpected token")) &&
|
|
443
|
+
(errorString.includes("'<'") ||
|
|
444
|
+
errorString.includes('"<"') ||
|
|
445
|
+
errorString.includes("<style") ||
|
|
446
|
+
errorString.includes("not valid json")));
|
|
447
|
+
|
|
448
|
+
// Check for VALIDATION_ERROR from output schema validation failures
|
|
449
|
+
// These occur when YNAB API returns error responses instead of data during rate limiting
|
|
450
|
+
// Example: {"code":"VALIDATION_ERROR","message":"Output validation failed for list_budgets",...}
|
|
451
|
+
const isValidationError =
|
|
452
|
+
errorString.includes("validation_error") ||
|
|
453
|
+
errorString.includes("output validation failed");
|
|
454
|
+
|
|
455
|
+
// Check error object properties
|
|
456
|
+
if (error && typeof error === "object") {
|
|
457
|
+
const statusCode = error.status || error.statusCode || error.error?.id;
|
|
458
|
+
if (statusCode === 429 || statusCode === "429") return true;
|
|
459
|
+
|
|
460
|
+
const errorName = error.name || error.error?.name || "";
|
|
461
|
+
if (errorName.toLowerCase().includes("too_many_requests")) return true;
|
|
462
|
+
|
|
463
|
+
// Check nested error objects
|
|
464
|
+
if (error.error && typeof error.error === "object") {
|
|
465
|
+
const nestedId = error.error.id;
|
|
466
|
+
const nestedName = error.error.name;
|
|
467
|
+
if (nestedId === "429" || nestedName === "too_many_requests") return true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return hasRateLimitMessage || isHTMLResponse || isValidationError;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Determine whether a value represents an authentication/authorization error.
|
|
476
|
+
*/
|
|
477
|
+
export function isAuthError(error: any): boolean {
|
|
478
|
+
if (!error) return false;
|
|
479
|
+
|
|
480
|
+
const errorString = error.toString
|
|
481
|
+
? error.toString().toLowerCase()
|
|
482
|
+
: String(error).toLowerCase();
|
|
483
|
+
const hasAuthMessage =
|
|
484
|
+
errorString.includes("unauthorized") ||
|
|
485
|
+
errorString.includes("invalid or expired") ||
|
|
486
|
+
errorString.includes("authenticationerror") ||
|
|
487
|
+
errorString.includes("forbidden") ||
|
|
488
|
+
errorString.includes("401") ||
|
|
489
|
+
errorString.includes("403");
|
|
490
|
+
|
|
491
|
+
if (error && typeof error === "object") {
|
|
492
|
+
const statusCode = error.status || error.statusCode || error.error?.id;
|
|
493
|
+
if (
|
|
494
|
+
statusCode === 401 ||
|
|
495
|
+
statusCode === "401" ||
|
|
496
|
+
statusCode === 403 ||
|
|
497
|
+
statusCode === "403"
|
|
498
|
+
) {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const errorName = error.name || error.error?.name || "";
|
|
503
|
+
if (
|
|
504
|
+
errorName.toLowerCase().includes("unauthorized") ||
|
|
505
|
+
errorName.toLowerCase().includes("authentication")
|
|
506
|
+
) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return hasAuthMessage;
|
|
428
512
|
}
|
|
429
513
|
|
|
430
514
|
/**
|
|
@@ -432,49 +516,54 @@ export function isRateLimitError(error: any): boolean {
|
|
|
432
516
|
* Returns true and optionally skips the current test when a rate limit is found.
|
|
433
517
|
*/
|
|
434
518
|
export function skipIfRateLimitedResult(
|
|
435
|
-
|
|
436
|
-
|
|
519
|
+
result: CallToolResult,
|
|
520
|
+
context?: { skip?: () => void },
|
|
437
521
|
): boolean {
|
|
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
|
-
|
|
522
|
+
const markSkipped = () => {
|
|
523
|
+
console.warn(
|
|
524
|
+
"[rate-limit] Skipping test due to YNAB API rate limit (embedded payload)",
|
|
525
|
+
);
|
|
526
|
+
context?.skip?.();
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const content = result.content?.[0];
|
|
530
|
+
const text = content && content.type === "text" ? content.text : "";
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const parsed =
|
|
534
|
+
typeof text === "string" && text.trim().length > 0
|
|
535
|
+
? JSON.parse(text)
|
|
536
|
+
: null;
|
|
537
|
+
const candidates: any[] = [];
|
|
538
|
+
|
|
539
|
+
if (parsed && typeof parsed === "object") {
|
|
540
|
+
const parsedObj = parsed as Record<string, unknown>;
|
|
541
|
+
if ("error" in parsedObj) candidates.push(parsedObj["error"]);
|
|
542
|
+
if ("data" in parsedObj) {
|
|
543
|
+
const data = (parsedObj as any).data;
|
|
544
|
+
candidates.push(data?.error ?? data);
|
|
545
|
+
}
|
|
546
|
+
candidates.push(parsed);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (typeof text === "string") {
|
|
550
|
+
candidates.push(text);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
for (const candidate of candidates) {
|
|
554
|
+
if (isRateLimitError(candidate)) {
|
|
555
|
+
markSkipped();
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch (parseError) {
|
|
560
|
+
if (isRateLimitError(parseError) || isRateLimitError(text)) {
|
|
561
|
+
markSkipped();
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
// If parsing fails and no rate limit markers are present, fall through.
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
478
567
|
}
|
|
479
568
|
|
|
480
569
|
/**
|
|
@@ -485,25 +574,28 @@ export function skipIfRateLimitedResult(
|
|
|
485
574
|
* @returns The value returned by `testFn` or `undefined` if the test was skipped due to a rate limit.
|
|
486
575
|
*/
|
|
487
576
|
export async function skipOnRateLimit<T>(
|
|
488
|
-
|
|
489
|
-
|
|
577
|
+
testFn: () => Promise<T>,
|
|
578
|
+
context?: { skip: () => void },
|
|
490
579
|
): Promise<T | undefined> {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
580
|
+
try {
|
|
581
|
+
return await testFn();
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (isRateLimitError(error) || isAuthError(error)) {
|
|
584
|
+
// Log the skip reason
|
|
585
|
+
const reason = isAuthError(error)
|
|
586
|
+
? "authentication failure"
|
|
587
|
+
: "YNAB API rate limit";
|
|
588
|
+
console.warn(`⏭️ Skipping test due to ${reason}`);
|
|
589
|
+
|
|
590
|
+
// Skip the test if context is provided
|
|
591
|
+
if (context?.skip) {
|
|
592
|
+
context.skip();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Return void to satisfy type system
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// Re-throw non-rate-limit errors
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
509
601
|
}
|