@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,503 @@
1
+ /**
2
+ * Cache Manager for YNAB API responses
3
+ * Provides in-memory caching with TTL to reduce API calls and improve performance
4
+ */
5
+
6
+ interface CacheEntry<T> {
7
+ data: T;
8
+ timestamp: number;
9
+ ttl: number;
10
+ staleWhileRevalidate?: number;
11
+ }
12
+
13
+ interface CacheSetOptions {
14
+ ttl?: number;
15
+ /**
16
+ * Stale-while-revalidate window in milliseconds.
17
+ * When explicitly set to undefined, uses the default stale window.
18
+ * When omitted entirely, no stale-while-revalidate is applied.
19
+ */
20
+ staleWhileRevalidate?: number;
21
+ }
22
+
23
+ export class CacheManager {
24
+ private cache = new Map<string, CacheEntry<unknown>>();
25
+ private readonly defaultTTL: number;
26
+ private hits = 0;
27
+ private misses = 0;
28
+ private evictions = 0;
29
+ private lastCleanup: number | null = null;
30
+ private maxEntries: number;
31
+ private defaultStaleWindow: number;
32
+ private pendingFetches = new Map<string, Promise<unknown>>();
33
+ private pendingRefresh = new Set<string>();
34
+
35
+ constructor() {
36
+ this.maxEntries = this.parseEnvInt('YNAB_MCP_CACHE_MAX_ENTRIES', 1000);
37
+ this.defaultStaleWindow = this.parseEnvInt('YNAB_MCP_CACHE_STALE_MS', 2 * 60 * 1000);
38
+ this.defaultTTL = this.parseEnvInt('YNAB_MCP_CACHE_DEFAULT_TTL_MS', 300000);
39
+ }
40
+
41
+ /**
42
+ * Get cached data if valid, null if expired or not found
43
+ */
44
+ get<T>(key: string): T | null {
45
+ const entry = this.cache.get(key);
46
+
47
+ if (!entry) {
48
+ this.misses++;
49
+ return null;
50
+ }
51
+
52
+ const now = Date.now();
53
+ const age = now - entry.timestamp;
54
+
55
+ // Check if entry is expired
56
+ if (age > entry.ttl) {
57
+ // Check if we're within stale-while-revalidate window
58
+ const staleWindow = entry.staleWhileRevalidate || 0;
59
+ if (staleWindow > 0 && age <= entry.ttl + staleWindow) {
60
+ this.hits++;
61
+ // Update access order for LRU
62
+ this.cache.delete(key);
63
+ this.cache.set(key, entry);
64
+ // Mark for background refresh
65
+ this.pendingRefresh.add(key);
66
+ return entry.data as T;
67
+ }
68
+
69
+ this.cache.delete(key);
70
+ this.pendingFetches.delete(key);
71
+ this.pendingRefresh.delete(key);
72
+ this.misses++;
73
+ return null;
74
+ }
75
+
76
+ this.hits++;
77
+ // Update access order for LRU
78
+ this.cache.delete(key);
79
+ this.cache.set(key, entry);
80
+ return entry.data as T;
81
+ }
82
+
83
+ /**
84
+ * Check if a valid cache entry exists without updating hit/miss counters
85
+ */
86
+ has(key: string): boolean {
87
+ const entry = this.cache.get(key);
88
+ if (!entry) {
89
+ return false;
90
+ }
91
+
92
+ const now = Date.now();
93
+ const age = now - entry.timestamp;
94
+ if (age > entry.ttl) {
95
+ const staleWindow = entry.staleWhileRevalidate || 0;
96
+ if (staleWindow > 0 && age <= entry.ttl + staleWindow) {
97
+ return true;
98
+ }
99
+
100
+ this.cache.delete(key);
101
+ this.pendingFetches.delete(key);
102
+ this.pendingRefresh.delete(key);
103
+ return false;
104
+ }
105
+
106
+ return true;
107
+ }
108
+
109
+ /**
110
+ * Set cache entry with optional TTL or options
111
+ *
112
+ * @param key - Cache key
113
+ * @param data - Data to cache
114
+ * @param ttlOrOptions - TTL in milliseconds (number) or options object
115
+ *
116
+ * Note: Default stale-while-revalidate window is applied only when:
117
+ * - An options object is provided AND
118
+ * - The staleWhileRevalidate property is explicitly present (even if undefined)
119
+ *
120
+ * When using the simple number interface or when staleWhileRevalidate property
121
+ * is not present in the options object, no default stale window is applied.
122
+ */
123
+ set<T>(key: string, data: T, ttlOrOptions?: number | CacheSetOptions): void {
124
+ // Don't cache anything if maxEntries is 0
125
+ if (this.maxEntries <= 0) {
126
+ return;
127
+ }
128
+
129
+ const isUpdate = this.cache.has(key);
130
+ if (!isUpdate) {
131
+ this.evictIfNeeded();
132
+ }
133
+
134
+ let ttl: number;
135
+ let staleWhileRevalidate: number | undefined;
136
+
137
+ if (typeof ttlOrOptions === 'number') {
138
+ ttl = Number.isFinite(ttlOrOptions) ? ttlOrOptions : this.defaultTTL;
139
+ // When using simple number interface, no stale window is applied
140
+ staleWhileRevalidate = undefined;
141
+ } else if (ttlOrOptions === undefined) {
142
+ // When called without any options (simple set), use defaults but NO stale window
143
+ ttl = this.defaultTTL;
144
+ staleWhileRevalidate = undefined;
145
+ } else {
146
+ const providedTtl = ttlOrOptions?.ttl;
147
+ ttl = providedTtl !== undefined ? providedTtl : this.defaultTTL;
148
+ if (ttlOrOptions && 'staleWhileRevalidate' in ttlOrOptions) {
149
+ staleWhileRevalidate = ttlOrOptions.staleWhileRevalidate;
150
+ } else {
151
+ staleWhileRevalidate = ttlOrOptions?.staleWhileRevalidate;
152
+ }
153
+ // Apply default stale window only when options object is provided and staleWhileRevalidate is undefined
154
+ if (staleWhileRevalidate === undefined && this.defaultStaleWindow > 0) {
155
+ staleWhileRevalidate = this.defaultStaleWindow;
156
+ }
157
+ }
158
+ const entry: CacheEntry<T> = {
159
+ data,
160
+ timestamp: Date.now(),
161
+ ttl,
162
+ };
163
+ if (staleWhileRevalidate !== undefined) {
164
+ entry.staleWhileRevalidate = staleWhileRevalidate;
165
+ }
166
+
167
+ if (isUpdate) {
168
+ // When updating, delete then set to preserve MRU ordering
169
+ this.cache.delete(key);
170
+ }
171
+ this.cache.set(key, entry);
172
+ // Clear any pending operations since we have fresh data
173
+ this.pendingFetches.delete(key);
174
+ this.pendingRefresh.delete(key);
175
+ }
176
+
177
+ /**
178
+ * Clear specific cache entry
179
+ */
180
+ delete(key: string): boolean {
181
+ const deleted = this.cache.delete(key);
182
+ if (deleted) {
183
+ this.pendingFetches.delete(key);
184
+ this.pendingRefresh.delete(key);
185
+ }
186
+ return deleted;
187
+ }
188
+
189
+ /**
190
+ * Delete multiple cache entries in a single operation
191
+ */
192
+ deleteMany(keys: Iterable<string>): void {
193
+ for (const key of keys) {
194
+ this.cache.delete(key);
195
+ this.pendingFetches.delete(key);
196
+ this.pendingRefresh.delete(key);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Delete cache entries whose keys begin with the provided prefix.
202
+ * Useful for invalidating a specific resource type across budgets.
203
+ *
204
+ * @param prefix - Cache key prefix (e.g., 'transactions:' or 'accounts:list:')
205
+ * @returns The number of entries removed
206
+ */
207
+ deleteByPrefix(prefix: string): number {
208
+ if (!prefix) {
209
+ return 0;
210
+ }
211
+
212
+ const normalizedPrefix = prefix.endsWith(':') ? prefix.slice(0, -1) : prefix;
213
+ const prefixWithColon = `${normalizedPrefix}:`;
214
+
215
+ let removed = 0;
216
+ for (const key of this.cache.keys()) {
217
+ if (key === normalizedPrefix || key.startsWith(prefixWithColon)) {
218
+ this.cache.delete(key);
219
+ this.pendingFetches.delete(key);
220
+ this.pendingRefresh.delete(key);
221
+ removed++;
222
+ }
223
+ }
224
+ return removed;
225
+ }
226
+
227
+ /**
228
+ * Delete cache entries that belong to a specific budget.
229
+ * Matches keys containing the budget ID (e.g., '...:budget-123:...').
230
+ *
231
+ * @param budgetId - Budget identifier to match
232
+ */
233
+ deleteByBudgetId(budgetId: string): number {
234
+ if (!budgetId) {
235
+ return 0;
236
+ }
237
+
238
+ let removed = 0;
239
+ for (const key of this.cache.keys()) {
240
+ const segments = key.split(':');
241
+ if (segments.some((segment) => segment === budgetId)) {
242
+ this.cache.delete(key);
243
+ this.pendingFetches.delete(key);
244
+ this.pendingRefresh.delete(key);
245
+ removed++;
246
+ }
247
+ }
248
+ return removed;
249
+ }
250
+
251
+ /**
252
+ * Return all cache keys for debugging and diagnostics.
253
+ *
254
+ * @returns Snapshot of cache keys in insertion order
255
+ */
256
+ getKeys(): string[] {
257
+ return Array.from(this.cache.keys());
258
+ }
259
+
260
+ /**
261
+ * Clear all cache entries
262
+ */
263
+ clear(): void {
264
+ this.cache.clear();
265
+ this.hits = 0;
266
+ this.misses = 0;
267
+ this.evictions = 0;
268
+ this.lastCleanup = null;
269
+ this.pendingFetches.clear();
270
+ this.pendingRefresh.clear();
271
+ }
272
+
273
+ /**
274
+ * Get cache statistics
275
+ */
276
+ getStats(): {
277
+ size: number;
278
+ keys: string[];
279
+ hits: number;
280
+ misses: number;
281
+ evictions: number;
282
+ lastCleanup: number | null;
283
+ maxEntries: number;
284
+ hitRate: number;
285
+ } {
286
+ const totalRequests = this.hits + this.misses;
287
+ return {
288
+ size: this.cache.size,
289
+ keys: Array.from(this.cache.keys()),
290
+ hits: this.hits,
291
+ misses: this.misses,
292
+ evictions: this.evictions,
293
+ lastCleanup: this.lastCleanup,
294
+ maxEntries: this.maxEntries,
295
+ hitRate: totalRequests > 0 ? this.hits / totalRequests : 0,
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Provide a filtered snapshot for cache size estimation without exposing expired entries.
301
+ */
302
+ getEntriesForSizeEstimation(): [string, CacheEntry<unknown>][] {
303
+ const now = Date.now();
304
+ return Array.from(this.cache.entries()).filter(
305
+ ([, entry]) => now - entry.timestamp <= entry.ttl,
306
+ );
307
+ }
308
+
309
+ /**
310
+ * Get lightweight cache metadata for size estimation without full entry data.
311
+ * Returns summaries with keys, timestamps, and TTLs for estimating memory usage.
312
+ */
313
+ getCacheMetadata(): {
314
+ key: string;
315
+ timestamp: number;
316
+ ttl: number;
317
+ staleWhileRevalidate?: number;
318
+ dataType: string;
319
+ isExpired: boolean;
320
+ }[] {
321
+ const now = Date.now();
322
+ return Array.from(this.cache.entries()).map(([key, entry]) => {
323
+ const metadata: {
324
+ key: string;
325
+ timestamp: number;
326
+ ttl: number;
327
+ staleWhileRevalidate?: number;
328
+ dataType: string;
329
+ isExpired: boolean;
330
+ } = {
331
+ key,
332
+ timestamp: entry.timestamp,
333
+ ttl: entry.ttl,
334
+ dataType: typeof entry.data,
335
+ isExpired: now - entry.timestamp > entry.ttl,
336
+ };
337
+ if (entry.staleWhileRevalidate !== undefined) {
338
+ metadata.staleWhileRevalidate = entry.staleWhileRevalidate;
339
+ }
340
+ return metadata;
341
+ });
342
+ }
343
+
344
+ /**
345
+ * Clean up expired entries
346
+ */
347
+ cleanup(): number {
348
+ const result = this.cleanupDetailed();
349
+ return result.cleaned;
350
+ }
351
+
352
+ /**
353
+ * Clean up expired entries with detailed information
354
+ */
355
+ cleanupDetailed(): { cleaned: number; evictions: number } {
356
+ const now = Date.now();
357
+ let cleaned = 0;
358
+ const initialEvictions = this.evictions;
359
+
360
+ for (const [key, entry] of this.cache.entries()) {
361
+ if (now - entry.timestamp > entry.ttl) {
362
+ this.cache.delete(key);
363
+ this.pendingFetches.delete(key);
364
+ this.pendingRefresh.delete(key);
365
+ cleaned++;
366
+ this.evictions++;
367
+ }
368
+ }
369
+
370
+ this.lastCleanup = now;
371
+ return { cleaned, evictions: this.evictions - initialEvictions };
372
+ }
373
+
374
+ /**
375
+ * Wrap a loader function with caching and concurrent deduplication
376
+ */
377
+ async wrap<T>(key: string, options: CacheSetOptions & { loader: () => Promise<T> }): Promise<T> {
378
+ // Check cache first and preserve existing entry for background refresh
379
+ const existingEntry = this.cache.get(key);
380
+ const cached = this.get<T>(key);
381
+ if (cached !== null) {
382
+ // Check if this key was marked for background refresh (stale-while-revalidate)
383
+ if (this.pendingRefresh.has(key) && !this.pendingFetches.has(key)) {
384
+ // Start background refresh
385
+ const refreshPromise = options.loader().then(
386
+ (result) => {
387
+ // Preserve existing TTL/SWR if not specified in options
388
+ const refreshOptions: CacheSetOptions = {};
389
+ const ttl = options.ttl ?? existingEntry?.ttl;
390
+ if (ttl !== undefined) {
391
+ refreshOptions.ttl = ttl;
392
+ }
393
+ const staleWhileRevalidate =
394
+ options.staleWhileRevalidate ?? existingEntry?.staleWhileRevalidate;
395
+ if (staleWhileRevalidate !== undefined) {
396
+ refreshOptions.staleWhileRevalidate = staleWhileRevalidate;
397
+ }
398
+ // Cache the successful result
399
+ this.set(key, result, refreshOptions);
400
+ // Clean up
401
+ this.pendingFetches.delete(key);
402
+ this.pendingRefresh.delete(key);
403
+ return result;
404
+ },
405
+ (error) => {
406
+ // Clean up on error
407
+ this.pendingFetches.delete(key);
408
+ this.pendingRefresh.delete(key);
409
+ throw error;
410
+ },
411
+ );
412
+ this.pendingFetches.set(key, refreshPromise);
413
+ }
414
+ return cached;
415
+ }
416
+
417
+ // Check if there's already a pending fetch for this key
418
+ const existingFetch = this.pendingFetches.get(key) as Promise<T> | undefined;
419
+ if (existingFetch) {
420
+ return existingFetch;
421
+ }
422
+
423
+ // Execute the loader
424
+ const fetchPromise = options.loader().then(
425
+ (result) => {
426
+ // Cache the successful result using provided options (no existing entry to preserve)
427
+ this.set(key, result, options);
428
+ // Clean up pending fetch
429
+ this.pendingFetches.delete(key);
430
+ this.pendingRefresh.delete(key);
431
+ return result;
432
+ },
433
+ (error) => {
434
+ // Clean up on error, don't cache failures
435
+ this.pendingFetches.delete(key);
436
+ this.pendingRefresh.delete(key);
437
+ throw error;
438
+ },
439
+ );
440
+
441
+ // Store the pending fetch
442
+ this.pendingFetches.set(key, fetchPromise);
443
+ return fetchPromise;
444
+ }
445
+
446
+ /**
447
+ * Evict least recently used entries if cache is at capacity
448
+ */
449
+ private evictIfNeeded(): void {
450
+ if (this.maxEntries <= 0) return;
451
+
452
+ while (this.cache.size >= this.maxEntries) {
453
+ // Get the first (oldest) entry
454
+ const firstKey = this.cache.keys().next().value;
455
+ if (firstKey) {
456
+ this.cache.delete(firstKey);
457
+ this.pendingFetches.delete(firstKey);
458
+ this.pendingRefresh.delete(firstKey);
459
+ this.evictions++;
460
+ } else {
461
+ break;
462
+ }
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Parse environment variable as integer with fallback
468
+ */
469
+ private parseEnvInt(key: string, defaultValue: number): number {
470
+ const value = process.env[key];
471
+ if (!value) return defaultValue;
472
+
473
+ const parsed = parseInt(value, 10);
474
+ return isNaN(parsed) ? defaultValue : parsed;
475
+ }
476
+
477
+ /**
478
+ * Generate cache key from parameters
479
+ */
480
+ static generateKey(prefix: string, ...params: (string | number | boolean | undefined)[]): string {
481
+ const cleanParams = params
482
+ .filter((p) => p !== undefined)
483
+ .map((p) => String(p))
484
+ .join(':');
485
+
486
+ return `${prefix}:${cleanParams}`;
487
+ }
488
+ }
489
+
490
+ // Cache TTL configurations for different data types
491
+ export const CACHE_TTLS = {
492
+ BUDGETS: 10 * 60 * 1000, // 10 minutes - budgets don't change often
493
+ ACCOUNTS: 5 * 60 * 1000, // 5 minutes - account info is fairly static
494
+ CATEGORIES: 5 * 60 * 1000, // 5 minutes - categories change infrequently
495
+ PAYEES: 10 * 60 * 1000, // 10 minutes - payees are relatively stable
496
+ TRANSACTIONS: 2 * 60 * 1000, // 2 minutes - transactions change more frequently
497
+ SCHEDULED_TRANSACTIONS: 5 * 60 * 1000, // 5 minutes - scheduled transactions rarely change rapidly
498
+ USER_INFO: 30 * 60 * 1000, // 30 minutes - user info rarely changes
499
+ MONTHS: 5 * 60 * 1000, // 5 minutes - month data changes with new transactions
500
+ } as const;
501
+
502
+ // Singleton cache manager instance
503
+ export const cacheManager = new CacheManager();
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Configuration module for YNAB MCP Server
3
+ *
4
+ * Handles environment validation and server configuration.
5
+ * Extracted from YNABMCPServer to provide focused, testable configuration management.
6
+ */
7
+
8
+ import { ServerConfig, ConfigurationError } from '../types/index.js';
9
+
10
+ /**
11
+ * Create a ServerConfig from environment variables after validating required values.
12
+ *
13
+ * @returns The validated ServerConfig.
14
+ * @throws ConfigurationError if `YNAB_ACCESS_TOKEN` is missing or not a non-empty string.
15
+ */
16
+ export function validateEnvironment(): ServerConfig {
17
+ const accessToken = process.env['YNAB_ACCESS_TOKEN'];
18
+ const defaultBudgetId = process.env['YNAB_DEFAULT_BUDGET_ID'];
19
+
20
+ if (accessToken === undefined) {
21
+ throw new ConfigurationError('YNAB_ACCESS_TOKEN environment variable is required but not set');
22
+ }
23
+
24
+ if (typeof accessToken !== 'string' || accessToken.trim().length === 0) {
25
+ throw new ConfigurationError('YNAB_ACCESS_TOKEN must be a non-empty string');
26
+ }
27
+
28
+ const trimmedDefaultBudgetId = defaultBudgetId?.trim();
29
+
30
+ const config: ServerConfig = {
31
+ accessToken: accessToken.trim(),
32
+ };
33
+
34
+ if (trimmedDefaultBudgetId && trimmedDefaultBudgetId.length > 0) {
35
+ config.defaultBudgetId = trimmedDefaultBudgetId;
36
+ }
37
+
38
+ return config;
39
+ }
40
+
41
+ export type { ServerConfig } from '../types/index.js';
@@ -0,0 +1,149 @@
1
+ import type { MergeFn, MergeOptions } from './deltaCache.js';
2
+ import * as ynab from 'ynab';
3
+
4
+ export function mergeFlatEntities<T extends { id: string; deleted?: boolean }>(
5
+ snapshot: T[],
6
+ delta: T[],
7
+ options?: MergeOptions,
8
+ ): T[] {
9
+ const entityMap = new Map(snapshot.map((entity) => [entity.id, { ...entity }]));
10
+
11
+ for (const entity of delta) {
12
+ if (entity.deleted && !options?.preserveDeleted) {
13
+ entityMap.delete(entity.id);
14
+ continue;
15
+ }
16
+
17
+ const base = entityMap.get(entity.id) ?? {};
18
+ entityMap.set(entity.id, { ...base, ...entity });
19
+ }
20
+
21
+ return Array.from(entityMap.values()) as T[];
22
+ }
23
+
24
+ export const mergeMonths: MergeFn<ynab.MonthSummary> = (snapshot, delta, options) => {
25
+ const preserveDeleted = Boolean(options?.preserveDeleted);
26
+ const monthMap = new Map(snapshot.map((month) => [month.month, { ...month }]));
27
+
28
+ for (const month of delta) {
29
+ if (month.deleted && !preserveDeleted) {
30
+ monthMap.delete(month.month);
31
+ continue;
32
+ }
33
+
34
+ const base = monthMap.get(month.month) ?? {};
35
+ monthMap.set(month.month, { ...base, ...month });
36
+ }
37
+
38
+ return Array.from(monthMap.values());
39
+ };
40
+
41
+ export const mergeCategories: MergeFn<ynab.CategoryGroupWithCategories> = (
42
+ snapshot,
43
+ delta,
44
+ options,
45
+ ) => {
46
+ const preserveDeleted = Boolean(options?.preserveDeleted);
47
+ const groupMap = new Map(snapshot.map((group) => [group.id, cloneCategoryGroup(group)]));
48
+
49
+ for (const deltaGroup of delta) {
50
+ if (deltaGroup.deleted && !preserveDeleted) {
51
+ groupMap.delete(deltaGroup.id);
52
+ continue;
53
+ }
54
+
55
+ const existingGroup = groupMap.get(deltaGroup.id);
56
+ if (!existingGroup) {
57
+ groupMap.set(deltaGroup.id, cloneCategoryGroup(deltaGroup));
58
+ continue;
59
+ }
60
+
61
+ const mergedGroup: ynab.CategoryGroupWithCategories = {
62
+ ...existingGroup,
63
+ ...deltaGroup,
64
+ categories: existingGroup.categories
65
+ ? existingGroup.categories.map((cat) => ({ ...cat }))
66
+ : existingGroup.categories,
67
+ };
68
+
69
+ if (deltaGroup.categories) {
70
+ const categoryMap = new Map(
71
+ (existingGroup.categories ?? []).map((cat) => [cat.id, { ...cat }]),
72
+ );
73
+ for (const deltaCategory of deltaGroup.categories) {
74
+ if (deltaCategory.deleted && !preserveDeleted) {
75
+ categoryMap.delete(deltaCategory.id);
76
+ } else {
77
+ const base = categoryMap.get(deltaCategory.id) ?? {};
78
+ categoryMap.set(deltaCategory.id, { ...base, ...deltaCategory });
79
+ }
80
+ }
81
+ mergedGroup.categories = Array.from(categoryMap.values());
82
+ }
83
+
84
+ groupMap.set(deltaGroup.id, mergedGroup);
85
+ }
86
+
87
+ return Array.from(groupMap.values());
88
+ };
89
+
90
+ export const mergeTransactions: MergeFn<ynab.TransactionDetail> = (snapshot, delta, options) => {
91
+ const preserveDeleted = Boolean(options?.preserveDeleted);
92
+ const txnMap = new Map(snapshot.map((txn) => [txn.id, cloneTransaction(txn)]));
93
+
94
+ for (const deltaTxn of delta) {
95
+ if (deltaTxn.deleted && !preserveDeleted) {
96
+ txnMap.delete(deltaTxn.id);
97
+ continue;
98
+ }
99
+
100
+ const existingTxn = txnMap.get(deltaTxn.id);
101
+ if (!existingTxn) {
102
+ txnMap.set(deltaTxn.id, cloneTransaction(deltaTxn));
103
+ continue;
104
+ }
105
+
106
+ const mergedTxn: ynab.TransactionDetail = {
107
+ ...existingTxn,
108
+ ...deltaTxn,
109
+ subtransactions: existingTxn.subtransactions
110
+ ? existingTxn.subtransactions.map((sub) => ({ ...sub }))
111
+ : existingTxn.subtransactions,
112
+ };
113
+
114
+ if (deltaTxn.subtransactions) {
115
+ const subMap = new Map(
116
+ (existingTxn.subtransactions ?? []).map((sub) => [sub.id, { ...sub }]),
117
+ );
118
+ for (const deltaSub of deltaTxn.subtransactions) {
119
+ if (deltaSub.deleted && !preserveDeleted) {
120
+ subMap.delete(deltaSub.id);
121
+ } else {
122
+ const base = subMap.get(deltaSub.id) ?? {};
123
+ subMap.set(deltaSub.id, { ...base, ...deltaSub });
124
+ }
125
+ }
126
+ mergedTxn.subtransactions = Array.from(subMap.values());
127
+ }
128
+
129
+ txnMap.set(deltaTxn.id, mergedTxn);
130
+ }
131
+
132
+ return Array.from(txnMap.values());
133
+ };
134
+
135
+ const cloneCategoryGroup = (
136
+ group: ynab.CategoryGroupWithCategories,
137
+ ): ynab.CategoryGroupWithCategories => ({
138
+ ...group,
139
+ categories: group.categories
140
+ ? group.categories.map((category) => ({ ...category }))
141
+ : group.categories,
142
+ });
143
+
144
+ const cloneTransaction = (transaction: ynab.TransactionDetail): ynab.TransactionDetail => ({
145
+ ...transaction,
146
+ subtransactions: transaction.subtransactions
147
+ ? transaction.subtransactions.map((sub) => ({ ...sub }))
148
+ : transaction.subtransactions,
149
+ });