@dizzlkheinz/ynab-mcpb 0.12.2 → 0.15.0

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 (262) hide show
  1. package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +3 -0
  2. package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +3 -0
  3. package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +1 -0
  4. package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +5 -0
  5. package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +1569 -0
  6. package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +3 -0
  7. package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +2832 -0
  8. package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +2709 -0
  9. package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +2832 -0
  10. package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +2832 -0
  11. package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +2709 -0
  12. package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +1 -0
  13. package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +5217 -0
  14. package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +2594 -0
  15. package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +2594 -0
  16. package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +231 -0
  17. package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +2590 -0
  18. package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +5195 -0
  19. package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +286 -0
  20. package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +218 -0
  21. package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +180 -0
  22. package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +3 -0
  23. package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +1 -0
  24. package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +747 -0
  25. package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +1 -0
  26. package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +2594 -0
  27. package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +2594 -0
  28. package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +144 -0
  29. package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +416 -0
  30. package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +2590 -0
  31. package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +2590 -0
  32. package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +2590 -0
  33. package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +790 -0
  34. package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +766 -0
  35. package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +790 -0
  36. package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +2594 -0
  37. package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +1000 -0
  38. package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +3489 -0
  39. package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +766 -0
  40. package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +2594 -0
  41. package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +2456 -0
  42. package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +2594 -0
  43. package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +18 -0
  44. package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +48 -0
  45. package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +1 -0
  46. package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +1 -0
  47. package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +1 -0
  48. package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +1 -0
  49. package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +1271 -0
  50. package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +1570 -0
  51. package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +2590 -0
  52. package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +3 -0
  53. package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +3299 -0
  54. package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +3299 -0
  55. package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +1882 -0
  56. package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +2594 -0
  57. package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +1 -0
  58. package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +170 -0
  59. package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +1 -0
  60. package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +3 -0
  61. package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +1 -0
  62. package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +1 -0
  63. package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +3 -0
  64. package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +1 -0
  65. package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +1 -0
  66. package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +5 -0
  67. package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +1 -0
  68. package/.github/workflows/ci-tests.yml +6 -2
  69. package/.github/workflows/publish.yml +3 -3
  70. package/.github/workflows/release.yml +4 -0
  71. package/CHANGELOG.md +89 -1
  72. package/NUL +1 -1
  73. package/README.md +36 -10
  74. package/dist/bundle/index.cjs +65 -42
  75. package/dist/index.js +9 -20
  76. package/dist/server/YNABMCPServer.d.ts +2 -1
  77. package/dist/server/YNABMCPServer.js +61 -27
  78. package/dist/server/cacheKeys.d.ts +8 -0
  79. package/dist/server/cacheKeys.js +8 -0
  80. package/dist/server/config.d.ts +22 -3
  81. package/dist/server/config.js +16 -17
  82. package/dist/server/errorHandler.d.ts +2 -0
  83. package/dist/server/errorHandler.js +49 -5
  84. package/dist/server/securityMiddleware.js +3 -6
  85. package/dist/server/toolRegistry.js +8 -10
  86. package/dist/tools/accountTools.js +4 -3
  87. package/dist/tools/categoryTools.js +8 -7
  88. package/dist/tools/monthTools.js +2 -1
  89. package/dist/tools/payeeTools.js +2 -1
  90. package/dist/tools/reconcileAdapter.js +10 -5
  91. package/dist/tools/reconciliation/analyzer.d.ts +4 -2
  92. package/dist/tools/reconciliation/analyzer.js +120 -404
  93. package/dist/tools/reconciliation/csvParser.d.ts +51 -0
  94. package/dist/tools/reconciliation/csvParser.js +413 -0
  95. package/dist/tools/reconciliation/executor.d.ts +8 -0
  96. package/dist/tools/reconciliation/executor.js +277 -50
  97. package/dist/tools/reconciliation/index.d.ts +7 -7
  98. package/dist/tools/reconciliation/index.js +115 -39
  99. package/dist/tools/reconciliation/matcher.d.ts +24 -3
  100. package/dist/tools/reconciliation/matcher.js +175 -133
  101. package/dist/tools/reconciliation/recommendationEngine.js +22 -18
  102. package/dist/tools/reconciliation/reportFormatter.js +9 -8
  103. package/dist/tools/reconciliation/signDetector.d.ts +2 -0
  104. package/dist/tools/reconciliation/signDetector.js +54 -0
  105. package/dist/tools/reconciliation/types.d.ts +20 -34
  106. package/dist/tools/reconciliation/types.js +1 -7
  107. package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
  108. package/dist/tools/reconciliation/ynabAdapter.js +15 -0
  109. package/dist/tools/transactionTools.d.ts +3 -17
  110. package/dist/tools/transactionTools.js +5 -17
  111. package/dist/types/reconciliation.d.ts +24 -0
  112. package/dist/types/reconciliation.js +1 -0
  113. package/dist/utils/baseError.d.ts +3 -0
  114. package/dist/utils/baseError.js +7 -0
  115. package/dist/utils/errors.d.ts +13 -0
  116. package/dist/utils/errors.js +15 -0
  117. package/dist/utils/validationError.d.ts +3 -0
  118. package/dist/utils/validationError.js +3 -0
  119. package/docs/guides/ARCHITECTURE.md +12 -129
  120. package/docs/plans/2025-11-20-reloadable-config-token-validation.md +93 -0
  121. package/docs/plans/2025-11-21-fix-transaction-cached-property.md +362 -0
  122. package/docs/plans/2025-11-21-reconciliation-error-handling.md +90 -0
  123. package/docs/plans/2025-11-21-v014-hardening.md +153 -0
  124. package/docs/plans/reconciliation-v2-redesign.md +1571 -0
  125. package/package.json +8 -2
  126. package/scripts/run-throttled-integration-tests.js +9 -3
  127. package/scripts/test-recommendations.ts +1 -1
  128. package/src/__tests__/performance.test.ts +12 -5
  129. package/src/__tests__/testUtils.ts +62 -5
  130. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
  131. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
  132. package/src/__tests__/workflows.e2e.test.ts +33 -0
  133. package/src/index.ts +8 -31
  134. package/src/server/YNABMCPServer.ts +81 -42
  135. package/src/server/__tests__/YNABMCPServer.integration.test.ts +10 -12
  136. package/src/server/__tests__/YNABMCPServer.test.ts +27 -15
  137. package/src/server/__tests__/config.test.ts +76 -152
  138. package/src/server/__tests__/server-startup.integration.test.ts +42 -14
  139. package/src/server/__tests__/toolRegistry.test.ts +1 -1
  140. package/src/server/cacheKeys.ts +8 -0
  141. package/src/server/config.ts +20 -38
  142. package/src/server/errorHandler.ts +52 -5
  143. package/src/server/securityMiddleware.ts +3 -7
  144. package/src/server/toolRegistry.ts +14 -10
  145. package/src/tools/__tests__/categoryTools.test.ts +37 -19
  146. package/src/tools/__tests__/transactionTools.test.ts +58 -2
  147. package/src/tools/accountTools.ts +8 -3
  148. package/src/tools/categoryTools.ts +12 -7
  149. package/src/tools/monthTools.ts +7 -1
  150. package/src/tools/payeeTools.ts +7 -1
  151. package/src/tools/reconcileAdapter.ts +10 -5
  152. package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
  153. package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
  154. package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
  155. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +26 -6
  156. package/src/tools/reconciliation/__tests__/executor.test.ts +133 -60
  157. package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
  158. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
  159. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
  160. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
  161. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
  162. package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
  163. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
  164. package/src/tools/reconciliation/analyzer.ts +174 -545
  165. package/src/tools/reconciliation/csvParser.ts +617 -0
  166. package/src/tools/reconciliation/executor.ts +344 -58
  167. package/src/tools/reconciliation/index.ts +141 -48
  168. package/src/tools/reconciliation/matcher.ts +234 -214
  169. package/src/tools/reconciliation/recommendationEngine.ts +23 -19
  170. package/src/tools/reconciliation/reportFormatter.ts +16 -11
  171. package/src/tools/reconciliation/signDetector.ts +117 -0
  172. package/src/tools/reconciliation/types.ts +39 -61
  173. package/src/tools/reconciliation/ynabAdapter.ts +33 -0
  174. package/src/tools/schemas/outputs/utilityOutputs.ts +1 -1
  175. package/src/tools/transactionTools.ts +7 -18
  176. package/src/types/reconciliation.ts +49 -0
  177. package/src/utils/baseError.ts +7 -0
  178. package/src/utils/errors.ts +21 -0
  179. package/src/utils/validationError.ts +3 -0
  180. package/temp-recon.ts +126 -0
  181. package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
  182. package/test_mcp_tools.mjs +75 -0
  183. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
  184. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
  185. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
  186. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
  187. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
  188. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
  189. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
  190. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
  191. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
  192. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
  193. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
  194. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
  195. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
  196. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
  197. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
  198. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
  199. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
  200. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
  201. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
  202. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
  203. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
  204. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
  205. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
  206. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
  207. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
  208. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
  209. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
  210. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
  211. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
  212. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
  213. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
  214. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
  215. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
  216. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
  217. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
  218. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
  219. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
  220. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
  221. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
  222. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
  223. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
  224. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
  225. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
  226. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
  227. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
  228. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
  229. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
  230. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
  231. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
  232. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
  233. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
  234. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
  235. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
  236. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
  237. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
  238. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
  239. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
  240. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
  241. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
  242. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
  243. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
  244. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
  245. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
  246. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
  247. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
  248. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
  249. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
  250. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
  251. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
  252. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
  253. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
  254. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
  255. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
  256. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
  257. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
  258. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
  259. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
  260. package/.gemini/settings.json +0 -8
  261. package/ADOS-2-Module-1-Complete-Manual.md +0 -757
  262. package/WARP.md +0 -245
