@dizzlkheinz/ynab-mcpb 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.chunkhound.json +11 -0
- package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +8 -0
- package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +3 -0
- package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +3 -0
- package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +1 -0
- package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +12 -0
- package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +1 -0
- package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +1 -0
- package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +5 -0
- package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +1453 -0
- package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +8 -0
- package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +9 -0
- package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +1 -0
- package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +5 -0
- package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +81 -0
- package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +610 -0
- package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +1 -0
- package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +1 -0
- package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +1 -0
- package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +3 -0
- package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +8 -0
- package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +5 -0
- package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +1 -0
- package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +9 -0
- package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +1 -0
- package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +13 -0
- package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +14 -0
- package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +5 -0
- package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +5 -0
- package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +5 -0
- package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +3 -0
- package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +1 -0
- package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +1 -0
- package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +5 -0
- package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +5 -0
- package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +8 -0
- package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +8 -0
- package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +5 -0
- package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +1 -0
- package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +1 -0
- package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +1 -0
- package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +1 -0
- package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +1 -0
- package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +5 -0
- package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +1 -0
- package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +1 -0
- package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +1 -0
- package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +3 -0
- package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +8 -0
- package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +1 -0
- package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +1 -0
- package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +8 -0
- package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +3 -0
- package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +5 -0
- package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +1 -0
- package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +1 -0
- package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +5 -0
- package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +1 -0
- package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +14 -0
- package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +1 -0
- package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +5 -0
- package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +5 -0
- package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +11 -0
- package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +1 -0
- package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +8 -0
- package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +1 -0
- package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +3 -0
- package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +3 -0
- package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +8 -0
- package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +1 -0
- package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +5 -0
- package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +1 -0
- package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +1 -0
- package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +1 -0
- package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +1 -0
- package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +3 -0
- package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +8 -0
- package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +1 -0
- package/.dxtignore +57 -0
- package/.env.example +44 -0
- package/.gemini/settings.json +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
- package/.github/ISSUE_TEMPLATE/release_checklist.md +31 -0
- package/.github/pull_request_template.md +41 -0
- package/.github/workflows/ci-tests.yml +41 -0
- package/.github/workflows/claude-code-review.yml +57 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/full-integration.yml +22 -0
- package/.github/workflows/pr-description-check.yml +88 -0
- package/.github/workflows/publish.yml +33 -0
- package/.github/workflows/release.yml +89 -0
- package/.mcpbignore +58 -0
- package/.prettierignore +10 -0
- package/.prettierrc.json +10 -0
- package/ADOS-2-Module-1-Complete-Manual.md +757 -0
- package/AGENTS.md +36 -0
- package/CHANGELOG.md +187 -0
- package/CLAUDE.md +414 -0
- package/CODEREVIEW_RESPONSE.md +128 -0
- package/LICENSE +17 -0
- package/NUL +1 -0
- package/README.md +222 -0
- package/SCHEMA_IMPROVEMENT_SUMMARY.md +120 -0
- package/TESTING_NOTES.md +217 -0
- package/WARP.md +245 -0
- package/accountactivity-merged.csv +149 -0
- package/bin/ynab-mcp-server.cjs +4 -0
- package/bin/ynab-mcp-server.js +8 -0
- package/bundle-analysis.html +13110 -0
- package/dist/bundle/index.cjs +124 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +85 -0
- package/dist/server/YNABMCPServer.d.ts +264 -0
- package/dist/server/YNABMCPServer.js +845 -0
- package/dist/server/budgetResolver.d.ts +15 -0
- package/dist/server/budgetResolver.js +99 -0
- package/dist/server/cacheManager.d.ts +74 -0
- package/dist/server/cacheManager.js +306 -0
- package/dist/server/config.d.ts +3 -0
- package/dist/server/config.js +19 -0
- package/dist/server/deltaCache.d.ts +61 -0
- package/dist/server/deltaCache.js +206 -0
- package/dist/server/deltaCache.merge.d.ts +9 -0
- package/dist/server/deltaCache.merge.js +111 -0
- package/dist/server/diagnostics.d.ts +90 -0
- package/dist/server/diagnostics.js +163 -0
- package/dist/server/errorHandler.d.ts +69 -0
- package/dist/server/errorHandler.js +524 -0
- package/dist/server/prompts.d.ts +31 -0
- package/dist/server/prompts.js +205 -0
- package/dist/server/rateLimiter.d.ts +27 -0
- package/dist/server/rateLimiter.js +82 -0
- package/dist/server/requestLogger.d.ts +62 -0
- package/dist/server/requestLogger.js +190 -0
- package/dist/server/resources.d.ts +39 -0
- package/dist/server/resources.js +85 -0
- package/dist/server/responseFormatter.d.ts +14 -0
- package/dist/server/responseFormatter.js +42 -0
- package/dist/server/securityMiddleware.d.ts +87 -0
- package/dist/server/securityMiddleware.js +117 -0
- package/dist/server/serverKnowledgeStore.d.ts +11 -0
- package/dist/server/serverKnowledgeStore.js +42 -0
- package/dist/server/toolRegistry.d.ts +85 -0
- package/dist/server/toolRegistry.js +272 -0
- package/dist/tools/__tests__/deltaTestUtils.d.ts +18 -0
- package/dist/tools/__tests__/deltaTestUtils.js +26 -0
- package/dist/tools/accountTools.d.ts +37 -0
- package/dist/tools/accountTools.js +175 -0
- package/dist/tools/budgetTools.d.ts +10 -0
- package/dist/tools/budgetTools.js +68 -0
- package/dist/tools/categoryTools.d.ts +27 -0
- package/dist/tools/categoryTools.js +232 -0
- package/dist/tools/compareTransactions/formatter.d.ts +71 -0
- package/dist/tools/compareTransactions/formatter.js +97 -0
- package/dist/tools/compareTransactions/index.d.ts +30 -0
- package/dist/tools/compareTransactions/index.js +160 -0
- package/dist/tools/compareTransactions/matcher.d.ts +12 -0
- package/dist/tools/compareTransactions/matcher.js +140 -0
- package/dist/tools/compareTransactions/parser.d.ts +14 -0
- package/dist/tools/compareTransactions/parser.js +430 -0
- package/dist/tools/compareTransactions/types.d.ts +27 -0
- package/dist/tools/compareTransactions/types.js +1 -0
- package/dist/tools/compareTransactions.d.ts +1 -0
- package/dist/tools/compareTransactions.js +1 -0
- package/dist/tools/deltaFetcher.d.ts +22 -0
- package/dist/tools/deltaFetcher.js +137 -0
- package/dist/tools/deltaSupport.d.ts +20 -0
- package/dist/tools/deltaSupport.js +176 -0
- package/dist/tools/exportTransactions.d.ts +17 -0
- package/dist/tools/exportTransactions.js +191 -0
- package/dist/tools/monthTools.d.ts +16 -0
- package/dist/tools/monthTools.js +107 -0
- package/dist/tools/payeeTools.d.ts +17 -0
- package/dist/tools/payeeTools.js +82 -0
- package/dist/tools/reconcileAdapter.d.ts +25 -0
- package/dist/tools/reconcileAdapter.js +167 -0
- package/dist/tools/reconciliation/analyzer.d.ts +3 -0
- package/dist/tools/reconciliation/analyzer.js +567 -0
- package/dist/tools/reconciliation/executor.d.ts +94 -0
- package/dist/tools/reconciliation/executor.js +611 -0
- package/dist/tools/reconciliation/index.d.ts +54 -0
- package/dist/tools/reconciliation/index.js +249 -0
- package/dist/tools/reconciliation/matcher.d.ts +3 -0
- package/dist/tools/reconciliation/matcher.js +160 -0
- package/dist/tools/reconciliation/payeeNormalizer.d.ts +6 -0
- package/dist/tools/reconciliation/payeeNormalizer.js +77 -0
- package/dist/tools/reconciliation/recommendationEngine.d.ts +2 -0
- package/dist/tools/reconciliation/recommendationEngine.js +273 -0
- package/dist/tools/reconciliation/reportFormatter.d.ts +13 -0
- package/dist/tools/reconciliation/reportFormatter.js +214 -0
- package/dist/tools/reconciliation/types.d.ts +172 -0
- package/dist/tools/reconciliation/types.js +7 -0
- package/dist/tools/schemas/outputs/accountOutputs.d.ts +58 -0
- package/dist/tools/schemas/outputs/accountOutputs.js +24 -0
- package/dist/tools/schemas/outputs/budgetOutputs.d.ts +48 -0
- package/dist/tools/schemas/outputs/budgetOutputs.js +15 -0
- package/dist/tools/schemas/outputs/categoryOutputs.d.ts +93 -0
- package/dist/tools/schemas/outputs/categoryOutputs.js +37 -0
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +269 -0
- package/dist/tools/schemas/outputs/comparisonOutputs.js +181 -0
- package/dist/tools/schemas/outputs/index.d.ts +14 -0
- package/dist/tools/schemas/outputs/index.js +14 -0
- package/dist/tools/schemas/outputs/monthOutputs.d.ts +122 -0
- package/dist/tools/schemas/outputs/monthOutputs.js +51 -0
- package/dist/tools/schemas/outputs/payeeOutputs.d.ts +34 -0
- package/dist/tools/schemas/outputs/payeeOutputs.js +16 -0
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +1275 -0
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +377 -0
- package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +717 -0
- package/dist/tools/schemas/outputs/transactionMutationOutputs.js +260 -0
- package/dist/tools/schemas/outputs/transactionOutputs.d.ts +98 -0
- package/dist/tools/schemas/outputs/transactionOutputs.js +49 -0
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +219 -0
- package/dist/tools/schemas/outputs/utilityOutputs.js +120 -0
- package/dist/tools/schemas/shared/commonOutputs.d.ts +24 -0
- package/dist/tools/schemas/shared/commonOutputs.js +27 -0
- package/dist/tools/toolCategories.d.ts +32 -0
- package/dist/tools/toolCategories.js +32 -0
- package/dist/tools/transactionTools.d.ts +315 -0
- package/dist/tools/transactionTools.js +1722 -0
- package/dist/tools/utilityTools.d.ts +10 -0
- package/dist/tools/utilityTools.js +56 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.js +16 -0
- package/dist/types/toolAnnotations.d.ts +7 -0
- package/dist/types/toolAnnotations.js +1 -0
- package/dist/utils/amountUtils.d.ts +3 -0
- package/dist/utils/amountUtils.js +10 -0
- package/dist/utils/dateUtils.d.ts +9 -0
- package/dist/utils/dateUtils.js +43 -0
- package/dist/utils/money.d.ts +21 -0
- package/dist/utils/money.js +51 -0
- package/docs/README.md +72 -0
- package/docs/assets/examples/reconciliation-with-recommendations.json +68 -0
- package/docs/assets/schemas/reconciliation-v2.json +338 -0
- package/docs/getting-started/CONFIGURATION.md +175 -0
- package/docs/getting-started/INSTALLATION.md +333 -0
- package/docs/getting-started/QUICKSTART.md +282 -0
- package/docs/guides/ARCHITECTURE.md +650 -0
- package/docs/guides/DEPLOYMENT.md +189 -0
- package/docs/guides/INTEGRATION_TESTING.md +730 -0
- package/docs/guides/TESTING.md +591 -0
- package/docs/reconciliation-flow.md +83 -0
- package/docs/reference/API.md +1450 -0
- package/docs/reference/EXAMPLES.md +946 -0
- package/docs/reference/TOOLS.md +348 -0
- package/docs/reference/TROUBLESHOOTING.md +481 -0
- package/esbuild.config.mjs +68 -0
- package/eslint.config.js +49 -0
- package/fix-types.sh +17 -0
- package/meta.json +12550 -0
- package/package.json +105 -0
- package/package.json.tmp +105 -0
- package/scripts/analyze-bundle.mjs +41 -0
- package/scripts/create-pr-description.js +203 -0
- package/scripts/generate-mcpb.ps1 +96 -0
- package/scripts/run-domain-integration-tests.js +33 -0
- package/scripts/run-generate-mcpb.js +29 -0
- package/scripts/run-throttled-integration-tests.js +116 -0
- package/scripts/test-delta-params.mjs +140 -0
- package/scripts/test-recommendations.ts +53 -0
- package/scripts/tmpTransaction.ts +48 -0
- package/scripts/validate-env.js +122 -0
- package/scripts/verify-build.js +105 -0
- package/scripts/watch-and-restart.ps1 +50 -0
- package/src/__tests__/comprehensive.integration.test.ts +1196 -0
- package/src/__tests__/delta.performance.test.ts +80 -0
- package/src/__tests__/performance.test.ts +725 -0
- package/src/__tests__/setup.ts +449 -0
- package/src/__tests__/testRunner.ts +444 -0
- package/src/__tests__/testUtils.ts +563 -0
- package/src/__tests__/workflows.e2e.test.ts +1675 -0
- package/src/index.ts +124 -0
- package/src/server/.gitkeep +1 -0
- package/src/server/YNABMCPServer.ts +1188 -0
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +903 -0
- package/src/server/__tests__/YNABMCPServer.test.ts +894 -0
- package/src/server/__tests__/budgetResolver.test.ts +425 -0
- package/src/server/__tests__/cacheManager.test.ts +880 -0
- package/src/server/__tests__/config.test.ts +166 -0
- package/src/server/__tests__/deltaCache.merge.test.ts +724 -0
- package/src/server/__tests__/deltaCache.swr.test.ts +168 -0
- package/src/server/__tests__/deltaCache.test.ts +774 -0
- package/src/server/__tests__/diagnostics.test.ts +823 -0
- package/src/server/__tests__/errorHandler.integration.test.ts +466 -0
- package/src/server/__tests__/errorHandler.test.ts +416 -0
- package/src/server/__tests__/prompts.test.ts +354 -0
- package/src/server/__tests__/rateLimiter.test.ts +314 -0
- package/src/server/__tests__/requestLogger.test.ts +408 -0
- package/src/server/__tests__/resources.test.ts +299 -0
- package/src/server/__tests__/security.integration.test.ts +426 -0
- package/src/server/__tests__/securityMiddleware.test.ts +449 -0
- package/src/server/__tests__/server-startup.integration.test.ts +477 -0
- package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -0
- package/src/server/__tests__/toolRegistry.test.ts +855 -0
- package/src/server/budgetResolver.ts +235 -0
- package/src/server/cacheManager.ts +503 -0
- package/src/server/config.ts +41 -0
- package/src/server/deltaCache.merge.ts +149 -0
- package/src/server/deltaCache.ts +341 -0
- package/src/server/diagnostics.ts +338 -0
- package/src/server/errorHandler.ts +756 -0
- package/src/server/prompts.ts +291 -0
- package/src/server/rateLimiter.ts +156 -0
- package/src/server/requestLogger.ts +344 -0
- package/src/server/resources.ts +168 -0
- package/src/server/responseFormatter.ts +51 -0
- package/src/server/securityMiddleware.ts +236 -0
- package/src/server/serverKnowledgeStore.ts +91 -0
- package/src/server/toolRegistry.ts +489 -0
- package/src/tools/.gitkeep +1 -0
- package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -0
- package/src/tools/__tests__/accountTools.integration.test.ts +117 -0
- package/src/tools/__tests__/accountTools.test.ts +653 -0
- package/src/tools/__tests__/budgetTools.delta.integration.test.ts +90 -0
- package/src/tools/__tests__/budgetTools.integration.test.ts +134 -0
- package/src/tools/__tests__/budgetTools.test.ts +423 -0
- package/src/tools/__tests__/categoryTools.delta.integration.test.ts +80 -0
- package/src/tools/__tests__/categoryTools.integration.test.ts +295 -0
- package/src/tools/__tests__/categoryTools.test.ts +622 -0
- package/src/tools/__tests__/compareTransactions/formatter.test.ts +486 -0
- package/src/tools/__tests__/compareTransactions/index.test.ts +383 -0
- package/src/tools/__tests__/compareTransactions/matcher.test.ts +410 -0
- package/src/tools/__tests__/compareTransactions/parser.test.ts +764 -0
- package/src/tools/__tests__/compareTransactions.test.ts +342 -0
- package/src/tools/__tests__/compareTransactions.window.test.ts +147 -0
- package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +76 -0
- package/src/tools/__tests__/deltaFetcher.test.ts +270 -0
- package/src/tools/__tests__/deltaSupport.test.ts +188 -0
- package/src/tools/__tests__/deltaTestUtils.ts +46 -0
- package/src/tools/__tests__/exportTransactions.test.ts +213 -0
- package/src/tools/__tests__/monthTools.delta.integration.test.ts +80 -0
- package/src/tools/__tests__/monthTools.integration.test.ts +174 -0
- package/src/tools/__tests__/monthTools.test.ts +523 -0
- package/src/tools/__tests__/payeeTools.delta.integration.test.ts +80 -0
- package/src/tools/__tests__/payeeTools.integration.test.ts +150 -0
- package/src/tools/__tests__/payeeTools.test.ts +445 -0
- package/src/tools/__tests__/transactionTools.integration.test.ts +762 -0
- package/src/tools/__tests__/transactionTools.test.ts +3521 -0
- package/src/tools/__tests__/utilityTools.integration.test.ts +128 -0
- package/src/tools/__tests__/utilityTools.test.ts +205 -0
- package/src/tools/accountTools.ts +283 -0
- package/src/tools/budgetTools.ts +112 -0
- package/src/tools/categoryTools.ts +366 -0
- package/src/tools/compareTransactions/formatter.ts +163 -0
- package/src/tools/compareTransactions/index.ts +228 -0
- package/src/tools/compareTransactions/matcher.ts +240 -0
- package/src/tools/compareTransactions/parser.ts +557 -0
- package/src/tools/compareTransactions/types.ts +60 -0
- package/src/tools/compareTransactions.ts +3 -0
- package/src/tools/deltaFetcher.ts +278 -0
- package/src/tools/deltaSupport.ts +293 -0
- package/src/tools/exportTransactions.ts +273 -0
- package/src/tools/monthTools.ts +164 -0
- package/src/tools/payeeTools.ts +140 -0
- package/src/tools/reconcileAdapter.ts +312 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +122 -0
- package/src/tools/reconciliation/__tests__/adapter.test.ts +234 -0
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +406 -0
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +366 -0
- package/src/tools/reconciliation/__tests__/executor.test.ts +779 -0
- package/src/tools/reconciliation/__tests__/matcher.test.ts +650 -0
- package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +278 -0
- package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +658 -0
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1000 -0
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +151 -0
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +573 -0
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +78 -0
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +47 -0
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +61 -0
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +49 -0
- package/src/tools/reconciliation/analyzer.ts +824 -0
- package/src/tools/reconciliation/executor.ts +880 -0
- package/src/tools/reconciliation/index.ts +400 -0
- package/src/tools/reconciliation/matcher.ts +269 -0
- package/src/tools/reconciliation/payeeNormalizer.ts +167 -0
- package/src/tools/reconciliation/recommendationEngine.ts +506 -0
- package/src/tools/reconciliation/reportFormatter.ts +363 -0
- package/src/tools/reconciliation/types.ts +314 -0
- package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +424 -0
- package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +310 -0
- package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +448 -0
- package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +519 -0
- package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +155 -0
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +288 -0
- package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +478 -0
- package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +370 -0
- package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +401 -0
- package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +213 -0
- package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +474 -0
- package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +333 -0
- package/src/tools/schemas/outputs/accountOutputs.ts +137 -0
- package/src/tools/schemas/outputs/budgetOutputs.ts +86 -0
- package/src/tools/schemas/outputs/categoryOutputs.ts +194 -0
- package/src/tools/schemas/outputs/comparisonOutputs.ts +600 -0
- package/src/tools/schemas/outputs/index.ts +270 -0
- package/src/tools/schemas/outputs/monthOutputs.ts +243 -0
- package/src/tools/schemas/outputs/payeeOutputs.ts +105 -0
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +796 -0
- package/src/tools/schemas/outputs/transactionMutationOutputs.ts +758 -0
- package/src/tools/schemas/outputs/transactionOutputs.ts +243 -0
- package/src/tools/schemas/outputs/utilityOutputs.ts +411 -0
- package/src/tools/schemas/shared/commonOutputs.ts +140 -0
- package/src/tools/toolCategories.ts +140 -0
- package/src/tools/transactionTools.ts +2509 -0
- package/src/tools/utilityTools.ts +90 -0
- package/src/types/.gitkeep +1 -0
- package/src/types/__tests__/index.test.ts +52 -0
- package/src/types/index.ts +67 -0
- package/src/types/integration-tests.d.ts +35 -0
- package/src/types/toolAnnotations.ts +44 -0
- package/src/utils/__tests__/dateUtils.test.ts +170 -0
- package/src/utils/__tests__/money.test.ts +189 -0
- package/src/utils/amountUtils.ts +32 -0
- package/src/utils/dateUtils.ts +108 -0
- package/src/utils/money.ts +123 -0
- package/test-csv-sample.csv +28 -0
- package/test-exports/sample_bank_statement.csv +7 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +23 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +23 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +44 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +58 -0
- package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +115 -0
- package/test-reconcile-autodetect.js +40 -0
- package/test-reconcile-tool.js +152 -0
- package/test-reconcile-with-csv.cjs +89 -0
- package/test-statement.csv +8 -0
- package/test_debug.js +47 -0
- package/test_simple.mjs +16 -0
- package/tsconfig.json +31 -0
- package/tsconfig.prod.json +18 -0
- package/vitest-reporters/split-json-reporter.ts +211 -0
- package/vitest.config.ts +96 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
|
|
2
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
|
|
4
|
+
import { YNABMCPServer } from '../YNABMCPServer.js';
|
|
5
|
+
import { AuthenticationError, ConfigurationError, ValidationError } from '../../types/index.js';
|
|
6
|
+
import { ToolRegistry } from '../toolRegistry.js';
|
|
7
|
+
import { cacheManager } from '../../server/cacheManager.js';
|
|
8
|
+
import { responseFormatter } from '../../server/responseFormatter.js';
|
|
9
|
+
import { createErrorHandler, ErrorHandler } from '../errorHandler.js';
|
|
10
|
+
|
|
11
|
+
function parseCallToolJson<T = Record<string, unknown>>(result: CallToolResult): T {
|
|
12
|
+
const text = result.content?.[0]?.text;
|
|
13
|
+
const raw = typeof text === 'string' ? text : (JSON.stringify(text ?? {}) ?? '{}');
|
|
14
|
+
return JSON.parse(raw) as T;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Real YNAB API tests using token from .env (YNAB_ACCESS_TOKEN)
|
|
19
|
+
*/
|
|
20
|
+
describe('YNABMCPServer', () => {
|
|
21
|
+
const originalEnv = process.env;
|
|
22
|
+
|
|
23
|
+
// Shared constant for expected tool names
|
|
24
|
+
const expectedToolNames = [
|
|
25
|
+
'list_budgets',
|
|
26
|
+
'get_budget',
|
|
27
|
+
'set_default_budget',
|
|
28
|
+
'get_default_budget',
|
|
29
|
+
'list_accounts',
|
|
30
|
+
'get_account',
|
|
31
|
+
'create_account',
|
|
32
|
+
'list_transactions',
|
|
33
|
+
'export_transactions',
|
|
34
|
+
'compare_transactions',
|
|
35
|
+
'reconcile_account',
|
|
36
|
+
'get_transaction',
|
|
37
|
+
'create_transaction',
|
|
38
|
+
'update_transaction',
|
|
39
|
+
'delete_transaction',
|
|
40
|
+
'list_categories',
|
|
41
|
+
'get_category',
|
|
42
|
+
'update_category',
|
|
43
|
+
'list_payees',
|
|
44
|
+
'get_payee',
|
|
45
|
+
'get_month',
|
|
46
|
+
'list_months',
|
|
47
|
+
'get_user',
|
|
48
|
+
'convert_amount',
|
|
49
|
+
'diagnostic_info',
|
|
50
|
+
'clear_cache',
|
|
51
|
+
'set_output_format',
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
beforeAll(() => {
|
|
55
|
+
if (!process.env['YNAB_ACCESS_TOKEN']) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'YNAB_ACCESS_TOKEN is required. Set it in your .env file to run integration tests.',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
// Don't restore env completely, keep the API key loaded
|
|
64
|
+
Object.keys(process.env).forEach((key) => {
|
|
65
|
+
if (key !== 'YNAB_ACCESS_TOKEN' && key !== 'YNAB_BUDGET_ID') {
|
|
66
|
+
if (originalEnv[key] !== undefined) {
|
|
67
|
+
process.env[key] = originalEnv[key];
|
|
68
|
+
} else {
|
|
69
|
+
// Use Reflect.deleteProperty to avoid ESLint dynamic delete warning
|
|
70
|
+
Reflect.deleteProperty(process.env, key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Constructor and Environment Validation', () => {
|
|
77
|
+
it('should create server instance with valid access token', () => {
|
|
78
|
+
const server = new YNABMCPServer();
|
|
79
|
+
expect(server).toBeInstanceOf(YNABMCPServer);
|
|
80
|
+
expect(server.getYNABAPI()).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is missing', () => {
|
|
84
|
+
const originalToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
85
|
+
delete process.env['YNAB_ACCESS_TOKEN'];
|
|
86
|
+
|
|
87
|
+
expect(() => new YNABMCPServer()).toThrow(ConfigurationError);
|
|
88
|
+
expect(() => new YNABMCPServer()).toThrow(
|
|
89
|
+
'YNAB_ACCESS_TOKEN environment variable is required but not set',
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Restore token
|
|
93
|
+
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is empty string', () => {
|
|
97
|
+
const originalToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
98
|
+
process.env['YNAB_ACCESS_TOKEN'] = '';
|
|
99
|
+
|
|
100
|
+
expect(() => new YNABMCPServer()).toThrow(ConfigurationError);
|
|
101
|
+
expect(() => new YNABMCPServer()).toThrow('YNAB_ACCESS_TOKEN must be a non-empty string');
|
|
102
|
+
|
|
103
|
+
// Restore token
|
|
104
|
+
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is only whitespace', () => {
|
|
108
|
+
const originalToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
109
|
+
process.env['YNAB_ACCESS_TOKEN'] = ' ';
|
|
110
|
+
|
|
111
|
+
expect(() => new YNABMCPServer()).toThrow(ConfigurationError);
|
|
112
|
+
expect(() => new YNABMCPServer()).toThrow('YNAB_ACCESS_TOKEN must be a non-empty string');
|
|
113
|
+
|
|
114
|
+
// Restore token
|
|
115
|
+
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should trim whitespace from access token', () => {
|
|
119
|
+
const originalToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
120
|
+
process.env['YNAB_ACCESS_TOKEN'] = ` ${originalToken} `;
|
|
121
|
+
|
|
122
|
+
const server = new YNABMCPServer();
|
|
123
|
+
expect(server).toBeInstanceOf(YNABMCPServer);
|
|
124
|
+
|
|
125
|
+
// Restore token
|
|
126
|
+
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('Real YNAB API Integration', () => {
|
|
131
|
+
let server: YNABMCPServer;
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
server = new YNABMCPServer(false); // Don't exit on error in tests
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should successfully validate real YNAB token', async () => {
|
|
138
|
+
const isValid = await server.validateToken();
|
|
139
|
+
expect(isValid).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should successfully get user information', async () => {
|
|
143
|
+
// Verify we can get user info
|
|
144
|
+
const ynabAPI = server.getYNABAPI();
|
|
145
|
+
const userResponse = await ynabAPI.user.getUser();
|
|
146
|
+
|
|
147
|
+
expect(userResponse.data.user).toBeDefined();
|
|
148
|
+
expect(userResponse.data.user.id).toBeDefined();
|
|
149
|
+
console.warn(`✅ Connected to YNAB user: ${userResponse.data.user.id}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should successfully get budgets', async () => {
|
|
153
|
+
const ynabAPI = server.getYNABAPI();
|
|
154
|
+
const budgetsResponse = await ynabAPI.budgets.getBudgets();
|
|
155
|
+
|
|
156
|
+
expect(budgetsResponse.data.budgets).toBeDefined();
|
|
157
|
+
expect(Array.isArray(budgetsResponse.data.budgets)).toBe(true);
|
|
158
|
+
expect(budgetsResponse.data.budgets.length).toBeGreaterThan(0);
|
|
159
|
+
|
|
160
|
+
console.warn(`✅ Found ${budgetsResponse.data.budgets.length} budget(s)`);
|
|
161
|
+
budgetsResponse.data.budgets.forEach((budget) => {
|
|
162
|
+
console.warn(` - ${budget.name} (${budget.id})`);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should handle invalid token gracefully', async () => {
|
|
167
|
+
const originalToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
168
|
+
process.env['YNAB_ACCESS_TOKEN'] = 'invalid-token-format';
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const invalidServer = new YNABMCPServer(false);
|
|
172
|
+
await expect(invalidServer.validateToken()).rejects.toThrow(AuthenticationError);
|
|
173
|
+
} finally {
|
|
174
|
+
// Restore original token
|
|
175
|
+
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should successfully start and connect MCP server', async () => {
|
|
180
|
+
// This test verifies the full server startup process
|
|
181
|
+
// Note: We can't fully test the stdio connection in a test environment,
|
|
182
|
+
// but we can verify the server initializes without errors
|
|
183
|
+
|
|
184
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {
|
|
185
|
+
// Mock implementation for testing
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// The run method will validate the token and attempt to connect
|
|
190
|
+
// In a test environment, the stdio connection will fail, but token validation should succeed
|
|
191
|
+
await server.run();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// Expected to fail on stdio connection in test environment
|
|
194
|
+
// But should not fail on token validation
|
|
195
|
+
expect(error).not.toBeInstanceOf(AuthenticationError);
|
|
196
|
+
expect(error).not.toBeInstanceOf(ConfigurationError);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
consoleSpy.mockRestore();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle multiple rapid API calls without rate limiting issues', async () => {
|
|
203
|
+
// Make multiple validation calls to test rate limiting behavior
|
|
204
|
+
const promises = Array(3)
|
|
205
|
+
.fill(null)
|
|
206
|
+
.map(() => server.validateToken());
|
|
207
|
+
|
|
208
|
+
// All should succeed (YNAB API is generally permissive for user info calls)
|
|
209
|
+
const results = await Promise.all(promises);
|
|
210
|
+
results.forEach((result) => expect(result).toBe(true));
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('MCP Server Functionality', () => {
|
|
215
|
+
let server: YNABMCPServer;
|
|
216
|
+
let registry: ToolRegistry;
|
|
217
|
+
|
|
218
|
+
const accessToken = () => {
|
|
219
|
+
const token = process.env['YNAB_ACCESS_TOKEN'];
|
|
220
|
+
if (!token) {
|
|
221
|
+
throw new Error('YNAB_ACCESS_TOKEN must be defined for integration tests');
|
|
222
|
+
}
|
|
223
|
+
return token;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const ensureDefaultBudget = async (): Promise<string> => {
|
|
227
|
+
const budgetsResult = await registry.executeTool({
|
|
228
|
+
name: 'list_budgets',
|
|
229
|
+
accessToken: accessToken(),
|
|
230
|
+
arguments: {},
|
|
231
|
+
});
|
|
232
|
+
const budgetsPayload = parseCallToolJson(budgetsResult);
|
|
233
|
+
const firstBudget = budgetsPayload.budgets?.[0];
|
|
234
|
+
expect(firstBudget?.id).toBeDefined();
|
|
235
|
+
|
|
236
|
+
await registry.executeTool({
|
|
237
|
+
name: 'set_default_budget',
|
|
238
|
+
accessToken: accessToken(),
|
|
239
|
+
arguments: { budget_id: firstBudget.id },
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return firstBudget.id as string;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
beforeEach(() => {
|
|
246
|
+
server = new YNABMCPServer(false);
|
|
247
|
+
registry = (server as unknown as { toolRegistry: ToolRegistry }).toolRegistry;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should expose the complete registered tool list via the registry', () => {
|
|
251
|
+
const tools = registry.listTools();
|
|
252
|
+
const names = tools.map((tool) => tool.name).sort();
|
|
253
|
+
expect(names).toEqual([...expectedToolNames].sort());
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should execute get_user tool via the registry', async () => {
|
|
257
|
+
const result = await registry.executeTool({
|
|
258
|
+
name: 'get_user',
|
|
259
|
+
accessToken: accessToken(),
|
|
260
|
+
arguments: {},
|
|
261
|
+
});
|
|
262
|
+
const payload = parseCallToolJson(result);
|
|
263
|
+
expect(payload.user?.id).toBeDefined();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should set and retrieve default budget using tools', async () => {
|
|
267
|
+
const budgetId = await ensureDefaultBudget();
|
|
268
|
+
|
|
269
|
+
const defaultResult = await registry.executeTool({
|
|
270
|
+
name: 'get_default_budget',
|
|
271
|
+
accessToken: accessToken(),
|
|
272
|
+
arguments: {},
|
|
273
|
+
});
|
|
274
|
+
const defaultPayload = parseCallToolJson(defaultResult);
|
|
275
|
+
expect(defaultPayload.default_budget_id).toBe(budgetId);
|
|
276
|
+
expect(defaultPayload.has_default).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should trigger cache warming after setting default budget', async () => {
|
|
280
|
+
// Clear cache before test
|
|
281
|
+
await registry.executeTool({
|
|
282
|
+
name: 'clear_cache',
|
|
283
|
+
accessToken: accessToken(),
|
|
284
|
+
arguments: {},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const statsBeforeSet = cacheManager.getStats();
|
|
288
|
+
const initialSize = statsBeforeSet.size;
|
|
289
|
+
|
|
290
|
+
// Get a budget ID
|
|
291
|
+
const budgetsResult = await registry.executeTool({
|
|
292
|
+
name: 'list_budgets',
|
|
293
|
+
accessToken: accessToken(),
|
|
294
|
+
arguments: {},
|
|
295
|
+
});
|
|
296
|
+
const budgetsPayload = parseCallToolJson(budgetsResult);
|
|
297
|
+
const firstBudget = budgetsPayload.budgets?.[0];
|
|
298
|
+
expect(firstBudget?.id).toBeDefined();
|
|
299
|
+
|
|
300
|
+
// Set default budget (this should trigger cache warming)
|
|
301
|
+
await registry.executeTool({
|
|
302
|
+
name: 'set_default_budget',
|
|
303
|
+
accessToken: accessToken(),
|
|
304
|
+
arguments: { budget_id: firstBudget.id },
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Wait for cache warming to complete with polling (it's fire-and-forget)
|
|
308
|
+
const timeoutMs = 5000; // 5 second timeout
|
|
309
|
+
const pollIntervalMs = 50; // Check every 50ms
|
|
310
|
+
const startTime = Date.now();
|
|
311
|
+
let statsAfterSet = cacheManager.getStats();
|
|
312
|
+
|
|
313
|
+
while (statsAfterSet.size <= initialSize && Date.now() - startTime < timeoutMs) {
|
|
314
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
315
|
+
statsAfterSet = cacheManager.getStats();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Fail test if timeout was reached without cache growth
|
|
319
|
+
if (statsAfterSet.size <= initialSize) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Cache warming failed to complete within ${timeoutMs}ms. ` +
|
|
322
|
+
`Initial size: ${initialSize}, Final size: ${statsAfterSet.size}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Cache should have more entries due to warming
|
|
327
|
+
expect(statsAfterSet.size).toBeGreaterThan(initialSize);
|
|
328
|
+
|
|
329
|
+
// Verify that common data types were cached
|
|
330
|
+
const allKeys = cacheManager.getAllKeys();
|
|
331
|
+
const hasAccountsCache = allKeys.some((key) => key.includes('accounts:list'));
|
|
332
|
+
const hasCategoriesCache = allKeys.some((key) => key.includes('categories:list'));
|
|
333
|
+
const hasPayeesCache = allKeys.some((key) => key.includes('payees:list'));
|
|
334
|
+
|
|
335
|
+
// At least some cache warming should have occurred
|
|
336
|
+
expect(hasAccountsCache || hasCategoriesCache || hasPayeesCache).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should handle cache warming errors gracefully', async () => {
|
|
340
|
+
// Get a real budget ID first, since API validation is in place
|
|
341
|
+
const budgetsResult = await registry.executeTool({
|
|
342
|
+
name: 'list_budgets',
|
|
343
|
+
accessToken: accessToken(),
|
|
344
|
+
arguments: {},
|
|
345
|
+
});
|
|
346
|
+
const budgetsPayload = parseCallToolJson(budgetsResult);
|
|
347
|
+
const firstBudget = budgetsPayload.budgets?.[0];
|
|
348
|
+
expect(firstBudget?.id).toBeDefined();
|
|
349
|
+
const realBudgetId = firstBudget.id as string;
|
|
350
|
+
|
|
351
|
+
// This should succeed with API validation in place
|
|
352
|
+
const result = await registry.executeTool({
|
|
353
|
+
name: 'set_default_budget',
|
|
354
|
+
accessToken: accessToken(),
|
|
355
|
+
arguments: { budget_id: realBudgetId },
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// The set_default_budget operation should succeed
|
|
359
|
+
const payload = parseCallToolJson(result);
|
|
360
|
+
expect(payload.message).toContain('Default budget set to:');
|
|
361
|
+
expect(payload.default_budget_id).toBe(realBudgetId);
|
|
362
|
+
|
|
363
|
+
// Wait a moment for cache warming attempts to complete
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
365
|
+
|
|
366
|
+
// Server should still be functional
|
|
367
|
+
const defaultResult = await registry.executeTool({
|
|
368
|
+
name: 'get_default_budget',
|
|
369
|
+
accessToken: accessToken(),
|
|
370
|
+
arguments: {},
|
|
371
|
+
});
|
|
372
|
+
const defaultPayload = parseCallToolJson(defaultResult);
|
|
373
|
+
expect(defaultPayload.default_budget_id).toBe(realBudgetId);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should execute list tools that rely on the default budget', async () => {
|
|
377
|
+
await ensureDefaultBudget();
|
|
378
|
+
|
|
379
|
+
const accountsResult = await registry.executeTool({
|
|
380
|
+
name: 'list_accounts',
|
|
381
|
+
accessToken: accessToken(),
|
|
382
|
+
arguments: {},
|
|
383
|
+
});
|
|
384
|
+
const accountsPayload = parseCallToolJson(accountsResult);
|
|
385
|
+
expect(Array.isArray(accountsPayload.accounts)).toBe(true);
|
|
386
|
+
|
|
387
|
+
const categoriesResult = await registry.executeTool({
|
|
388
|
+
name: 'list_categories',
|
|
389
|
+
accessToken: accessToken(),
|
|
390
|
+
arguments: {},
|
|
391
|
+
});
|
|
392
|
+
const categoriesPayload = parseCallToolJson(categoriesResult);
|
|
393
|
+
expect(Array.isArray(categoriesPayload.categories)).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should provide diagnostic info with requested sections', async () => {
|
|
397
|
+
const diagResult = await registry.executeTool({
|
|
398
|
+
name: 'diagnostic_info',
|
|
399
|
+
accessToken: accessToken(),
|
|
400
|
+
arguments: {
|
|
401
|
+
include_server: true,
|
|
402
|
+
include_security: true,
|
|
403
|
+
include_cache: true,
|
|
404
|
+
include_memory: false,
|
|
405
|
+
include_environment: false,
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
const diagnostics = parseCallToolJson(diagResult);
|
|
409
|
+
expect(diagnostics.timestamp).toBeDefined();
|
|
410
|
+
expect(diagnostics.server).toBeDefined();
|
|
411
|
+
expect(diagnostics.security).toBeDefined();
|
|
412
|
+
expect(diagnostics.cache).toBeDefined();
|
|
413
|
+
expect(diagnostics.memory).toBeUndefined();
|
|
414
|
+
expect(diagnostics.environment).toBeUndefined();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should clear cache using the clear_cache tool', async () => {
|
|
418
|
+
cacheManager.set('test:key', { value: 1 }, 1000);
|
|
419
|
+
const statsBeforeClear = cacheManager.getStats();
|
|
420
|
+
expect(statsBeforeClear.size).toBeGreaterThan(0);
|
|
421
|
+
|
|
422
|
+
await registry.executeTool({
|
|
423
|
+
name: 'clear_cache',
|
|
424
|
+
accessToken: accessToken(),
|
|
425
|
+
arguments: {},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const statsAfterClear = cacheManager.getStats();
|
|
429
|
+
expect(statsAfterClear.size).toBe(0);
|
|
430
|
+
expect(statsAfterClear.hits).toBe(0);
|
|
431
|
+
expect(statsAfterClear.misses).toBe(0);
|
|
432
|
+
expect(statsAfterClear.evictions).toBe(0);
|
|
433
|
+
expect(statsAfterClear.lastCleanup).toBe(null);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should track cache hits and misses through tool execution', async () => {
|
|
437
|
+
const initialStats = cacheManager.getStats();
|
|
438
|
+
const initialHits = initialStats.hits;
|
|
439
|
+
|
|
440
|
+
// Execute a tool that should use caching
|
|
441
|
+
await registry.executeTool({
|
|
442
|
+
name: 'list_budgets',
|
|
443
|
+
accessToken: accessToken(),
|
|
444
|
+
arguments: {},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const statsAfterFirstCall = cacheManager.getStats();
|
|
448
|
+
expect(statsAfterFirstCall.size).toBeGreaterThan(initialStats.size);
|
|
449
|
+
|
|
450
|
+
// Execute the same tool again - should hit cache
|
|
451
|
+
await registry.executeTool({
|
|
452
|
+
name: 'list_budgets',
|
|
453
|
+
accessToken: accessToken(),
|
|
454
|
+
arguments: {},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const statsAfterSecondCall = cacheManager.getStats();
|
|
458
|
+
expect(statsAfterSecondCall.hits).toBeGreaterThan(initialHits);
|
|
459
|
+
expect(statsAfterSecondCall.hitRate).toBeGreaterThan(0);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should respect maxEntries configuration from environment', () => {
|
|
463
|
+
// Test that maxEntries is properly configured
|
|
464
|
+
const stats = cacheManager.getStats();
|
|
465
|
+
expect(stats.maxEntries).toEqual(expect.any(Number));
|
|
466
|
+
expect(stats.maxEntries).toBeGreaterThan(0);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should surface enhanced cache metrics in diagnostics', async () => {
|
|
470
|
+
// Generate some cache activity
|
|
471
|
+
cacheManager.set('test:metric1', { data: 'value1' }, 1000);
|
|
472
|
+
cacheManager.get('test:metric1'); // Hit
|
|
473
|
+
cacheManager.get('test:nonexistent'); // Miss
|
|
474
|
+
|
|
475
|
+
const result = await registry.executeTool({
|
|
476
|
+
name: 'diagnostic_info',
|
|
477
|
+
accessToken: accessToken(),
|
|
478
|
+
arguments: {
|
|
479
|
+
include_server: false,
|
|
480
|
+
include_memory: false,
|
|
481
|
+
include_environment: false,
|
|
482
|
+
include_security: false,
|
|
483
|
+
include_cache: true,
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const diagnostics = parseCallToolJson(result);
|
|
488
|
+
expect(diagnostics.cache).toBeDefined();
|
|
489
|
+
expect(diagnostics.cache.entries).toEqual(expect.any(Number));
|
|
490
|
+
expect(diagnostics.cache.hits).toEqual(expect.any(Number));
|
|
491
|
+
expect(diagnostics.cache.misses).toEqual(expect.any(Number));
|
|
492
|
+
expect(diagnostics.cache.evictions).toEqual(expect.any(Number));
|
|
493
|
+
expect(diagnostics.cache.maxEntries).toEqual(expect.any(Number));
|
|
494
|
+
expect(diagnostics.cache.hitRate).toEqual(expect.stringMatching(/^\d+\.\d{2}%$/));
|
|
495
|
+
expect(diagnostics.cache.performance_summary).toEqual(expect.any(String));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should configure output formatter via set_output_format tool', async () => {
|
|
499
|
+
const baseline = responseFormatter.format({ probe: true });
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
await registry.executeTool({
|
|
503
|
+
name: 'set_output_format',
|
|
504
|
+
accessToken: accessToken(),
|
|
505
|
+
arguments: { default_minify: false, pretty_spaces: 4 },
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const formatted = responseFormatter.format({ probe: true });
|
|
509
|
+
expect(formatted).not.toBe(baseline);
|
|
510
|
+
expect(formatted).toContain('\n');
|
|
511
|
+
} finally {
|
|
512
|
+
await registry.executeTool({
|
|
513
|
+
name: 'set_output_format',
|
|
514
|
+
accessToken: accessToken(),
|
|
515
|
+
arguments: { default_minify: true, pretty_spaces: 2 },
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('should surface validation errors for invalid inputs', async () => {
|
|
521
|
+
const result = await registry.executeTool({
|
|
522
|
+
name: 'get_budget',
|
|
523
|
+
accessToken: accessToken(),
|
|
524
|
+
arguments: {} as Record<string, unknown>,
|
|
525
|
+
});
|
|
526
|
+
const payload = parseCallToolJson(result);
|
|
527
|
+
expect(payload.error).toBeDefined();
|
|
528
|
+
expect(payload.error.code).toBe('VALIDATION_ERROR');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('Budget Resolution Error Handling', () => {
|
|
532
|
+
let freshServer: YNABMCPServer;
|
|
533
|
+
let freshRegistry: ToolRegistry;
|
|
534
|
+
|
|
535
|
+
beforeEach(() => {
|
|
536
|
+
// Create a fresh server with no default budget set
|
|
537
|
+
freshServer = new YNABMCPServer(false);
|
|
538
|
+
freshRegistry = (freshServer as unknown as { toolRegistry: ToolRegistry }).toolRegistry;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const budgetDependentTools = [
|
|
542
|
+
'list_accounts',
|
|
543
|
+
'get_account',
|
|
544
|
+
'create_account',
|
|
545
|
+
'list_transactions',
|
|
546
|
+
'get_transaction',
|
|
547
|
+
'create_transaction',
|
|
548
|
+
'update_transaction',
|
|
549
|
+
'delete_transaction',
|
|
550
|
+
'list_categories',
|
|
551
|
+
'get_category',
|
|
552
|
+
'update_category',
|
|
553
|
+
'list_payees',
|
|
554
|
+
'get_payee',
|
|
555
|
+
'get_month',
|
|
556
|
+
'list_months',
|
|
557
|
+
'export_transactions',
|
|
558
|
+
'compare_transactions',
|
|
559
|
+
'reconcile_account',
|
|
560
|
+
] as const;
|
|
561
|
+
|
|
562
|
+
budgetDependentTools.forEach((toolName) => {
|
|
563
|
+
it(`should return standardized error for ${toolName} when no default budget is set`, async () => {
|
|
564
|
+
const result = await freshRegistry.executeTool({
|
|
565
|
+
name: toolName,
|
|
566
|
+
accessToken: accessToken(),
|
|
567
|
+
arguments: {},
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const payload = parseCallToolJson(result);
|
|
571
|
+
expect(payload.error).toBeDefined();
|
|
572
|
+
expect(payload.error.code).toBe('VALIDATION_ERROR');
|
|
573
|
+
expect(payload.error.message).toContain(
|
|
574
|
+
'No budget ID provided and no default budget set',
|
|
575
|
+
);
|
|
576
|
+
expect(payload.error.userMessage).toContain('invalid');
|
|
577
|
+
expect(payload.error.suggestions).toBeDefined();
|
|
578
|
+
expect(Array.isArray(payload.error.suggestions)).toBe(true);
|
|
579
|
+
expect(
|
|
580
|
+
payload.error.suggestions.some(
|
|
581
|
+
(suggestion: string) =>
|
|
582
|
+
suggestion.includes('set_default_budget') ||
|
|
583
|
+
suggestion.includes('budget_id parameter'),
|
|
584
|
+
),
|
|
585
|
+
).toBe(true);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('should return standardized error for invalid budget ID format', async () => {
|
|
590
|
+
const invalidBudgetId = 'not-a-valid-uuid';
|
|
591
|
+
const result = await freshRegistry.executeTool({
|
|
592
|
+
name: 'list_accounts',
|
|
593
|
+
accessToken: accessToken(),
|
|
594
|
+
arguments: { budget_id: invalidBudgetId },
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const payload = parseCallToolJson(result);
|
|
598
|
+
expect(payload.error).toBeDefined();
|
|
599
|
+
expect(payload.error.code).toBe('VALIDATION_ERROR');
|
|
600
|
+
expect(payload.error.message).toContain('Invalid budget ID format');
|
|
601
|
+
expect(payload.error.userMessage).toContain('invalid');
|
|
602
|
+
expect(payload.error.suggestions).toBeDefined();
|
|
603
|
+
expect(Array.isArray(payload.error.suggestions)).toBe(true);
|
|
604
|
+
expect(
|
|
605
|
+
payload.error.suggestions.some(
|
|
606
|
+
(suggestion: string) =>
|
|
607
|
+
suggestion.includes('UUID v4 format') || suggestion.includes('list_budgets'),
|
|
608
|
+
),
|
|
609
|
+
).toBe(true);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should work normally after setting a default budget', async () => {
|
|
613
|
+
// First, ensure we get the "no default budget" error
|
|
614
|
+
let result = await freshRegistry.executeTool({
|
|
615
|
+
name: 'list_accounts',
|
|
616
|
+
accessToken: accessToken(),
|
|
617
|
+
arguments: {},
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
let payload = parseCallToolJson(result);
|
|
621
|
+
expect(payload.error).toBeDefined();
|
|
622
|
+
expect(payload.error.code).toBe('VALIDATION_ERROR');
|
|
623
|
+
|
|
624
|
+
// Now set a default budget
|
|
625
|
+
const defaultBudgetId = await ensureDefaultBudget();
|
|
626
|
+
await freshRegistry.executeTool({
|
|
627
|
+
name: 'set_default_budget',
|
|
628
|
+
accessToken: accessToken(),
|
|
629
|
+
arguments: { budget_id: defaultBudgetId },
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Now the same call should work
|
|
633
|
+
result = await freshRegistry.executeTool({
|
|
634
|
+
name: 'list_accounts',
|
|
635
|
+
accessToken: accessToken(),
|
|
636
|
+
arguments: {},
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
payload = parseCallToolJson(result);
|
|
640
|
+
// Should have accounts data or be valid response, not an error
|
|
641
|
+
expect(payload.error).toBeUndefined();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('should have consistent error response structure across all budget-dependent tools', async () => {
|
|
645
|
+
const promises = budgetDependentTools.map((toolName) =>
|
|
646
|
+
freshRegistry.executeTool({
|
|
647
|
+
name: toolName,
|
|
648
|
+
accessToken: accessToken(),
|
|
649
|
+
arguments: {},
|
|
650
|
+
}),
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
const results = await Promise.all(promises);
|
|
654
|
+
|
|
655
|
+
results.forEach((result) => {
|
|
656
|
+
const payload = parseCallToolJson(result);
|
|
657
|
+
|
|
658
|
+
// All should have the same error structure
|
|
659
|
+
expect(payload).toHaveProperty(
|
|
660
|
+
'error',
|
|
661
|
+
expect.objectContaining({
|
|
662
|
+
code: 'VALIDATION_ERROR',
|
|
663
|
+
message: expect.stringContaining('No budget ID provided and no default budget set'),
|
|
664
|
+
userMessage: expect.any(String),
|
|
665
|
+
suggestions: expect.arrayContaining([
|
|
666
|
+
expect.stringMatching(/set_default_budget|budget_id parameter/),
|
|
667
|
+
]),
|
|
668
|
+
}),
|
|
669
|
+
);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
describe('Modular Architecture Integration', () => {
|
|
676
|
+
let server: YNABMCPServer;
|
|
677
|
+
|
|
678
|
+
beforeEach(() => {
|
|
679
|
+
server = new YNABMCPServer(false);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('should initialize all service modules during construction', () => {
|
|
683
|
+
// Verify the server has been constructed successfully with all modules
|
|
684
|
+
expect(server).toBeInstanceOf(YNABMCPServer);
|
|
685
|
+
|
|
686
|
+
// Check that core functionality from modules works through public interface
|
|
687
|
+
expect(server.getYNABAPI()).toBeDefined();
|
|
688
|
+
expect(server.getServer()).toBeDefined();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should use config module for environment validation', () => {
|
|
692
|
+
// The fact that constructor succeeds means config module is working
|
|
693
|
+
// This test verifies the integration is seamless
|
|
694
|
+
expect(server.getYNABAPI()).toBeDefined();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('should handle resource requests through resource manager', async () => {
|
|
698
|
+
// Test that resources work (this goes through the resource manager now)
|
|
699
|
+
const mcpServer = server.getServer();
|
|
700
|
+
expect(mcpServer).toBeDefined();
|
|
701
|
+
|
|
702
|
+
// The server should be properly configured with resource handlers
|
|
703
|
+
// If the integration failed, the server wouldn't have the handlers
|
|
704
|
+
expect(() => server.getYNABAPI()).not.toThrow();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('should handle prompt requests through prompt manager', async () => {
|
|
708
|
+
// Test that the server has prompt handling capability
|
|
709
|
+
// The integration ensures prompt handlers are properly set up
|
|
710
|
+
const mcpServer = server.getServer();
|
|
711
|
+
expect(mcpServer).toBeDefined();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should handle diagnostic requests through diagnostic manager', async () => {
|
|
715
|
+
// Test that diagnostic tools work through the tool registry integration
|
|
716
|
+
const registry = (server as unknown as { toolRegistry: ToolRegistry }).toolRegistry;
|
|
717
|
+
|
|
718
|
+
// Verify diagnostic tool is registered
|
|
719
|
+
const tools = registry.listTools();
|
|
720
|
+
const diagnosticTool = tools.find((tool) => tool.name === 'diagnostic_info');
|
|
721
|
+
expect(diagnosticTool).toBeDefined();
|
|
722
|
+
expect(diagnosticTool?.description).toContain('diagnostic information');
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('should maintain backward compatibility after modular refactoring', async () => {
|
|
726
|
+
// Test that all expected tools are still available
|
|
727
|
+
const registry = (server as unknown as { toolRegistry: ToolRegistry }).toolRegistry;
|
|
728
|
+
const tools = registry.listTools();
|
|
729
|
+
|
|
730
|
+
// Use the shared expectedToolNames constant defined at the top of the test file
|
|
731
|
+
|
|
732
|
+
const actualToolNames = tools.map((tool) => tool.name).sort();
|
|
733
|
+
expect(actualToolNames).toEqual(expectedToolNames.sort());
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('should maintain same error handling behavior after refactoring', () => {
|
|
737
|
+
// Test that configuration errors are still properly thrown
|
|
738
|
+
const originalToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
739
|
+
delete process.env['YNAB_ACCESS_TOKEN'];
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
expect(() => new YNABMCPServer()).toThrow(ConfigurationError);
|
|
743
|
+
expect(() => new YNABMCPServer()).toThrow(
|
|
744
|
+
'YNAB_ACCESS_TOKEN environment variable is required but not set',
|
|
745
|
+
);
|
|
746
|
+
} finally {
|
|
747
|
+
// Restore token
|
|
748
|
+
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('should delegate diagnostic collection to diagnostic manager', async () => {
|
|
753
|
+
const registry = (server as unknown as { toolRegistry: ToolRegistry }).toolRegistry;
|
|
754
|
+
const accessToken = process.env['YNAB_ACCESS_TOKEN']!;
|
|
755
|
+
|
|
756
|
+
// Test that diagnostic_info tool works and returns expected structure
|
|
757
|
+
const result = await registry.executeTool({
|
|
758
|
+
name: 'diagnostic_info',
|
|
759
|
+
accessToken,
|
|
760
|
+
arguments: {
|
|
761
|
+
include_server: true,
|
|
762
|
+
include_memory: false,
|
|
763
|
+
include_environment: false,
|
|
764
|
+
include_security: false,
|
|
765
|
+
include_cache: false,
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const diagnostics = parseCallToolJson(result);
|
|
770
|
+
expect(diagnostics.timestamp).toBeDefined();
|
|
771
|
+
expect(diagnostics.server).toBeDefined();
|
|
772
|
+
expect(diagnostics.server.name).toBe('ynab-mcp-server');
|
|
773
|
+
expect(diagnostics.server.version).toBeDefined();
|
|
774
|
+
|
|
775
|
+
// These should be undefined because we set include flags to false
|
|
776
|
+
expect(diagnostics.memory).toBeUndefined();
|
|
777
|
+
expect(diagnostics.environment).toBeUndefined();
|
|
778
|
+
expect(diagnostics.security).toBeUndefined();
|
|
779
|
+
expect(diagnostics.cache).toBeUndefined();
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
describe('Deprecated Methods', () => {
|
|
784
|
+
let server: YNABMCPServer;
|
|
785
|
+
|
|
786
|
+
beforeEach(() => {
|
|
787
|
+
// Create server with valid token for testing deprecated method
|
|
788
|
+
const originalToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
789
|
+
if (!originalToken) {
|
|
790
|
+
throw new Error('YNAB_ACCESS_TOKEN must be defined for getBudgetId tests');
|
|
791
|
+
}
|
|
792
|
+
server = new YNABMCPServer(false);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
describe('getBudgetId', () => {
|
|
796
|
+
it('should throw ValidationError when no budget ID provided and no default set', () => {
|
|
797
|
+
// Ensure no default budget is set
|
|
798
|
+
expect(server.getDefaultBudget()).toBeUndefined();
|
|
799
|
+
|
|
800
|
+
// Should throw ValidationError (not YNABAPIError)
|
|
801
|
+
expect(() => {
|
|
802
|
+
server.getBudgetId();
|
|
803
|
+
}).toThrow(ValidationError);
|
|
804
|
+
|
|
805
|
+
expect(() => {
|
|
806
|
+
server.getBudgetId();
|
|
807
|
+
}).toThrow('No budget ID provided and no default budget set');
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('should throw ValidationError for invalid budget ID format', () => {
|
|
811
|
+
expect(() => {
|
|
812
|
+
server.getBudgetId('invalid-id');
|
|
813
|
+
}).toThrow(ValidationError);
|
|
814
|
+
|
|
815
|
+
expect(() => {
|
|
816
|
+
server.getBudgetId('invalid-id');
|
|
817
|
+
}).toThrow(/Invalid budget ID format/);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('should return valid budget ID when provided with valid UUID', () => {
|
|
821
|
+
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
822
|
+
const result = server.getBudgetId(validUuid);
|
|
823
|
+
expect(result).toBe(validUuid);
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
describe('ErrorHandler Integration', () => {
|
|
829
|
+
let server: YNABMCPServer;
|
|
830
|
+
|
|
831
|
+
beforeEach(() => {
|
|
832
|
+
server = new YNABMCPServer(false);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('should create ErrorHandler instance with responseFormatter', () => {
|
|
836
|
+
// Verify that createErrorHandler was called with the formatter
|
|
837
|
+
expect(server).toBeInstanceOf(YNABMCPServer);
|
|
838
|
+
|
|
839
|
+
// The server should be successfully constructed with ErrorHandler injection
|
|
840
|
+
expect(server.getYNABAPI()).toBeDefined();
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('should set global ErrorHandler formatter for backward compatibility', () => {
|
|
844
|
+
// This test verifies that the global formatter was set
|
|
845
|
+
// by checking that static ErrorHandler methods work
|
|
846
|
+
const result = ErrorHandler.createValidationError('Test error');
|
|
847
|
+
|
|
848
|
+
expect(result.content).toBeDefined();
|
|
849
|
+
expect(result.content[0].type).toBe('text');
|
|
850
|
+
expect(() => JSON.parse(result.content[0].text)).not.toThrow();
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('should use the same formatter for ErrorHandler and ToolRegistry', () => {
|
|
854
|
+
// Verify that the server uses dependency injection correctly
|
|
855
|
+
expect(server).toBeInstanceOf(YNABMCPServer);
|
|
856
|
+
|
|
857
|
+
// The fact that the server constructs successfully means dependency injection worked
|
|
858
|
+
// and both ErrorHandler and ToolRegistry are using the same formatter instance
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('should maintain existing error response format', async () => {
|
|
862
|
+
const registry = (server as unknown as { toolRegistry: ToolRegistry }).toolRegistry;
|
|
863
|
+
|
|
864
|
+
// Test that error responses still have the expected structure
|
|
865
|
+
const result = await registry.executeTool({
|
|
866
|
+
name: 'get_budget',
|
|
867
|
+
accessToken: process.env['YNAB_ACCESS_TOKEN']!,
|
|
868
|
+
arguments: {} as Record<string, unknown>,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
const payload = parseCallToolJson(result);
|
|
872
|
+
expect(payload.error).toBeDefined();
|
|
873
|
+
expect(payload.error.code).toBe('VALIDATION_ERROR');
|
|
874
|
+
|
|
875
|
+
// Verify the response is properly formatted JSON
|
|
876
|
+
expect(() => JSON.parse(result.content[0].text)).not.toThrow();
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('should handle formatter consistency across static and instance methods', () => {
|
|
880
|
+
const formatter = { format: (value: unknown) => JSON.stringify(value) };
|
|
881
|
+
const errorHandler = createErrorHandler(formatter);
|
|
882
|
+
ErrorHandler.setFormatter(formatter);
|
|
883
|
+
|
|
884
|
+
const error = new ValidationError('Test error');
|
|
885
|
+
const instanceResult = errorHandler.handleError(error, 'testing');
|
|
886
|
+
const staticResult = ErrorHandler.handleError(error, 'testing');
|
|
887
|
+
|
|
888
|
+
// Both should produce the same result structure
|
|
889
|
+
expect(instanceResult.content[0].type).toBe(staticResult.content[0].type);
|
|
890
|
+
expect(() => JSON.parse(instanceResult.content[0].text)).not.toThrow();
|
|
891
|
+
expect(() => JSON.parse(staticResult.content[0].text)).not.toThrow();
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
});
|