@dizzlkheinz/ynab-mcpb 0.18.4 → 0.19.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 (343) hide show
  1. package/CLAUDE.md +87 -8
  2. package/bin/ynab-mcp-server.cjs +2 -2
  3. package/bin/ynab-mcp-server.js +3 -3
  4. package/biome.json +39 -0
  5. package/dist/bundle/index.cjs +67 -67
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +27 -27
  8. package/dist/server/YNABMCPServer.d.ts +3 -4
  9. package/dist/server/YNABMCPServer.js +111 -116
  10. package/dist/server/budgetResolver.d.ts +6 -5
  11. package/dist/server/budgetResolver.js +46 -36
  12. package/dist/server/cacheKeys.js +6 -6
  13. package/dist/server/cacheManager.js +14 -11
  14. package/dist/server/completions.d.ts +2 -2
  15. package/dist/server/completions.js +20 -15
  16. package/dist/server/config.d.ts +10 -5
  17. package/dist/server/config.js +24 -7
  18. package/dist/server/deltaCache.d.ts +2 -2
  19. package/dist/server/deltaCache.js +22 -16
  20. package/dist/server/deltaCache.merge.d.ts +2 -2
  21. package/dist/server/diagnostics.d.ts +4 -4
  22. package/dist/server/diagnostics.js +38 -32
  23. package/dist/server/errorHandler.d.ts +5 -12
  24. package/dist/server/errorHandler.js +219 -217
  25. package/dist/server/prompts.d.ts +2 -2
  26. package/dist/server/prompts.js +45 -45
  27. package/dist/server/rateLimiter.js +4 -4
  28. package/dist/server/requestLogger.d.ts +1 -1
  29. package/dist/server/requestLogger.js +40 -35
  30. package/dist/server/resources.d.ts +3 -3
  31. package/dist/server/resources.js +55 -52
  32. package/dist/server/responseFormatter.js +6 -6
  33. package/dist/server/securityMiddleware.d.ts +2 -2
  34. package/dist/server/securityMiddleware.js +22 -20
  35. package/dist/server/serverKnowledgeStore.js +1 -1
  36. package/dist/server/toolRegistry.d.ts +3 -3
  37. package/dist/server/toolRegistry.js +47 -40
  38. package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
  39. package/dist/tools/__tests__/deltaTestUtils.js +2 -2
  40. package/dist/tools/accountTools.d.ts +9 -8
  41. package/dist/tools/accountTools.js +47 -47
  42. package/dist/tools/adapters.d.ts +13 -8
  43. package/dist/tools/adapters.js +21 -11
  44. package/dist/tools/budgetTools.d.ts +8 -7
  45. package/dist/tools/budgetTools.js +22 -22
  46. package/dist/tools/categoryTools.d.ts +9 -8
  47. package/dist/tools/categoryTools.js +68 -59
  48. package/dist/tools/compareTransactions/formatter.d.ts +3 -3
  49. package/dist/tools/compareTransactions/formatter.js +9 -9
  50. package/dist/tools/compareTransactions/index.d.ts +6 -6
  51. package/dist/tools/compareTransactions/index.js +58 -43
  52. package/dist/tools/compareTransactions/matcher.d.ts +1 -1
  53. package/dist/tools/compareTransactions/matcher.js +28 -15
  54. package/dist/tools/compareTransactions/parser.d.ts +2 -2
  55. package/dist/tools/compareTransactions/parser.js +144 -138
  56. package/dist/tools/compareTransactions/types.d.ts +4 -4
  57. package/dist/tools/compareTransactions.d.ts +1 -1
  58. package/dist/tools/compareTransactions.js +1 -1
  59. package/dist/tools/deltaFetcher.d.ts +2 -2
  60. package/dist/tools/deltaFetcher.js +16 -15
  61. package/dist/tools/deltaSupport.d.ts +4 -4
  62. package/dist/tools/deltaSupport.js +35 -41
  63. package/dist/tools/exportTransactions.d.ts +5 -4
  64. package/dist/tools/exportTransactions.js +61 -59
  65. package/dist/tools/monthTools.d.ts +7 -6
  66. package/dist/tools/monthTools.js +31 -29
  67. package/dist/tools/payeeTools.d.ts +7 -6
  68. package/dist/tools/payeeTools.js +28 -28
  69. package/dist/tools/reconcileAdapter.d.ts +2 -2
  70. package/dist/tools/reconcileAdapter.js +19 -12
  71. package/dist/tools/reconciliation/analyzer.d.ts +4 -4
  72. package/dist/tools/reconciliation/analyzer.js +73 -59
  73. package/dist/tools/reconciliation/csvParser.d.ts +3 -3
  74. package/dist/tools/reconciliation/csvParser.js +128 -104
  75. package/dist/tools/reconciliation/executor.d.ts +4 -4
  76. package/dist/tools/reconciliation/executor.js +148 -109
  77. package/dist/tools/reconciliation/index.d.ts +10 -10
  78. package/dist/tools/reconciliation/index.js +96 -83
  79. package/dist/tools/reconciliation/matcher.d.ts +3 -3
  80. package/dist/tools/reconciliation/matcher.js +17 -16
  81. package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
  82. package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
  83. package/dist/tools/reconciliation/recommendationEngine.js +40 -40
  84. package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
  85. package/dist/tools/reconciliation/reportFormatter.js +59 -58
  86. package/dist/tools/reconciliation/signDetector.d.ts +1 -1
  87. package/dist/tools/reconciliation/types.d.ts +16 -16
  88. package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
  89. package/dist/tools/schemas/common.d.ts +1 -1
  90. package/dist/tools/schemas/common.js +1 -1
  91. package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
  92. package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
  93. package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
  94. package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
  95. package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
  96. package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
  97. package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
  98. package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
  99. package/dist/tools/schemas/outputs/index.d.ts +14 -14
  100. package/dist/tools/schemas/outputs/index.js +14 -14
  101. package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
  102. package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
  103. package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
  104. package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
  105. package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
  106. package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
  107. package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
  108. package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
  109. package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
  110. package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
  111. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
  112. package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
  113. package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
  114. package/dist/tools/schemas/shared/commonOutputs.js +15 -9
  115. package/dist/tools/transactionReadTools.d.ts +11 -0
  116. package/dist/tools/transactionReadTools.js +202 -0
  117. package/dist/tools/transactionSchemas.d.ts +7 -7
  118. package/dist/tools/transactionSchemas.js +77 -57
  119. package/dist/tools/transactionTools.d.ts +6 -24
  120. package/dist/tools/transactionTools.js +7 -1499
  121. package/dist/tools/transactionUtils.d.ts +6 -6
  122. package/dist/tools/transactionUtils.js +78 -63
  123. package/dist/tools/transactionWriteTools.d.ts +20 -0
  124. package/dist/tools/transactionWriteTools.js +1342 -0
  125. package/dist/tools/utilityTools.d.ts +5 -4
  126. package/dist/tools/utilityTools.js +11 -11
  127. package/dist/types/index.d.ts +7 -7
  128. package/dist/types/index.js +6 -6
  129. package/dist/types/reconciliation.d.ts +1 -1
  130. package/dist/types/toolRegistration.d.ts +14 -12
  131. package/dist/utils/amountUtils.js +1 -1
  132. package/dist/utils/dateUtils.js +4 -4
  133. package/dist/utils/errors.d.ts +3 -3
  134. package/dist/utils/errors.js +4 -4
  135. package/dist/utils/money.d.ts +2 -2
  136. package/dist/utils/money.js +8 -8
  137. package/dist/utils/validationError.d.ts +1 -1
  138. package/dist/utils/validationError.js +1 -1
  139. package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
  140. package/docs/assets/schemas/reconciliation-v2.json +360 -336
  141. package/esbuild.config.mjs +53 -50
  142. package/meta.json +12548 -12548
  143. package/package.json +98 -111
  144. package/scripts/analyze-bundle.mjs +33 -30
  145. package/scripts/create-pr-description.js +169 -120
  146. package/scripts/run-all-tests.js +178 -169
  147. package/scripts/run-domain-integration-tests.js +28 -18
  148. package/scripts/run-generate-mcpb.js +19 -17
  149. package/scripts/run-throttled-integration-tests.js +92 -83
  150. package/scripts/test-delta-params.mjs +149 -120
  151. package/scripts/test-recommendations.ts +36 -32
  152. package/scripts/tmpTransaction.ts +80 -43
  153. package/scripts/validate-env.js +98 -91
  154. package/scripts/verify-build.js +78 -76
  155. package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
  156. package/src/__tests__/performance.test.ts +723 -671
  157. package/src/__tests__/setup.ts +442 -395
  158. package/src/__tests__/smoke.e2e.test.ts +41 -39
  159. package/src/__tests__/testRunner.ts +314 -295
  160. package/src/__tests__/testUtils.ts +456 -364
  161. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
  162. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
  163. package/src/index.ts +68 -59
  164. package/src/server/CLAUDE.md +480 -0
  165. package/src/server/YNABMCPServer.ts +821 -794
  166. package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
  167. package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
  168. package/src/server/__tests__/budgetResolver.test.ts +466 -423
  169. package/src/server/__tests__/cacheManager.test.ts +891 -874
  170. package/src/server/__tests__/completions.integration.test.ts +115 -106
  171. package/src/server/__tests__/completions.test.ts +334 -313
  172. package/src/server/__tests__/config.test.ts +98 -86
  173. package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
  174. package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
  175. package/src/server/__tests__/deltaCache.test.ts +946 -759
  176. package/src/server/__tests__/diagnostics.test.ts +825 -792
  177. package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
  178. package/src/server/__tests__/errorHandler.test.ts +402 -397
  179. package/src/server/__tests__/prompts.test.ts +424 -347
  180. package/src/server/__tests__/rateLimiter.test.ts +313 -309
  181. package/src/server/__tests__/requestLogger.test.ts +443 -403
  182. package/src/server/__tests__/resources.template.test.ts +196 -185
  183. package/src/server/__tests__/resources.test.ts +294 -288
  184. package/src/server/__tests__/security.integration.test.ts +487 -421
  185. package/src/server/__tests__/securityMiddleware.test.ts +519 -444
  186. package/src/server/__tests__/server-startup.integration.test.ts +509 -490
  187. package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
  188. package/src/server/__tests__/toolRegistration.test.ts +239 -210
  189. package/src/server/__tests__/toolRegistry.test.ts +907 -845
  190. package/src/server/budgetResolver.ts +221 -181
  191. package/src/server/cacheKeys.ts +6 -6
  192. package/src/server/cacheManager.ts +498 -484
  193. package/src/server/completions.ts +267 -243
  194. package/src/server/config.ts +35 -14
  195. package/src/server/deltaCache.merge.ts +146 -128
  196. package/src/server/deltaCache.ts +352 -309
  197. package/src/server/diagnostics.ts +257 -242
  198. package/src/server/errorHandler.ts +747 -744
  199. package/src/server/prompts.ts +181 -176
  200. package/src/server/rateLimiter.ts +131 -129
  201. package/src/server/requestLogger.ts +350 -322
  202. package/src/server/resources.ts +442 -374
  203. package/src/server/responseFormatter.ts +41 -37
  204. package/src/server/securityMiddleware.ts +223 -205
  205. package/src/server/serverKnowledgeStore.ts +67 -67
  206. package/src/server/toolRegistry.ts +508 -474
  207. package/src/tools/CLAUDE.md +604 -0
  208. package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
  209. package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
  210. package/src/tools/__tests__/accountTools.test.ts +685 -638
  211. package/src/tools/__tests__/adapters.test.ts +142 -108
  212. package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
  213. package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
  214. package/src/tools/__tests__/budgetTools.test.ts +442 -413
  215. package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
  216. package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
  217. package/src/tools/__tests__/categoryTools.test.ts +656 -625
  218. package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
  219. package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
  220. package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
  221. package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
  222. package/src/tools/__tests__/compareTransactions.test.ts +352 -332
  223. package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
  224. package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
  225. package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
  226. package/src/tools/__tests__/deltaSupport.test.ts +211 -184
  227. package/src/tools/__tests__/deltaTestUtils.ts +37 -33
  228. package/src/tools/__tests__/exportTransactions.test.ts +205 -200
  229. package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
  230. package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
  231. package/src/tools/__tests__/monthTools.test.ts +561 -512
  232. package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
  233. package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
  234. package/src/tools/__tests__/payeeTools.test.ts +486 -434
  235. package/src/tools/__tests__/transactionSchemas.test.ts +1202 -1186
  236. package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
  237. package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
  238. package/src/tools/__tests__/transactionUtils.test.ts +1004 -977
  239. package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
  240. package/src/tools/__tests__/utilityTools.test.ts +68 -58
  241. package/src/tools/accountTools.ts +293 -271
  242. package/src/tools/adapters.ts +120 -63
  243. package/src/tools/budgetTools.ts +121 -116
  244. package/src/tools/categoryTools.ts +379 -339
  245. package/src/tools/compareTransactions/formatter.ts +131 -119
  246. package/src/tools/compareTransactions/index.ts +249 -214
  247. package/src/tools/compareTransactions/matcher.ts +259 -209
  248. package/src/tools/compareTransactions/parser.ts +517 -487
  249. package/src/tools/compareTransactions/types.ts +38 -38
  250. package/src/tools/compareTransactions.ts +1 -1
  251. package/src/tools/deltaFetcher.ts +281 -260
  252. package/src/tools/deltaSupport.ts +264 -259
  253. package/src/tools/exportTransactions.ts +230 -218
  254. package/src/tools/monthTools.ts +180 -165
  255. package/src/tools/payeeTools.ts +152 -140
  256. package/src/tools/reconcileAdapter.ts +297 -252
  257. package/src/tools/reconciliation/CLAUDE.md +506 -0
  258. package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +133 -124
  259. package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -230
  260. package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -400
  261. package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
  262. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
  263. package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
  264. package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
  265. package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
  266. package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
  267. package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
  268. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -989
  269. package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
  270. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -533
  271. package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -74
  272. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -62
  273. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
  274. package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +56 -55
  275. package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
  276. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
  277. package/src/tools/reconciliation/analyzer.ts +564 -504
  278. package/src/tools/reconciliation/csvParser.ts +656 -609
  279. package/src/tools/reconciliation/executor.ts +1290 -1128
  280. package/src/tools/reconciliation/index.ts +580 -528
  281. package/src/tools/reconciliation/matcher.ts +256 -240
  282. package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
  283. package/src/tools/reconciliation/recommendationEngine.ts +357 -345
  284. package/src/tools/reconciliation/reportFormatter.ts +343 -307
  285. package/src/tools/reconciliation/signDetector.ts +89 -83
  286. package/src/tools/reconciliation/types.ts +164 -159
  287. package/src/tools/reconciliation/ynabAdapter.ts +17 -15
  288. package/src/tools/schemas/CLAUDE.md +546 -0
  289. package/src/tools/schemas/common.ts +1 -1
  290. package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
  291. package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
  292. package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
  293. package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
  294. package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
  295. package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
  296. package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
  297. package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
  298. package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
  299. package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
  300. package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
  301. package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
  302. package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
  303. package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
  304. package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
  305. package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
  306. package/src/tools/schemas/outputs/index.ts +163 -163
  307. package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
  308. package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
  309. package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
  310. package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
  311. package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
  312. package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
  313. package/src/tools/schemas/shared/commonOutputs.ts +27 -19
  314. package/src/tools/toolCategories.ts +114 -114
  315. package/src/tools/transactionReadTools.ts +327 -0
  316. package/src/tools/transactionSchemas.ts +322 -291
  317. package/src/tools/transactionTools.ts +84 -2246
  318. package/src/tools/transactionUtils.ts +507 -422
  319. package/src/tools/transactionWriteTools.ts +2110 -0
  320. package/src/tools/utilityTools.ts +46 -41
  321. package/src/types/CLAUDE.md +477 -0
  322. package/src/types/__tests__/index.test.ts +51 -51
  323. package/src/types/index.ts +43 -39
  324. package/src/types/integration-tests.d.ts +26 -26
  325. package/src/types/reconciliation.ts +29 -29
  326. package/src/types/toolAnnotations.ts +30 -30
  327. package/src/types/toolRegistration.ts +43 -32
  328. package/src/utils/CLAUDE.md +508 -0
  329. package/src/utils/__tests__/dateUtils.test.ts +174 -168
  330. package/src/utils/__tests__/money.test.ts +193 -187
  331. package/src/utils/amountUtils.ts +5 -5
  332. package/src/utils/baseError.ts +5 -5
  333. package/src/utils/dateUtils.ts +29 -26
  334. package/src/utils/errors.ts +14 -14
  335. package/src/utils/money.ts +66 -52
  336. package/src/utils/validationError.ts +1 -1
  337. package/tsconfig.json +29 -29
  338. package/tsconfig.prod.json +16 -16
  339. package/vitest-reporters/split-json-reporter.ts +247 -204
  340. package/vitest.config.ts +99 -95
  341. package/.prettierignore +0 -10
  342. package/.prettierrc.json +0 -10
  343. package/eslint.config.js +0 -49
