@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,341 @@
1
+ import { CacheManager } from './cacheManager.js';
2
+ import globalRequestLogger from './requestLogger.js';
3
+ import { ServerKnowledgeStore } from './serverKnowledgeStore.js';
4
+
5
+ export interface Logger {
6
+ info(message: string, meta?: Record<string, unknown>): void;
7
+ warn(message: string, meta?: Record<string, unknown>): void;
8
+ error(message: string, meta?: Record<string, unknown>): void;
9
+ }
10
+
11
+ const LARGE_KNOWLEDGE_GAP_THRESHOLD = 100;
12
+
13
+ const requestLoggerAdapter: Logger = {
14
+ info(message, meta) {
15
+ globalRequestLogger.logSuccess('delta-cache', message, meta ? { ...meta } : {});
16
+ },
17
+ warn(message, meta) {
18
+ const parameters: Record<string, unknown> = {
19
+ ...(meta ? { ...meta } : {}),
20
+ severity: 'warn',
21
+ };
22
+ globalRequestLogger.logSuccess('delta-cache', message, parameters);
23
+ },
24
+ error(message, meta) {
25
+ const parameters: Record<string, unknown> = meta ? { ...meta } : {};
26
+ const errorField = parameters['error'];
27
+ const errorDetail = typeof errorField === 'string' ? errorField : message;
28
+ globalRequestLogger.logError('delta-cache', message, parameters, errorDetail);
29
+ },
30
+ };
31
+
32
+ /**
33
+ * Options for merge operations
34
+ */
35
+ export interface MergeOptions {
36
+ /** Whether to preserve deleted entities (default: false) */
37
+ preserveDeleted?: boolean;
38
+ /**
39
+ * Custom equality function for deduplication.
40
+ *
41
+ * NOTE: The built-in merge functions (mergeFlatEntities, mergeCategories, mergeTransactions)
42
+ * ignore this field and use ID-based equality (`entity.id`). This field is available for
43
+ * caller-supplied custom merge functions that need non-ID-based equality semantics.
44
+ *
45
+ * @example
46
+ * // Custom merge function using equalityFn:
47
+ * const customMerge: MergeFn<MyEntity> = (snapshot, delta, options) => {
48
+ * const equals = options?.equalityFn || ((a, b) => a.id === b.id);
49
+ * // Use equals() for custom deduplication logic
50
+ * };
51
+ */
52
+ equalityFn?: (a: unknown, b: unknown) => boolean;
53
+ }
54
+
55
+ export type MergeFn<T> = (snapshot: T[], delta: T[], options?: MergeOptions) => T[];
56
+
57
+ export interface DeltaFetchResult<T> {
58
+ data: T[];
59
+ wasCached: boolean;
60
+ usedDelta: boolean;
61
+ serverKnowledge: number;
62
+ }
63
+
64
+ export interface DeltaCacheEntry<T> {
65
+ snapshot: T[];
66
+ serverKnowledge: number;
67
+ timestamp: number;
68
+ ttl: number;
69
+ staleWhileRevalidate?: number;
70
+ }
71
+
72
+ export interface DeltaCacheStats {
73
+ deltaHits: number;
74
+ deltaMisses: number;
75
+ mergeOperations: number;
76
+ knowledgeGapEvents: number;
77
+ }
78
+
79
+ type DeltaFetcher<T> = (lastKnowledge?: number) => Promise<{ data: T[]; serverKnowledge: number }>;
80
+
81
+ /**
82
+ * DeltaCache coordinates cache entries with server knowledge tracking to issue delta-aware requests.
83
+ * The feature is gated by YNAB_MCP_ENABLE_DELTA for safe rollouts.
84
+ */
85
+ export class DeltaCache {
86
+ private deltaHits = 0;
87
+ private deltaMisses = 0;
88
+ private mergeOperations = 0;
89
+ private knowledgeGapEvents = 0;
90
+
91
+ constructor(
92
+ private readonly cacheManager: CacheManager,
93
+ private readonly knowledgeStore: ServerKnowledgeStore,
94
+ private readonly logger: Logger = requestLoggerAdapter,
95
+ ) {}
96
+
97
+ /**
98
+ * Fetch data with optional delta semantics using cached snapshots and merge functions.
99
+ *
100
+ * @param options.ttl - Required TTL in milliseconds. Callers must explicitly specify the appropriate
101
+ * resource-specific TTL (e.g., CACHE_TTLS.ACCOUNTS, CACHE_TTLS.CATEGORIES, etc.)
102
+ * to ensure each resource type uses correct cache expiration.
103
+ * @param options.staleWhileRevalidate - Optional stale-while-revalidate window in milliseconds.
104
+ * Allows serving stale cache entries while refreshing in background.
105
+ */
106
+ async fetchWithDelta<T extends { deleted?: boolean }>(
107
+ cacheKey: string,
108
+ budgetId: string,
109
+ fetcher: DeltaFetcher<T>,
110
+ merger: MergeFn<T>,
111
+ options: {
112
+ ttl: number;
113
+ forceFullRefresh?: boolean;
114
+ mergeOptions?: MergeOptions;
115
+ staleWhileRevalidate?: number;
116
+ },
117
+ ): Promise<DeltaFetchResult<T>> {
118
+ const effectiveTtl = this.assertFiniteTtl('fetchWithDelta', cacheKey, options.ttl);
119
+
120
+ if (!this.isDeltaEnabled()) {
121
+ return this.fetchWithoutDelta(cacheKey, budgetId, fetcher, options);
122
+ }
123
+
124
+ // Always check cache, even with forceFullRefresh (which only disables delta merging)
125
+ const cachedEntry = this.cacheManager.get<DeltaCacheEntry<T>>(cacheKey);
126
+
127
+ // If forceFullRefresh is true but we have a fresh cache entry, return it
128
+ if (options.forceFullRefresh && cachedEntry) {
129
+ const age = Date.now() - cachedEntry.timestamp;
130
+ const isStale = age > cachedEntry.ttl;
131
+ if (!isStale) {
132
+ return {
133
+ data: cachedEntry.snapshot,
134
+ wasCached: true,
135
+ usedDelta: false,
136
+ serverKnowledge: cachedEntry.serverKnowledge,
137
+ };
138
+ }
139
+ }
140
+
141
+ const lastKnowledge = options.forceFullRefresh ? undefined : this.knowledgeStore.get(cacheKey);
142
+ const canUseDelta = Boolean(
143
+ !options.forceFullRefresh && cachedEntry && lastKnowledge !== undefined,
144
+ );
145
+ const requestedKnowledge = canUseDelta ? lastKnowledge : undefined;
146
+
147
+ let response = await fetcher(requestedKnowledge);
148
+ const knowledgeGap =
149
+ requestedKnowledge !== undefined ? response.serverKnowledge - requestedKnowledge : 0;
150
+
151
+ let forcedFullRefreshDueToGap = false;
152
+
153
+ if (knowledgeGap > LARGE_KNOWLEDGE_GAP_THRESHOLD) {
154
+ this.logger.warn('delta-cache.knowledge-gap', {
155
+ budgetId,
156
+ cacheKey,
157
+ lastKnowledge: requestedKnowledge,
158
+ serverKnowledge: response.serverKnowledge,
159
+ gap: knowledgeGap,
160
+ threshold: LARGE_KNOWLEDGE_GAP_THRESHOLD,
161
+ action: 'full-refresh',
162
+ recommendation: 'Consider forcing a full refresh to resync cache.',
163
+ });
164
+ forcedFullRefreshDueToGap = true;
165
+ this.knowledgeGapEvents++;
166
+ response = await fetcher(undefined);
167
+ }
168
+
169
+ const receivedDelta =
170
+ !forcedFullRefreshDueToGap &&
171
+ requestedKnowledge !== undefined &&
172
+ response.serverKnowledge > requestedKnowledge;
173
+
174
+ let finalSnapshot: T[];
175
+ let usedDelta = false;
176
+
177
+ if (receivedDelta && cachedEntry) {
178
+ this.mergeOperations++;
179
+ finalSnapshot = merger(cachedEntry.snapshot, response.data, options.mergeOptions);
180
+ usedDelta = true;
181
+ } else if (cachedEntry && requestedKnowledge !== undefined && !forcedFullRefreshDueToGap) {
182
+ // No changes were reported, so reuse the cached snapshot
183
+ finalSnapshot = cachedEntry.snapshot;
184
+ } else {
185
+ finalSnapshot = this.filterDeleted(response.data);
186
+ }
187
+
188
+ const cacheEntry: DeltaCacheEntry<T> = {
189
+ snapshot: finalSnapshot,
190
+ serverKnowledge: response.serverKnowledge,
191
+ timestamp: Date.now(),
192
+ ttl: effectiveTtl,
193
+ };
194
+
195
+ if (options.staleWhileRevalidate !== undefined) {
196
+ cacheEntry.staleWhileRevalidate = options.staleWhileRevalidate;
197
+ }
198
+
199
+ const cacheOptions: { ttl: number; staleWhileRevalidate?: number } = {
200
+ ttl: effectiveTtl,
201
+ };
202
+ if (options.staleWhileRevalidate !== undefined) {
203
+ cacheOptions.staleWhileRevalidate = options.staleWhileRevalidate;
204
+ }
205
+
206
+ this.cacheManager.set(cacheKey, cacheEntry, cacheOptions);
207
+ this.knowledgeStore.update(cacheKey, response.serverKnowledge);
208
+
209
+ if (canUseDelta) {
210
+ this.deltaHits++;
211
+ } else {
212
+ this.deltaMisses++;
213
+ }
214
+
215
+ return {
216
+ data: finalSnapshot,
217
+ wasCached: Boolean(cachedEntry),
218
+ usedDelta,
219
+ serverKnowledge: response.serverKnowledge,
220
+ };
221
+ }
222
+
223
+ getStats(): DeltaCacheStats {
224
+ return {
225
+ deltaHits: this.deltaHits,
226
+ deltaMisses: this.deltaMisses,
227
+ mergeOperations: this.mergeOperations,
228
+ knowledgeGapEvents: this.knowledgeGapEvents,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Invalidate cached entries for a budget, optionally scoped to a resource type.
234
+ * Does not reset server knowledge to allow graceful follow-up delta requests.
235
+ */
236
+ invalidate(budgetId: string, resourceType?: string): void {
237
+ if (!budgetId) {
238
+ return;
239
+ }
240
+
241
+ if (resourceType) {
242
+ const prefix = `${resourceType}:list:${budgetId}`;
243
+ this.cacheManager.deleteByPrefix(prefix);
244
+ } else {
245
+ this.cacheManager.deleteByBudgetId(budgetId);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Force a full refresh by invalidating caches and resetting knowledge entries.
251
+ */
252
+ forceFullRefresh(budgetId?: string, resourceType?: string): void {
253
+ if (budgetId) {
254
+ this.invalidate(budgetId, resourceType);
255
+ } else {
256
+ this.cacheManager.clear();
257
+ }
258
+
259
+ if (resourceType && budgetId) {
260
+ this.knowledgeStore.reset(`${resourceType}:list:${budgetId}`);
261
+ } else if (budgetId) {
262
+ this.knowledgeStore.resetByBudgetId(budgetId);
263
+ } else {
264
+ this.knowledgeStore.reset();
265
+ }
266
+ }
267
+
268
+ private async fetchWithoutDelta<T extends { deleted?: boolean }>(
269
+ cacheKey: string,
270
+ _budgetId: string,
271
+ fetcher: DeltaFetcher<T>,
272
+ options: { ttl: number; forceFullRefresh?: boolean; staleWhileRevalidate?: number },
273
+ ): Promise<DeltaFetchResult<T>> {
274
+ const effectiveTtl = this.assertFiniteTtl('fetchWithoutDelta', cacheKey, options.ttl);
275
+
276
+ // Always check cache, even with forceFullRefresh (which only disables delta merging)
277
+ const cachedEntry = this.cacheManager.get<DeltaCacheEntry<T>>(cacheKey);
278
+
279
+ // If we have a fresh cache entry, return it
280
+ if (cachedEntry) {
281
+ const age = Date.now() - cachedEntry.timestamp;
282
+ const isStale = age > cachedEntry.ttl;
283
+ if (!isStale || !options.forceFullRefresh) {
284
+ return {
285
+ data: cachedEntry.snapshot,
286
+ wasCached: true,
287
+ usedDelta: false,
288
+ serverKnowledge: cachedEntry.serverKnowledge,
289
+ };
290
+ }
291
+ }
292
+
293
+ const response = await fetcher(undefined);
294
+ const cleanedData = this.filterDeleted(response.data);
295
+ const cacheEntry: DeltaCacheEntry<T> = {
296
+ snapshot: cleanedData,
297
+ serverKnowledge: response.serverKnowledge,
298
+ timestamp: Date.now(),
299
+ ttl: effectiveTtl,
300
+ };
301
+
302
+ if (options.staleWhileRevalidate !== undefined) {
303
+ cacheEntry.staleWhileRevalidate = options.staleWhileRevalidate;
304
+ }
305
+
306
+ const cacheOptions: { ttl: number; staleWhileRevalidate?: number } = {
307
+ ttl: effectiveTtl,
308
+ };
309
+ if (options.staleWhileRevalidate !== undefined) {
310
+ cacheOptions.staleWhileRevalidate = options.staleWhileRevalidate;
311
+ }
312
+
313
+ this.cacheManager.set(cacheKey, cacheEntry, cacheOptions);
314
+ this.knowledgeStore.update(cacheKey, response.serverKnowledge);
315
+
316
+ return {
317
+ data: cleanedData,
318
+ wasCached: false,
319
+ usedDelta: false,
320
+ serverKnowledge: response.serverKnowledge,
321
+ };
322
+ }
323
+
324
+ private filterDeleted<T extends { deleted?: boolean }>(items: T[]): T[] {
325
+ return items.filter((item) => !item.deleted);
326
+ }
327
+
328
+ private assertFiniteTtl(methodName: string, cacheKey: string, ttl: number): number {
329
+ if (!Number.isFinite(ttl)) {
330
+ throw new Error(
331
+ `DeltaCache.${methodName} requires a finite ttl for cache key "${cacheKey}". Received: ${String(ttl)}.`,
332
+ );
333
+ }
334
+
335
+ return ttl;
336
+ }
337
+
338
+ private isDeltaEnabled(): boolean {
339
+ return process.env['YNAB_MCP_ENABLE_DELTA'] === 'true';
340
+ }
341
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Diagnostics module for YNAB MCP Server
3
+ *
4
+ * Handles comprehensive system diagnostics collection.
5
+ * Extracted from YNABMCPServer to provide focused, testable diagnostics management.
6
+ */
7
+
8
+ import type { CacheManager } from './cacheManager.js';
9
+ import type { ServerKnowledgeStore } from './serverKnowledgeStore.js';
10
+ import type { DeltaCache } from './deltaCache.js';
11
+
12
+ /**
13
+ * Security stats provider interface
14
+ */
15
+ export interface SecurityStatsProvider {
16
+ getSecurityStats(): unknown;
17
+ }
18
+
19
+ /**
20
+ * Response formatter interface to avoid direct dependency on concrete implementation
21
+ */
22
+ interface ResponseFormatter {
23
+ format(data: unknown): string;
24
+ }
25
+
26
+ /**
27
+ * Diagnostic options for configuring what diagnostics to include
28
+ */
29
+ export interface DiagnosticOptions {
30
+ include_server?: boolean;
31
+ include_memory?: boolean;
32
+ include_environment?: boolean;
33
+ include_security?: boolean;
34
+ include_cache?: boolean;
35
+ include_delta?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Diagnostic data structure
40
+ */
41
+ export interface DiagnosticData {
42
+ timestamp: string;
43
+ server?: {
44
+ name: string;
45
+ version: string;
46
+ node_version: string;
47
+ platform: string;
48
+ arch: string;
49
+ pid: number;
50
+ uptime_ms: number;
51
+ uptime_readable: string;
52
+ env: {
53
+ node_env: string;
54
+ minify_output: string;
55
+ };
56
+ };
57
+ memory?: {
58
+ rss_mb: number;
59
+ heap_used_mb: number;
60
+ heap_total_mb: number;
61
+ external_mb: number;
62
+ array_buffers_mb: number;
63
+ description: {
64
+ rss: string;
65
+ heap_used: string;
66
+ heap_total: string;
67
+ external: string;
68
+ array_buffers: string;
69
+ };
70
+ };
71
+ environment?: {
72
+ token_present: boolean;
73
+ token_length: number;
74
+ token_preview: string | null;
75
+ ynab_env_keys_present: string[];
76
+ working_directory: string;
77
+ };
78
+ security?: unknown;
79
+ cache?: {
80
+ entries: number;
81
+ estimated_size_kb: number;
82
+ keys: string[];
83
+ hits?: number;
84
+ misses?: number;
85
+ evictions?: number;
86
+ lastCleanup?: string | null;
87
+ maxEntries?: number;
88
+ hitRate?: string;
89
+ performance_summary?: string;
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Injectable dependencies for diagnostic manager
95
+ */
96
+ export interface DiagnosticDependencies {
97
+ securityMiddleware: SecurityStatsProvider;
98
+ cacheManager: CacheManager;
99
+ responseFormatter: ResponseFormatter;
100
+ serverVersion: string;
101
+ serverKnowledgeStore?: ServerKnowledgeStore;
102
+ deltaCache?: DeltaCache;
103
+ }
104
+
105
+ /**
106
+ * Formats a duration given in milliseconds into a concise human-readable uptime string.
107
+ *
108
+ * @param ms - Duration in milliseconds
109
+ * @returns A string such as "1d 2h 3m 4s", "2h 3m 4s", "3m 4s", or "45s" depending on the magnitude
110
+ */
111
+ export function formatUptime(ms: number): string {
112
+ const seconds = Math.floor(ms / 1000);
113
+ const minutes = Math.floor(seconds / 60);
114
+ const hours = Math.floor(minutes / 60);
115
+ const days = Math.floor(hours / 24);
116
+
117
+ if (days > 0) {
118
+ return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`;
119
+ } else if (hours > 0) {
120
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
121
+ } else if (minutes > 0) {
122
+ return `${minutes}m ${seconds % 60}s`;
123
+ } else {
124
+ return `${seconds}s`;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Convert a byte count to megabytes with two-decimal precision.
130
+ *
131
+ * @param bytes - The number of bytes to convert
132
+ * @returns The size in megabytes rounded to two decimal places
133
+ */
134
+ export function formatBytes(bytes: number): number {
135
+ return Math.round((bytes / 1024 / 1024) * 100) / 100;
136
+ }
137
+
138
+ /**
139
+ * Produce a masked preview of an access token for safe display.
140
+ *
141
+ * The masking rules:
142
+ * - If `token` is falsy, returns `null`.
143
+ * - If `token` length is less than 8, returns the first character followed by `***`.
144
+ * - Otherwise, returns the first four characters, `...`, and the last four characters.
145
+ * - If the token contains a trailing hyphen segment within 6 characters of the end, the trailing segment (including the hyphen) is used as the last part.
146
+ *
147
+ * @param token - The token to mask
148
+ * @returns The masked token preview, or `null` if `token` is falsy
149
+ */
150
+ export function maskToken(token: string | undefined): string | null {
151
+ if (!token) return null;
152
+
153
+ if (token.length < 8) {
154
+ return `${token.slice(0, 1)}***`;
155
+ }
156
+
157
+ const firstPart = token.slice(0, 4);
158
+ let lastPart = token.slice(-4);
159
+
160
+ const trailingHyphenIndex = token.lastIndexOf('-');
161
+ if (trailingHyphenIndex !== -1 && token.length - trailingHyphenIndex <= 6) {
162
+ lastPart = token.slice(trailingHyphenIndex);
163
+ }
164
+
165
+ return `${firstPart}...${lastPart}`;
166
+ }
167
+
168
+ /**
169
+ * DiagnosticManager class that handles diagnostic data collection
170
+ */
171
+ export class DiagnosticManager {
172
+ private dependencies: DiagnosticDependencies;
173
+
174
+ constructor(dependencies: DiagnosticDependencies) {
175
+ this.dependencies = dependencies;
176
+ }
177
+
178
+ /**
179
+ * Collects comprehensive diagnostic information
180
+ */
181
+ async collectDiagnostics(options: DiagnosticOptions): Promise<{
182
+ content: { type: 'text'; text: string }[];
183
+ }> {
184
+ const diagnostics: Record<string, unknown> = {
185
+ timestamp: new Date().toISOString(),
186
+ };
187
+
188
+ if (options.include_server) {
189
+ const uptimeMs = Math.round(process.uptime() * 1000);
190
+ diagnostics['server'] = {
191
+ name: 'ynab-mcp-server',
192
+ version: this.dependencies.serverVersion,
193
+ node_version: process.version,
194
+ platform: process.platform,
195
+ arch: process.arch,
196
+ pid: process.pid,
197
+ uptime_ms: uptimeMs,
198
+ uptime_readable: formatUptime(uptimeMs),
199
+ env: {
200
+ node_env: process.env['NODE_ENV'] || 'development',
201
+ minify_output: process.env['YNAB_MCP_MINIFY_OUTPUT'] ?? 'true',
202
+ },
203
+ };
204
+ }
205
+
206
+ if (options.include_memory) {
207
+ const memUsage = process.memoryUsage();
208
+ diagnostics['memory'] = {
209
+ rss_mb: formatBytes(memUsage.rss),
210
+ heap_used_mb: formatBytes(memUsage.heapUsed),
211
+ heap_total_mb: formatBytes(memUsage.heapTotal),
212
+ external_mb: formatBytes(memUsage.external),
213
+ array_buffers_mb: formatBytes(memUsage.arrayBuffers ?? 0),
214
+ description: {
215
+ rss: 'Resident Set Size - total memory allocated for the process',
216
+ heap_used: 'Used heap memory (objects, closures, etc.)',
217
+ heap_total: 'Total heap memory allocated',
218
+ external: 'Memory used by C++ objects bound to JavaScript objects',
219
+ array_buffers: 'Memory allocated for ArrayBuffer and SharedArrayBuffer',
220
+ },
221
+ };
222
+ }
223
+
224
+ if (options.include_environment) {
225
+ const token = process.env['YNAB_ACCESS_TOKEN'];
226
+ const envKeys = Object.keys(process.env ?? {});
227
+ const ynabEnvKeys = envKeys.filter((key) => key.toUpperCase().includes('YNAB'));
228
+ const rawTokenLength = token?.length ?? 0;
229
+ // Round masked token lengths up to the nearest even value to avoid leaking exact size
230
+ const reportedTokenLength =
231
+ token && token.length >= 8 ? rawTokenLength + (rawTokenLength % 2) : rawTokenLength;
232
+
233
+ diagnostics['environment'] = {
234
+ token_present: !!token,
235
+ token_length: reportedTokenLength,
236
+ token_preview: maskToken(token),
237
+ ynab_env_keys_present: ynabEnvKeys,
238
+ working_directory: process.cwd(),
239
+ };
240
+ }
241
+
242
+ if (options.include_security) {
243
+ diagnostics['security'] = this.dependencies.securityMiddleware.getSecurityStats();
244
+ }
245
+
246
+ if (options.include_cache) {
247
+ const cacheStats = this.dependencies.cacheManager.getStats();
248
+ const estimateCacheSize = () => {
249
+ try {
250
+ // Use lightweight metadata instead of full entry data for size estimation
251
+ const metadata = this.dependencies.cacheManager.getCacheMetadata();
252
+ const serialized = JSON.stringify(metadata);
253
+ return Math.round(Buffer.byteLength(serialized, 'utf8') / 1024);
254
+ } catch {
255
+ return 0;
256
+ }
257
+ };
258
+
259
+ // Build performance summary
260
+ const performanceParts = [];
261
+ if ('hitRate' in cacheStats) {
262
+ const hitRatePercent = (cacheStats.hitRate * 100).toFixed(1);
263
+ performanceParts.push(
264
+ `Hit rate: ${hitRatePercent}% (${cacheStats.hits} hits, ${cacheStats.misses} misses)`,
265
+ );
266
+ }
267
+ if ('evictions' in cacheStats && cacheStats.evictions > 0) {
268
+ performanceParts.push(`LRU evictions: ${cacheStats.evictions}`);
269
+ }
270
+ if ('lastCleanup' in cacheStats && cacheStats.lastCleanup) {
271
+ const lastCleanupDate = new Date(cacheStats.lastCleanup);
272
+ const minutesAgo = Math.round((Date.now() - lastCleanupDate.getTime()) / (60 * 1000));
273
+ performanceParts.push(`Last cleanup: ${minutesAgo} minutes ago`);
274
+ }
275
+
276
+ const cacheData: {
277
+ entries: number;
278
+ estimated_size_kb: number;
279
+ keys: string[];
280
+ hits?: number;
281
+ misses?: number;
282
+ evictions?: number;
283
+ lastCleanup?: string | null;
284
+ maxEntries?: number;
285
+ hitRate?: string;
286
+ performance_summary?: string;
287
+ } = {
288
+ entries: cacheStats.size,
289
+ estimated_size_kb: estimateCacheSize(),
290
+ keys: cacheStats.keys,
291
+ };
292
+
293
+ // Add enhanced metrics if available
294
+ if ('hits' in cacheStats) {
295
+ cacheData.hits = cacheStats.hits;
296
+ cacheData.misses = cacheStats.misses;
297
+ cacheData.evictions = cacheStats.evictions;
298
+ cacheData.lastCleanup = cacheStats.lastCleanup
299
+ ? new Date(cacheStats.lastCleanup).toISOString()
300
+ : null;
301
+ cacheData.maxEntries = cacheStats.maxEntries;
302
+ cacheData.hitRate = `${(cacheStats.hitRate * 100).toFixed(2)}%`;
303
+
304
+ if (performanceParts.length > 0) {
305
+ cacheData.performance_summary = performanceParts.join(', ');
306
+ }
307
+ }
308
+
309
+ diagnostics['cache'] = cacheData;
310
+ }
311
+
312
+ if (
313
+ options.include_delta === true &&
314
+ this.dependencies.serverKnowledgeStore &&
315
+ this.dependencies.deltaCache
316
+ ) {
317
+ const knowledgeStats = this.dependencies.serverKnowledgeStore.getStats();
318
+ const deltaStats = this.dependencies.deltaCache.getStats();
319
+ const totalDeltaRequests = deltaStats.deltaHits + deltaStats.deltaMisses;
320
+ const deltaHitRate = totalDeltaRequests > 0 ? deltaStats.deltaHits / totalDeltaRequests : 0;
321
+ diagnostics['delta'] = {
322
+ enabled: process.env['YNAB_MCP_ENABLE_DELTA'] === 'true',
323
+ knowledge_entries: Object.keys(knowledgeStats.entries).length, // Canonical count from entries
324
+ knowledge_stats: knowledgeStats.entries,
325
+ feature_flag: process.env['YNAB_MCP_ENABLE_DELTA'] ?? 'false',
326
+ delta_hits: deltaStats.deltaHits,
327
+ delta_misses: deltaStats.deltaMisses,
328
+ delta_hit_rate: Number(deltaHitRate.toFixed(4)),
329
+ merge_operations: deltaStats.mergeOperations,
330
+ knowledge_gap_events: deltaStats.knowledgeGapEvents,
331
+ };
332
+ }
333
+
334
+ return {
335
+ content: [{ type: 'text', text: this.dependencies.responseFormatter.format(diagnostics) }],
336
+ };
337
+ }
338
+ }