@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,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
+ });