@@ -1,57 +1,63 @@
1
- import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';
2
- import { z, toJSONSchema } from 'zod/v4';
3
- import { fromZodError } from 'zod-validation-error';
4
- import type { MCPToolAnnotations } from '../types/toolAnnotations.js';
1
+ import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import { fromZodError } from "zod-validation-error";
3
+ import { toJSONSchema, z } from "zod/v4";
4
+ import type { MCPToolAnnotations } from "../types/toolAnnotations.js";
5
5
 
6
6
  export type SecurityWrapperFactory = <T extends Record<string, unknown>>(
7
- namespace: string,
8
- operation: string,
9
- schema: z.ZodSchema<T>,
7
+ namespace: string,
8
+ operation: string,
9
+ schema: z.ZodSchema<T>,
10
10
  ) => (
11
- accessToken: string,
11
+ accessToken: string,
12
12
  ) => (
13
- params: Record<string, unknown>,
14
- ) => (handler: (validated: T) => Promise<CallToolResult>) => Promise<CallToolResult>;
13
+ params: Record<string, unknown>,
14
+ ) => (
15
+ handler: (validated: T) => Promise<CallToolResult>,
16
+ ) => Promise<CallToolResult>;
15
17
 
16
18
  export interface ErrorHandlerContract {
17
- handleError(error: unknown, context: string): CallToolResult;
18
- createValidationError(message: string, details?: string, suggestions?: string[]): CallToolResult;
19
+ handleError(error: unknown, context: string): CallToolResult;
20
+ createValidationError(
21
+ message: string,
22
+ details?: string,
23
+ suggestions?: string[],
24
+ ): CallToolResult;
19
25
  }