@@ -1,166 +1,90 @@
1
- /**
2
- * Unit tests for config module
3
- *
4
- * Tests environment validation and server configuration.
5
- */
1
+ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
6
2
 
7
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
8
- import { validateEnvironment } from '../config.js';
9
- import { ConfigurationError } from '../../types/index.js';
10
-
11
- describe('config module', () => {
12
- const originalEnv = process.env;
3
+ const originalEnv = { ...process.env };
13
4
 
5
+ describe('Config Module', () => {
14
6
  beforeEach(() => {
15
- // Reset modules and environment
16
7
  vi.resetModules();
17
8
  process.env = { ...originalEnv };
9
+ if (!process.env.YNAB_ACCESS_TOKEN) {
10
+ process.env.YNAB_ACCESS_TOKEN = 'test-token-placeholder';
11
+ }
18
12
  });
19
13
 
20
14
  afterEach(() => {
21
- // Restore original environment
22
- process.env = originalEnv;
15
+ process.env = { ...originalEnv };
23
16
  });
24
17
 
25
- describe('validateEnvironment', () => {
26
- it('should return valid configuration when YNAB_ACCESS_TOKEN is set', () => {
27
- const testToken = 'test-token-12345';
28
- process.env.YNAB_ACCESS_TOKEN = testToken;
29
-
30
- const result = validateEnvironment();
31
-
32
- expect(result).toEqual({
33
- accessToken: testToken,
34
- defaultBudgetId: undefined,
35
- });
36
- });
37
-
38
- it('should trim whitespace from access token', () => {
39
- const testToken = ' test-token-with-spaces ';
40
- const expectedToken = 'test-token-with-spaces';
41
- process.env.YNAB_ACCESS_TOKEN = testToken;
42
-
43
- const result = validateEnvironment();
44
-
45
- expect(result).toEqual({
46
- accessToken: expectedToken,
47
- defaultBudgetId: undefined,
48
- });
49
- });
50
-
51
- it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is not set', () => {
52
- delete process.env.YNAB_ACCESS_TOKEN;
53
-
54
- expect(() => validateEnvironment()).toThrow(ConfigurationError);
55
- expect(() => validateEnvironment()).toThrow(
56
- 'YNAB_ACCESS_TOKEN environment variable is required but not set',
57
- );
58
- });
59
-
60
- it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is undefined', () => {
61
- delete process.env.YNAB_ACCESS_TOKEN;
62
-
63
- expect(() => validateEnvironment()).toThrow(ConfigurationError);
64
- expect(() => validateEnvironment()).toThrow(
65
- 'YNAB_ACCESS_TOKEN environment variable is required but not set',
66
- );
67
- });
68
-
69
- it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is empty string', () => {
70
- process.env.YNAB_ACCESS_TOKEN = '';
71
-
72
- expect(() => validateEnvironment()).toThrow(ConfigurationError);
73
- expect(() => validateEnvironment()).toThrow('YNAB_ACCESS_TOKEN must be a non-empty string');
74
- });
75
-
76
- it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is only whitespace', () => {
77
- process.env.YNAB_ACCESS_TOKEN = ' ';
78
-
79
- expect(() => validateEnvironment()).toThrow(ConfigurationError);
80
- expect(() => validateEnvironment()).toThrow('YNAB_ACCESS_TOKEN must be a non-empty string');
81
- });
82
-
83
- it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is not a string', () => {
84
- // TypeScript normally prevents this, but test runtime validation
85
- (process.env as any).YNAB_ACCESS_TOKEN = 123;
86
-
87
- expect(() => validateEnvironment()).toThrow(ConfigurationError);
88
- expect(() => validateEnvironment()).toThrow('YNAB_ACCESS_TOKEN must be a non-empty string');
89
- });
90
-
91
- it('should handle various valid token formats', () => {
92
- const validTokens = [
93
- 'abc123',
94
- 'token-with-dashes',
95
- 'token_with_underscores',
96
- 'MixedCaseToken',
97
- '1234567890',
98
- 'very-long-token-with-many-characters-abcdefghijklmnopqrstuvwxyz',
99
- ];
100
-
101
- validTokens.forEach((token) => {
102
- process.env.YNAB_ACCESS_TOKEN = token;
103
- const result = validateEnvironment();
104
- expect(result.accessToken).toBe(token);
105
- expect(result.defaultBudgetId).toBeUndefined();
106
- });
107
- });
108
-
109
- it('should handle edge cases with leading and trailing whitespace', () => {
110
- const testCases = [
111
- { input: '\ntest-token\n', expected: 'test-token' },
112
- { input: '\ttest-token\t', expected: 'test-token' },
113
- { input: ' \t\ntest-token \t\n', expected: 'test-token' },
114
- ];
115
-
116
- testCases.forEach(({ input, expected }) => {
117
- process.env.YNAB_ACCESS_TOKEN = input;
118
- const result = validateEnvironment();
119
- expect(result.accessToken).toBe(expected);
120
- expect(result.defaultBudgetId).toBeUndefined();
121
- });
122
- });
18
+ it('reloads environment variables on each loadConfig call', async () => {
19
+ const { loadConfig } = await import('../config');
20
+ process.env.YNAB_ACCESS_TOKEN = 'test-token-123';
21
+ expect(loadConfig().YNAB_ACCESS_TOKEN).toBe('test-token-123');
22
+
23
+ process.env.YNAB_ACCESS_TOKEN = 'updated-token-456';
24
+ expect(loadConfig().YNAB_ACCESS_TOKEN).toBe('updated-token-456');
123
25
  });
