@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,756 @@
1
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+
3
+ /**
4
+ * Response formatter contract for dependency injection in error handling
5
+ */
6
+ interface ErrorResponseFormatter {
7
+ format(value: unknown): string;
8
+ }
9
+
10
+ /**
11
+ * YNAB API error codes and their corresponding HTTP status codes
12
+ */
13
+
14
+ export const enum YNABErrorCode {
15
+ UNAUTHORIZED = 401,
16
+ FORBIDDEN = 403,
17
+ NOT_FOUND = 404,
18
+ TOO_MANY_REQUESTS = 429,
19
+ INTERNAL_SERVER_ERROR = 500,
20
+ }
21
+
22
+ /**
23
+ * Security-related error codes
24
+ */
25
+ export const enum SecurityErrorCode {
26
+ RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
27
+ VALIDATION_ERROR = 'VALIDATION_ERROR',
28
+ UNKNOWN_ERROR = 'UNKNOWN_ERROR',
29
+ }
30
+
31
+ /**
32
+ * Standardized error response structure
33
+ */
34
+ export interface ErrorResponse {
35
+ error: {
36
+ code: YNABErrorCode | SecurityErrorCode;
37
+ message: string;
38
+ userMessage: string; // User-friendly message
39
+ details?: string | Record<string, unknown>;
40
+ suggestions?: string[]; // Actionable suggestions for the user
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Custom error classes for different error types
46
+ */
47
+ export class YNABAPIError extends Error {
48
+ public readonly code: YNABErrorCode;
49
+ public readonly originalError?: unknown;
50
+
51
+ constructor(code: YNABErrorCode, message: string, originalError?: unknown) {
52
+ super(message);
53
+ this.name = 'YNABAPIError';
54
+ this.code = code;
55
+ this.originalError = originalError;
56
+ }
57
+ }
58
+
59
+ export class ValidationError extends Error {
60
+ public readonly details?: string | undefined;
61
+ public readonly suggestions?: string[] | undefined;
62
+
63
+ constructor(message: string, details?: string | undefined, suggestions?: string[] | undefined) {
64
+ super(message);
65
+ this.name = 'ValidationError';
66
+ this.details = details;
67
+ this.suggestions = suggestions;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Centralized error handling middleware for all YNAB MCP tools
73
+ */
74
+ export class ErrorHandler {
75
+ private formatter: ErrorResponseFormatter;
76
+ private static defaultInstance: ErrorHandler;
77
+
78
+ constructor(formatter: ErrorResponseFormatter) {
79
+ this.formatter = formatter;
80
+ }
81
+
82
+ /**
83
+ * Creates a fallback formatter for when no formatter is injected
84
+ */
85
+ private static createFallbackFormatter(): ErrorResponseFormatter {
86
+ return {
87
+ format: (value: unknown) => JSON.stringify(value, null, 2),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Sets the formatter for the default instance (backward compatibility)
93
+ */
94
+ static setFormatter(formatter: ErrorResponseFormatter): void {
95
+ ErrorHandler.defaultInstance = new ErrorHandler(formatter);
96
+ }
97
+
98
+ /**
99
+ * Handles errors from YNAB API calls and returns standardized MCP responses
100
+ */
101
+ handleError(error: unknown, context: string): CallToolResult {
102
+ const errorResponse = this.createErrorResponse(error, context);
103
+
104
+ let formattedText: string;
105
+ try {
106
+ formattedText = this.formatter.format(errorResponse);
107
+ } catch {
108
+ // Fallback to JSON.stringify if formatter fails
109
+ formattedText = JSON.stringify(errorResponse, null, 2);
110
+ }
111
+
112
+ return {
113
+ content: [
114
+ {
115
+ type: 'text',
116
+ text: formattedText,
117
+ },
118
+ ],
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Static method for backward compatibility
124
+ */
125
+ static handleError(error: unknown, context: string): CallToolResult {
126
+ if (!ErrorHandler.defaultInstance) {
127
+ ErrorHandler.defaultInstance = new ErrorHandler(ErrorHandler.createFallbackFormatter());
128
+ }
129
+ return ErrorHandler.defaultInstance.handleError(error, context);
130
+ }
131
+
132
+ /**
133
+ * Creates a standardized error response based on the error type
134
+ */
135
+ private createErrorResponse(error: unknown, context: string): ErrorResponse {
136
+ // Handle custom error types
137
+ if (error instanceof YNABAPIError) {
138
+ const sanitizedDetails = this.sanitizeErrorDetails(error.originalError);
139
+ return {
140
+ error: {
141
+ code: error.code,
142
+ message: this.getErrorMessage(error.code, context),
143
+ userMessage: this.getUserFriendlyMessage(error.code, context),
144
+ suggestions: this.getErrorSuggestions(error.code, context),
145
+ ...(sanitizedDetails && { details: sanitizedDetails }),
146
+ },
147
+ };
148
+ }
149
+
150
+ if (error instanceof ValidationError) {
151
+ const sanitizedDetails = error.details ? this.sanitizeErrorDetails(error.details) : undefined;
152
+ const suggestions =
153
+ error.suggestions && error.suggestions.length > 0
154
+ ? error.suggestions
155
+ : this.getErrorSuggestions(SecurityErrorCode.VALIDATION_ERROR, context);
156
+ return {
157
+ error: {
158
+ code: SecurityErrorCode.VALIDATION_ERROR,
159
+ message: error.message,
160
+ userMessage: this.getUserFriendlyMessage(SecurityErrorCode.VALIDATION_ERROR, context),
161
+ suggestions,
162
+ ...(sanitizedDetails && { details: sanitizedDetails }),
163
+ },
164
+ };
165
+ }
166
+
167
+ const ynabApiError = this.extractYNABApiError(error);
168
+ if (ynabApiError) {
169
+ const sanitizedDetails = ynabApiError.details
170
+ ? this.sanitizeErrorDetails(ynabApiError.details)
171
+ : undefined;
172
+ return {
173
+ error: {
174
+ code: ynabApiError.code,
175
+ message: this.getErrorMessage(ynabApiError.code, context),
176
+ userMessage: this.getUserFriendlyMessage(ynabApiError.code, context),
177
+ suggestions: this.getErrorSuggestions(ynabApiError.code, context),
178
+ ...(sanitizedDetails && { details: sanitizedDetails }),
179
+ },
180
+ };
181
+ }
182
+
183
+ // Handle generic errors by analyzing the error message
184
+
185
+ const httpStatus = this.extractHttpStatus(error);
186
+ if (httpStatus !== null) {
187
+ const code = this.mapHttpStatusToErrorCode(httpStatus);
188
+ if (code) {
189
+ const details = this.extractHttpStatusDetails(error);
190
+ return {
191
+ error: {
192
+ code,
193
+ message: this.getErrorMessage(code, context),
194
+ userMessage: this.getUserFriendlyMessage(code, context),
195
+ suggestions: this.getErrorSuggestions(code, context),
196
+ ...(details && { details }),
197
+ },
198
+ };
199
+ }
200
+ }
201
+
202
+ // Handle generic errors by analyzing the error message
203
+ if (error instanceof Error) {
204
+ const detectedCode = this.detectErrorCode(error);
205
+ if (detectedCode) {
206
+ return {
207
+ error: {
208
+ code: detectedCode,
209
+ message: this.getErrorMessage(detectedCode, context),
210
+ userMessage: this.getUserFriendlyMessage(detectedCode, context),
211
+ suggestions: this.getErrorSuggestions(detectedCode, context),
212
+ },
213
+ };
214
+ }
215
+ }
216
+
217
+ // Fallback for unknown errors
218
+ // Preserve the original error message for debugging while sanitizing sensitive data
219
+ const errorMessage = error instanceof Error ? error.message : String(error);
220
+ const sanitizedDetails = this.sanitizeErrorDetails(errorMessage);
221
+
222
+ return {
223
+ error: {
224
+ code: SecurityErrorCode.UNKNOWN_ERROR,
225
+ message: this.getGenericErrorMessage(context),
226
+ userMessage: this.getUserFriendlyGenericMessage(context),
227
+ suggestions: [
228
+ 'Try the operation again',
229
+ 'Check your internet connection',
230
+ 'Contact support if the issue persists',
231
+ ],
232
+ ...(sanitizedDetails && { details: sanitizedDetails }),
233
+ },
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Detects YNAB error codes from error messages
239
+ */
240
+ private detectErrorCode(error: Error): YNABErrorCode | null {
241
+ const message = error.message.toLowerCase();
242
+
243
+ if (message.includes('401') || message.includes('unauthorized')) {
244
+ return YNABErrorCode.UNAUTHORIZED;
245
+ }
246
+ if (message.includes('403') || message.includes('forbidden')) {
247
+ return YNABErrorCode.FORBIDDEN;
248
+ }
249
+ if (message.includes('404') || message.includes('not found')) {
250
+ return YNABErrorCode.NOT_FOUND;
251
+ }
252
+ if (message.includes('429') || message.includes('too many requests')) {
253
+ return YNABErrorCode.TOO_MANY_REQUESTS;
254
+ }
255
+ if (message.includes('500') || message.includes('internal server error')) {
256
+ return YNABErrorCode.INTERNAL_SERVER_ERROR;
257
+ }
258
+
259
+ return null;
260
+ }
261
+
262
+ /**
263
+ * Returns user-friendly error messages for end users
264
+ */
265
+ private getUserFriendlyMessage(code: YNABErrorCode | SecurityErrorCode, context: string): string {
266
+ switch (code) {
267
+ case YNABErrorCode.UNAUTHORIZED:
268
+ return 'Your YNAB access token is invalid or has expired. Please check your token and try again.';
269
+ case YNABErrorCode.FORBIDDEN:
270
+ return "You don't have permission to access this YNAB data. Please check your account permissions.";
271
+ case YNABErrorCode.NOT_FOUND:
272
+ return this.getUserFriendlyNotFoundMessage(context);
273
+ case YNABErrorCode.TOO_MANY_REQUESTS:
274
+ return "We're making too many requests to YNAB. Please wait a moment and try again.";
275
+ case YNABErrorCode.INTERNAL_SERVER_ERROR:
276
+ return "YNAB's servers are having issues. Please try again in a few minutes.";
277
+ case SecurityErrorCode.VALIDATION_ERROR:
278
+ return 'Some of the information provided is invalid. Please check your inputs and try again.';
279
+ case SecurityErrorCode.RATE_LIMIT_EXCEEDED:
280
+ return 'Too many requests have been made. Please wait before trying again.';
281
+ default:
282
+ return this.getUserFriendlyGenericMessage(context);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Returns actionable suggestions for users based on error type
288
+ */
289
+ private getErrorSuggestions(code: YNABErrorCode | SecurityErrorCode, context: string): string[] {
290
+ switch (code) {
291
+ case YNABErrorCode.UNAUTHORIZED:
292
+ return [
293
+ 'Go to https://app.youneedabudget.com/settings/developer to generate a new access token',
294
+ 'Make sure you copied the entire token without any extra spaces',
295
+ "Check that your token hasn't expired",
296
+ ];
297
+ case YNABErrorCode.FORBIDDEN:
298
+ return [
299
+ 'Verify that your YNAB account has access to the requested budget',
300
+ 'Check if your YNAB subscription is active',
301
+ 'Try logging into YNAB directly to confirm access',
302
+ ];
303
+ case YNABErrorCode.NOT_FOUND:
304
+ return this.getNotFoundSuggestions(context);
305
+ case YNABErrorCode.TOO_MANY_REQUESTS:
306
+ return [
307
+ 'Wait 1-2 minutes before trying again',
308
+ 'Try making fewer requests at once',
309
+ 'The system will automatically retry after a short delay',
310
+ ];
311
+ case YNABErrorCode.INTERNAL_SERVER_ERROR:
312
+ return [
313
+ "Check YNAB's status page at https://status.youneedabudget.com",
314
+ 'Try again in a few minutes',
315
+ 'Contact YNAB support if the issue persists',
316
+ ];
317
+ case SecurityErrorCode.VALIDATION_ERROR:
318
+ return [
319
+ 'Double-check all required fields are filled out',
320
+ 'Verify that amounts are in the correct format',
321
+ 'Make sure dates are valid and in the right format',
322
+ ];
323
+ default:
324
+ return [
325
+ 'Try the operation again',
326
+ 'Check your internet connection',
327
+ 'Contact support if the issue persists',
328
+ ];
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Returns user-friendly not found messages
334
+ */
335
+ private getUserFriendlyNotFoundMessage(context: string): string {
336
+ if (context.includes('account')) {
337
+ return "We couldn't find the budget or account you're looking for.";
338
+ }
339
+ if (context.includes('budget')) {
340
+ return "We couldn't find that budget. It may have been deleted or you may not have access.";
341
+ }
342
+ if (context.includes('category')) {
343
+ return "We couldn't find that category. It may have been deleted or moved.";
344
+ }
345
+ if (context.includes('transaction')) {
346
+ return "We couldn't find that transaction. It may have been deleted or moved.";
347
+ }
348
+ if (context.includes('payee')) {
349
+ return "We couldn't find that payee in your budget.";
350
+ }
351
+ return "We couldn't find what you're looking for. Please check that all information is correct.";
352
+ }
353
+
354
+ /**
355
+ * Returns suggestions for not found errors
356
+ */
357
+ private getNotFoundSuggestions(context: string): string[] {
358
+ const baseSuggestions = [
359
+ 'Double-check that the name or ID is spelled correctly',
360
+ 'Try refreshing your budget data',
361
+ "Make sure you're using the right budget",
362
+ ];
363
+
364
+ if (context.includes('account')) {
365
+ return [...baseSuggestions, 'Check if the account was recently closed or renamed'];
366
+ }
367
+ if (context.includes('category')) {
368
+ return [
369
+ ...baseSuggestions,
370
+ 'Check if the category was deleted or moved to a different group',
371
+ ];
372
+ }
373
+ if (context.includes('transaction')) {
374
+ return [
375
+ ...baseSuggestions,
376
+ 'Check if the transaction was deleted or is in a different account',
377
+ ];
378
+ }
379
+
380
+ return baseSuggestions;
381
+ }
382
+
383
+ /**
384
+ * Returns user-friendly generic error message
385
+ */
386
+ private getUserFriendlyGenericMessage(context: string): string {
387
+ if (context.includes('transaction')) {
388
+ return 'There was a problem with your transaction. Please check your information and try again.';
389
+ }
390
+ if (context.includes('budget')) {
391
+ return 'There was a problem accessing your budget data. Please try again.';
392
+ }
393
+ if (context.includes('account')) {
394
+ return 'There was a problem accessing your account information. Please try again.';
395
+ }
396
+ return 'Something went wrong. Please try again in a moment.';
397
+ }
398
+
399
+ /**
400
+ * Returns user-friendly error messages for different error codes
401
+ */
402
+ private getErrorMessage(code: YNABErrorCode, context: string): string {
403
+ switch (code) {
404
+ case YNABErrorCode.UNAUTHORIZED:
405
+ return 'Invalid or expired YNAB access token';
406
+ case YNABErrorCode.FORBIDDEN:
407
+ return 'Insufficient permissions to access YNAB data';
408
+ case YNABErrorCode.NOT_FOUND:
409
+ return this.getNotFoundMessage(context);
410
+ case YNABErrorCode.TOO_MANY_REQUESTS:
411
+ return 'Rate limit exceeded. Please try again later';
412
+ case YNABErrorCode.INTERNAL_SERVER_ERROR:
413
+ return 'YNAB service is currently unavailable';
414
+ default:
415
+ return this.getGenericErrorMessage(context);
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Returns context-specific not found error messages
421
+ */
422
+ private getNotFoundMessage(context: string): string {
423
+ if (context.includes('listing accounts')) {
424
+ return 'Failed to list accounts - budget or account not found';
425
+ }
426
+ if (context.includes('getting account')) {
427
+ return 'Failed to get account - budget or account not found';
428
+ }
429
+ if (context.includes('listing budgets') || context.includes('getting budget')) {
430
+ return 'Budget not found';
431
+ }
432
+ if (context.includes('listing categories') || context.includes('getting category')) {
433
+ return 'Budget or category not found';
434
+ }
435
+ if (context.includes('listing months') || context.includes('getting month')) {
436
+ return 'Budget or month not found';
437
+ }
438
+ if (context.includes('listing payees') || context.includes('getting payee')) {
439
+ return 'Budget or payee not found';
440
+ }
441
+ if (context.includes('listing transactions') || context.includes('getting transaction')) {
442
+ return 'Budget, account, category, or transaction not found';
443
+ }
444
+ return 'The requested resource was not found. Please verify the provided IDs are correct.';
445
+ }
446
+
447
+ /**
448
+ * Returns context-specific generic error messages
449
+ */
450
+ private getGenericErrorMessage(context: string): string {
451
+ if (context.includes('listing accounts')) {
452
+ return 'Failed to list accounts';
453
+ }
454
+ if (context.includes('getting account')) {
455
+ return 'Failed to get account';
456
+ }
457
+ if (context.includes('creating account')) {
458
+ return 'Failed to create account';
459
+ }
460
+ if (context.includes('listing budgets')) {
461
+ return 'Failed to list budgets';
462
+ }
463
+ if (context.includes('getting budget')) {
464
+ return 'Failed to get budget';
465
+ }
466
+ if (context.includes('listing categories')) {
467
+ return 'Failed to list categories';
468
+ }
469
+ if (context.includes('getting category')) {
470
+ return 'Failed to get category';
471
+ }
472
+ if (context.includes('updating category')) {
473
+ return 'Failed to update category';
474
+ }
475
+ if (context.includes('listing months')) {
476
+ return 'Failed to list months';
477
+ }
478
+ if (context.includes('getting month')) {
479
+ return 'Failed to get month data';
480
+ }
481
+ if (context.includes('listing payees')) {
482
+ return 'Failed to list payees';
483
+ }
484
+ if (context.includes('getting payee')) {
485
+ return 'Failed to get payee';
486
+ }
487
+ if (context.includes('listing transactions')) {
488
+ return 'Failed to list transactions';
489
+ }
490
+ if (context.includes('getting transaction')) {
491
+ return 'Failed to get transaction';
492
+ }
493
+ if (context.includes('creating transaction')) {
494
+ return 'Failed to create transaction';
495
+ }
496
+ if (context.includes('updating transaction')) {
497
+ return 'Failed to update transaction';
498
+ }
499
+ if (context.includes('getting user')) {
500
+ return 'Failed to get user information';
501
+ }
502
+ return `An error occurred while ${context}`;
503
+ }
504
+
505
+ /**
506
+ * Extracts HTTP status code from various error shapes
507
+ */
508
+ private extractHttpStatus(error: unknown): number | null {
509
+ if (!error || typeof error !== 'object') {
510
+ return null;
511
+ }
512
+
513
+ const directStatus = (error as { status?: unknown }).status;
514
+ if (typeof directStatus === 'number' && Number.isInteger(directStatus) && directStatus > 0) {
515
+ return directStatus;
516
+ }
517
+
518
+ const response = (error as { response?: unknown }).response;
519
+ if (response && typeof response === 'object') {
520
+ const responseStatus = (response as { status?: unknown }).status;
521
+ if (
522
+ typeof responseStatus === 'number' &&
523
+ Number.isInteger(responseStatus) &&
524
+ responseStatus > 0
525
+ ) {
526
+ return responseStatus;
527
+ }
528
+ }
529
+
530
+ return null;
531
+ }
532
+
533
+ /**
534
+ * Maps HTTP status codes to standardized YNAB error codes
535
+ */
536
+ private mapHttpStatusToErrorCode(status: number): YNABErrorCode | null {
537
+ switch (status) {
538
+ case YNABErrorCode.UNAUTHORIZED:
539
+ case YNABErrorCode.FORBIDDEN:
540
+ case YNABErrorCode.NOT_FOUND:
541
+ case YNABErrorCode.TOO_MANY_REQUESTS:
542
+ case YNABErrorCode.INTERNAL_SERVER_ERROR:
543
+ return status as YNABErrorCode;
544
+ default:
545
+ return null;
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Extracts sanitized details from HTTP error responses
551
+ */
552
+ private extractHttpStatusDetails(error: unknown): string | undefined {
553
+ if (error && typeof error === 'object') {
554
+ const response = (error as { response?: unknown }).response;
555
+ if (response && typeof response === 'object') {
556
+ const statusText = (response as { statusText?: unknown }).statusText;
557
+ if (typeof statusText === 'string' && statusText.trim().length > 0) {
558
+ return this.sanitizeErrorDetails(statusText);
559
+ }
560
+ }
561
+ }
562
+
563
+ if (error instanceof Error && error.message) {
564
+ return this.sanitizeErrorDetails(error.message);
565
+ }
566
+
567
+ return undefined;
568
+ }
569
+
570
+ /**
571
+ * Extracts structured YNAB API error information
572
+ */
573
+ private extractYNABApiError(error: unknown): { code: YNABErrorCode; details?: string } | null {
574
+ if (!error || typeof error !== 'object' || !('error' in (error as Record<string, unknown>))) {
575
+ return null;
576
+ }
577
+
578
+ const payload = (error as { error?: unknown }).error;
579
+ if (!payload || typeof payload !== 'object') {
580
+ return null;
581
+ }
582
+
583
+ const id = (payload as { id?: unknown }).id;
584
+ const name = (payload as { name?: unknown }).name;
585
+ const detail = (payload as { detail?: unknown }).detail;
586
+
587
+ let code: YNABErrorCode | null = null;
588
+
589
+ if (typeof id === 'string') {
590
+ const numeric = parseInt(id, 10);
591
+ if (!Number.isNaN(numeric)) {
592
+ code = this.mapHttpStatusToErrorCode(numeric);
593
+ }
594
+ }
595
+
596
+ if (!code && typeof name === 'string') {
597
+ const normalized = name.toLowerCase();
598
+ if (normalized.includes('unauthorized')) {
599
+ code = YNABErrorCode.UNAUTHORIZED;
600
+ } else if (normalized.includes('forbidden')) {
601
+ code = YNABErrorCode.FORBIDDEN;
602
+ } else if (normalized.includes('not_found')) {
603
+ code = YNABErrorCode.NOT_FOUND;
604
+ } else if (normalized.includes('too_many_requests') || normalized.includes('rate_limit')) {
605
+ code = YNABErrorCode.TOO_MANY_REQUESTS;
606
+ } else if (normalized.includes('internal_server_error')) {
607
+ code = YNABErrorCode.INTERNAL_SERVER_ERROR;
608
+ }
609
+ }
610
+
611
+ if (!code) {
612
+ return null;
613
+ }
614
+
615
+ const details = typeof detail === 'string' ? detail : undefined;
616
+ const result: { code: YNABErrorCode; details?: string } = { code };
617
+ if (details !== undefined) {
618
+ result.details = details;
619
+ }
620
+ return result;
621
+ }
622
+
623
+ /**
624
+ * Sanitizes error details to prevent sensitive data leakage
625
+ */
626
+ private sanitizeErrorDetails(error: unknown): string | undefined {
627
+ if (!error) return undefined;
628
+
629
+ let details = '';
630
+ if (error instanceof Error) {
631
+ details = error.message;
632
+ } else if (typeof error === 'string') {
633
+ details = error;
634
+ } else {
635
+ details = 'Unknown error details';
636
+ }
637
+
638
+ // Remove sensitive information patterns
639
+ details = details
640
+ // token=..., token: ..., token ... → redact until delimiter or whitespace
641
+ .replace(/token[s]?[:\s=]+([^\s,"']+)/gi, 'token=***')
642
+ .replace(/key[s]?[:\s=]+([^\s,"']+)/gi, 'key=***')
643
+ .replace(/password[s]?[:\s=]+([^\s,"']+)/gi, 'password=***')
644
+ // Authorization header (any scheme), redact rest of value
645
+ .replace(/authorization[:\s=]+[^\r\n]+/gi, 'authorization=***')
646
+ // Common Bearer/JWT forms in free text
647
+ .replace(/\bBearer\s+[A-Za-z0-9._-]+/gi, 'Bearer ***');
648
+
649
+ return details;
650
+ }
651
+
652
+ /**
653
+ * Wraps async functions with error handling
654
+ */
655
+ async withErrorHandling<T>(
656
+ operation: () => Promise<T>,
657
+ context: string,
658
+ ): Promise<T | CallToolResult> {
659
+ try {
660
+ return await operation();
661
+ } catch (error) {
662
+ return this.handleError(error, context);
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Static method for backward compatibility
668
+ */
669
+ static async withErrorHandling<T>(
670
+ operation: () => Promise<T>,
671
+ context: string,
672
+ ): Promise<T | CallToolResult> {
673
+ if (!ErrorHandler.defaultInstance) {
674
+ ErrorHandler.defaultInstance = new ErrorHandler(ErrorHandler.createFallbackFormatter());
675
+ }
676
+ return ErrorHandler.defaultInstance.withErrorHandling(operation, context);
677
+ }
678
+
679
+ /**
680
+ * Creates a validation error for invalid parameters
681
+ */
682
+ createValidationError(message: string, details?: string, suggestions?: string[]): CallToolResult {
683
+ return this.handleError(
684
+ new ValidationError(message, details, suggestions),
685
+ 'validating parameters',
686
+ );
687
+ }
688
+
689
+ /**
690
+ * Static method for backward compatibility
691
+ */
692
+ static createValidationError(
693
+ message: string,
694
+ details?: string,
695
+ suggestions?: string[],
696
+ ): CallToolResult {
697
+ if (!ErrorHandler.defaultInstance) {
698
+ ErrorHandler.defaultInstance = new ErrorHandler(ErrorHandler.createFallbackFormatter());
699
+ }
700
+ return ErrorHandler.defaultInstance.createValidationError(message, details, suggestions);
701
+ }
702
+
703
+ /**
704
+ * Creates a YNAB API error with specific error code
705
+ */
706
+ createYNABError(code: YNABErrorCode, context: string, originalError?: unknown): YNABAPIError {
707
+ const message = this.getErrorMessage(code, context);
708
+ return new YNABAPIError(code, message, originalError);
709
+ }
710
+
711
+ /**
712
+ * Static method for backward compatibility
713
+ */
714
+ static createYNABError(
715
+ code: YNABErrorCode,
716
+ context: string,
717
+ originalError?: unknown,
718
+ ): YNABAPIError {
719
+ if (!ErrorHandler.defaultInstance) {
720
+ ErrorHandler.defaultInstance = new ErrorHandler(ErrorHandler.createFallbackFormatter());
721
+ }
722
+ return ErrorHandler.defaultInstance.createYNABError(code, context, originalError);
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Create an ErrorHandler configured with the given response formatter.
728
+ *
729
+ * @param formatter - Formatter used to convert structured error responses into strings for tool output
730
+ * @returns A new ErrorHandler configured to use the provided `formatter`
731
+ */
732
+ export function createErrorHandler(formatter: ErrorResponseFormatter): ErrorHandler {
733
+ return new ErrorHandler(formatter);
734
+ }
735
+
736
+ /**
737
+ * Utility function for handling errors in tool handlers
738
+ */
739
+ export function handleToolError(
740
+ error: unknown,
741
+ toolName: string,
742
+ operation: string,
743
+ ): CallToolResult {
744
+ return ErrorHandler.handleError(error, `executing ${toolName} - ${operation}`);
745
+ }
746
+
747
+ /**
748
+ * Utility function for wrapping tool operations with error handling
749
+ */
750
+ export async function withToolErrorHandling<T>(
751
+ operation: () => Promise<T>,
752
+ toolName: string,
753
+ operationName: string,
754
+ ): Promise<T | CallToolResult> {
755
+ return ErrorHandler.withErrorHandling(operation, `executing ${toolName} - ${operationName}`);
756
+ }