20
26
 
21
27
  export interface ResponseFormatterContract {
22
- runWithMinifyOverride<T>(minifyOverride: boolean | undefined, fn: () => T): T;
28
+ runWithMinifyOverride<T>(minifyOverride: boolean | undefined, fn: () => T): T;
23
29
  }
24
30
 
25
31
  export interface ToolRegistryCacheHelpers {
26
- generateKey?: (...segments: unknown[]) => string;
27
- invalidate?: (key: string) => void | Promise<void>;
28
- clear?: () => void | Promise<void>;
32
+ generateKey?: (...segments: unknown[]) => string;
33
+ invalidate?: (key: string) => void | Promise<void>;
34
+ clear?: () => void | Promise<void>;
29
35
  }
30
36
 
31
37
  export interface DefaultArgumentResolverContext {
32
- name: string;
33
- accessToken: string;
34
- rawArguments: Record<string, unknown>;
38
+ name: string;
39
+ accessToken: string;
40
+ rawArguments: Record<string, unknown>;
35
41
  }
36
42
  export class DefaultArgumentResolutionError extends Error {
37
- constructor(public readonly result: CallToolResult) {
38
- super('Default argument resolution failed');
39
- this.name = 'DefaultArgumentResolutionError';
40
- }
43
+ constructor(public readonly result: CallToolResult) {
44
+ super("Default argument resolution failed");
45
+ this.name = "DefaultArgumentResolutionError";
46
+ }
41
47
  }
42
48
 
43
49
  export type DefaultArgumentResolver<TInput extends Record<string, unknown>> = (
44
- context: DefaultArgumentResolverContext,
50
+ context: DefaultArgumentResolverContext,
45
51
  ) => Partial<TInput> | Promise<Partial<TInput> | undefined> | undefined;
46
52
 
47
53
  export interface ToolSecurityOptions {
48
- namespace?: string;
49
- operation?: string;
54
+ namespace?: string;
55
+ operation?: string;
50
56
  }
51
57
 
52
58
  export interface ToolMetadataOptions {
53
- inputJsonSchema?: Record<string, unknown>;
54
- annotations?: MCPToolAnnotations;
59
+ inputJsonSchema?: Record<string, unknown>;
60
+ annotations?: MCPToolAnnotations;
55
61
  }