124
26
 
125
- describe('error handling', () => {
126
- it('should throw proper ConfigurationError instances', () => {
127
- delete process.env.YNAB_ACCESS_TOKEN;
128
-
129
- try {
130
- validateEnvironment();
131
- throw new Error('Should have thrown an error');
132
- } catch (error) {
133
- expect(error).toBeInstanceOf(ConfigurationError);
134
- expect(error).toBeInstanceOf(Error);
135
- expect((error as ConfigurationError).name).toBe('ConfigurationError');
136
- }
137
- });
138
-
139
- it('should provide helpful error messages', () => {
140
- const testCases = [
141
- {
142
- setup: () => delete process.env.YNAB_ACCESS_TOKEN,
143
- expectedMessage: 'YNAB_ACCESS_TOKEN environment variable is required but not set',
144
- },
145
- {
146
- setup: () => (process.env.YNAB_ACCESS_TOKEN = ''),
147
- expectedMessage: 'YNAB_ACCESS_TOKEN must be a non-empty string',
148
- },
149
- {
150
- setup: () => (process.env.YNAB_ACCESS_TOKEN = ' '),
151
- expectedMessage: 'YNAB_ACCESS_TOKEN must be a non-empty string',
152
- },
153
- ];
154
-
155
- testCases.forEach(({ setup, expectedMessage }) => {
156
- setup();
157
- try {
158
- validateEnvironment();
159
- expect.fail('Should have thrown an error');
160
- } catch (error) {
161
- expect((error as Error).message).toBe(expectedMessage);
162
- }
163
- });
164
- });
27
+ it('keeps the config singleton as a one-time parse', async () => {
28
+ process.env.YNAB_ACCESS_TOKEN = 'initial-token';
29
+ const { config, loadConfig } = await import('../config');
30
+ expect(config.YNAB_ACCESS_TOKEN).toBe('initial-token');
31
+
32
+ process.env.YNAB_ACCESS_TOKEN = 'later-token';
33
+ expect(config.YNAB_ACCESS_TOKEN).toBe('initial-token');
34
+ expect(loadConfig().YNAB_ACCESS_TOKEN).toBe('later-token');
35
+ });
36
+
37
+ it('throws a detailed error if YNAB_ACCESS_TOKEN is missing', async () => {
38
+ const { loadConfig } = await import('../config');
39
+ const env = { ...process.env };
40
+ delete env.YNAB_ACCESS_TOKEN;
41
+
42
+ expect.assertions(2);
43
+ try {
44
+ loadConfig(env);
45
+ } catch (error) {
46
+ expect((error as { name?: string }).name).toBe('ValidationError');
47
+ expect((error as Error).message).toMatch(/YNAB_ACCESS_TOKEN/i);
48
+ }
49
+ });
50
+
51
+ it('parses optional MCP_PORT correctly', async () => {
52
+ const { loadConfig } = await import('../config');
53
+ const env = { ...process.env, YNAB_ACCESS_TOKEN: 'token', MCP_PORT: '8080' };
54
+
55
+ const parsed = loadConfig(env);
56
+ expect(parsed.MCP_PORT).toBe(8080);
57
+ });
58
+
59
+ it('handles missing optional MCP_PORT', async () => {
60
+ const { loadConfig } = await import('../config');
61
+ const env = { ...process.env, YNAB_ACCESS_TOKEN: 'token' };
62
+ delete env.MCP_PORT;
63
+
64
+ const parsed = loadConfig(env);
65
+ expect(parsed.MCP_PORT).toBeUndefined();
66
+ });
67
+
68
+ it('throws an error for an invalid MCP_PORT', async () => {
69
+ const { loadConfig } = await import('../config');
70
+ const env = { ...process.env, YNAB_ACCESS_TOKEN: 'token', MCP_PORT: 'invalid-port' };
71
+
72
+ expect.assertions(2);
73
+ try {
74
+ loadConfig(env);
75
+ } catch (error) {
76
+ expect((error as { name?: string }).name).toBe('ValidationError');
77
+ expect((error as Error).message).toMatch(/MCP_PORT/i);
78
+ }
79
+ });
80
+
81
+ it('parses LOG_LEVEL and defaults to info', async () => {
82
+ const { loadConfig } = await import('../config');
83
+ const envWithLog = { ...process.env, YNAB_ACCESS_TOKEN: 'token', LOG_LEVEL: 'debug' };
84
+ expect(loadConfig(envWithLog).LOG_LEVEL).toBe('debug');
85
+
86
+ const envWithoutLog = { ...envWithLog };
87
+ delete envWithoutLog.LOG_LEVEL; // Ensure LOG_LEVEL is not set
88
+ expect(loadConfig(envWithoutLog).LOG_LEVEL).toBe('info');
165
89
  });
166
90
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { YNABMCPServer } from '../YNABMCPServer';
3
- import { AuthenticationError, ConfigurationError } from '../../types/index';
3
+ import { ValidationError } from '../../types/index';
4
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
5
  import { skipOnRateLimit } from '../../__tests__/testUtils.js';
6
6
  // StdioServerTransport import removed as it's not used in tests
@@ -55,10 +55,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
55
55
  const originalToken = process.env['YNAB_ACCESS_TOKEN'];
56
56
  delete process.env['YNAB_ACCESS_TOKEN'];
57
57
 
58
- expect(() => new YNABMCPServer(false)).toThrow(ConfigurationError);
59
- expect(() => new YNABMCPServer(false)).toThrow(
60
- 'YNAB_ACCESS_TOKEN environment variable is required but not set',
61
- );
58
+ expect(() => new YNABMCPServer(false)).toThrow(/YNAB_ACCESS_TOKEN/i);
62
59
 
63
60
  // Restore token
64
61
  process.env['YNAB_ACCESS_TOKEN'] = originalToken;
@@ -72,7 +69,6 @@ describeIntegration('Server Startup and Transport Integration', () => {
72
69
  const originalToken = process.env['YNAB_ACCESS_TOKEN'];
73
70
  process.env['YNAB_ACCESS_TOKEN'] = '';
74
71
 
75
- expect(() => new YNABMCPServer(false)).toThrow(ConfigurationError);
76
72
  expect(() => new YNABMCPServer(false)).toThrow(
77
73
  'YNAB_ACCESS_TOKEN must be a non-empty string',
78
74
  );
@@ -111,7 +107,10 @@ describeIntegration('Server Startup and Transport Integration', () => {
111
107
 
112
108
  try {
113
109
  const invalidServer = new YNABMCPServer(false);
114
- await expect(invalidServer.validateToken()).rejects.toThrow(AuthenticationError);
110
+ await expect(invalidServer.validateToken()).rejects.toHaveProperty(
111
+ 'name',
112
+ 'AuthenticationError',
113
+ );
115
114
  } finally {
116
115
  process.env['YNAB_ACCESS_TOKEN'] = originalToken;
117
116
  }
@@ -129,13 +128,16 @@ describeIntegration('Server Startup and Transport Integration', () => {
129
128
 
130
129
  try {
131
130
  const invalidServer = new YNABMCPServer(false);
132
- await expect(invalidServer.validateToken()).rejects.toThrow(AuthenticationError);
131
+ await expect(invalidServer.validateToken()).rejects.toHaveProperty(
132
+ 'name',
133
+ 'AuthenticationError',
134
+ );
133
135
 
134
136
  // Verify the error message contains relevant information
135
137
  try {
136
138
  await invalidServer.validateToken();
137
139
  } catch (error) {
138
- expect(error).toBeInstanceOf(AuthenticationError);
140
+ expect((error as { name?: string }).name).toBe('AuthenticationError');
139
141
  expect(error.message).toContain('Token validation failed');
140
142
  }
141
143
  } finally {
@@ -144,6 +146,29 @@ describeIntegration('Server Startup and Transport Integration', () => {
144
146
  }, ctx);
145
147
  },
146
148
  );
149
+
150
+ it(
151
+ 'should surface malformed token responses as AuthenticationError',
152
+ { meta: { tier: 'domain', domain: 'server' } },
153
+ async () => {
154
+ const syntaxError = new SyntaxError('Unexpected token < in JSON at position 0');
155
+ const getUserSpy = vi
156
+ .spyOn(server.getYNABAPI().user, 'getUser')
157
+ .mockRejectedValue(syntaxError);
158
+
159
+ try {
160
+ await expect(server.validateToken()).rejects.toHaveProperty(
161
+ 'name',
162
+ 'AuthenticationError',
163
+ );
164
+ await expect(server.validateToken()).rejects.toThrow(
165
+ 'Unexpected response from YNAB during token validation',
166
+ );
167
+ } finally {
168
+ getUserSpy.mockRestore();
169
+ }
170
+ },
171
+ );
147
172
  });
148
173
 
149
174
  describe('Tool Registration', () => {
@@ -262,7 +287,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
262
287
  } catch (error) {
263
288
  // Expected to fail on stdio connection in test environment
264
289
  // Token was already validated above, so this error should be transport-related
265
- expect(error).not.toBeInstanceOf(ConfigurationError);
290
+ expect(error).not.toBeInstanceOf(ValidationError);
266
291
  }
267
292
 
268
293
  consoleSpy.mockRestore();
@@ -325,7 +350,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
325
350
 
326
351
  expect(() => new YNABMCPServer(false)).toThrow(
327
352
  expect.objectContaining({
328
- message: 'YNAB_ACCESS_TOKEN environment variable is required but not set',
353
+ message: expect.stringMatching(/YNAB_ACCESS_TOKEN/i),
329
354
  }),
330
355
  );
331
356
 
@@ -343,7 +368,10 @@ describeIntegration('Server Startup and Transport Integration', () => {
343
368
 
344
369
  try {
345
370
  const server = new YNABMCPServer(false);
346
- await expect(server.validateToken()).rejects.toThrow(AuthenticationError);
371
+ await expect(server.validateToken()).rejects.toHaveProperty(
372
+ 'name',
373
+ 'AuthenticationError',
374
+ );
347
375
  } finally {
348
376
  process.env['YNAB_ACCESS_TOKEN'] = originalToken;
349
377
  }
@@ -448,7 +476,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
448
476
  delete process.env['YNAB_ACCESS_TOKEN'];
449
477
 
450
478
  // Should fail immediately on construction, not during run()
451
- expect(() => new YNABMCPServer(false)).toThrow(ConfigurationError);
479
+ expect(() => new YNABMCPServer(false)).toThrow(/YNAB_ACCESS_TOKEN/i);
452
480
 
453
481
  process.env['YNAB_ACCESS_TOKEN'] = originalToken;
454
482
  },
@@ -466,7 +494,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
466
494
  const server = new YNABMCPServer(false);
467
495
 
468
496
  // Should fail on token validation, before transport setup
469
- await expect(server.run()).rejects.toThrow(AuthenticationError);
497
+ await expect(server.run()).rejects.toHaveProperty('name', 'AuthenticationError');
470
498
  } finally {
471
499
  process.env['YNAB_ACCESS_TOKEN'] = originalToken;
472
500
  }
@@ -169,9 +169,9 @@ describe('ToolRegistry', () => {
169
169
  expect(tools[0]?.name).toBe('sample_tool');
170
170
  const schema = tools[0]?.inputSchema as Record<string, unknown> | undefined;
171
171
  expect(schema).toBeDefined();
172
+ // Input schemas use io:'input' mode which doesn't set additionalProperties
172
173
  expect(schema).toMatchObject({
173
174
  type: 'object',
174
- additionalProperties: false,
175
175
  properties: expect.objectContaining({
176
176
  id: expect.objectContaining({ type: 'string' }),
177
177
  minify: expect.objectContaining({ type: 'boolean' }),
@@ -0,0 +1,8 @@
1
+ export const CacheKeys = {
2
+ ACCOUNTS: 'accounts',
3
+ BUDGETS: 'budgets',
4
+ CATEGORIES: 'categories',
5
+ PAYEES: 'payees',
6
+ TRANSACTIONS: 'transactions',
7
+ MONTHS: 'months',
8
+ } as const;
@@ -1,41 +1,23 @@
1
- /**
2
- * Configuration module for YNAB MCP Server
3
- *
4
- * Handles environment validation and server configuration.
5
- * Extracted from YNABMCPServer to provide focused, testable configuration management.
6
- */
7
-
8
- import { ServerConfig, ConfigurationError } from '../types/index.js';
9
-
10
- /**
11
- * Create a ServerConfig from environment variables after validating required values.
12
- *
13
- * @returns The validated ServerConfig.
14
- * @throws ConfigurationError if `YNAB_ACCESS_TOKEN` is missing or not a non-empty string.
15
- */
16
- export function validateEnvironment(): ServerConfig {
17
- const accessToken = process.env['YNAB_ACCESS_TOKEN'];
18
- const defaultBudgetId = process.env['YNAB_DEFAULT_BUDGET_ID'];
19
-
20
- if (accessToken === undefined) {
21
- throw new ConfigurationError('YNAB_ACCESS_TOKEN environment variable is required but not set');
22
- }
23
-
24
- if (typeof accessToken !== 'string' || accessToken.trim().length === 0) {
25
- throw new ConfigurationError('YNAB_ACCESS_TOKEN must be a non-empty string');
1
+ import 'dotenv/config';
2
+ import { z } from 'zod';
3
+ import { fromZodError } from 'zod-validation-error';
4
+ import { ValidationError } from '../utils/errors.js';
5
+
6
+ const envSchema = z.object({
7
+ YNAB_ACCESS_TOKEN: z.string().trim().min(1, 'YNAB_ACCESS_TOKEN must be a non-empty string'),
8
+ MCP_PORT: z.coerce.number().int().positive().optional(),
9
+ LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
10
+ });
11
+
12
+ export type AppConfig = z.infer<typeof envSchema>;
13
+
14
+ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
15
+ const result = envSchema.safeParse(env);
16
+ if (!result.success) {
17
+ const validationError = fromZodError(result.error);
18
+ throw new ValidationError(validationError.toString());
26
19
  }
27
-
28
- const trimmedDefaultBudgetId = defaultBudgetId?.trim();
29
-
30
- const config: ServerConfig = {
31
- accessToken: accessToken.trim(),
32
- };
33
-
34
- if (trimmedDefaultBudgetId && trimmedDefaultBudgetId.length > 0) {
35
- config.defaultBudgetId = trimmedDefaultBudgetId;
36
- }
37
-
38
- return config;
20
+ return result.data;
39
21
  }
40
22
 
41
- export type { ServerConfig } from '../types/index.js';
23
+ export const config = loadConfig();
@@ -12,6 +12,7 @@ interface ErrorResponseFormatter {
12
12
  */
13
13
 
14
14
  export const enum YNABErrorCode {
15
+ BAD_REQUEST = 400,
15
16
  UNAUTHORIZED = 401,
16
17
  FORBIDDEN = 403,
17
18
  NOT_FOUND = 404,
@@ -54,6 +55,11 @@ export class YNABAPIError extends Error {
54
55
  this.code = code;
55
56
  this.originalError = originalError;
56
57
  }
58
+
59
+ // Expose status as an alias for code for backward compatibility with tests
60
+ get status(): YNABErrorCode {
61
+ return this.code;
62
+ }
57
63
  }
58
64
 
59
65
  export class ValidationError extends Error {
@@ -106,7 +112,12 @@ export class ErrorHandler {
106
112
  formattedText = this.formatter.format(errorResponse);
107
113
  } catch {
108
114
  // Fallback to JSON.stringify if formatter fails
109
- formattedText = JSON.stringify(errorResponse, null, 2);
115
+ try {
116
+ formattedText = JSON.stringify(errorResponse, null, 2);
117
+ } catch {
118
+ // Final fallback if JSON serialization fails (e.g. circular references)
119
+ formattedText = `Error processing request: ${this.getGenericErrorMessage(context)}`;
120
+ }
110
121
  }
111
122
 
112
123
  return {
@@ -135,7 +146,9 @@ export class ErrorHandler {
135
146
  private createErrorResponse(error: unknown, context: string): ErrorResponse {
136
147
  // Handle custom error types
137
148
  if (error instanceof YNABAPIError) {
138
- const sanitizedDetails = this.sanitizeErrorDetails(error.originalError);
149
+ const ynabDetails = this.extractYNABApiError(error.originalError);
150
+ const detailsToSanitize = ynabDetails?.details || error.originalError;
151
+ const sanitizedDetails = this.sanitizeErrorDetails(detailsToSanitize);
139
152
  return {
140
153
  error: {
141
154
  code: error.code,
@@ -216,7 +229,22 @@ export class ErrorHandler {
216
229
 
217
230
  // Fallback for unknown errors
218
231
  // Preserve the original error message for debugging while sanitizing sensitive data
219
- const errorMessage = error instanceof Error ? error.message : String(error);
232
+ let errorMessage: string;
233
+ if (error instanceof Error) {
234
+ errorMessage = error.message;
235
+ } else if (typeof error === 'string') {
236
+ errorMessage = error;
237
+ } else if (error && typeof error === 'object') {
238
+ // Handle plain objects (e.g., YNAB SDK errors that aren't Error instances)
239
+ try {
240
+ errorMessage = JSON.stringify(error, null, 2);
241
+ } catch {
242
+ // Circular reference or other JSON issue
243
+ errorMessage = Object.prototype.toString.call(error);
244
+ }
245
+ } else {
246
+ errorMessage = String(error);
247
+ }
220
248
  const sanitizedDetails = this.sanitizeErrorDetails(errorMessage);
221
249
 
222
250
  return {
@@ -264,6 +292,8 @@ export class ErrorHandler {
264
292
  */
265
293
  private getUserFriendlyMessage(code: YNABErrorCode | SecurityErrorCode, context: string): string {
266
294
  switch (code) {
295
+ case YNABErrorCode.BAD_REQUEST:
296
+ return 'The request was invalid. Please check your input data.';
267
297
  case YNABErrorCode.UNAUTHORIZED:
268
298
  return 'Your YNAB access token is invalid or has expired. Please check your token and try again.';
269
299
  case YNABErrorCode.FORBIDDEN:
@@ -288,6 +318,12 @@ export class ErrorHandler {
288
318
  */
289
319
  private getErrorSuggestions(code: YNABErrorCode | SecurityErrorCode, context: string): string[] {
290
320
  switch (code) {
321
+ case YNABErrorCode.BAD_REQUEST:
322
+ return [
323
+ 'Check that all required fields are correct',
324
+ 'Verify that dates are in the correct format (ISO 8601)',
325
+ 'Ensure amounts are valid numbers',
326
+ ];
291
327
  case YNABErrorCode.UNAUTHORIZED:
292
328
  return [
293
329
  'Go to https://app.youneedabudget.com/settings/developer to generate a new access token',
@@ -401,6 +437,8 @@ export class ErrorHandler {
401
437
  */
402
438
  private getErrorMessage(code: YNABErrorCode, context: string): string {
403
439
  switch (code) {
440
+ case YNABErrorCode.BAD_REQUEST:
441
+ return 'Bad request - invalid parameters';
404
442
  case YNABErrorCode.UNAUTHORIZED:
405
443
  return 'Invalid or expired YNAB access token';
406
444
  case YNABErrorCode.FORBIDDEN:
@@ -535,6 +573,7 @@ export class ErrorHandler {
535
573
  */
536
574
  private mapHttpStatusToErrorCode(status: number): YNABErrorCode | null {
537
575
  switch (status) {
576
+ case YNABErrorCode.BAD_REQUEST:
538
577
  case YNABErrorCode.UNAUTHORIZED:
539
578
  case YNABErrorCode.FORBIDDEN:
540
579
  case YNABErrorCode.NOT_FOUND:
@@ -571,11 +610,19 @@ export class ErrorHandler {
571
610
  * Extracts structured YNAB API error information
572
611
  */
573
612
  private extractYNABApiError(error: unknown): { code: YNABErrorCode; details?: string } | null {
574
- if (!error || typeof error !== 'object' || !('error' in (error as Record<string, unknown>))) {
613
+ if (!error || typeof error !== 'object') {
575
614
  return null;
576
615
  }
577
616
 
578
- const payload = (error as { error?: unknown }).error;
617
+ let payload = (error as { error?: unknown }).error;
618
+
619
+ if (!payload) {
620
+ const responseData = (error as { response?: { data?: unknown } }).response?.data;
621
+ if (responseData && typeof responseData === 'object') {
622
+ payload = (responseData as { error?: unknown }).error;
623
+ }
624
+ }
625
+
579
626
  if (!payload || typeof payload !== 'object') {
580
627
  return null;
581
628
  }
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
6
6
  import { z } from 'zod/v4';
7
+ import { fromZodError } from 'zod-validation-error';
7
8
  import { globalRateLimiter, RateLimitError } from './rateLimiter.js';
8
9
  import { globalRequestLogger } from './requestLogger.js';
9
10
  import { ErrorHandler } from './errorHandler.js';
@@ -112,13 +113,8 @@ export class SecurityMiddleware {
112
113
  return schema.parse(parameters);
113
114
  } catch (error) {
114
115
  if (error instanceof z.ZodError) {
115
- const errorMessage =
116
- error.issues && error.issues.length > 0
117
- ? error.issues
118
- .map((err: z.ZodIssue) => `${err.path.join('.')}: ${err.message}`)
119
- .join(', ')
120
- : error.message || 'Validation failed';
121
- throw new Error(`Validation failed: ${errorMessage}`);
116
+ const validationError = fromZodError(error);
117
+ throw new Error(`Validation failed: ${validationError.message}`);
122
118
  }
123
119
  throw error;
124
120
  }