@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,489 @@
1
+ import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z, toJSONSchema } from 'zod/v4';
3
+ import type { MCPToolAnnotations } from '../types/toolAnnotations.js';
4
+
5
+ export type SecurityWrapperFactory = <T extends Record<string, unknown>>(
6
+ namespace: string,
7
+ operation: string,
8
+ schema: z.ZodSchema<T>,
9
+ ) => (
10
+ accessToken: string,
11
+ ) => (
12
+ params: Record<string, unknown>,
13
+ ) => (handler: (validated: T) => Promise<CallToolResult>) => Promise<CallToolResult>;
14
+
15
+ export interface ErrorHandlerContract {
16
+ handleError(error: unknown, context: string): CallToolResult;
17
+ createValidationError(message: string, details?: string, suggestions?: string[]): CallToolResult;
18
+ }
19
+
20
+ export interface ResponseFormatterContract {
21
+ runWithMinifyOverride<T>(minifyOverride: boolean | undefined, fn: () => T): T;
22
+ }
23
+
24
+ export interface ToolRegistryCacheHelpers {
25
+ generateKey?: (...segments: unknown[]) => string;
26
+ invalidate?: (key: string) => void | Promise<void>;
27
+ clear?: () => void | Promise<void>;
28
+ }
29
+
30
+ export interface DefaultArgumentResolverContext {
31
+ name: string;
32
+ accessToken: string;
33
+ rawArguments: Record<string, unknown>;
34
+ }
35
+ export class DefaultArgumentResolutionError extends Error {
36
+ constructor(public readonly result: CallToolResult) {
37
+ super('Default argument resolution failed');
38
+ this.name = 'DefaultArgumentResolutionError';
39
+ }
40
+ }
41
+
42
+ export type DefaultArgumentResolver<TInput extends Record<string, unknown>> = (
43
+ context: DefaultArgumentResolverContext,
44
+ ) => Partial<TInput> | Promise<Partial<TInput> | undefined> | undefined;
45
+
46
+ export interface ToolSecurityOptions {
47
+ namespace?: string;
48
+ operation?: string;
49
+ }
50
+
51
+ export interface ToolMetadataOptions {
52
+ inputJsonSchema?: Record<string, unknown>;
53
+ annotations?: MCPToolAnnotations;
54
+ }
55
+
56
+ export interface ToolExecutionContext {
57
+ accessToken: string;
58
+ name: string;
59
+ operation: string;
60
+ rawArguments: Record<string, unknown>;
61
+ cache?: ToolRegistryCacheHelpers;
62
+ }
63
+
64
+ export interface ToolExecutionPayload<TInput extends Record<string, unknown>> {
65
+ input: TInput;
66
+ context: ToolExecutionContext;
67
+ }
68
+
69
+ export type ToolHandler<TInput extends Record<string, unknown>> = (
70
+ payload: ToolExecutionPayload<TInput>,
71
+ ) => Promise<CallToolResult>;
72
+
73
+ export interface ToolDefinition<
74
+ TInput extends Record<string, unknown> = Record<string, unknown>,
75
+ TOutput extends Record<string, unknown> = Record<string, unknown>,
76
+ > {
77
+ name: string;
78
+ description: string;
79
+ inputSchema: z.ZodSchema<TInput>;
80
+ outputSchema?: z.ZodSchema<TOutput>;
81
+ handler: ToolHandler<TInput>;
82
+ security?: ToolSecurityOptions;
83
+ metadata?: ToolMetadataOptions;
84
+ defaultArgumentResolver?: DefaultArgumentResolver<TInput>;
85
+ }
86
+
87
+ interface RegisteredTool<
88
+ TInput extends Record<string, unknown>,
89
+ TOutput extends Record<string, unknown>,
90
+ > extends ToolDefinition<TInput, TOutput> {
91
+ readonly security: Required<ToolSecurityOptions>;
92
+ }
93
+
94
+ export interface ToolExecutionOptions {
95
+ name: string;
96
+ accessToken: string;
97
+ arguments?: Record<string, unknown>;
98
+ minifyOverride?: boolean;
99
+ }
100
+
101
+ export interface ToolRegistryDependencies {
102
+ withSecurityWrapper: SecurityWrapperFactory;
103
+ errorHandler: ErrorHandlerContract;
104
+ responseFormatter: ResponseFormatterContract;
105
+ cacheHelpers?: ToolRegistryCacheHelpers;
106
+ validateAccessToken?: (token: string) => Promise<void> | void;
107
+ }
108
+
109
+ const MINIFY_HINT_KEYS = ['minify', '_minify', '__minify'] as const;
110
+
111
+ export class ToolRegistry {
112
+ private readonly tools = new Map<
113
+ string,
114
+ RegisteredTool<Record<string, unknown>, Record<string, unknown>>
115
+ >();
116
+ private readonly outputValidators = new Map<string, z.ZodSchema<Record<string, unknown>>>();
117
+
118
+ constructor(private readonly deps: ToolRegistryDependencies) {}
119
+
120
+ register<TInput extends Record<string, unknown>, TOutput extends Record<string, unknown>>(
121
+ definition: ToolDefinition<TInput, TOutput>,
122
+ ): void {
123
+ this.assertValidDefinition(definition);
124
+
125
+ if (this.tools.has(definition.name)) {
126
+ throw new Error(`Tool '${definition.name}' is already registered`);
127
+ }
128
+
129
+ const resolved: RegisteredTool<TInput, TOutput> = {
130
+ ...definition,
131
+ security: {
132
+ namespace: definition.security?.namespace ?? 'ynab',
133
+ operation: definition.security?.operation ?? definition.name,
134
+ },
135
+ };
136
+
137
+ // Type assertion is safe here because TInput/TOutput extend Record<string, unknown>
138
+ // and RegisteredTool is covariant in its type parameters for storage purposes
139
+ const registeredTool = resolved as RegisteredTool<
140
+ Record<string, unknown>,
141
+ Record<string, unknown>
142
+ >;
143
+ this.tools.set(definition.name, registeredTool);
144
+
145
+ // Cache output validator if present
146
+ if (definition.outputSchema) {
147
+ this.outputValidators.set(
148
+ definition.name,
149
+ definition.outputSchema as z.ZodSchema<Record<string, unknown>>,
150
+ );
151
+ }
152
+ }
153
+
154
+ listTools(): Tool[] {
155
+ return Array.from(this.tools.values()).map((tool) => {
156
+ const inputSchema =
157
+ (tool.metadata?.inputJsonSchema as Tool['inputSchema'] | undefined) ??
158
+ (this.generateJsonSchema(tool.inputSchema) as Tool['inputSchema']);
159
+ const result: Tool = {
160
+ name: tool.name,
161
+ description: tool.description,
162
+ inputSchema,
163
+ };
164
+ if (tool.outputSchema) {
165
+ const outputSchema = this.generateJsonSchema(tool.outputSchema) as Tool['outputSchema'];
166
+ result.outputSchema = outputSchema;
167
+ }
168
+ if (tool.metadata?.annotations) {
169
+ result.annotations = tool.metadata.annotations;
170
+ }
171
+ return result;
172
+ });
173
+ }
174
+
175
+ getToolDefinitions(): ToolDefinition[] {
176
+ return Array.from(this.tools.values()).map((tool) => {
177
+ const definition: ToolDefinition = {
178
+ name: tool.name,
179
+ description: tool.description,
180
+ inputSchema: tool.inputSchema,
181
+ handler: tool.handler,
182
+ security: tool.security,
183
+ };
184
+ if (tool.outputSchema) {
185
+ definition.outputSchema = tool.outputSchema;
186
+ }
187
+ if (tool.metadata) {
188
+ definition.metadata = tool.metadata;
189
+ }
190
+ if (tool.defaultArgumentResolver) {
191
+ definition.defaultArgumentResolver = tool.defaultArgumentResolver;
192
+ }
193
+ return definition;
194
+ });
195
+ }
196
+
197
+ async executeTool(options: ToolExecutionOptions): Promise<CallToolResult> {
198
+ const tool = this.tools.get(options.name);
199
+ if (!tool) {
200
+ return this.deps.errorHandler.createValidationError(
201
+ `Unknown tool: ${options.name}`,
202
+ 'The requested tool is not registered with the server',
203
+ );
204
+ }
205
+
206
+ if (this.deps.validateAccessToken) {
207
+ try {
208
+ await this.deps.validateAccessToken(options.accessToken);
209
+ } catch (error) {
210
+ if (this.isCallToolResult(error)) {
211
+ return error;
212
+ }
213
+ return this.deps.errorHandler.handleError(error, `authenticating ${tool.name}`);
214
+ }
215
+ }
216
+
217
+ let defaults: Partial<Record<string, unknown>> | undefined;
218
+
219
+ if (tool.defaultArgumentResolver) {
220
+ try {
221
+ defaults = await tool.defaultArgumentResolver({
222
+ name: tool.name,
223
+ accessToken: options.accessToken,
224
+ rawArguments: options.arguments ?? {},
225
+ });
226
+ } catch (error) {
227
+ if (error instanceof DefaultArgumentResolutionError) {
228
+ return error.result;
229
+ }
230
+ if (this.isCallToolResult(error)) {
231
+ return error;
232
+ }
233
+ return this.deps.errorHandler.createValidationError(
234
+ 'Invalid parameters',
235
+ error instanceof Error
236
+ ? error.message
237
+ : 'Unknown error during default argument resolution',
238
+ );
239
+ }
240
+ }
241
+
242
+ const rawArguments: Record<string, unknown> = {
243
+ ...(defaults ?? {}),
244
+ ...(options.arguments ?? {}),
245
+ };
246
+
247
+ const minifyOverride = this.extractMinifyOverride(options, rawArguments);
248
+
249
+ const run = async (): Promise<CallToolResult> => {
250
+ try {
251
+ const secured = this.deps.withSecurityWrapper(
252
+ tool.security.namespace,
253
+ tool.security.operation,
254
+ tool.inputSchema,
255
+ )(options.accessToken)(rawArguments);
256
+
257
+ return await secured(async (validated) => {
258
+ try {
259
+ const context: ToolExecutionContext = {
260
+ accessToken: options.accessToken,
261
+ name: tool.name,
262
+ operation: tool.security.operation,
263
+ rawArguments,
264
+ };
265
+ if (this.deps.cacheHelpers) {
266
+ context.cache = this.deps.cacheHelpers;
267
+ }
268
+ const handlerResult = await tool.handler({
269
+ input: validated,
270
+ context,
271
+ });
272
+ // Validate output against schema if present
273
+ // Skip validation if handler returned an error
274
+ if (handlerResult.isError) {
275
+ return handlerResult;
276
+ }
277
+ return this.validateOutput(tool.name, handlerResult);
278
+ } catch (handlerError) {
279
+ return this.deps.errorHandler.handleError(
280
+ handlerError,
281
+ `executing ${tool.name} - ${tool.security.operation}`,
282
+ );
283
+ }
284
+ });
285
+ } catch (securityError) {
286
+ return this.normalizeSecurityError(securityError, tool);
287
+ }
288
+ };
289
+
290
+ try {
291
+ return await this.deps.responseFormatter.runWithMinifyOverride(minifyOverride, run);
292
+ } catch (formatterError) {
293
+ return this.deps.errorHandler.handleError(
294
+ formatterError,
295
+ `formatting response for ${tool.name}`,
296
+ );
297
+ }
298
+ }
299
+
300
+ private isCallToolResult(value: unknown): value is CallToolResult {
301
+ return (
302
+ typeof value === 'object' &&
303
+ value !== null &&
304
+ 'content' in (value as Record<string, unknown>) &&
305
+ Array.isArray((value as { content?: unknown }).content)
306
+ );
307
+ }
308
+
309
+ private normalizeSecurityError(
310
+ error: unknown,
311
+ tool: RegisteredTool<Record<string, unknown>, Record<string, unknown>>,
312
+ ): CallToolResult {
313
+ if (error instanceof z.ZodError) {
314
+ return this.deps.errorHandler.createValidationError(
315
+ `Invalid parameters for ${tool.name}`,
316
+ error.message,
317
+ );
318
+ }
319
+
320
+ if (error instanceof Error && error.message.includes('Validation failed')) {
321
+ return this.deps.errorHandler.createValidationError(
322
+ `Invalid parameters for ${tool.name}`,
323
+ error.message,
324
+ );
325
+ }
326
+
327
+ return this.deps.errorHandler.handleError(error, `executing ${tool.name}`);
328
+ }
329
+
330
+ private extractMinifyOverride(
331
+ options: ToolExecutionOptions,
332
+ args: Record<string, unknown>,
333
+ ): boolean | undefined {
334
+ if (typeof options.minifyOverride === 'boolean') {
335
+ return options.minifyOverride;
336
+ }
337
+
338
+ for (const key of MINIFY_HINT_KEYS) {
339
+ const value = args[key];
340
+ if (typeof value === 'boolean') {
341
+ // Remove the minify hint key from args
342
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
343
+ delete args[key];
344
+ return value;
345
+ }
346
+ }
347
+
348
+ return undefined;
349
+ }
350
+
351
+ private assertValidDefinition<
352
+ TInput extends Record<string, unknown>,
353
+ TOutput extends Record<string, unknown>,
354
+ >(definition: ToolDefinition<TInput, TOutput>): void {
355
+ if (!definition || typeof definition !== 'object') {
356
+ throw new Error('Tool definition must be an object');
357
+ }
358
+
359
+ if (!definition.name || typeof definition.name !== 'string') {
360
+ throw new Error('Tool definition requires a non-empty name');
361
+ }
362
+
363
+ if (!definition.description || typeof definition.description !== 'string') {
364
+ throw new Error(`Tool '${definition.name}' requires a description`);
365
+ }
366
+
367
+ if (!definition.inputSchema || typeof definition.inputSchema.parse !== 'function') {
368
+ throw new Error(`Tool '${definition.name}' requires a valid Zod schema`);
369
+ }
370
+
371
+ if (definition.outputSchema && typeof definition.outputSchema.parse !== 'function') {
372
+ throw new Error(
373
+ `Tool '${definition.name}' outputSchema must be a valid Zod schema when provided`,
374
+ );
375
+ }
376
+
377
+ if (typeof definition.handler !== 'function') {
378
+ throw new Error(`Tool '${definition.name}' requires a handler function`);
379
+ }
380
+
381
+ if (
382
+ definition.defaultArgumentResolver &&
383
+ typeof definition.defaultArgumentResolver !== 'function'
384
+ ) {
385
+ throw new Error(
386
+ `Tool '${definition.name}' defaultArgumentResolver must be a function when provided`,
387
+ );
388
+ }
389
+ }
390
+
391
+ private generateJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
392
+ try {
393
+ return toJSONSchema(schema, { target: 'draft-2020-12', io: 'output' });
394
+ } catch (error) {
395
+ console.warn(`Failed to generate JSON schema for tool: ${error}`);
396
+ return { type: 'object', additionalProperties: true };
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Validates handler output against the tool's output schema if present
402
+ */
403
+ private validateOutput(toolName: string, output: CallToolResult): CallToolResult {
404
+ const validator = this.outputValidators.get(toolName);
405
+ if (!validator) {
406
+ // No output schema defined, skip validation
407
+ return output;
408
+ }
409
+
410
+ // Extract the actual data from the CallToolResult
411
+ // CallToolResult is { content: Array<{ type: string, text: string, ... }> }
412
+ // We need to parse the text content and validate it
413
+ if (!output.content || output.content.length === 0) {
414
+ return this.deps.errorHandler.createValidationError(
415
+ `Output validation failed for ${toolName}`,
416
+ 'Handler returned empty content',
417
+ ['Ensure the handler returns valid content in the response'],
418
+ );
419
+ }
420
+
421
+ // Validate all content items (not just the first one)
422
+ const invalidItems: { index: number; reason: string }[] = [];
423
+
424
+ for (let i = 0; i < output.content.length; i++) {
425
+ const item = output.content[i];
426
+ if (!item) {
427
+ invalidItems.push({ index: i, reason: 'item is null or undefined' });
428
+ } else if (item.type !== 'text') {
429
+ invalidItems.push({ index: i, reason: `type is "${item.type}" instead of "text"` });
430
+ } else if (typeof item.text !== 'string') {
431
+ invalidItems.push({
432
+ index: i,
433
+ reason: `text property is ${typeof item.text} instead of string`,
434
+ });
435
+ }
436
+ }
437
+
438
+ if (invalidItems.length > 0) {
439
+ const invalidItemsDetails = invalidItems
440
+ .map((inv) => ` - Item ${inv.index}: ${inv.reason}`)
441
+ .join('\n');
442
+
443
+ return this.deps.errorHandler.createValidationError(
444
+ `Output validation failed for ${toolName}`,
445
+ `Handler returned invalid content items (${invalidItems.length} of ${output.content.length} failed):\n${invalidItemsDetails}`,
446
+ ['Ensure all content items have type="text" and a valid text property'],
447
+ );
448
+ }
449
+
450
+ const firstContent = output.content[0]!;
451
+ // TypeScript: After validation above, we know firstContent.type === 'text'
452
+ if (firstContent.type !== 'text') {
453
+ throw new Error('Unexpected: firstContent is not text after validation');
454
+ }
455
+
456
+ let parsedOutput: unknown;
457
+ try {
458
+ parsedOutput = JSON.parse(firstContent.text);
459
+ } catch (parseError) {
460
+ return this.deps.errorHandler.createValidationError(
461
+ `Output validation failed for ${toolName}`,
462
+ `Invalid JSON in handler output: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
463
+ ['Ensure the handler returns valid JSON'],
464
+ );
465
+ }
466
+
467
+ // Validate against schema
468
+ const result = validator.safeParse(parsedOutput);
469
+ if (!result.success) {
470
+ const validationErrors = result.error.issues
471
+ .map((err) => {
472
+ const path = err.path.join('.');
473
+ return path ? `${path}: ${err.message}` : err.message;
474
+ })
475
+ .join('; ');
476
+ return this.deps.errorHandler.createValidationError(
477
+ `Output validation failed for ${toolName}`,
478
+ `Handler output does not match declared output schema: ${validationErrors}`,
479
+ [
480
+ 'Check that the handler returns data matching the output schema',
481
+ 'Review the tool definition output schema',
482
+ ],
483
+ );
484
+ }
485
+
486
+ // Validation passed, return original output
487
+ return output;
488
+ }
489
+ }
@@ -0,0 +1 @@
1
+ # This file ensures the tools directory is tracked by git
@@ -0,0 +1,128 @@
1
+ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
2
+ import * as ynab from 'ynab';
3
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
4
+ import { handleListAccounts } from '../accountTools.js';
5
+ import { handleListTransactions } from '../transactionTools.js';
6
+ import { CacheManager } from '../../server/cacheManager.js';
7
+ import { ServerKnowledgeStore } from '../../server/serverKnowledgeStore.js';
8
+ import { DeltaCache } from '../../server/deltaCache.js';
9
+ import { DeltaFetcher } from '../deltaFetcher.js';
10
+
11
+ const shouldSkip = ['true', '1', 'yes', 'y', 'on'].includes(
12
+ (process.env['SKIP_E2E_TESTS'] || '').toLowerCase().trim(),
13
+ );
14
+ const hasToken = !!process.env['YNAB_ACCESS_TOKEN'];
15
+ const skipTests = shouldSkip || !hasToken;
16
+ const describeIntegration = skipTests ? describe.skip : describe;
17
+
18
+ describeIntegration('Delta-backed account tool handlers', () => {
19
+ let ynabAPI: ynab.API;
20
+ let testBudgetId: string;
21
+ let testAccountId: string;
22
+ let deltaFetcher: DeltaFetcher;
23
+ let previousNodeEnv: string | undefined;
24
+
25
+ beforeAll(async () => {
26
+ const accessToken = process.env['YNAB_ACCESS_TOKEN']!;
27
+ ynabAPI = new ynab.API(accessToken);
28
+ const budgetsResponse = await ynabAPI.budgets.getBudgets();
29
+ const budget = budgetsResponse.data.budgets[0];
30
+ if (!budget) {
31
+ throw new Error('No budgets available for delta integration tests.');
32
+ }
33
+ testBudgetId = budget.id;
34
+
35
+ const accountsResponse = await ynabAPI.accounts.getAccounts(testBudgetId);
36
+ const account = accountsResponse.data.accounts.find((acct) => !acct.closed);
37
+ if (!account) {
38
+ throw new Error('No open accounts available for delta integration tests.');
39
+ }
40
+ testAccountId = account.id;
41
+ });
42
+
43
+ beforeEach(() => {
44
+ previousNodeEnv = process.env['NODE_ENV'];
45
+ process.env['NODE_ENV'] = 'integration';
46
+ const cacheManager = new CacheManager();
47
+ const knowledgeStore = new ServerKnowledgeStore();
48
+ const deltaCache = new DeltaCache(cacheManager, knowledgeStore);
49
+ deltaFetcher = new DeltaFetcher(ynabAPI, deltaCache);
50
+ process.env['YNAB_MCP_ENABLE_DELTA'] = 'true';
51
+ });
52
+
53
+ afterEach(() => {
54
+ delete process.env['YNAB_MCP_ENABLE_DELTA'];
55
+ if (previousNodeEnv === undefined) {
56
+ delete process.env['NODE_ENV'];
57
+ } else {
58
+ process.env['NODE_ENV'] = previousNodeEnv;
59
+ }
60
+ previousNodeEnv = undefined;
61
+ });
62
+
63
+ const parseResponse = (result: CallToolResult) => {
64
+ const content = result.content?.[0];
65
+ if (!content || content.type !== 'text') {
66
+ throw new Error('Unexpected tool response format');
67
+ }
68
+ return JSON.parse(content.text);
69
+ };
70
+
71
+ it(
72
+ 'serves cached account results on the second invocation',
73
+ { meta: { tier: 'domain', domain: 'delta' } },
74
+ async () => {
75
+ const params = { budget_id: testBudgetId };
76
+ const firstCall = await handleListAccounts(ynabAPI, deltaFetcher, params);
77
+ const firstPayload = parseResponse(firstCall);
78
+ expect(firstPayload.cached).toBe(false);
79
+
80
+ const secondCall = await handleListAccounts(ynabAPI, deltaFetcher, params);
81
+ const secondPayload = parseResponse(secondCall);
82
+ expect(secondPayload.cached).toBe(true);
83
+ expect(secondPayload.cache_info).toMatch(/cache/i);
84
+ },
85
+ );
86
+
87
+ it(
88
+ 'reports delta usage for list_transactions after a change',
89
+ { meta: { tier: 'domain', domain: 'delta' } },
90
+ async () => {
91
+ const params = { budget_id: testBudgetId, account_id: testAccountId };
92
+ const firstCall = await handleListTransactions(ynabAPI, deltaFetcher, params);
93
+ const firstPayload = parseResponse(firstCall);
94
+ expect(firstPayload.cached).toBe(false);
95
+
96
+ const transactionDate = new Date().toISOString().split('T')[0];
97
+ const memo = `delta-integration-${Date.now()}`;
98
+ const transactionPayload: ynab.SaveTransaction = {
99
+ account_id: testAccountId,
100
+ date: transactionDate,
101
+ amount: -1000,
102
+ memo,
103
+ payee_name: 'Delta Integration Test',
104
+ approved: false,
105
+ cleared: 'uncleared',
106
+ };
107
+
108
+ const createResponse = await ynabAPI.transactions.createTransaction(testBudgetId, {
109
+ transaction: transactionPayload,
110
+ });
111
+ expect(createResponse.data.transaction).toBeDefined();
112
+ const createdId = createResponse.data.transaction?.id;
113
+ expect(createdId).toBeTruthy();
114
+
115
+ try {
116
+ const secondCall = await handleListTransactions(ynabAPI, deltaFetcher, params);
117
+ const secondPayload = parseResponse(secondCall);
118
+ expect(secondPayload.cached).toBe(true);
119
+ // Check for delta-related keywords (flexible assertion)
120
+ expect(secondPayload.cache_info).toMatch(/delta.*merge|merge.*delta/i);
121
+ } finally {
122
+ if (createdId) {
123
+ await ynabAPI.transactions.deleteTransaction(testBudgetId, createdId);
124
+ }
125
+ }
126
+ },
127
+ );
128
+ });