56
62
 
57
63
  /**
@@ -59,477 +65,505 @@ export interface ToolMetadataOptions {
59
65
  * Follows MCP spec: notifications/progress
60
66
  */
61
67
  export type ProgressCallback = (params: {
62
- progress: number;
63
- total?: number;
64
- message?: string;
68
+ progress: number;
69
+ total?: number;
70
+ message?: string;
65
71
  }) => Promise<void>;
66
72
 
67
73
  export interface ToolExecutionContext {
68
- accessToken: string;
69
- name: string;
70
- operation: string;
71
- rawArguments: Record<string, unknown>;
72
- cache?: ToolRegistryCacheHelpers;
73
- /**
74
- * Optional progress callback for emitting MCP progress notifications.
75
- * Available when the client provides a progressToken in the request.
76
- */
77
- sendProgress?: ProgressCallback;
74
+ accessToken: string;
75
+ name: string;
76
+ operation: string;
77
+ rawArguments: Record<string, unknown>;
78
+ cache?: ToolRegistryCacheHelpers;
79
+ /**
80
+ * Optional progress callback for emitting MCP progress notifications.
81
+ * Available when the client provides a progressToken in the request.
82
+ */
83
+ sendProgress?: ProgressCallback;
78
84
  }
79
85
 
80
86
  export interface ToolExecutionPayload<TInput extends Record<string, unknown>> {
81
- input: TInput;
82
- context: ToolExecutionContext;
87
+ input: TInput;
88
+ context: ToolExecutionContext;
83
89
  }
84
90
 
85
91
  export type ToolHandler<TInput extends Record<string, unknown>> = (
86
- payload: ToolExecutionPayload<TInput>,
92
+ payload: ToolExecutionPayload<TInput>,
87
93
  ) => Promise<CallToolResult>;
88
94
 
89
95
  export interface ToolDefinition<
90
- TInput extends Record<string, unknown> = Record<string, unknown>,
91
- TOutput extends Record<string, unknown> = Record<string, unknown>,
96
+ TInput extends Record<string, unknown> = Record<string, unknown>,
97
+ TOutput extends Record<string, unknown> = Record<string, unknown>,
92
98
  > {
93
- name: string;
94
- description: string;
95
- inputSchema: z.ZodSchema<TInput>;
96
- outputSchema?: z.ZodSchema<TOutput>;
97
- handler: ToolHandler<TInput>;
98
- security?: ToolSecurityOptions;
99
- metadata?: ToolMetadataOptions;
100
- defaultArgumentResolver?: DefaultArgumentResolver<TInput>;
99
+ name: string;
100
+ description: string;
101
+ inputSchema: z.ZodSchema<TInput>;
102
+ outputSchema?: z.ZodSchema<TOutput>;
103
+ handler: ToolHandler<TInput>;
104
+ security?: ToolSecurityOptions;
105
+ metadata?: ToolMetadataOptions;
106
+ defaultArgumentResolver?: DefaultArgumentResolver<TInput>;
101
107
  }
102
108
 
103
109
  interface RegisteredTool<
104
- TInput extends Record<string, unknown>,
105
- TOutput extends Record<string, unknown>,
110
+ TInput extends Record<string, unknown>,
111
+ TOutput extends Record<string, unknown>,
106
112
  > extends ToolDefinition<TInput, TOutput> {
107
- readonly security: Required<ToolSecurityOptions>;
113
+ readonly security: Required<ToolSecurityOptions>;
108
114
  }
109
115
 
110
116
  export interface ToolExecutionOptions {
111
- name: string;
112
- accessToken: string;
113
- arguments?: Record<string, unknown>;
114
- minifyOverride?: boolean;
115
- /**
116
- * Optional progress callback for emitting MCP progress notifications.
117
- * Should be provided when the request includes a progressToken.
118
- */
119
- sendProgress?: ProgressCallback;
117
+ name: string;
118
+ accessToken: string;
119
+ arguments?: Record<string, unknown>;
120
+ minifyOverride?: boolean;
121
+ /**
122
+ * Optional progress callback for emitting MCP progress notifications.
123
+ * Should be provided when the request includes a progressToken.
124
+ */
125
+ sendProgress?: ProgressCallback;
120
126
  }
121
127
 
122
128
  export interface ToolRegistryDependencies {
123
- withSecurityWrapper: SecurityWrapperFactory;
124
- errorHandler: ErrorHandlerContract;
125
- responseFormatter: ResponseFormatterContract;
126
- cacheHelpers?: ToolRegistryCacheHelpers;
127
- validateAccessToken?: (token: string) => Promise<void> | void;
129
+ withSecurityWrapper: SecurityWrapperFactory;
130
+ errorHandler: ErrorHandlerContract;
131
+ responseFormatter: ResponseFormatterContract;
132
+ cacheHelpers?: ToolRegistryCacheHelpers;
133
+ validateAccessToken?: (token: string) => Promise<void> | void;
128
134
  }
129
135
 
130
- const MINIFY_HINT_KEYS = ['minify', '_minify', '__minify'] as const;
136
+ const MINIFY_HINT_KEYS = ["minify", "_minify", "__minify"] as const;
131
137
 
