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