132
138
  export class ToolRegistry {
133
- private readonly tools = new Map<
134
- string,
135
- RegisteredTool<Record<string, unknown>, Record<string, unknown>>
136
- >();
137
- private readonly outputValidators = new Map<string, z.ZodSchema<Record<string, unknown>>>();
138
-
139
- constructor(private readonly deps: ToolRegistryDependencies) {}
140
-
141
- register<TInput extends Record<string, unknown>, TOutput extends Record<string, unknown>>(
142
- definition: ToolDefinition<TInput, TOutput>,
143
- ): void {
144
- this.assertValidDefinition(definition);
145
-
146
- if (this.tools.has(definition.name)) {
147
- throw new Error(`Tool '${definition.name}' is already registered`);
148
- }
149
-
150
- const resolved: RegisteredTool<TInput, TOutput> = {
151
- ...definition,
152
- security: {
153
- namespace: definition.security?.namespace ?? 'ynab',
154
- operation: definition.security?.operation ?? definition.name,
155
- },
156
- };
157
-
158
- // Type assertion is safe here because TInput/TOutput extend Record<string, unknown>
159
- // and RegisteredTool is covariant in its type parameters for storage purposes
160
- const registeredTool = resolved as RegisteredTool<
161
- Record<string, unknown>,
162
- Record<string, unknown>
163
- >;
164
- this.tools.set(definition.name, registeredTool);
165
-
166
- // Cache output validator if present
167
- if (definition.outputSchema) {
168
- this.outputValidators.set(
169
- definition.name,
170
- definition.outputSchema as z.ZodSchema<Record<string, unknown>>,
171
- );
172
- }
173
- }
174
-
175
- listTools(): Tool[] {
176
- return Array.from(this.tools.values()).map((tool) => {
177
- const inputSchema =
178
- (tool.metadata?.inputJsonSchema as Tool['inputSchema'] | undefined) ??
179
- (this.generateJsonSchema(tool.inputSchema) as Tool['inputSchema']);
180
- const result: Tool = {
181
- name: tool.name,
182
- description: tool.description,
183
- inputSchema,
184
- };
185
- if (tool.outputSchema) {
186
- const outputSchema = this.generateJsonSchema(
187
- tool.outputSchema,
188
- 'output',
189
- ) as Tool['outputSchema'];
190
- result.outputSchema = outputSchema;
191
- }
192
- if (tool.metadata?.annotations) {
193
- result.annotations = tool.metadata.annotations;
194
- }
195
- return result;
196
- });
197
- }
198
-
199
- hasTool(name: string): boolean {
200
- return this.tools.has(name);
201
- }
202
-
203
- getToolDefinitions(): ToolDefinition[] {
204
- return Array.from(this.tools.values()).map((tool) => {
205
- const definition: ToolDefinition = {
206
- name: tool.name,
207
- description: tool.description,
208
- inputSchema: tool.inputSchema,
209
- handler: tool.handler,
210
- security: tool.security,
211
- };
212
- if (tool.outputSchema) {
213
- definition.outputSchema = tool.outputSchema;
214
- }
215
- if (tool.metadata) {
216
- definition.metadata = tool.metadata;
217
- }
218
- if (tool.defaultArgumentResolver) {
219
- definition.defaultArgumentResolver = tool.defaultArgumentResolver;
220
- }
221
- return definition;
222
- });
223
- }
224
-
225
- async executeTool(options: ToolExecutionOptions): Promise<CallToolResult> {
226
- const tool = this.tools.get(options.name);
227
- if (!tool) {
228
- return this.deps.errorHandler.createValidationError(
229
- `Unknown tool: ${options.name}`,
230
- 'The requested tool is not registered with the server',
231
- );
232
- }
233
-
234
- if (this.deps.validateAccessToken) {
235
- try {
236
- await this.deps.validateAccessToken(options.accessToken);
237
- } catch (error) {
238
- if (this.isCallToolResult(error)) {
239
- return error;
240
- }
241
- return this.deps.errorHandler.handleError(error, `authenticating ${tool.name}`);
242
- }
243
- }
244
-
245
- let defaults: Partial<Record<string, unknown>> | undefined;
246
-
247
- if (tool.defaultArgumentResolver) {
248
- try {
249
- defaults = await tool.defaultArgumentResolver({
250
- name: tool.name,
251
- accessToken: options.accessToken,
252
- rawArguments: options.arguments ?? {},
253
- });
254
- } catch (error) {
255
- if (error instanceof DefaultArgumentResolutionError) {
256
- return error.result;
257
- }
258
- if (this.isCallToolResult(error)) {
259
- return error;
260
- }
261
- return this.deps.errorHandler.createValidationError(
262
- 'Invalid parameters',
263
- error instanceof Error
264
- ? error.message
265
- : 'Unknown error during default argument resolution',
266
- );
267
- }
268
- }
269
-
270
- const rawArguments: Record<string, unknown> = {
271
- ...(defaults ?? {}),
272
- ...(options.arguments ?? {}),
273
- };
274
-
275
- const minifyOverride = this.extractMinifyOverride(options, rawArguments);
276
-
277
- const run = async (): Promise<CallToolResult> => {
278
- try {
279
- const secured = this.deps.withSecurityWrapper(
280
- tool.security.namespace,
281
- tool.security.operation,
282
- tool.inputSchema,
283
- )(options.accessToken)(rawArguments);
284
-
285
- return await secured(async (validated) => {
286
- try {
287
- const context: ToolExecutionContext = {
288
- accessToken: options.accessToken,
289
- name: tool.name,
290
- operation: tool.security.operation,
291
- rawArguments,
292
- };
293
- if (this.deps.cacheHelpers) {
294
- context.cache = this.deps.cacheHelpers;
295
- }
296
- if (options.sendProgress) {
297
- context.sendProgress = options.sendProgress;
298
- }
299
- const handlerResult = await tool.handler({
300
- input: validated,
301
- context,
302
- });
303
- // Validate output against schema if present
304
- // Skip validation if handler returned an error
305
- if (handlerResult.isError) {
306
- return handlerResult;
307
- }
308
- return this.validateOutput(tool.name, handlerResult);
309
- } catch (handlerError) {
310
- return this.deps.errorHandler.handleError(
311
- handlerError,
312
- `executing ${tool.name} - ${tool.security.operation}`,
313
- );
314
- }
315
- });
316
- } catch (securityError) {
317
- return this.normalizeSecurityError(securityError, tool);
318
- }
319
- };
320
-
321
- try {
322
- return await this.deps.responseFormatter.runWithMinifyOverride(minifyOverride, run);
323
- } catch (formatterError) {
324
- return this.deps.errorHandler.handleError(
325
- formatterError,
326
- `formatting response for ${tool.name}`,
327
- );
328
- }
329
- }
330
-
331
- private isCallToolResult(value: unknown): value is CallToolResult {
332
- return (
333
- typeof value === 'object' &&
334
- value !== null &&
335
- 'content' in (value as Record<string, unknown>) &&
336
- Array.isArray((value as { content?: unknown }).content)
337
- );
338
- }
339
-
340
- private normalizeSecurityError(
341
- error: unknown,
342
- tool: RegisteredTool<Record<string, unknown>, Record<string, unknown>>,
343
- ): CallToolResult {
344
- if (error instanceof z.ZodError) {
345
- const validationError = fromZodError(error);
346
- return this.deps.errorHandler.createValidationError(
347
- `Invalid parameters for ${tool.name}`,
348
- validationError.message,
349
- );
350
- }
351
-
352
- if (error instanceof Error && error.message.includes('Validation failed')) {
353
- return this.deps.errorHandler.createValidationError(
354
- `Invalid parameters for ${tool.name}`,
355
- error.message,
356
- );
357
- }
358
-
359
- return this.deps.errorHandler.handleError(error, `executing ${tool.name}`);
360
- }
361
-
362
- private extractMinifyOverride(
363
- options: ToolExecutionOptions,
364
- args: Record<string, unknown>,
365
- ): boolean | undefined {
366
- if (typeof options.minifyOverride === 'boolean') {
367
- return options.minifyOverride;
368
- }
369
-
370
- for (const key of MINIFY_HINT_KEYS) {
371
- const value = args[key];
372
- if (typeof value === 'boolean') {
373
- // Remove the minify hint key from args
374
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
375
- delete args[key];
376
- return value;
377
- }
378
- }
379
-
380
- return undefined;
381
- }
382
-
383
- /**
384
- * Regex pattern for MCP-compliant tool names.
385
- * Tool names SHOULD be 1-128 chars, case-sensitive, only [a-zA-Z0-9_.-]
386
- * @see https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
387
- */
388
- private static readonly MCP_TOOL_NAME_REGEX = /^[a-zA-Z0-9_.-]{1,128}$/;
389
-
390
- private assertValidDefinition<
391
- TInput extends Record<string, unknown>,
392
- TOutput extends Record<string, unknown>,
393
- >(definition: ToolDefinition<TInput, TOutput>): void {
394
- if (!definition || typeof definition !== 'object') {
395
- throw new Error('Tool definition must be an object');
396
- }
397
-
398
- if (!definition.name || typeof definition.name !== 'string') {
399
- throw new Error('Tool definition requires a non-empty name');
400
- }
401
-
402
- // Validate tool name follows MCP specification guidelines
403
- if (!ToolRegistry.MCP_TOOL_NAME_REGEX.test(definition.name)) {
404
- throw new Error(
405
- `Tool name '${definition.name}' violates MCP guidelines: ` +
406
- `must be 1-128 chars using only [a-zA-Z0-9_.-]`,
407
- );
408
- }
409
-
410
- if (!definition.description || typeof definition.description !== 'string') {
411
- throw new Error(`Tool '${definition.name}' requires a description`);
412
- }
413
-
414
- if (!definition.inputSchema || typeof definition.inputSchema.parse !== 'function') {
415
- throw new Error(`Tool '${definition.name}' requires a valid Zod schema`);
416
- }
417
-
418
- if (definition.outputSchema && typeof definition.outputSchema.parse !== 'function') {
419
- throw new Error(
420
- `Tool '${definition.name}' outputSchema must be a valid Zod schema when provided`,
421
- );
422
- }
423
-
424
- if (typeof definition.handler !== 'function') {
425
- throw new Error(`Tool '${definition.name}' requires a handler function`);
426
- }
427
-
428
- if (
429
- definition.defaultArgumentResolver &&
430
- typeof definition.defaultArgumentResolver !== 'function'
431
- ) {
432
- throw new Error(
433
- `Tool '${definition.name}' defaultArgumentResolver must be a function when provided`,
434
- );
435
- }
436
- }
437
-
438
- private generateJsonSchema(
439
- schema: z.ZodTypeAny,
440
- ioMode: 'input' | 'output' = 'input',
441
- ): Record<string, unknown> {
442
- try {
443
- return toJSONSchema(schema, { target: 'draft-2020-12', io: ioMode });
444
- } catch (error) {
445
- console.warn(`Failed to generate JSON schema for tool: ${error}`);
446
- return { type: 'object', additionalProperties: true };
447
- }
448
- }
449
-
450
- /**
451
- * Validates handler output against the tool's output schema if present
452
- */
453
- private validateOutput(toolName: string, output: CallToolResult): CallToolResult {
454
- const validator = this.outputValidators.get(toolName);
455
- if (!validator) {
456
- // No output schema defined, skip validation
457
- return output;
458
- }
459
-
460
- // Extract the actual data from the CallToolResult
461
- // CallToolResult is { content: Array<{ type: string, text: string, ... }> }
462
- // We need to parse the text content and validate it
463
- if (!output.content || output.content.length === 0) {
464
- return this.deps.errorHandler.createValidationError(
465
- `Output validation failed for ${toolName}`,
466
- 'Handler returned empty content',
467
- ['Ensure the handler returns valid content in the response'],
468
- );
469
- }
470
-
471
- // Validate all content items (not just the first one)
472
- const invalidItems: { index: number; reason: string }[] = [];
473
-
474
- for (let i = 0; i < output.content.length; i++) {
475
- const item = output.content[i];
476
- if (!item) {
477
- invalidItems.push({ index: i, reason: 'item is null or undefined' });
478
- } else if (item.type !== 'text') {
479
- invalidItems.push({ index: i, reason: `type is "${item.type}" instead of "text"` });
480
- } else if (typeof item.text !== 'string') {
481
- invalidItems.push({
482
- index: i,
483
- reason: `text property is ${typeof item.text} instead of string`,
484
- });
485
- }
486
- }
487
-
488
- if (invalidItems.length > 0) {
489
- const invalidItemsDetails = invalidItems
490
- .map((inv) => ` - Item ${inv.index}: ${inv.reason}`)
491
- .join('\n');
492
-
493
- return this.deps.errorHandler.createValidationError(
494
- `Output validation failed for ${toolName}`,
495
- `Handler returned invalid content items (${invalidItems.length} of ${output.content.length} failed):\n${invalidItemsDetails}`,
496
- ['Ensure all content items have type="text" and a valid text property'],
497
- );
498
- }
499
-
500
- const firstContent = output.content[0]!;
501
- // TypeScript: After validation above, we know firstContent.type === 'text'
502
- if (firstContent.type !== 'text') {
503
- throw new Error('Unexpected: firstContent is not text after validation');
504
- }
505
-
506
- let parsedOutput: unknown;
507
- try {
508
- parsedOutput = JSON.parse(firstContent.text);
509
- } catch (parseError) {
510
- return this.deps.errorHandler.createValidationError(
511
- `Output validation failed for ${toolName}`,
512
- `Invalid JSON in handler output: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
513
- ['Ensure the handler returns valid JSON'],
514
- );
515
- }
516
-
517
- // Validate against schema
518
- const result = validator.safeParse(parsedOutput);
519
- if (!result.success) {
520
- const validationError = fromZodError(result.error);
521
- const validationErrors = validationError.message;
522
- return this.deps.errorHandler.createValidationError(
523
- `Output validation failed for ${toolName}`,
524
- `Handler output does not match declared output schema: ${validationErrors}`,
525
- [
526
- 'Check that the handler returns data matching the output schema',
527
- 'Review the tool definition output schema',
528
- ],
529
- );
530
- }
531
-
532
- // Validation passed, return original output
533
- return output;
534
- }
139
+ private readonly tools = new Map<
140
+ string,
141
+ RegisteredTool<Record<string, unknown>, Record<string, unknown>>
142
+ >();
143
+ private readonly outputValidators = new Map<
144
+ string,
145
+ z.ZodSchema<Record<string, unknown>>
146
+ >();
147
+
148
+ constructor(private readonly deps: ToolRegistryDependencies) {}
149
+
150
+ register<
151
+ TInput extends Record<string, unknown>,
152
+ TOutput extends Record<string, unknown>,
153
+ >(definition: ToolDefinition<TInput, TOutput>): void {
154
+ this.assertValidDefinition(definition);
155
+
156
+ if (this.tools.has(definition.name)) {
157
+ throw new Error(`Tool '${definition.name}' is already registered`);
158
+ }
159
+
160
+ const resolved: RegisteredTool<TInput, TOutput> = {
161
+ ...definition,
162
+ security: {
163
+ namespace: definition.security?.namespace ?? "ynab",
164
+ operation: definition.security?.operation ?? definition.name,
165
+ },
166
+ };
167
+
168
+ // Type assertion is safe here because TInput/TOutput extend Record<string, unknown>
169
+ // and RegisteredTool is covariant in its type parameters for storage purposes
170
+ const registeredTool = resolved as RegisteredTool<
171
+ Record<string, unknown>,
172
+ Record<string, unknown>
173
+ >;
174
+ this.tools.set(definition.name, registeredTool);
175
+
176
+ // Cache output validator if present
177
+ if (definition.outputSchema) {
178
+ this.outputValidators.set(
179
+ definition.name,
180
+ definition.outputSchema as z.ZodSchema<Record<string, unknown>>,
181
+ );
182
+ }
183
+ }
184
+
185
+ listTools(): Tool[] {
186
+ return Array.from(this.tools.values()).map((tool) => {
187
+ const inputSchema =
188
+ (tool.metadata?.inputJsonSchema as Tool["inputSchema"] | undefined) ??
189
+ (this.generateJsonSchema(tool.inputSchema) as Tool["inputSchema"]);
190
+ const result: Tool = {
191
+ name: tool.name,
192
+ description: tool.description,
193
+ inputSchema,
194
+ };
195
+ if (tool.outputSchema) {
196
+ const outputSchema = this.generateJsonSchema(
197
+ tool.outputSchema,
198
+ "output",
199
+ ) as Tool["outputSchema"];
200
+ result.outputSchema = outputSchema;
201
+ }
202
+ if (tool.metadata?.annotations) {
203
+ result.annotations = tool.metadata.annotations;
204
+ }
205
+ return result;
206
+ });
207
+ }
208
+
209
+ hasTool(name: string): boolean {
210
+ return this.tools.has(name);
211
+ }
212
+
213
+ getToolDefinitions(): ToolDefinition[] {
214
+ return Array.from(this.tools.values()).map((tool) => {
215
+ const definition: ToolDefinition = {
216
+ name: tool.name,
217
+ description: tool.description,
218
+ inputSchema: tool.inputSchema,
219
+ handler: tool.handler,
220
+ security: tool.security,
221
+ };
222
+ if (tool.outputSchema) {
223
+ definition.outputSchema = tool.outputSchema;
224
+ }
225
+ if (tool.metadata) {
226
+ definition.metadata = tool.metadata;
227
+ }
228
+ if (tool.defaultArgumentResolver) {
229
+ definition.defaultArgumentResolver = tool.defaultArgumentResolver;
230
+ }
231
+ return definition;
232
+ });
233
+ }
234
+
235
+ async executeTool(options: ToolExecutionOptions): Promise<CallToolResult> {
236
+ const tool = this.tools.get(options.name);
237
+ if (!tool) {
238
+ return this.deps.errorHandler.createValidationError(
239
+ `Unknown tool: ${options.name}`,
240
+ "The requested tool is not registered with the server",
241
+ );
242
+ }
243
+
244
+ if (this.deps.validateAccessToken) {
245
+ try {
246
+ await this.deps.validateAccessToken(options.accessToken);
247
+ } catch (error) {
248
+ if (this.isCallToolResult(error)) {
249
+ return error;
250
+ }
251
+ return this.deps.errorHandler.handleError(
252
+ error,
253
+ `authenticating ${tool.name}`,
254
+ );
255
+ }
256
+ }
257
+
258
+ let defaults: Partial<Record<string, unknown>> | undefined;
259
+
260
+ if (tool.defaultArgumentResolver) {
261
+ try {
262
+ defaults = await tool.defaultArgumentResolver({
263
+ name: tool.name,
264
+ accessToken: options.accessToken,
265
+ rawArguments: options.arguments ?? {},
266
+ });
267
+ } catch (error) {
268
+ if (error instanceof DefaultArgumentResolutionError) {
269
+ return error.result;
270
+ }
271
+ if (this.isCallToolResult(error)) {
272
+ return error;
273
+ }
274
+ return this.deps.errorHandler.createValidationError(
275
+ "Invalid parameters",
276
+ error instanceof Error
277
+ ? error.message
278
+ : "Unknown error during default argument resolution",
279
+ );
280
+ }
281
+ }
282
+
283
+ const rawArguments: Record<string, unknown> = {
284
+ ...(defaults ?? {}),
285
+ ...(options.arguments ?? {}),
286
+ };
287
+
288
+ const minifyOverride = this.extractMinifyOverride(options, rawArguments);
289
+
290
+ const run = async (): Promise<CallToolResult> => {
291
+ try {
292
+ const secured = this.deps.withSecurityWrapper(
293
+ tool.security.namespace,
294
+ tool.security.operation,
295
+ tool.inputSchema,
296
+ )(options.accessToken)(rawArguments);
297
+
298
+ return await secured(async (validated) => {
299
+ try {
300
+ const context: ToolExecutionContext = {
301
+ accessToken: options.accessToken,
302
+ name: tool.name,
303
+ operation: tool.security.operation,
304
+ rawArguments,
305
+ };
306
+ if (this.deps.cacheHelpers) {
307
+ context.cache = this.deps.cacheHelpers;
308
+ }
309
+ if (options.sendProgress) {
310
+ context.sendProgress = options.sendProgress;
311
+ }
312
+ const handlerResult = await tool.handler({
313
+ input: validated,
314
+ context,
315
+ });
316
+ // Validate output against schema if present
317
+ // Skip validation if handler returned an error
318
+ if (handlerResult.isError) {
319
+ return handlerResult;
320
+ }
321
+ return this.validateOutput(tool.name, handlerResult);
322
+ } catch (handlerError) {
323
+ return this.deps.errorHandler.handleError(
324
+ handlerError,
325
+ `executing ${tool.name} - ${tool.security.operation}`,
326
+ );
327
+ }
328
+ });
329
+ } catch (securityError) {
330
+ return this.normalizeSecurityError(securityError, tool);
331
+ }
332
+ };
333
+
334
+ try {
335
+ return await this.deps.responseFormatter.runWithMinifyOverride(
336
+ minifyOverride,
337
+ run,
338
+ );
339
+ } catch (formatterError) {
340
+ return this.deps.errorHandler.handleError(
341
+ formatterError,
342
+ `formatting response for ${tool.name}`,
343
+ );
344
+ }
345
+ }
346
+
347
+ private isCallToolResult(value: unknown): value is CallToolResult {
348
+ return (
349
+ typeof value === "object" &&
350
+ value !== null &&
351
+ "content" in (value as Record<string, unknown>) &&
352
+ Array.isArray((value as { content?: unknown }).content)
353
+ );
354
+ }
355
+
356
+ private normalizeSecurityError(
357
+ error: unknown,
358
+ tool: RegisteredTool<Record<string, unknown>, Record<string, unknown>>,
359
+ ): CallToolResult {
360
+ if (error instanceof z.ZodError) {
361
+ const validationError = fromZodError(error);
362
+ return this.deps.errorHandler.createValidationError(
363
+ `Invalid parameters for ${tool.name}`,
364
+ validationError.message,
365
+ );
366
+ }
367
+
368
+ if (error instanceof Error && error.message.includes("Validation failed")) {
369
+ return this.deps.errorHandler.createValidationError(
370
+ `Invalid parameters for ${tool.name}`,
371
+ error.message,
372
+ );
373
+ }
374
+
375
+ return this.deps.errorHandler.handleError(error, `executing ${tool.name}`);
376
+ }
377
+
378
+ private extractMinifyOverride(
379
+ options: ToolExecutionOptions,
380
+ args: Record<string, unknown>,
381
+ ): boolean | undefined {
382
+ if (typeof options.minifyOverride === "boolean") {
383
+ return options.minifyOverride;
384
+ }
385
+
386
+ for (const key of MINIFY_HINT_KEYS) {
387
+ const value = args[key];
388
+ if (typeof value === "boolean") {
389
+ // Remove the minify hint key from args
390
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
391
+ delete args[key];
392
+ return value;
393
+ }
394
+ }
395
+
396
+ return undefined;
397
+ }
398
+
399
+ /**
400
+ * Regex pattern for MCP-compliant tool names.
401
+ * Tool names SHOULD be 1-128 chars, case-sensitive, only [a-zA-Z0-9_.-]
402
+ * @see https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
403
+ */
404
+ private static readonly MCP_TOOL_NAME_REGEX = /^[a-zA-Z0-9_.-]{1,128}$/;
405
+
406
+ private assertValidDefinition<
407
+ TInput extends Record<string, unknown>,
408
+ TOutput extends Record<string, unknown>,
409
+ >(definition: ToolDefinition<TInput, TOutput>): void {
410
+ if (!definition || typeof definition !== "object") {
411
+ throw new Error("Tool definition must be an object");
412
+ }
413
+
414
+ if (!definition.name || typeof definition.name !== "string") {
415
+ throw new Error("Tool definition requires a non-empty name");
416
+ }
417
+
418
+ // Validate tool name follows MCP specification guidelines
419
+ if (!ToolRegistry.MCP_TOOL_NAME_REGEX.test(definition.name)) {
420
+ throw new Error(
421
+ `Tool name '${definition.name}' violates MCP guidelines: must be 1-128 chars using only [a-zA-Z0-9_.-]`,
422
+ );
423
+ }
424
+
425
+ if (!definition.description || typeof definition.description !== "string") {
426
+ throw new Error(`Tool '${definition.name}' requires a description`);
427
+ }
428
+
429
+ if (
430
+ !definition.inputSchema ||
431
+ typeof definition.inputSchema.parse !== "function"
432
+ ) {
433
+ throw new Error(`Tool '${definition.name}' requires a valid Zod schema`);
434
+ }
435
+
436
+ if (
437
+ definition.outputSchema &&
438
+ typeof definition.outputSchema.parse !== "function"
439
+ ) {
440
+ throw new Error(
441
+ `Tool '${definition.name}' outputSchema must be a valid Zod schema when provided`,
442
+ );
443
+ }
444
+
445
+ if (typeof definition.handler !== "function") {
446
+ throw new Error(`Tool '${definition.name}' requires a handler function`);
447
+ }
448
+
449
+ if (
450
+ definition.defaultArgumentResolver &&
451
+ typeof definition.defaultArgumentResolver !== "function"
452
+ ) {
453
+ throw new Error(
454
+ `Tool '${definition.name}' defaultArgumentResolver must be a function when provided`,
455
+ );
456
+ }
457
+ }
458
+
459
+ private generateJsonSchema(
460
+ schema: z.ZodTypeAny,
461
+ ioMode: "input" | "output" = "input",
462
+ ): Record<string, unknown> {
463
+ try {
464
+ return toJSONSchema(schema, { target: "draft-2020-12", io: ioMode });
465
+ } catch (error) {
466
+ console.warn(`Failed to generate JSON schema for tool: ${error}`);
467
+ return { type: "object", additionalProperties: true };
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Validates handler output against the tool's output schema if present
473
+ */
474
+ private validateOutput(
475
+ toolName: string,
476
+ output: CallToolResult,
477
+ ): CallToolResult {
478
+ const validator = this.outputValidators.get(toolName);
479
+ if (!validator) {
480
+ // No output schema defined, skip validation
481
+ return output;
482
+ }
483
+
484
+ // Extract the actual data from the CallToolResult
485
+ // CallToolResult is { content: Array<{ type: string, text: string, ... }> }
486
+ // We need to parse the text content and validate it
487
+ if (!output.content || output.content.length === 0) {
488
+ return this.deps.errorHandler.createValidationError(
489
+ `Output validation failed for ${toolName}`,
490
+ "Handler returned empty content",
491
+ ["Ensure the handler returns valid content in the response"],
492
+ );
493
+ }
494
+
495
+ // Validate all content items (not just the first one)
496
+ const invalidItems: { index: number; reason: string }[] = [];
497
+
498
+ for (let i = 0; i < output.content.length; i++) {
499
+ const item = output.content[i];
500
+ if (!item) {
501
+ invalidItems.push({ index: i, reason: "item is null or undefined" });
502
+ } else if (item.type !== "text") {
503
+ invalidItems.push({
504
+ index: i,
505
+ reason: `type is "${item.type}" instead of "text"`,
506
+ });
507
+ } else if (typeof item.text !== "string") {
508
+ invalidItems.push({
509
+ index: i,
510
+ reason: `text property is ${typeof item.text} instead of string`,
511
+ });
512
+ }
513
+ }
514
+
515
+ if (invalidItems.length > 0) {
516
+ const invalidItemsDetails = invalidItems
517
+ .map((inv) => ` - Item ${inv.index}: ${inv.reason}`)
518
+ .join("\n");
519
+
520
+ return this.deps.errorHandler.createValidationError(
521
+ `Output validation failed for ${toolName}`,
522
+ `Handler returned invalid content items (${invalidItems.length} of ${output.content.length} failed):\n${invalidItemsDetails}`,
523
+ ['Ensure all content items have type="text" and a valid text property'],
524
+ );
525
+ }
526
+
527
+ const firstContent = output.content[0];
528
+ if (!firstContent) {
529
+ return this.deps.errorHandler.createValidationError(
530
+ `Output validation failed for ${toolName}`,
531
+ "Handler returned empty content",
532
+ ["Ensure the handler returns valid content in the response"],
533
+ );
534
+ }
535
+ // TypeScript: After validation above, we know firstContent.type === 'text'
536
+ if (firstContent.type !== "text") {
537
+ throw new Error("Unexpected: firstContent is not text after validation");
538
+ }
539
+
540
+ let parsedOutput: unknown;
541
+ try {
542
+ parsedOutput = JSON.parse(firstContent.text);
543
+ } catch (parseError) {
544
+ return this.deps.errorHandler.createValidationError(
545
+ `Output validation failed for ${toolName}`,
546
+ `Invalid JSON in handler output: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
547
+ ["Ensure the handler returns valid JSON"],
548
+ );
549
+ }
550
+
551
+ // Validate against schema
552
+ const result = validator.safeParse(parsedOutput);
553
+ if (!result.success) {
554
+ const validationError = fromZodError(result.error);
555
+ const validationErrors = validationError.message;
556
+ return this.deps.errorHandler.createValidationError(
557
+ `Output validation failed for ${toolName}`,
558
+ `Handler output does not match declared output schema: ${validationErrors}`,
559
+ [
560
+ "Check that the handler returns data matching the output schema",
561
+ "Review the tool definition output schema",
562
+ ],
563
+ );
564
+ }
565
+
566
+ // Validation passed, return original output
567
+ return output;
568
+ }
535
569
  }