@daniel.stefan/metalink 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/README.md +160 -0
  2. package/package.json +96 -0
  3. package/packages/cli/dist/bin/cli.d.ts +3 -0
  4. package/packages/cli/dist/bin/cli.d.ts.map +1 -0
  5. package/packages/cli/dist/bin/cli.js +4 -0
  6. package/packages/cli/dist/bin/cli.js.map +1 -0
  7. package/packages/cli/dist/commands/config/init.d.ts +9 -0
  8. package/packages/cli/dist/commands/config/init.d.ts.map +1 -0
  9. package/packages/cli/dist/commands/config/init.js +38 -0
  10. package/packages/cli/dist/commands/config/init.js.map +1 -0
  11. package/packages/cli/dist/commands/config/validate.d.ts +9 -0
  12. package/packages/cli/dist/commands/config/validate.d.ts.map +1 -0
  13. package/packages/cli/dist/commands/config/validate.js +34 -0
  14. package/packages/cli/dist/commands/config/validate.js.map +1 -0
  15. package/packages/cli/dist/commands/daemon/restart.d.ts +15 -0
  16. package/packages/cli/dist/commands/daemon/restart.d.ts.map +1 -0
  17. package/packages/cli/dist/commands/daemon/restart.js +184 -0
  18. package/packages/cli/dist/commands/daemon/restart.js.map +1 -0
  19. package/packages/cli/dist/commands/daemon/start.d.ts +7 -0
  20. package/packages/cli/dist/commands/daemon/start.d.ts.map +1 -0
  21. package/packages/cli/dist/commands/daemon/start.js +85 -0
  22. package/packages/cli/dist/commands/daemon/start.js.map +1 -0
  23. package/packages/cli/dist/commands/daemon/status.d.ts +7 -0
  24. package/packages/cli/dist/commands/daemon/status.d.ts.map +1 -0
  25. package/packages/cli/dist/commands/daemon/status.js +69 -0
  26. package/packages/cli/dist/commands/daemon/status.js.map +1 -0
  27. package/packages/cli/dist/commands/daemon/stop.d.ts +8 -0
  28. package/packages/cli/dist/commands/daemon/stop.d.ts.map +1 -0
  29. package/packages/cli/dist/commands/daemon/stop.js +77 -0
  30. package/packages/cli/dist/commands/daemon/stop.js.map +1 -0
  31. package/packages/cli/dist/commands/import/mcpm.d.ts +10 -0
  32. package/packages/cli/dist/commands/import/mcpm.d.ts.map +1 -0
  33. package/packages/cli/dist/commands/import/mcpm.js +58 -0
  34. package/packages/cli/dist/commands/import/mcpm.js.map +1 -0
  35. package/packages/cli/dist/commands/safety/add-risky-pattern.d.ts +16 -0
  36. package/packages/cli/dist/commands/safety/add-risky-pattern.d.ts.map +1 -0
  37. package/packages/cli/dist/commands/safety/add-risky-pattern.js +72 -0
  38. package/packages/cli/dist/commands/safety/add-risky-pattern.js.map +1 -0
  39. package/packages/cli/dist/commands/safety/add-risky.d.ts +12 -0
  40. package/packages/cli/dist/commands/safety/add-risky.d.ts.map +1 -0
  41. package/packages/cli/dist/commands/safety/add-risky.js +52 -0
  42. package/packages/cli/dist/commands/safety/add-risky.js.map +1 -0
  43. package/packages/cli/dist/commands/safety/add-safe-pattern.d.ts +16 -0
  44. package/packages/cli/dist/commands/safety/add-safe-pattern.d.ts.map +1 -0
  45. package/packages/cli/dist/commands/safety/add-safe-pattern.js +72 -0
  46. package/packages/cli/dist/commands/safety/add-safe-pattern.js.map +1 -0
  47. package/packages/cli/dist/commands/safety/add-safe.d.ts +12 -0
  48. package/packages/cli/dist/commands/safety/add-safe.d.ts.map +1 -0
  49. package/packages/cli/dist/commands/safety/add-safe.js +52 -0
  50. package/packages/cli/dist/commands/safety/add-safe.js.map +1 -0
  51. package/packages/cli/dist/commands/safety/check.d.ts +9 -0
  52. package/packages/cli/dist/commands/safety/check.d.ts.map +1 -0
  53. package/packages/cli/dist/commands/safety/check.js +36 -0
  54. package/packages/cli/dist/commands/safety/check.js.map +1 -0
  55. package/packages/cli/dist/commands/safety/export.d.ts +10 -0
  56. package/packages/cli/dist/commands/safety/export.d.ts.map +1 -0
  57. package/packages/cli/dist/commands/safety/export.js +48 -0
  58. package/packages/cli/dist/commands/safety/export.js.map +1 -0
  59. package/packages/cli/dist/commands/safety/import.d.ts +12 -0
  60. package/packages/cli/dist/commands/safety/import.d.ts.map +1 -0
  61. package/packages/cli/dist/commands/safety/import.js +78 -0
  62. package/packages/cli/dist/commands/safety/import.js.map +1 -0
  63. package/packages/cli/dist/commands/safety/index.d.ts +8 -0
  64. package/packages/cli/dist/commands/safety/index.d.ts.map +1 -0
  65. package/packages/cli/dist/commands/safety/index.js +46 -0
  66. package/packages/cli/dist/commands/safety/index.js.map +1 -0
  67. package/packages/cli/dist/commands/safety/list.d.ts +6 -0
  68. package/packages/cli/dist/commands/safety/list.d.ts.map +1 -0
  69. package/packages/cli/dist/commands/safety/list.js +77 -0
  70. package/packages/cli/dist/commands/safety/list.js.map +1 -0
  71. package/packages/cli/dist/commands/safety/remove.d.ts +9 -0
  72. package/packages/cli/dist/commands/safety/remove.d.ts.map +1 -0
  73. package/packages/cli/dist/commands/safety/remove.js +46 -0
  74. package/packages/cli/dist/commands/safety/remove.js.map +1 -0
  75. package/packages/cli/dist/commands/safety/reset.d.ts +9 -0
  76. package/packages/cli/dist/commands/safety/reset.d.ts.map +1 -0
  77. package/packages/cli/dist/commands/safety/reset.js +46 -0
  78. package/packages/cli/dist/commands/safety/reset.js.map +1 -0
  79. package/packages/cli/dist/commands/safety/validate.d.ts +9 -0
  80. package/packages/cli/dist/commands/safety/validate.d.ts.map +1 -0
  81. package/packages/cli/dist/commands/safety/validate.js +51 -0
  82. package/packages/cli/dist/commands/safety/validate.js.map +1 -0
  83. package/packages/cli/dist/commands/secret/get.d.ts +9 -0
  84. package/packages/cli/dist/commands/secret/get.d.ts.map +1 -0
  85. package/packages/cli/dist/commands/secret/get.js +26 -0
  86. package/packages/cli/dist/commands/secret/get.js.map +1 -0
  87. package/packages/cli/dist/commands/secret/set.d.ts +10 -0
  88. package/packages/cli/dist/commands/secret/set.d.ts.map +1 -0
  89. package/packages/cli/dist/commands/secret/set.js +22 -0
  90. package/packages/cli/dist/commands/secret/set.js.map +1 -0
  91. package/packages/cli/dist/commands/server/__tests__/server-management-e2e.test.d.ts +2 -0
  92. package/packages/cli/dist/commands/server/__tests__/server-management-e2e.test.d.ts.map +1 -0
  93. package/packages/cli/dist/commands/server/__tests__/server-management-e2e.test.js +234 -0
  94. package/packages/cli/dist/commands/server/__tests__/server-management-e2e.test.js.map +1 -0
  95. package/packages/cli/dist/commands/server/add.d.ts +14 -0
  96. package/packages/cli/dist/commands/server/add.d.ts.map +1 -0
  97. package/packages/cli/dist/commands/server/add.js +86 -0
  98. package/packages/cli/dist/commands/server/add.js.map +1 -0
  99. package/packages/cli/dist/commands/server/available.d.ts +10 -0
  100. package/packages/cli/dist/commands/server/available.d.ts.map +1 -0
  101. package/packages/cli/dist/commands/server/available.js +62 -0
  102. package/packages/cli/dist/commands/server/available.js.map +1 -0
  103. package/packages/cli/dist/commands/server/debug.d.ts +18 -0
  104. package/packages/cli/dist/commands/server/debug.d.ts.map +1 -0
  105. package/packages/cli/dist/commands/server/debug.js +165 -0
  106. package/packages/cli/dist/commands/server/debug.js.map +1 -0
  107. package/packages/cli/dist/commands/server/info.d.ts +13 -0
  108. package/packages/cli/dist/commands/server/info.d.ts.map +1 -0
  109. package/packages/cli/dist/commands/server/info.js +62 -0
  110. package/packages/cli/dist/commands/server/info.js.map +1 -0
  111. package/packages/cli/dist/commands/server/list.d.ts +10 -0
  112. package/packages/cli/dist/commands/server/list.d.ts.map +1 -0
  113. package/packages/cli/dist/commands/server/list.js +105 -0
  114. package/packages/cli/dist/commands/server/list.js.map +1 -0
  115. package/packages/cli/dist/commands/server/refresh-tools.d.ts +13 -0
  116. package/packages/cli/dist/commands/server/refresh-tools.d.ts.map +1 -0
  117. package/packages/cli/dist/commands/server/refresh-tools.js +46 -0
  118. package/packages/cli/dist/commands/server/refresh-tools.js.map +1 -0
  119. package/packages/cli/dist/commands/server/remove.d.ts +12 -0
  120. package/packages/cli/dist/commands/server/remove.d.ts.map +1 -0
  121. package/packages/cli/dist/commands/server/remove.js +39 -0
  122. package/packages/cli/dist/commands/server/remove.js.map +1 -0
  123. package/packages/cli/dist/commands/server/restart.d.ts +9 -0
  124. package/packages/cli/dist/commands/server/restart.d.ts.map +1 -0
  125. package/packages/cli/dist/commands/server/restart.js +30 -0
  126. package/packages/cli/dist/commands/server/restart.js.map +1 -0
  127. package/packages/cli/dist/commands/server/start.d.ts +9 -0
  128. package/packages/cli/dist/commands/server/start.d.ts.map +1 -0
  129. package/packages/cli/dist/commands/server/start.js +37 -0
  130. package/packages/cli/dist/commands/server/start.js.map +1 -0
  131. package/packages/cli/dist/commands/server/status.d.ts +9 -0
  132. package/packages/cli/dist/commands/server/status.d.ts.map +1 -0
  133. package/packages/cli/dist/commands/server/status.js +30 -0
  134. package/packages/cli/dist/commands/server/status.js.map +1 -0
  135. package/packages/cli/dist/commands/server/stop.d.ts +9 -0
  136. package/packages/cli/dist/commands/server/stop.d.ts.map +1 -0
  137. package/packages/cli/dist/commands/server/stop.js +31 -0
  138. package/packages/cli/dist/commands/server/stop.js.map +1 -0
  139. package/packages/cli/dist/commands/server/validate.d.ts +15 -0
  140. package/packages/cli/dist/commands/server/validate.d.ts.map +1 -0
  141. package/packages/cli/dist/commands/server/validate.js +87 -0
  142. package/packages/cli/dist/commands/server/validate.js.map +1 -0
  143. package/packages/cli/dist/commands/stdio.d.ts +36 -0
  144. package/packages/cli/dist/commands/stdio.d.ts.map +1 -0
  145. package/packages/cli/dist/commands/stdio.js +85 -0
  146. package/packages/cli/dist/commands/stdio.js.map +1 -0
  147. package/packages/cli/dist/commands/tool/execute-confirm.d.ts +12 -0
  148. package/packages/cli/dist/commands/tool/execute-confirm.d.ts.map +1 -0
  149. package/packages/cli/dist/commands/tool/execute-confirm.js +98 -0
  150. package/packages/cli/dist/commands/tool/execute-confirm.js.map +1 -0
  151. package/packages/cli/dist/commands/tool/execute.d.ts +12 -0
  152. package/packages/cli/dist/commands/tool/execute.d.ts.map +1 -0
  153. package/packages/cli/dist/commands/tool/execute.js +55 -0
  154. package/packages/cli/dist/commands/tool/execute.js.map +1 -0
  155. package/packages/cli/dist/commands/tool/list.d.ts +13 -0
  156. package/packages/cli/dist/commands/tool/list.d.ts.map +1 -0
  157. package/packages/cli/dist/commands/tool/list.js +91 -0
  158. package/packages/cli/dist/commands/tool/list.js.map +1 -0
  159. package/packages/cli/dist/commands/tool/search.d.ts +12 -0
  160. package/packages/cli/dist/commands/tool/search.d.ts.map +1 -0
  161. package/packages/cli/dist/commands/tool/search.js +87 -0
  162. package/packages/cli/dist/commands/tool/search.js.map +1 -0
  163. package/packages/cli/dist/index.d.ts +2 -0
  164. package/packages/cli/dist/index.d.ts.map +1 -0
  165. package/packages/cli/dist/index.js +9 -0
  166. package/packages/cli/dist/index.js.map +1 -0
  167. package/packages/cli/dist/utils/daemon-checker.d.ts +18 -0
  168. package/packages/cli/dist/utils/daemon-checker.d.ts.map +1 -0
  169. package/packages/cli/dist/utils/daemon-checker.js +69 -0
  170. package/packages/cli/dist/utils/daemon-checker.js.map +1 -0
  171. package/packages/cli/dist/utils/daemon-endpoint.d.ts +7 -0
  172. package/packages/cli/dist/utils/daemon-endpoint.d.ts.map +1 -0
  173. package/packages/cli/dist/utils/daemon-endpoint.js +13 -0
  174. package/packages/cli/dist/utils/daemon-endpoint.js.map +1 -0
  175. package/packages/cli/dist/utils/get-configured-port.d.ts +43 -0
  176. package/packages/cli/dist/utils/get-configured-port.d.ts.map +1 -0
  177. package/packages/cli/dist/utils/get-configured-port.js +141 -0
  178. package/packages/cli/dist/utils/get-configured-port.js.map +1 -0
  179. package/packages/cli/dist/utils/stdio-bridge.d.ts +48 -0
  180. package/packages/cli/dist/utils/stdio-bridge.d.ts.map +1 -0
  181. package/packages/cli/dist/utils/stdio-bridge.js +181 -0
  182. package/packages/cli/dist/utils/stdio-bridge.js.map +1 -0
  183. package/packages/cli/package.json +48 -0
  184. package/packages/core/dist/config/defaults.d.ts +36 -0
  185. package/packages/core/dist/config/defaults.d.ts.map +1 -0
  186. package/packages/core/dist/config/defaults.js +324 -0
  187. package/packages/core/dist/config/defaults.js.map +1 -0
  188. package/packages/core/dist/config/index.d.ts +9 -0
  189. package/packages/core/dist/config/index.d.ts.map +1 -0
  190. package/packages/core/dist/config/index.js +14 -0
  191. package/packages/core/dist/config/index.js.map +1 -0
  192. package/packages/core/dist/config/loader.d.ts +269 -0
  193. package/packages/core/dist/config/loader.d.ts.map +1 -0
  194. package/packages/core/dist/config/loader.js +777 -0
  195. package/packages/core/dist/config/loader.js.map +1 -0
  196. package/packages/core/dist/config/registry.d.ts +212 -0
  197. package/packages/core/dist/config/registry.d.ts.map +1 -0
  198. package/packages/core/dist/config/registry.js +754 -0
  199. package/packages/core/dist/config/registry.js.map +1 -0
  200. package/packages/core/dist/config/schema.d.ts +4352 -0
  201. package/packages/core/dist/config/schema.d.ts.map +1 -0
  202. package/packages/core/dist/config/schema.js +267 -0
  203. package/packages/core/dist/config/schema.js.map +1 -0
  204. package/packages/core/dist/daemon.d.ts +7 -0
  205. package/packages/core/dist/daemon.d.ts.map +1 -0
  206. package/packages/core/dist/daemon.js +116 -0
  207. package/packages/core/dist/daemon.js.map +1 -0
  208. package/packages/core/dist/http-client-retry.d.ts +67 -0
  209. package/packages/core/dist/http-client-retry.d.ts.map +1 -0
  210. package/packages/core/dist/http-client-retry.js +133 -0
  211. package/packages/core/dist/http-client-retry.js.map +1 -0
  212. package/packages/core/dist/http-client-updated.d.ts +147 -0
  213. package/packages/core/dist/http-client-updated.d.ts.map +1 -0
  214. package/packages/core/dist/http-client-updated.js +452 -0
  215. package/packages/core/dist/http-client-updated.js.map +1 -0
  216. package/packages/core/dist/http-client.d.ts +207 -0
  217. package/packages/core/dist/http-client.d.ts.map +1 -0
  218. package/packages/core/dist/http-client.js +704 -0
  219. package/packages/core/dist/http-client.js.map +1 -0
  220. package/packages/core/dist/index.d.ts +13 -0
  221. package/packages/core/dist/index.d.ts.map +1 -0
  222. package/packages/core/dist/index.js +23 -0
  223. package/packages/core/dist/index.js.map +1 -0
  224. package/packages/core/dist/logging/index.d.ts +46 -0
  225. package/packages/core/dist/logging/index.d.ts.map +1 -0
  226. package/packages/core/dist/logging/index.js +74 -0
  227. package/packages/core/dist/logging/index.js.map +1 -0
  228. package/packages/core/dist/metrics/index.d.ts +339 -0
  229. package/packages/core/dist/metrics/index.d.ts.map +1 -0
  230. package/packages/core/dist/metrics/index.js +792 -0
  231. package/packages/core/dist/metrics/index.js.map +1 -0
  232. package/packages/core/dist/plugins/index.d.ts +49 -0
  233. package/packages/core/dist/plugins/index.d.ts.map +1 -0
  234. package/packages/core/dist/plugins/index.js +82 -0
  235. package/packages/core/dist/plugins/index.js.map +1 -0
  236. package/packages/core/dist/secrets/index.d.ts +6 -0
  237. package/packages/core/dist/secrets/index.d.ts.map +1 -0
  238. package/packages/core/dist/secrets/index.js +5 -0
  239. package/packages/core/dist/secrets/index.js.map +1 -0
  240. package/packages/core/dist/secrets/keyring.d.ts +54 -0
  241. package/packages/core/dist/secrets/keyring.d.ts.map +1 -0
  242. package/packages/core/dist/secrets/keyring.js +141 -0
  243. package/packages/core/dist/secrets/keyring.js.map +1 -0
  244. package/packages/core/dist/server/batch-executor.d.ts +83 -0
  245. package/packages/core/dist/server/batch-executor.d.ts.map +1 -0
  246. package/packages/core/dist/server/batch-executor.js +291 -0
  247. package/packages/core/dist/server/batch-executor.js.map +1 -0
  248. package/packages/core/dist/server/circuit-breaker.d.ts +215 -0
  249. package/packages/core/dist/server/circuit-breaker.d.ts.map +1 -0
  250. package/packages/core/dist/server/circuit-breaker.js +330 -0
  251. package/packages/core/dist/server/circuit-breaker.js.map +1 -0
  252. package/packages/core/dist/server/client-detection.d.ts +40 -0
  253. package/packages/core/dist/server/client-detection.d.ts.map +1 -0
  254. package/packages/core/dist/server/client-detection.js +242 -0
  255. package/packages/core/dist/server/client-detection.js.map +1 -0
  256. package/packages/core/dist/server/client-profiles.d.ts +102 -0
  257. package/packages/core/dist/server/client-profiles.d.ts.map +1 -0
  258. package/packages/core/dist/server/client-profiles.js +254 -0
  259. package/packages/core/dist/server/client-profiles.js.map +1 -0
  260. package/packages/core/dist/server/http.d.ts +386 -0
  261. package/packages/core/dist/server/http.d.ts.map +1 -0
  262. package/packages/core/dist/server/http.js +4253 -0
  263. package/packages/core/dist/server/http.js.map +1 -0
  264. package/packages/core/dist/server/index.d.ts +7 -0
  265. package/packages/core/dist/server/index.d.ts.map +1 -0
  266. package/packages/core/dist/server/index.js +6 -0
  267. package/packages/core/dist/server/index.js.map +1 -0
  268. package/packages/core/dist/server/manager.d.ts +458 -0
  269. package/packages/core/dist/server/manager.d.ts.map +1 -0
  270. package/packages/core/dist/server/manager.js +3255 -0
  271. package/packages/core/dist/server/manager.js.map +1 -0
  272. package/packages/core/dist/server/managers/HttpConnectionManager.d.ts +69 -0
  273. package/packages/core/dist/server/managers/HttpConnectionManager.d.ts.map +1 -0
  274. package/packages/core/dist/server/managers/HttpConnectionManager.js +214 -0
  275. package/packages/core/dist/server/managers/HttpConnectionManager.js.map +1 -0
  276. package/packages/core/dist/server/managers/ProcessManager.d.ts +128 -0
  277. package/packages/core/dist/server/managers/ProcessManager.d.ts.map +1 -0
  278. package/packages/core/dist/server/managers/ProcessManager.js +443 -0
  279. package/packages/core/dist/server/managers/ProcessManager.js.map +1 -0
  280. package/packages/core/dist/server/managers/SchemaCacheManager.d.ts +152 -0
  281. package/packages/core/dist/server/managers/SchemaCacheManager.d.ts.map +1 -0
  282. package/packages/core/dist/server/managers/SchemaCacheManager.js +426 -0
  283. package/packages/core/dist/server/managers/SchemaCacheManager.js.map +1 -0
  284. package/packages/core/dist/server/managers/index.d.ts +9 -0
  285. package/packages/core/dist/server/managers/index.d.ts.map +1 -0
  286. package/packages/core/dist/server/managers/index.js +9 -0
  287. package/packages/core/dist/server/managers/index.js.map +1 -0
  288. package/packages/core/dist/server/metrics.d.ts +134 -0
  289. package/packages/core/dist/server/metrics.d.ts.map +1 -0
  290. package/packages/core/dist/server/metrics.js +273 -0
  291. package/packages/core/dist/server/metrics.js.map +1 -0
  292. package/packages/core/dist/server/prompts.d.ts +58 -0
  293. package/packages/core/dist/server/prompts.d.ts.map +1 -0
  294. package/packages/core/dist/server/prompts.js +405 -0
  295. package/packages/core/dist/server/prompts.js.map +1 -0
  296. package/packages/core/dist/server/protocol-versions.d.ts +49 -0
  297. package/packages/core/dist/server/protocol-versions.d.ts.map +1 -0
  298. package/packages/core/dist/server/protocol-versions.js +173 -0
  299. package/packages/core/dist/server/protocol-versions.js.map +1 -0
  300. package/packages/core/dist/server/resources.d.ts +64 -0
  301. package/packages/core/dist/server/resources.d.ts.map +1 -0
  302. package/packages/core/dist/server/resources.js +243 -0
  303. package/packages/core/dist/server/resources.js.map +1 -0
  304. package/packages/core/dist/server/schema-store.d.ts +84 -0
  305. package/packages/core/dist/server/schema-store.d.ts.map +1 -0
  306. package/packages/core/dist/server/schema-store.js +234 -0
  307. package/packages/core/dist/server/schema-store.js.map +1 -0
  308. package/packages/core/dist/server/schema-validator.d.ts +51 -0
  309. package/packages/core/dist/server/schema-validator.d.ts.map +1 -0
  310. package/packages/core/dist/server/schema-validator.js +208 -0
  311. package/packages/core/dist/server/schema-validator.js.map +1 -0
  312. package/packages/core/dist/server/token-calculator.d.ts +44 -0
  313. package/packages/core/dist/server/token-calculator.d.ts.map +1 -0
  314. package/packages/core/dist/server/token-calculator.js +53 -0
  315. package/packages/core/dist/server/token-calculator.js.map +1 -0
  316. package/packages/core/dist/server/types.d.ts +45 -0
  317. package/packages/core/dist/server/types.d.ts.map +1 -0
  318. package/packages/core/dist/server/types.js +5 -0
  319. package/packages/core/dist/server/types.js.map +1 -0
  320. package/packages/core/dist/utils/file-lock.d.ts +73 -0
  321. package/packages/core/dist/utils/file-lock.d.ts.map +1 -0
  322. package/packages/core/dist/utils/file-lock.js +235 -0
  323. package/packages/core/dist/utils/file-lock.js.map +1 -0
  324. package/packages/core/package.json +36 -0
  325. package/packages/dashboard/dist/assets/index-B7hvkCMu.css +1 -0
  326. package/packages/dashboard/dist/assets/index-yZhLPpzr.js +1 -0
  327. package/packages/dashboard/dist/index.html +14 -0
  328. package/packages/dashboard/package.json +24 -0
  329. package/packages/shared/dist/version.d.ts +2 -0
  330. package/packages/shared/dist/version.d.ts.map +1 -0
  331. package/packages/shared/dist/version.js +2 -0
  332. package/packages/shared/dist/version.js.map +1 -0
  333. package/packages/shared/package.json +18 -0
@@ -0,0 +1,4253 @@
1
+ /**
2
+ * HTTP Server - Express-based API for MetaLink
3
+ */
4
+ import express from 'express';
5
+ import rateLimit from 'express-rate-limit';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import fs from 'fs';
9
+ import { randomUUID, createHmac } from 'crypto';
10
+ import { ServerManager } from './manager.js';
11
+ import { version } from "../index.js";
12
+ import { calculateTotalTokens } from './token-calculator.js';
13
+ import { globalMetrics, MetricsPersistence, MetricsAggregator } from '../metrics/index.js';
14
+ import { logger, generateRequestId, getOrCreateRequestId } from '../logging/index.js';
15
+ import { getPromptsList, getPrompt } from './prompts.js';
16
+ import { getResourcesList, getResourceTemplatesList, readResource } from './resources.js';
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ // MCP Protocol Version - MUST be set in all HTTP responses per spec 2025-06-18
19
+ const MCP_PROTOCOL_VERSION = '2025-06-18';
20
+ // Custom error class for invalid parameters (JSON-RPC error code -32602)
21
+ class InvalidParamsError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'InvalidParamsError';
25
+ }
26
+ }
27
+ // Custom error class for method not found (JSON-RPC error code -32601)
28
+ class MethodNotFoundError extends Error {
29
+ constructor(method) {
30
+ super(`Method not found: ${method}`);
31
+ this.name = 'MethodNotFoundError';
32
+ }
33
+ }
34
+ // Custom error class for session not initialized (triggers HTTP 404 for MCP spec compliance)
35
+ // Per MCP spec, clients MUST reinitialize when receiving 404 for invalid session
36
+ // Using HTTP 404 allows existing client auto-reinitialize handlers to work
37
+ class SessionNotInitializedError extends Error {
38
+ constructor(method) {
39
+ super(`Session must be initialized before calling ${method}. Call initialize first.`);
40
+ this.name = 'SessionNotInitializedError';
41
+ }
42
+ }
43
+ export class HttpServer {
44
+ constructor(configLoader) {
45
+ this.server = null; // HTTP server instance (keeps event loop alive)
46
+ this.eventClients = new Set();
47
+ // Streamable HTTP session management
48
+ this.sessions = new Map();
49
+ this.SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours (security best practice)
50
+ this.sessionCleanupInterval = null;
51
+ // SSE connections for bidirectional communication
52
+ this.sseConnections = new Map();
53
+ // SSE event tracking for Last-Event-ID resumption (per MCP spec 2025-06-18)
54
+ this.sseEventBuffer = new Map(); // sessionId -> events
55
+ this.sseEventCounter = 0;
56
+ this.MAX_EVENTS_PER_SESSION = 100; // Keep last 100 events for resumption
57
+ // SECURITY: Per-server tool execution rate limiting (OWASP DOS Prevention)
58
+ // Prevents denial-of-service attacks by limiting tool calls per server
59
+ this.toolExecutionRateLimiter = new Map();
60
+ this.TOOL_RATE_LIMIT_WINDOW_MS = 60000; // 1 minute window
61
+ this.TOOL_RATE_LIMIT_MAX_CALLS = 100; // Max 100 calls per server per minute
62
+ // SECURITY: Discovery endpoint rate limiting (P1 - OWASP DOS Prevention)
63
+ // Prevents abuse of search_tools and describe_tool endpoints
64
+ this.discoveryRateLimiter = new Map();
65
+ this.DISCOVERY_RATE_LIMIT_WINDOW_MS = 60000; // 1 minute window
66
+ this.DISCOVERY_RATE_LIMIT_MAX_CALLS = 200; // Max 200 discovery calls per session per minute
67
+ // Daemon uptime tracking
68
+ this.startTime = Date.now();
69
+ // === Session Persistence (v1.1.24+) ===
70
+ /**
71
+ * Session persistence file format
72
+ */
73
+ this.SESSION_PERSIST_VERSION = 1;
74
+ /**
75
+ * Send notifications/tools/list_changed to all connected SSE clients
76
+ * Throttled to max 1 notification per second
77
+ */
78
+ this.lastToolsListNotification = 0;
79
+ this.TOOLS_LIST_NOTIFICATION_THROTTLE_MS = 1000;
80
+ this.app = express();
81
+ this.configLoader = configLoader;
82
+ this.config = configLoader.getConfig();
83
+ // Pass config with schemasCacheTTL to ServerManager
84
+ this.serverManager = new ServerManager(this.config);
85
+ // v1.4.0: Register callback for discovery expiration notifications
86
+ this.serverManager.setToolsListChangedCallback(() => {
87
+ this.notifyToolsListChanged();
88
+ });
89
+ // v1.3.72: Enable proactive schema caching by providing server list
90
+ this.serverManager.setServerListProvider(() => this.configLoader.getServers());
91
+ // Phase 2: Set config loader for tool safety classification
92
+ this.serverManager.setConfigLoader(this.configLoader);
93
+ // Phase 4 - v1.4.0: Initialize metrics persistence and aggregation
94
+ const metricsDir = process.env.METALINK_METRICS_DIR || '~/.config/metalink';
95
+ this.metricsPersistence = new MetricsPersistence(metricsDir);
96
+ this.metricsAggregator = new MetricsAggregator();
97
+ // Session persistence path (v1.1.24+)
98
+ const configDir = (process.env.METALINK_CONFIG_DIR || '~/.config/metalink').replace(/^~/, process.env.HOME || '~');
99
+ this.sessionPersistPath = path.join(configDir, 'sessions.json');
100
+ // Load persisted metrics on startup
101
+ this.loadPersistedMetrics();
102
+ // Load persisted sessions on startup (v1.1.24+)
103
+ this.loadSessions();
104
+ this.setupMiddleware();
105
+ this.setupRoutes();
106
+ this.setupSSE();
107
+ // Start session cleanup for Streamable HTTP
108
+ this.startSessionCleanup();
109
+ // Start periodic metrics persistence (Phase 4 - v1.4.0)
110
+ this.startMetricsPersistence();
111
+ }
112
+ /**
113
+ * Get server manager instance
114
+ */
115
+ getServerManager() {
116
+ return this.serverManager;
117
+ }
118
+ /**
119
+ * Pagination support - Encode cursor data to opaque base64 string
120
+ */
121
+ encodeCursor(data) {
122
+ return Buffer.from(JSON.stringify(data)).toString('base64');
123
+ }
124
+ /**
125
+ * Pagination support - Decode cursor from base64 string
126
+ */
127
+ decodeCursor(cursor) {
128
+ try {
129
+ const decoded = Buffer.from(cursor, 'base64').toString('utf-8');
130
+ return JSON.parse(decoded);
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ }
136
+ /**
137
+ * Pagination support - Truncate string to character limit
138
+ */
139
+ truncateToChars(value, maxChars) {
140
+ const json = JSON.stringify(value);
141
+ if (json.length <= maxChars) {
142
+ return value;
143
+ }
144
+ // Truncate and add indicator
145
+ const truncated = json.substring(0, maxChars - 20) + '... [truncated]';
146
+ try {
147
+ return JSON.parse(truncated);
148
+ }
149
+ catch {
150
+ // If truncated JSON is invalid, return string
151
+ return truncated;
152
+ }
153
+ }
154
+ /**
155
+ * Pagination support - Apply pagination to tool result
156
+ *
157
+ * @param result - Tool execution result (any type)
158
+ * @param params - Pagination parameters from request
159
+ * @returns Paginated result with metadata
160
+ */
161
+ paginateResult(result, params) {
162
+ const DEFAULT_MAX_CHARS = 50000; // 50KB default limit per MCP spec
163
+ const maxChars = params.max_result_chars ?? DEFAULT_MAX_CHARS;
164
+ // Handle cursor continuation
165
+ let startOffset = 0;
166
+ if (params.cursor) {
167
+ const cursorData = this.decodeCursor(params.cursor);
168
+ if (cursorData) {
169
+ startOffset = cursorData.offset;
170
+ }
171
+ }
172
+ // Check if result is an array and apply max_results
173
+ if (Array.isArray(result) && params.max_results) {
174
+ const totalAvailable = result.length;
175
+ const endOffset = startOffset + params.max_results;
176
+ if (endOffset < totalAvailable) {
177
+ // More results available - create cursor
178
+ const nextCursor = this.encodeCursor({
179
+ offset: endOffset,
180
+ type: 'array'
181
+ });
182
+ return {
183
+ result: result.slice(startOffset, endOffset),
184
+ _pagination: {
185
+ nextCursor,
186
+ totalAvailable,
187
+ truncated: true
188
+ }
189
+ };
190
+ }
191
+ else if (startOffset > 0) {
192
+ // Last page
193
+ return {
194
+ result: result.slice(startOffset),
195
+ _pagination: {
196
+ totalAvailable,
197
+ truncated: false
198
+ }
199
+ };
200
+ }
201
+ }
202
+ // Check character limit
203
+ const json = JSON.stringify(result);
204
+ if (json.length > maxChars) {
205
+ // Generate cursor for continuation
206
+ const nextCursor = this.encodeCursor({
207
+ offset: maxChars,
208
+ type: 'chars'
209
+ });
210
+ return {
211
+ result: this.truncateToChars(result, maxChars),
212
+ _pagination: {
213
+ nextCursor,
214
+ totalAvailable: json.length,
215
+ truncated: true
216
+ }
217
+ };
218
+ }
219
+ // No pagination needed
220
+ return {
221
+ result,
222
+ _pagination: {
223
+ truncated: false
224
+ }
225
+ };
226
+ }
227
+ /**
228
+ * Setup Express middleware
229
+ */
230
+ setupMiddleware() {
231
+ // Request ID middleware - generate or extract correlation ID
232
+ this.app.use((req, _res, next) => {
233
+ const requestId = getOrCreateRequestId(req.headers['x-request-id']);
234
+ req.requestId = requestId;
235
+ next();
236
+ });
237
+ // Debug middleware - structured logging for all requests
238
+ this.app.use((req, _res, next) => {
239
+ const requestId = req.requestId;
240
+ logger.debug('Incoming request', {
241
+ requestId,
242
+ method: req.method,
243
+ path: req.path,
244
+ url: req.url,
245
+ userAgent: req.headers['user-agent'],
246
+ });
247
+ next();
248
+ });
249
+ // Metrics middleware (Phase 4 - v1.4.0)
250
+ this.app.use((req, res, next) => {
251
+ const startTime = Date.now();
252
+ const requestId = req.requestId;
253
+ globalMetrics.recordApiRequest();
254
+ res.on('finish', () => {
255
+ const latency = Date.now() - startTime;
256
+ globalMetrics.recordApiResponse(latency);
257
+ if (res.statusCode >= 400) {
258
+ globalMetrics.recordApiError();
259
+ }
260
+ // Log response with correlation ID
261
+ logger.info('Request completed', {
262
+ requestId,
263
+ method: req.method,
264
+ path: req.path,
265
+ status: res.statusCode,
266
+ duration_ms: latency,
267
+ });
268
+ });
269
+ next();
270
+ });
271
+ // Origin header validation to prevent DNS rebinding attacks
272
+ this.app.use((req, res, next) => {
273
+ // Only validate POST requests (where state changes can occur)
274
+ if (req.method === 'POST') {
275
+ const origin = req.headers.origin;
276
+ const requestId = req.requestId;
277
+ // Allow requests with no Origin header (non-browser clients, Electron apps)
278
+ if (!origin) {
279
+ next();
280
+ return;
281
+ }
282
+ // Parse origin and validate
283
+ try {
284
+ const originUrl = new URL(origin);
285
+ const hostname = originUrl.hostname;
286
+ // Allow localhost, 127.0.0.1, and IPv6 loopback
287
+ const allowedHosts = ['localhost', '127.0.0.1', '[::1]', '::1'];
288
+ if (!allowedHosts.includes(hostname)) {
289
+ logger.warn('Rejected request from unauthorized origin', {
290
+ requestId,
291
+ origin,
292
+ hostname,
293
+ security: 'dns_rebinding_protection',
294
+ });
295
+ res.status(403).json({
296
+ jsonrpc: '2.0',
297
+ error: {
298
+ code: -32600,
299
+ message: 'Invalid Request: Origin not allowed'
300
+ }
301
+ });
302
+ return;
303
+ }
304
+ }
305
+ catch (err) {
306
+ // Invalid origin URL
307
+ logger.warn('Rejected request with malformed origin', {
308
+ requestId,
309
+ origin,
310
+ error: err instanceof Error ? err.message : 'Unknown error',
311
+ security: 'dns_rebinding_protection',
312
+ });
313
+ res.status(403).json({
314
+ jsonrpc: '2.0',
315
+ error: {
316
+ code: -32600,
317
+ message: 'Invalid Request: Malformed origin'
318
+ }
319
+ });
320
+ return;
321
+ }
322
+ }
323
+ next();
324
+ });
325
+ // Raw body parser for MCP endpoints (must come before JSON parser)
326
+ this.app.use('/mcp', express.raw({ type: 'application/json' }));
327
+ this.app.use('/mcp/rpc', express.raw({ type: 'application/json' }));
328
+ this.app.use('/rpc', express.raw({ type: 'application/json' }));
329
+ this.app.use('/rpc/rpc', express.raw({ type: 'application/json' }));
330
+ // JSON body parser for other endpoints
331
+ this.app.use(express.json());
332
+ // Rate limiting
333
+ const daemonConfig = this.config.daemon;
334
+ if (daemonConfig && daemonConfig.rateLimit && daemonConfig.rateLimit.enabled) {
335
+ const limiter = rateLimit({
336
+ windowMs: daemonConfig.rateLimit.windowMs || 60000,
337
+ max: daemonConfig.rateLimit.max || 500,
338
+ keyGenerator: (req) => {
339
+ if (daemonConfig.rateLimit && daemonConfig.rateLimit.keyGenerator === 'user') {
340
+ return req.userId || req.ip || '';
341
+ }
342
+ return req.ip || '';
343
+ },
344
+ });
345
+ this.app.use(limiter);
346
+ }
347
+ // Authentication middleware (if enabled)
348
+ if (daemonConfig && daemonConfig.auth && daemonConfig.auth.enabled) {
349
+ this.app.use(this.authMiddleware.bind(this));
350
+ }
351
+ // Request logging
352
+ this.app.use((req, res, next) => {
353
+ const start = Date.now();
354
+ res.on('finish', () => {
355
+ const duration = Date.now() - start;
356
+ console.log(`[${req.method}] ${req.path} ${res.statusCode} ${duration}ms`);
357
+ });
358
+ next();
359
+ });
360
+ }
361
+ /**
362
+ * Authentication middleware
363
+ */
364
+ authMiddleware(req, res, next) {
365
+ const authHeader = req.headers.authorization;
366
+ if (!authHeader) {
367
+ res.status(401).json({ error: 'Missing Authorization header' });
368
+ return;
369
+ }
370
+ const [scheme] = authHeader.split(' ');
371
+ if (scheme !== 'Bearer') {
372
+ res.status(401).json({ error: 'Invalid Authorization scheme' });
373
+ return;
374
+ }
375
+ const token = authHeader.substring(7);
376
+ const secret = this.config?.daemon?.auth?.secret;
377
+ if (!secret) {
378
+ console.warn('[Auth] No auth secret configured, rejecting request');
379
+ res.status(500).json({ error: 'Auth not configured' });
380
+ return;
381
+ }
382
+ const expectedToken = createHmac('sha256', secret)
383
+ .update('metalink-auth-token').digest('hex');
384
+ if (token !== expectedToken) {
385
+ res.status(401).json({ error: 'Invalid authentication token' });
386
+ return;
387
+ }
388
+ req.authenticated = true;
389
+ req.userId = 'authenticated-user';
390
+ next();
391
+ }
392
+ /**
393
+ * Setup API routes
394
+ */
395
+ setupRoutes() {
396
+ const api = express.Router();
397
+ // Server endpoints
398
+ api.get('/servers', this.listServers.bind(this));
399
+ api.get('/servers/available/list', this.listAvailableServers.bind(this));
400
+ api.post('/servers/:name/start', this.startServer.bind(this));
401
+ api.post('/servers/:name/stop', this.stopServer.bind(this));
402
+ api.post('/servers/:name/restart', this.restartServer.bind(this));
403
+ api.post('/servers/:name/enable', this.enableServer.bind(this));
404
+ api.post('/servers/:name/disable', this.disableServer.bind(this));
405
+ api.get('/servers/:name/status', this.getServerStatus.bind(this));
406
+ api.get('/servers/:name/info', this.getServerInfo.bind(this));
407
+ api.post('/servers/:name/refresh-tools', this.refreshServerTools.bind(this)); // v1.4.2: Force refresh tools
408
+ // Tool endpoints
409
+ api.get('/servers/:name/tools', this.getServerTools.bind(this));
410
+ api.post('/servers/:name/tools/:toolName/execute', this.executeTool.bind(this));
411
+ // Configuration endpoints
412
+ api.get('/config', this.getConfig.bind(this));
413
+ api.put('/config', this.updateConfig.bind(this));
414
+ // Registry management endpoints
415
+ api.post('/registry/servers', this.addServer.bind(this));
416
+ api.delete('/registry/servers/:name', this.removeServer.bind(this));
417
+ api.post('/registry/validate', this.validateServer.bind(this));
418
+ // Health endpoints
419
+ api.get('/health', this.healthCheck.bind(this));
420
+ // Version endpoint
421
+ api.get('/version', this.getVersion.bind(this));
422
+ // Metrics endpoints (Phase 4 - v1.4.0)
423
+ api.get('/metrics', this.getMetrics.bind(this));
424
+ api.get('/metrics/prometheus', this.getPrometheusMetrics.bind(this));
425
+ api.get('/metrics/api', this.getApiMetrics.bind(this));
426
+ api.get('/metrics/servers/:name', this.getServerMetrics.bind(this));
427
+ api.get('/metrics/hourly', this.getHourlyMetrics.bind(this));
428
+ api.get('/metrics/daily', this.getDailyMetrics.bind(this));
429
+ api.get('/metrics/weekly', this.getWeeklyMetrics.bind(this));
430
+ api.get('/metrics/errors', this.getErrorAnalytics.bind(this));
431
+ api.get('/metrics/errors/:toolName', this.getToolErrorMetrics.bind(this));
432
+ api.get('/metrics/tools', this.getToolMetrics.bind(this)); // Test 187: Tool-specific granular metrics
433
+ // Safety endpoints (Phase 1 - v1.2.0)
434
+ api.get("/safety", this.getSafetyRules.bind(this));
435
+ api.get("/safety/check/:server/:tool", this.checkToolSafety.bind(this));
436
+ api.post("/safety/tools/safe", this.addSafeToolOverride.bind(this));
437
+ api.post("/safety/tools/risky", this.addRiskyToolOverride.bind(this));
438
+ api.post("/safety/patterns/safe", this.addSafePattern.bind(this));
439
+ api.post("/safety/patterns/risky", this.addRiskyPattern.bind(this));
440
+ api.delete("/safety/rules/:rule", this.removeRule.bind(this));
441
+ api.post("/safety/reset", this.resetSafetyRules.bind(this));
442
+ api.post("/safety/import", this.importSafetyRules.bind(this));
443
+ this.app.use('/api/v1', api);
444
+ // Also register /api/metrics endpoint (without /v1 prefix) for backward compatibility with tests
445
+ this.app.get('/api/metrics', this.getMetrics.bind(this));
446
+ // MCP endpoint (JSON-RPC over HTTP) - Primary endpoint per MCP 2025-06-18 spec
447
+ // GET /mcp for streaming/SSE connections (Claude Code HTTP transport)
448
+ this.app.get('/mcp', this.handleMcpRequest.bind(this));
449
+ // POST endpoints for standard JSON-RPC
450
+ this.app.post('/mcp', this.handleMcpRequest.bind(this));
451
+ // DELETE /mcp for session cleanup (per MCP spec)
452
+ this.app.delete('/mcp', this.handleMcpRequest.bind(this));
453
+ // Legacy SSE endpoints for backward compatibility (2025-03-26 and earlier)
454
+ this.app.get('/sse', this.handleMcpRequest.bind(this)); // Legacy SSE endpoint
455
+ this.app.post('/messages', this.handleMcpRequest.bind(this)); // Legacy JSON-RPC endpoint
456
+ // Alternative MCP endpoint paths for compatibility with various clients
457
+ // Grok HTTP transport expects /mcp/rpc path
458
+ this.app.post('/mcp/rpc', this.handleMcpRequest.bind(this));
459
+ // Root-level /rpc for clients that don't use /mcp prefix (e.g., Grok CLI)
460
+ this.app.post('/rpc', this.handleMcpRequest.bind(this));
461
+ // Grok appends /rpc to URL, creating /rpc/rpc when given http://.../rpc
462
+ this.app.post('/rpc/rpc', this.handleMcpRequest.bind(this));
463
+ // Prometheus metrics endpoint (standard /metrics path for scraping)
464
+ this.app.get('/metrics', this.getPrometheusMetrics.bind(this));
465
+ // Serve dashboard static files (if available)
466
+ const dashboardPath = path.join(__dirname, '../../../dashboard/dist');
467
+ const dashboardExists = fs.existsSync(dashboardPath);
468
+ const indexPath = path.join(dashboardPath, 'index.html');
469
+ const indexExists = dashboardExists && fs.existsSync(indexPath);
470
+ console.log('[HTTP] dashboardPath:', dashboardPath);
471
+ console.log('[HTTP] dashboard exists:', dashboardExists);
472
+ console.log('[HTTP] index.html exists:', indexExists);
473
+ // Serve dashboard - explicit root route
474
+ if (indexExists) {
475
+ console.log('[HTTP] Registering GET / route for dashboard');
476
+ this.app.get('/', (_req, res) => {
477
+ console.log('[HTTP] Serving root path from:', indexPath);
478
+ res.sendFile(indexPath);
479
+ });
480
+ console.log('[HTTP] GET / route registered successfully');
481
+ }
482
+ else {
483
+ console.log('[HTTP] Skipping GET / - index.html not found');
484
+ }
485
+ if (dashboardExists) {
486
+ console.log('[HTTP] Registering static file middleware for:', dashboardPath);
487
+ // Serve static assets from dashboard
488
+ this.app.use(express.static(dashboardPath, {
489
+ etag: false
490
+ }));
491
+ console.log('[HTTP] Static middleware registered');
492
+ }
493
+ // SPA fallback - serve index.html for client-side routing (only if dashboard exists)
494
+ if (indexExists) {
495
+ console.log('[HTTP] Registering fallback route (*)');
496
+ this.app.get('*', (_req, res) => {
497
+ console.log('[HTTP] Fallback route hit for:', _req.path);
498
+ res.sendFile(indexPath);
499
+ });
500
+ console.log('[HTTP] Fallback route registered');
501
+ }
502
+ else {
503
+ console.log('[HTTP] Skipping fallback route - dashboard not available');
504
+ }
505
+ // SECURITY: Error handler with sanitization to prevent credential leakage
506
+ // OWASP Reference: A3:2017-Sensitive Data Exposure
507
+ this.app.use((err, _req, res, _next) => {
508
+ // Log full error internally for debugging (not exposed to client)
509
+ console.error('API Error:', err);
510
+ // SECURITY: Sanitize error message before sending to client
511
+ const sanitizedMessage = this.sanitizeErrorMessage(err.message);
512
+ res.status(500).json({
513
+ error: 'Internal Server Error',
514
+ message: sanitizedMessage,
515
+ });
516
+ });
517
+ }
518
+ /**
519
+ * SECURITY: Sanitize error messages to prevent credential/path leakage.
520
+ *
521
+ * This function removes or masks sensitive information from error messages
522
+ * before they are sent to clients.
523
+ *
524
+ * OWASP Reference: A3:2017-Sensitive Data Exposure
525
+ *
526
+ * @param message - Original error message
527
+ * @returns Sanitized message safe for client exposure
528
+ */
529
+ sanitizeErrorMessage(message) {
530
+ if (!message)
531
+ return 'An error occurred';
532
+ // Patterns that indicate sensitive data that should be masked
533
+ const sensitivePatterns = [
534
+ // API keys and tokens (various formats)
535
+ /\b(api[_-]?key|apikey|token|bearer|auth)[=:]\s*['"]?[\w-]{20,}['"]?/gi,
536
+ /\b(sk|pk|rk)[-_][a-zA-Z0-9]{20,}/g, // Stripe-style keys
537
+ /\bghp_[a-zA-Z0-9]{36,}/g, // GitHub tokens
538
+ /\bxox[baprs]-[a-zA-Z0-9-]+/g, // Slack tokens
539
+ // Passwords
540
+ /password[=:]\s*['"]?[^'"\s]+['"]?/gi,
541
+ /pwd[=:]\s*['"]?[^'"\s]+['"]?/gi,
542
+ // Connection strings
543
+ /mongodb(\+srv)?:\/\/[^@]+@/gi,
544
+ /postgres(ql)?:\/\/[^@]+@/gi,
545
+ /mysql:\/\/[^@]+@/gi,
546
+ /redis:\/\/[^@]+@/gi,
547
+ // AWS credentials
548
+ /\bAKIA[A-Z0-9]{16}\b/g,
549
+ /\b[A-Za-z0-9/+=]{40}\b/g, // AWS secret keys (40 chars base64)
550
+ // Home directory paths (may reveal usernames)
551
+ /\/Users\/[a-zA-Z0-9_-]+/g,
552
+ /\/home\/[a-zA-Z0-9_-]+/g,
553
+ /C:\\Users\\[a-zA-Z0-9_-]+/gi,
554
+ // Environment variable dumps
555
+ /\benv\[['"]?[A-Z_]+['"]?\]\s*=\s*['"]?[^'"\s]+['"]?/gi,
556
+ ];
557
+ let sanitized = message;
558
+ for (const pattern of sensitivePatterns) {
559
+ sanitized = sanitized.replace(pattern, '[REDACTED]');
560
+ }
561
+ // Additional safety: truncate very long messages that might contain dumps
562
+ if (sanitized.length > 500) {
563
+ sanitized = sanitized.substring(0, 500) + '... [truncated]';
564
+ }
565
+ return sanitized;
566
+ }
567
+ /**
568
+ * Setup Server-Sent Events
569
+ */
570
+ setupSSE() {
571
+ this.app.get('/api/v1/events', (req, res) => {
572
+ // Check authentication
573
+ if (this.config.daemon?.auth?.enabled && !req.authenticated) {
574
+ res.status(401).json({ error: 'Unauthorized' });
575
+ return;
576
+ }
577
+ res.setHeader('Content-Type', 'text/event-stream');
578
+ res.setHeader('Cache-Control', 'no-cache');
579
+ res.setHeader('Connection', 'keep-alive');
580
+ // Send initial connection message
581
+ res.write('data: {"type":"connected"}\n\n');
582
+ this.eventClients.add(res);
583
+ // Clean up on disconnect
584
+ req.on('close', () => {
585
+ this.eventClients.delete(res);
586
+ });
587
+ });
588
+ // Broadcast events from server manager
589
+ this.serverManager.on('server:started', (data) => {
590
+ this.broadcastEvent({
591
+ type: 'server:started',
592
+ data,
593
+ });
594
+ });
595
+ this.serverManager.on('server:stopped', (data) => {
596
+ this.broadcastEvent({
597
+ type: 'server:stopped',
598
+ data,
599
+ });
600
+ });
601
+ this.serverManager.on('server:error', (data) => {
602
+ this.broadcastEvent({
603
+ type: 'server:error',
604
+ data,
605
+ });
606
+ });
607
+ this.serverManager.on('health:check', (data) => {
608
+ this.broadcastEvent({
609
+ type: 'health:check',
610
+ data,
611
+ });
612
+ });
613
+ // Listen for server removal events (from removeServer method)
614
+ this.serverManager.on('server:removed', (data) => {
615
+ this.broadcastEvent({
616
+ type: 'server:removed',
617
+ data,
618
+ });
619
+ });
620
+ }
621
+ /**
622
+ * Broadcast event to all SSE clients
623
+ */
624
+ broadcastEvent(event) {
625
+ const message = `data: ${JSON.stringify(event)}\n\n`;
626
+ for (const client of this.eventClients) {
627
+ client.write(message);
628
+ }
629
+ }
630
+ // === Route Handlers ===
631
+ /**
632
+ * List all servers
633
+ */
634
+ async listServers(_req, res) {
635
+ try {
636
+ const servers = this.configLoader.getServers();
637
+ const serverInfos = servers.map((config) => {
638
+ const isStdio = config.transport === 'stdio' || config.transport === undefined;
639
+ const tools = this.serverManager.getServerTools(config.name);
640
+ const baseInfo = {
641
+ name: config.name,
642
+ env: config.env || {},
643
+ status: (this.serverManager.getServerStatus(config.name)?.status || 'stopped'),
644
+ process: this.serverManager.getServerStatus(config.name),
645
+ toolCount: tools.length || 0,
646
+ tokenEstimate: calculateTotalTokens(tools),
647
+ };
648
+ return isStdio
649
+ ? {
650
+ ...baseInfo,
651
+ command: config.command,
652
+ args: config.args || [],
653
+ }
654
+ : {
655
+ ...baseInfo,
656
+ transport: config.transport,
657
+ url: config.url,
658
+ };
659
+ });
660
+ res.json({ servers: serverInfos });
661
+ }
662
+ catch (error) {
663
+ res.status(500).json({
664
+ error: error instanceof Error ? error.message : 'Failed to list servers',
665
+ });
666
+ }
667
+ }
668
+ /**
669
+ * List all available servers (including disabled ones)
670
+ */
671
+ async listAvailableServers(_req, res) {
672
+ try {
673
+ const allServers = this.configLoader.getAllServers();
674
+ const exposedServers = this.configLoader.getServers();
675
+ const exposedNames = new Set(exposedServers.map(s => s.name));
676
+ const serverInfos = allServers.map((config) => {
677
+ const isStdio = config.transport === 'stdio' || config.transport === undefined;
678
+ const tools = this.serverManager.getServerTools(config.name);
679
+ const baseInfo = {
680
+ name: config.name,
681
+ type: isStdio ? 'stdio' : 'http',
682
+ env: config.env || {},
683
+ status: (this.serverManager.getServerStatus(config.name)?.status || 'stopped'),
684
+ process: this.serverManager.getServerStatus(config.name),
685
+ enabled: exposedNames.has(config.name),
686
+ toolCount: tools.length || 0,
687
+ tokenEstimate: calculateTotalTokens(tools),
688
+ };
689
+ return isStdio
690
+ ? {
691
+ ...baseInfo,
692
+ command: config.command,
693
+ args: config.args || [],
694
+ fullCommand: `${config.command} ${(config.args || []).join(' ')}`.trim(),
695
+ }
696
+ : {
697
+ ...baseInfo,
698
+ transport: config.transport,
699
+ url: config.url,
700
+ };
701
+ });
702
+ res.json({ servers: serverInfos });
703
+ }
704
+ catch (error) {
705
+ res.status(500).json({
706
+ error: error instanceof Error ? error.message : 'Failed to list available servers',
707
+ });
708
+ }
709
+ }
710
+ /**
711
+ * Enable a server (add to EXPOSE_SERVERS)
712
+ */
713
+ async enableServer(req, res) {
714
+ try {
715
+ const { name } = req.params;
716
+ const config = this.configLoader.getServer(name);
717
+ if (!config) {
718
+ res.status(404).json({ error: `Server ${name} not found` });
719
+ return;
720
+ }
721
+ // Get current EXPOSE_SERVERS list
722
+ const currentExposed = process.env.EXPOSE_SERVERS
723
+ ? process.env.EXPOSE_SERVERS.split(',').map(s => s.trim())
724
+ : (this.config.base_servers || []);
725
+ // Add server if not already exposed
726
+ if (!currentExposed.includes(name)) {
727
+ currentExposed.push(name);
728
+ process.env.EXPOSE_SERVERS = currentExposed.join(',');
729
+ }
730
+ res.json({ success: true, message: `Server ${name} enabled` });
731
+ }
732
+ catch (error) {
733
+ res.status(400).json({
734
+ error: error instanceof Error ? error.message : 'Failed to enable server',
735
+ });
736
+ }
737
+ }
738
+ /**
739
+ * Disable a server (remove from EXPOSE_SERVERS)
740
+ */
741
+ async disableServer(req, res) {
742
+ try {
743
+ const { name } = req.params;
744
+ // Get current EXPOSE_SERVERS list
745
+ const currentExposed = process.env.EXPOSE_SERVERS
746
+ ? process.env.EXPOSE_SERVERS.split(',').map(s => s.trim())
747
+ : (this.config.base_servers || []);
748
+ // Remove server if exposed
749
+ const index = currentExposed.indexOf(name);
750
+ if (index >= 0) {
751
+ currentExposed.splice(index, 1);
752
+ process.env.EXPOSE_SERVERS = currentExposed.join(',');
753
+ }
754
+ res.json({ success: true, message: `Server ${name} disabled` });
755
+ }
756
+ catch (error) {
757
+ res.status(400).json({
758
+ error: error instanceof Error ? error.message : 'Failed to disable server',
759
+ });
760
+ }
761
+ }
762
+ /**
763
+ * Start a server
764
+ */
765
+ async startServer(req, res) {
766
+ try {
767
+ const { name } = req.params;
768
+ const config = this.configLoader.getServer(name);
769
+ if (!config) {
770
+ res.status(404).json({ error: `Server ${name} not found in configuration` });
771
+ return;
772
+ }
773
+ const process = await this.serverManager.startServer(config);
774
+ res.json({
775
+ success: true,
776
+ process,
777
+ });
778
+ }
779
+ catch (error) {
780
+ res.status(400).json({
781
+ error: error instanceof Error ? error.message : 'Failed to start server',
782
+ });
783
+ }
784
+ }
785
+ /**
786
+ * Stop a server
787
+ */
788
+ async stopServer(req, res) {
789
+ try {
790
+ const { name } = req.params;
791
+ await this.serverManager.stopServer(name);
792
+ res.json({
793
+ success: true,
794
+ message: `Server ${name} stopped`,
795
+ });
796
+ }
797
+ catch (error) {
798
+ res.status(400).json({
799
+ error: error instanceof Error ? error.message : 'Failed to stop server',
800
+ });
801
+ }
802
+ }
803
+ /**
804
+ * Restart a server
805
+ */
806
+ async restartServer(req, res) {
807
+ try {
808
+ const { name } = req.params;
809
+ const config = this.configLoader.getServer(name);
810
+ if (!config) {
811
+ res.status(404).json({ error: `Server ${name} not found in configuration` });
812
+ return;
813
+ }
814
+ const process = await this.serverManager.restartServer(config);
815
+ res.json({
816
+ success: true,
817
+ process,
818
+ });
819
+ }
820
+ catch (error) {
821
+ res.status(400).json({
822
+ error: error instanceof Error ? error.message : 'Failed to restart server',
823
+ });
824
+ }
825
+ }
826
+ /**
827
+ * Force refresh tools for a server
828
+ * v1.4.2: Clears cache and re-discovers tools from the MCP server
829
+ */
830
+ async refreshServerTools(req, res) {
831
+ try {
832
+ const { name } = req.params;
833
+ const config = this.configLoader.getServer(name);
834
+ if (!config) {
835
+ res.status(404).json({ error: `Server ${name} not found in configuration` });
836
+ return;
837
+ }
838
+ console.log(`[HTTP] Force refreshing tools for ${name}...`);
839
+ const tools = await this.serverManager.forceRefreshSchema(name, config);
840
+ res.json({
841
+ success: true,
842
+ serverName: name,
843
+ toolCount: tools.length,
844
+ tools: tools.map(t => t.name),
845
+ });
846
+ }
847
+ catch (error) {
848
+ res.status(400).json({
849
+ error: error instanceof Error ? error.message : 'Failed to refresh server tools',
850
+ });
851
+ }
852
+ }
853
+ /**
854
+ * Get server status
855
+ */
856
+ async getServerStatus(req, res) {
857
+ try {
858
+ const { name } = req.params;
859
+ // Check if server exists in registry first
860
+ const serverConfig = this.configLoader.getServer(name);
861
+ if (!serverConfig) {
862
+ res.status(404).json({ error: `Server ${name} not found` });
863
+ return;
864
+ }
865
+ // Get runtime status (may be undefined if stopped)
866
+ const status = this.serverManager.getServerStatus(name);
867
+ // If no runtime status, server exists but is stopped
868
+ if (!status) {
869
+ res.json({
870
+ server: name,
871
+ status: {
872
+ pid: 0,
873
+ status: 'stopped',
874
+ uptime: 0,
875
+ lastHealthCheck: 0,
876
+ errorCount: 0
877
+ }
878
+ });
879
+ return;
880
+ }
881
+ res.json({ server: name, status });
882
+ }
883
+ catch (error) {
884
+ res.status(500).json({
885
+ error: error instanceof Error ? error.message : 'Failed to get server status',
886
+ });
887
+ }
888
+ }
889
+ /**
890
+ * Get full server information including configuration and runtime status
891
+ * v1.1.29: Include tools list and support ?forceDiscovery=true to rediscover
892
+ */
893
+ async getServerInfo(req, res) {
894
+ try {
895
+ const { name } = req.params;
896
+ const forceDiscovery = req.query.forceDiscovery === 'true';
897
+ // Get server configuration from registry
898
+ const serverConfig = this.configLoader.getServer(name);
899
+ if (!serverConfig) {
900
+ res.status(404).json({ error: `Server ${name} not found` });
901
+ return;
902
+ }
903
+ // If forceDiscovery is requested, start server and refresh tools
904
+ if (forceDiscovery) {
905
+ console.log(`[ServerInfo] Force discovery requested for '${name}'`);
906
+ try {
907
+ // v1.1.47: Clear cache before rediscovery to force fresh tool fetch
908
+ // This ensures forceDiscovery actually queries the server, not cache
909
+ this.serverManager.invalidateServerSchema(name);
910
+ console.log(`[ServerInfo] Invalidated cache for '${name}'`);
911
+ await this.serverManager.ensureServerStarted(name, serverConfig);
912
+ console.log(`[ServerInfo] Rediscovered tools for '${name}'`);
913
+ }
914
+ catch (discoveryError) {
915
+ console.warn(`[ServerInfo] Force discovery failed for '${name}':`, discoveryError);
916
+ }
917
+ }
918
+ // Get runtime status
919
+ const runtimeStatus = this.serverManager.getServerStatus(name);
920
+ // Get tools from cache (memory or disk)
921
+ const tools = this.serverManager.getServerTools(name);
922
+ // Get list of exposed servers to determine if this server is enabled
923
+ const exposedServers = this.configLoader.getServers();
924
+ const enabled = exposedServers.some((s) => s.name === name);
925
+ // Build response - different fields for stdio vs HTTP servers
926
+ const isStdio = serverConfig.transport === 'stdio' || serverConfig.transport === undefined;
927
+ const baseResponse = {
928
+ name: serverConfig.name,
929
+ status: runtimeStatus?.status || 'stopped',
930
+ enabled,
931
+ pid: runtimeStatus?.pid,
932
+ uptime: runtimeStatus?.uptime,
933
+ lastHealthCheck: runtimeStatus?.lastHealthCheck,
934
+ errorCount: runtimeStatus?.errorCount,
935
+ tools: tools.map(t => t.name),
936
+ toolCount: tools.length,
937
+ };
938
+ const response = isStdio
939
+ ? {
940
+ ...baseResponse,
941
+ command: serverConfig.command,
942
+ args: serverConfig.args || [],
943
+ env: serverConfig.env || {},
944
+ }
945
+ : {
946
+ ...baseResponse,
947
+ transport: serverConfig.transport,
948
+ url: serverConfig.url,
949
+ auth: serverConfig.auth,
950
+ env: serverConfig.env || {},
951
+ };
952
+ res.json(response);
953
+ }
954
+ catch (error) {
955
+ res.status(500).json({
956
+ error: error instanceof Error ? error.message : 'Failed to get server info',
957
+ });
958
+ }
959
+ }
960
+ /**
961
+ * Get server tools (stub - Phase 2)
962
+ */
963
+ async getServerTools(req, res) {
964
+ try {
965
+ const { name } = req.params;
966
+ const status = this.serverManager.getServerStatus(name);
967
+ if (!status || status.status !== 'running') {
968
+ res.status(400).json({ error: `Server ${name} is not running` });
969
+ return;
970
+ }
971
+ // Get tools from ServerManager cache (populated during server start)
972
+ const tools = this.serverManager.getServerTools(name);
973
+ res.json({ server: name, tools });
974
+ }
975
+ catch (error) {
976
+ res.status(500).json({
977
+ error: error instanceof Error ? error.message : 'Failed to get server tools',
978
+ });
979
+ }
980
+ }
981
+ /**
982
+ * Execute tool (stub - Phase 2)
983
+ */
984
+ async executeTool(req, res) {
985
+ try {
986
+ const { name, toolName } = req.params;
987
+ const { arguments: args } = req.body;
988
+ const status = this.serverManager.getServerStatus(name);
989
+ if (!status || status.status !== 'running') {
990
+ res.status(400).json({ error: `Server ${name} is not running` });
991
+ return;
992
+ }
993
+ // Execute tool via ServerManager
994
+ const result = await this.serverManager.callTool(name, toolName, args);
995
+ const response = {
996
+ success: true,
997
+ result,
998
+ };
999
+ res.json(response);
1000
+ }
1001
+ catch (error) {
1002
+ const response = {
1003
+ success: false,
1004
+ error: error instanceof Error ? error.message : 'Failed to execute tool',
1005
+ };
1006
+ res.status(500).json(response);
1007
+ }
1008
+ }
1009
+ /**
1010
+ * Get configuration
1011
+ */
1012
+ async getConfig(_req, res) {
1013
+ try {
1014
+ const config = this.configLoader.getConfig();
1015
+ res.json(config);
1016
+ }
1017
+ catch (error) {
1018
+ res.status(500).json({
1019
+ error: error instanceof Error ? error.message : 'Failed to get configuration',
1020
+ });
1021
+ }
1022
+ }
1023
+ /**
1024
+ * Update configuration (stub - Phase 2)
1025
+ */
1026
+ async updateConfig(req, res) {
1027
+ try {
1028
+ // Extract config updates from request body
1029
+ const updates = req.body;
1030
+ if (!updates || typeof updates !== 'object') {
1031
+ res.status(400).json({ error: 'Invalid configuration: expected object' });
1032
+ return;
1033
+ }
1034
+ // Handle specific config sections that can be updated
1035
+ if (updates.toolSafetyRules) {
1036
+ await this.configLoader.setToolSafetyRules(updates.toolSafetyRules);
1037
+ }
1038
+ // Reload configuration to pick up any file-based changes
1039
+ await this.configLoader.reload();
1040
+ res.json({ success: true, message: 'Configuration updated' });
1041
+ }
1042
+ catch (error) {
1043
+ res.status(400).json({
1044
+ error: error instanceof Error ? error.message : 'Failed to update configuration',
1045
+ });
1046
+ }
1047
+ }
1048
+ /**
1049
+ * Health check endpoint
1050
+ */
1051
+ async healthCheck(_req, res) {
1052
+ try {
1053
+ // Calculate daemon uptime
1054
+ const uptimeMs = Date.now() - this.startTime;
1055
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
1056
+ // Get base servers from config
1057
+ const baseServers = this.config.base_servers || [];
1058
+ // Build health checks for base servers
1059
+ const baseServerChecks = {};
1060
+ let healthyCount = 0;
1061
+ let degradedCount = 0;
1062
+ for (const serverName of baseServers) {
1063
+ try {
1064
+ const isActive = this.serverManager.isServerActive(serverName);
1065
+ if (isActive) {
1066
+ const tools = this.serverManager.getServerTools(serverName);
1067
+ baseServerChecks[serverName] = {
1068
+ status: 'healthy',
1069
+ tools_count: tools.length
1070
+ };
1071
+ healthyCount++;
1072
+ }
1073
+ else {
1074
+ baseServerChecks[serverName] = {
1075
+ status: 'unhealthy',
1076
+ error: 'Server not running'
1077
+ };
1078
+ degradedCount++;
1079
+ }
1080
+ }
1081
+ catch (error) {
1082
+ baseServerChecks[serverName] = {
1083
+ status: 'unhealthy',
1084
+ error: error instanceof Error ? error.message : 'Unknown error'
1085
+ };
1086
+ degradedCount++;
1087
+ }
1088
+ }
1089
+ // Determine overall status
1090
+ let overallStatus;
1091
+ let httpStatusCode;
1092
+ if (degradedCount === 0) {
1093
+ // All base servers are healthy
1094
+ overallStatus = 'healthy';
1095
+ httpStatusCode = 200;
1096
+ }
1097
+ else if (healthyCount > 0) {
1098
+ // Some base servers are down but not all
1099
+ overallStatus = 'degraded';
1100
+ httpStatusCode = 200; // Still operational
1101
+ }
1102
+ else {
1103
+ // All base servers are down or critical failure
1104
+ overallStatus = 'unhealthy';
1105
+ httpStatusCode = 503; // Service Unavailable
1106
+ }
1107
+ const health = {
1108
+ status: overallStatus,
1109
+ version,
1110
+ uptime_seconds: uptimeSeconds,
1111
+ checks: {
1112
+ daemon: {
1113
+ status: 'healthy'
1114
+ },
1115
+ base_servers: baseServerChecks
1116
+ }
1117
+ };
1118
+ res.status(httpStatusCode).json(health);
1119
+ }
1120
+ catch (error) {
1121
+ // Critical daemon error
1122
+ res.status(503).json({
1123
+ status: 'unhealthy',
1124
+ version,
1125
+ uptime_seconds: Math.floor((Date.now() - this.startTime) / 1000),
1126
+ checks: {
1127
+ daemon: {
1128
+ status: 'unhealthy',
1129
+ error: error instanceof Error ? error.message : 'Health check failed'
1130
+ }
1131
+ }
1132
+ });
1133
+ }
1134
+ }
1135
+ /**
1136
+ * Get MetaLink version
1137
+ */
1138
+ async getVersion(_req, res) {
1139
+ try {
1140
+ res.json({ version });
1141
+ }
1142
+ catch (error) {
1143
+ res.status(500).json({
1144
+ error: error instanceof Error ? error.message : 'Failed to get version',
1145
+ });
1146
+ }
1147
+ }
1148
+ // === Metrics Endpoints (Phase 4 - v1.4.0) ===
1149
+ /**
1150
+ * GET /api/v1/metrics - JSON metrics endpoint
1151
+ */
1152
+ async getMetrics(_req, res) {
1153
+ try {
1154
+ res.json({
1155
+ version,
1156
+ timestamp: Date.now(),
1157
+ api: globalMetrics.getApiMetrics(),
1158
+ servers: globalMetrics.getAllServerMetrics(),
1159
+ tools: globalMetrics.getMetrics(),
1160
+ });
1161
+ }
1162
+ catch (error) {
1163
+ res.status(500).json({
1164
+ error: error instanceof Error ? error.message : 'Failed to get metrics',
1165
+ });
1166
+ }
1167
+ }
1168
+ /**
1169
+ * GET /api/v1/metrics/prometheus - Prometheus export endpoint
1170
+ */
1171
+ async getPrometheusMetrics(_req, res) {
1172
+ try {
1173
+ res.set('Content-Type', 'text/plain');
1174
+ res.send(globalMetrics.exportPrometheus());
1175
+ }
1176
+ catch (error) {
1177
+ res.status(500).send(`# Error exporting metrics: ${error instanceof Error ? error.message : 'Unknown error'}`);
1178
+ }
1179
+ }
1180
+ /**
1181
+ * GET /api/v1/metrics/api - API-level metrics
1182
+ */
1183
+ async getApiMetrics(_req, res) {
1184
+ try {
1185
+ res.json(globalMetrics.getApiMetrics());
1186
+ }
1187
+ catch (error) {
1188
+ res.status(500).json({
1189
+ error: error instanceof Error ? error.message : 'Failed to get API metrics',
1190
+ });
1191
+ }
1192
+ }
1193
+ /**
1194
+ * GET /api/v1/metrics/servers/:name - Server-specific metrics
1195
+ */
1196
+ async getServerMetrics(req, res) {
1197
+ try {
1198
+ const { name } = req.params;
1199
+ const serverMetrics = globalMetrics.getServerMetrics(name);
1200
+ if (!serverMetrics) {
1201
+ res.status(404).json({ error: `Server '${name}' not found or no metrics available` });
1202
+ return;
1203
+ }
1204
+ res.json(serverMetrics);
1205
+ }
1206
+ catch (error) {
1207
+ res.status(500).json({
1208
+ error: error instanceof Error ? error.message : 'Failed to get server metrics',
1209
+ });
1210
+ }
1211
+ }
1212
+ /**
1213
+ * GET /api/v1/metrics/hourly - Hourly aggregation
1214
+ */
1215
+ async getHourlyMetrics(req, res) {
1216
+ try {
1217
+ const hours = parseInt(req.query.hours || '24');
1218
+ const allMetrics = globalMetrics.getMetrics();
1219
+ const hourlyData = this.metricsAggregator.aggregateHourly(allMetrics, hours);
1220
+ res.json({
1221
+ period: 'hourly',
1222
+ hours,
1223
+ data: hourlyData,
1224
+ });
1225
+ }
1226
+ catch (error) {
1227
+ res.status(500).json({
1228
+ error: error instanceof Error ? error.message : 'Failed to get hourly metrics',
1229
+ });
1230
+ }
1231
+ }
1232
+ /**
1233
+ * GET /api/v1/metrics/daily - Daily aggregation
1234
+ */
1235
+ async getDailyMetrics(req, res) {
1236
+ try {
1237
+ const days = parseInt(req.query.days || '7');
1238
+ const allMetrics = globalMetrics.getMetrics();
1239
+ const dailyData = this.metricsAggregator.aggregateDaily(allMetrics, days);
1240
+ res.json({
1241
+ period: 'daily',
1242
+ days,
1243
+ data: dailyData,
1244
+ });
1245
+ }
1246
+ catch (error) {
1247
+ res.status(500).json({
1248
+ error: error instanceof Error ? error.message : 'Failed to get daily metrics',
1249
+ });
1250
+ }
1251
+ }
1252
+ /**
1253
+ * GET /api/v1/metrics/weekly - Weekly aggregation
1254
+ */
1255
+ async getWeeklyMetrics(req, res) {
1256
+ try {
1257
+ const weeks = parseInt(req.query.weeks || '4');
1258
+ const allMetrics = globalMetrics.getMetrics();
1259
+ const weeklyData = this.metricsAggregator.aggregateWeekly(allMetrics, weeks);
1260
+ res.json({
1261
+ period: 'weekly',
1262
+ weeks,
1263
+ data: weeklyData,
1264
+ });
1265
+ }
1266
+ catch (error) {
1267
+ res.status(500).json({
1268
+ error: error instanceof Error ? error.message : 'Failed to get weekly metrics',
1269
+ });
1270
+ }
1271
+ }
1272
+ /**
1273
+ * GET /api/v1/metrics/errors - Error analytics across all tools
1274
+ */
1275
+ async getErrorAnalytics(_req, res) {
1276
+ try {
1277
+ const analytics = globalMetrics.getErrorAnalytics();
1278
+ res.json(analytics);
1279
+ }
1280
+ catch (error) {
1281
+ res.status(500).json({
1282
+ error: error instanceof Error ? error.message : 'Failed to get error analytics',
1283
+ });
1284
+ }
1285
+ }
1286
+ /**
1287
+ * GET /api/v1/metrics/errors/:toolName - Per-tool error analytics
1288
+ * Query params: ?serverName=<name> (optional)
1289
+ */
1290
+ async getToolErrorMetrics(req, res) {
1291
+ try {
1292
+ const { toolName } = req.params;
1293
+ const serverName = req.query.serverName;
1294
+ const metrics = globalMetrics.getToolErrorMetrics(toolName, serverName);
1295
+ if (!metrics) {
1296
+ res.status(404).json({
1297
+ error: `No metrics found for tool '${toolName}'${serverName ? ` on server '${serverName}'` : ''}`
1298
+ });
1299
+ return;
1300
+ }
1301
+ res.json(metrics);
1302
+ }
1303
+ catch (error) {
1304
+ res.status(500).json({
1305
+ error: error instanceof Error ? error.message : 'Failed to get tool error metrics',
1306
+ });
1307
+ }
1308
+ }
1309
+ /**
1310
+ * GET /api/v1/metrics/tools - Granular tool-specific metrics (Test 187)
1311
+ * Query params:
1312
+ * ?serverName=<name> - Filter by server
1313
+ * ?slow=<limit> - Get slowest tools (by p95 latency)
1314
+ * ?errors=<threshold> - Get tools with high error rates (default: 5%)
1315
+ */
1316
+ async getToolMetrics(req, res) {
1317
+ try {
1318
+ const { serverName } = req.query;
1319
+ const slow = req.query.slow ? parseInt(req.query.slow, 10) : undefined;
1320
+ const errorsThreshold = req.query.errors ? parseFloat(req.query.errors) : undefined;
1321
+ let metrics = globalMetrics.getAllToolMetrics();
1322
+ // Filter by server if requested
1323
+ if (serverName) {
1324
+ metrics = metrics.filter(m => m.serverName === serverName);
1325
+ }
1326
+ // Return slowest tools if requested
1327
+ if (slow !== undefined) {
1328
+ metrics = globalMetrics.getSlowestTools(slow);
1329
+ }
1330
+ // Return high error rate tools if requested
1331
+ if (errorsThreshold !== undefined) {
1332
+ metrics = globalMetrics.getHighErrorRateTools(errorsThreshold);
1333
+ }
1334
+ res.json({
1335
+ tools: metrics,
1336
+ count: metrics.length,
1337
+ });
1338
+ }
1339
+ catch (error) {
1340
+ res.status(500).json({
1341
+ error: error instanceof Error ? error.message : 'Failed to get tool metrics',
1342
+ });
1343
+ }
1344
+ }
1345
+ /**
1346
+ * Load persisted metrics on startup (Phase 4 - v1.4.0)
1347
+ */
1348
+ async loadPersistedMetrics() {
1349
+ try {
1350
+ const data = await this.metricsPersistence.load();
1351
+ if (data) {
1352
+ globalMetrics.restoreMetrics(data);
1353
+ console.log('[MetricsPersistence] Successfully restored metrics from disk');
1354
+ }
1355
+ else {
1356
+ console.log('[MetricsPersistence] No persisted metrics found, starting fresh');
1357
+ }
1358
+ }
1359
+ catch (error) {
1360
+ console.error('[MetricsPersistence] Failed to load metrics:', error);
1361
+ console.log('[MetricsPersistence] Starting with fresh metrics');
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Start periodic metrics persistence (Phase 4 - v1.4.0)
1366
+ */
1367
+ startMetricsPersistence() {
1368
+ const getMetricsData = () => ({
1369
+ timestamp: Date.now(),
1370
+ metrics: globalMetrics.getMetrics(),
1371
+ serverMetrics: globalMetrics.getAllServerMetrics(),
1372
+ toolMetrics: Array.from(globalMetrics['toolMetrics'].values()), // Access private field for persistence
1373
+ apiMetrics: globalMetrics.getApiMetrics(),
1374
+ });
1375
+ // Save every 60 seconds
1376
+ this.metricsPersistence.startPeriodicWrites(getMetricsData, 60000);
1377
+ console.log('[MetricsPersistence] Started periodic writes (60s interval)');
1378
+ }
1379
+ // === Session Management (Streamable HTTP - MCP 2025-03-26) ===
1380
+ /**
1381
+ * Track SSE event for Last-Event-ID resumption
1382
+ */
1383
+ trackSseEvent(sessionId, eventData) {
1384
+ const eventId = ++this.sseEventCounter;
1385
+ let events = this.sseEventBuffer.get(sessionId);
1386
+ if (!events) {
1387
+ events = [];
1388
+ this.sseEventBuffer.set(sessionId, events);
1389
+ }
1390
+ events.push({
1391
+ id: eventId,
1392
+ timestamp: Date.now(),
1393
+ data: eventData
1394
+ });
1395
+ // Keep only last N events to avoid memory bloat
1396
+ if (events.length > this.MAX_EVENTS_PER_SESSION) {
1397
+ events.shift();
1398
+ }
1399
+ return eventId;
1400
+ }
1401
+ /**
1402
+ * Get events for resumption after Last-Event-ID
1403
+ */
1404
+ getEventsForResumption(sessionId, lastEventId) {
1405
+ const events = this.sseEventBuffer.get(sessionId) || [];
1406
+ if (!lastEventId)
1407
+ return events;
1408
+ // Return all events after lastEventId
1409
+ return events.filter(e => e.id > lastEventId);
1410
+ }
1411
+ createSession(clientInfo) {
1412
+ const sessionId = randomUUID();
1413
+ this.sessions.set(sessionId, {
1414
+ id: sessionId,
1415
+ createdAt: Date.now(),
1416
+ lastActivity: Date.now(),
1417
+ clientInfo,
1418
+ initialized: false,
1419
+ lastEventId: 0,
1420
+ protocolVersion: '2025-06-18'
1421
+ });
1422
+ const clientName = clientInfo?.name || 'unknown';
1423
+ const clientVersion = clientInfo?.version || 'unknown';
1424
+ console.log(`[MCP] Session created: ${sessionId} | Client: ${clientName}/${clientVersion}`);
1425
+ // Persist sessions to disk (v1.1.24+)
1426
+ this.persistSessions();
1427
+ return sessionId;
1428
+ }
1429
+ /**
1430
+ * Get and update session activity with validation logging
1431
+ */
1432
+ getSession(sessionId) {
1433
+ const session = this.sessions.get(sessionId);
1434
+ if (session) {
1435
+ session.lastActivity = Date.now();
1436
+ const elapsed = Date.now() - session.createdAt;
1437
+ console.log(`[MCP] Session accessed: ${sessionId} | Initialized: ${session.initialized} | Age: ${elapsed}ms`);
1438
+ }
1439
+ else {
1440
+ console.log(`[MCP] Session not found: ${sessionId}`);
1441
+ }
1442
+ return session;
1443
+ }
1444
+ /**
1445
+ * Clean up expired sessions
1446
+ */
1447
+ cleanupSessions() {
1448
+ const now = Date.now();
1449
+ let cleaned = 0;
1450
+ for (const [id, session] of this.sessions) {
1451
+ if (now - session.lastActivity > this.SESSION_TIMEOUT) {
1452
+ this.sessions.delete(id);
1453
+ cleaned++;
1454
+ }
1455
+ }
1456
+ if (cleaned > 0) {
1457
+ console.log(`[MCP] Cleaned up ${cleaned} expired sessions`);
1458
+ // Persist cleaned state to disk (v1.1.24+)
1459
+ this.persistSessions();
1460
+ }
1461
+ }
1462
+ /**
1463
+ * Start session cleanup timer
1464
+ */
1465
+ startSessionCleanup() {
1466
+ if (this.sessionCleanupInterval) {
1467
+ clearInterval(this.sessionCleanupInterval);
1468
+ }
1469
+ // Run cleanup every 5 minutes
1470
+ this.sessionCleanupInterval = setInterval(() => this.cleanupSessions(), 5 * 60 * 1000);
1471
+ }
1472
+ /**
1473
+ * Stop session cleanup timer
1474
+ */
1475
+ stopSessionCleanup() {
1476
+ if (this.sessionCleanupInterval) {
1477
+ clearInterval(this.sessionCleanupInterval);
1478
+ this.sessionCleanupInterval = null;
1479
+ }
1480
+ }
1481
+ /**
1482
+ * Persist sessions to disk with atomic write (temp file + rename)
1483
+ * Called after session creation and cleanup
1484
+ */
1485
+ persistSessions() {
1486
+ try {
1487
+ // Ensure directory exists
1488
+ const dir = path.dirname(this.sessionPersistPath);
1489
+ if (!fs.existsSync(dir)) {
1490
+ fs.mkdirSync(dir, { recursive: true });
1491
+ }
1492
+ // Convert Map to object for JSON serialization
1493
+ const sessionsObj = {};
1494
+ for (const [id, session] of this.sessions) {
1495
+ sessionsObj[id] = session;
1496
+ }
1497
+ const data = {
1498
+ version: this.SESSION_PERSIST_VERSION,
1499
+ persistedAt: Date.now(),
1500
+ sessions: sessionsObj
1501
+ };
1502
+ // Atomic write: write to temp file, then rename
1503
+ const tempPath = `${this.sessionPersistPath}.tmp`;
1504
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8');
1505
+ fs.renameSync(tempPath, this.sessionPersistPath);
1506
+ console.log(`[SessionPersistence] Saved ${this.sessions.size} sessions to disk`);
1507
+ }
1508
+ catch (error) {
1509
+ console.error('[SessionPersistence] Failed to persist sessions:', error);
1510
+ // Non-fatal: sessions still work in memory
1511
+ }
1512
+ }
1513
+ /**
1514
+ * Load sessions from disk on startup
1515
+ * Filters out expired sessions during load
1516
+ */
1517
+ loadSessions() {
1518
+ try {
1519
+ if (!fs.existsSync(this.sessionPersistPath)) {
1520
+ console.log('[SessionPersistence] No persisted sessions found, starting fresh');
1521
+ return;
1522
+ }
1523
+ const content = fs.readFileSync(this.sessionPersistPath, 'utf8');
1524
+ const data = JSON.parse(content);
1525
+ // Version check for future migrations
1526
+ if (data.version !== this.SESSION_PERSIST_VERSION) {
1527
+ console.log(`[SessionPersistence] Version mismatch (got ${data.version}, expected ${this.SESSION_PERSIST_VERSION}), starting fresh`);
1528
+ return;
1529
+ }
1530
+ const now = Date.now();
1531
+ let loaded = 0;
1532
+ let expired = 0;
1533
+ for (const [id, session] of Object.entries(data.sessions || {})) {
1534
+ const sess = session;
1535
+ // Filter out expired sessions during load
1536
+ if (now - sess.lastActivity > this.SESSION_TIMEOUT) {
1537
+ expired++;
1538
+ continue;
1539
+ }
1540
+ this.sessions.set(id, sess);
1541
+ loaded++;
1542
+ }
1543
+ console.log(`[SessionPersistence] Restored ${loaded} sessions from disk (${expired} expired sessions discarded)`);
1544
+ // If we discarded expired sessions, persist the cleaned state
1545
+ if (expired > 0) {
1546
+ this.persistSessions();
1547
+ }
1548
+ }
1549
+ catch (error) {
1550
+ const errCode = error.code;
1551
+ if (errCode === 'ENOENT') {
1552
+ console.log('[SessionPersistence] No persisted sessions found, starting fresh');
1553
+ }
1554
+ else {
1555
+ console.error('[SessionPersistence] Failed to load sessions:', error);
1556
+ console.log('[SessionPersistence] Starting with fresh sessions');
1557
+ }
1558
+ }
1559
+ }
1560
+ /**
1561
+ * Handle Streamable HTTP request (MCP 2025-03-26)
1562
+ * Returns tuple of [response, sessionId] for session tracking
1563
+ */
1564
+ async handleStreamableRequest(request, req) {
1565
+ const { method, params, id } = request;
1566
+ const sessionId = req?.headers['mcp-session-id'];
1567
+ // Validate JSON-RPC 2.0 structure
1568
+ if (!request.jsonrpc || request.jsonrpc !== '2.0') {
1569
+ return [{
1570
+ jsonrpc: '2.0',
1571
+ id,
1572
+ error: { code: -32600, message: 'Invalid Request: jsonrpc field must be "2.0"' }
1573
+ }, sessionId];
1574
+ }
1575
+ try {
1576
+ let result;
1577
+ let session;
1578
+ let responseSessionId;
1579
+ // Handle session-aware methods
1580
+ if (method === 'initialize') {
1581
+ const initParams = params;
1582
+ // Protocol version negotiation per spec 2025-11-25
1583
+ const requestedVersion = initParams.protocolVersion;
1584
+ const supportedVersions = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05']; // List in order of preference
1585
+ let negotiatedVersion = '2025-11-25'; // Default to latest
1586
+ if (requestedVersion) {
1587
+ if (supportedVersions.includes(requestedVersion)) {
1588
+ // Client requested supported version
1589
+ negotiatedVersion = requestedVersion;
1590
+ console.log(`[MCP] Protocol version negotiated: ${requestedVersion}`);
1591
+ }
1592
+ else {
1593
+ // Client requested unsupported version - offer fallback chain
1594
+ console.log(`[MCP] Unsupported protocol version requested: ${requestedVersion}. Offering fallback to ${negotiatedVersion}`);
1595
+ // Return error with supported versions for client to retry with fallback
1596
+ return [{
1597
+ jsonrpc: '2.0',
1598
+ id,
1599
+ error: {
1600
+ code: -32602,
1601
+ message: `Unsupported protocol version: ${requestedVersion}`,
1602
+ data: {
1603
+ requested: requestedVersion,
1604
+ supported: supportedVersions,
1605
+ recommended: negotiatedVersion
1606
+ }
1607
+ }
1608
+ }, undefined];
1609
+ }
1610
+ }
1611
+ // Create or retrieve session
1612
+ let newSessionId;
1613
+ if (sessionId && this.getSession(sessionId)) {
1614
+ // Session ID provided and exists - reuse it
1615
+ newSessionId = sessionId;
1616
+ }
1617
+ else if (sessionId) {
1618
+ // Session ID provided but doesn't exist - create it with that ID
1619
+ this.sessions.set(sessionId, {
1620
+ id: sessionId,
1621
+ createdAt: Date.now(),
1622
+ lastActivity: Date.now(),
1623
+ clientInfo: initParams.clientInfo,
1624
+ initialized: false,
1625
+ lastEventId: 0,
1626
+ protocolVersion: '2025-06-18'
1627
+ });
1628
+ newSessionId = sessionId;
1629
+ }
1630
+ else {
1631
+ // No session ID provided - create new one
1632
+ newSessionId = this.createSession(initParams.clientInfo);
1633
+ }
1634
+ session = this.getSession(newSessionId);
1635
+ session.initialized = true;
1636
+ session.protocolVersion = negotiatedVersion;
1637
+ session.userAgent = req?.headers?.['user-agent'] || 'unknown';
1638
+ session.lastEventId = 0;
1639
+ responseSessionId = newSessionId;
1640
+ // Store client capabilities for per-client feature adaptation
1641
+ if (initParams.capabilities) {
1642
+ session.clientCapabilities = initParams.capabilities;
1643
+ }
1644
+ // Extract callback URL from request headers for bidirectional communication (MCP callback protocol)
1645
+ if (req) {
1646
+ const callbackUrl = req.headers['x-callback-url'];
1647
+ const callbackPort = req.headers['x-callback-port'];
1648
+ if (callbackUrl || callbackPort) {
1649
+ const url = callbackUrl || `http://127.0.0.1:${callbackPort}/mcp`;
1650
+ session.callbackUrl = url;
1651
+ session.callbackPort = callbackPort ? parseInt(callbackPort, 10) : undefined;
1652
+ console.log(`[MCP] Callback registered: ${url}`);
1653
+ }
1654
+ }
1655
+ console.log(`[MCP] Initialize: ${newSessionId} | Protocol: ${negotiatedVersion} | Client: ${initParams.clientInfo?.name}/${initParams.clientInfo?.version}`);
1656
+ result = {
1657
+ protocolVersion: negotiatedVersion,
1658
+ capabilities: {
1659
+ tools: { listChanged: true },
1660
+ prompts: { listChanged: false },
1661
+ resources: { listChanged: false }
1662
+ },
1663
+ serverInfo: {
1664
+ name: 'metalink',
1665
+ version
1666
+ }
1667
+ };
1668
+ return [{
1669
+ jsonrpc: '2.0',
1670
+ id,
1671
+ result
1672
+ }, responseSessionId];
1673
+ }
1674
+ else if (method === 'notifications/initialized') {
1675
+ return [null, sessionId]; // No response for notifications
1676
+ }
1677
+ else {
1678
+ // Allow stateless operation without session
1679
+ responseSessionId = sessionId;
1680
+ if (sessionId) {
1681
+ session = this.getSession(sessionId);
1682
+ if (!session) {
1683
+ // FALLBACK: This should not be reached - caller validates session before calling this function.
1684
+ // If we get here, it means session was invalidated between caller check and this check.
1685
+ // Per MCP spec, should return HTTP 404, but we can't from this function.
1686
+ // Caller handles HTTP 404 at line ~2067. This is defensive programming only.
1687
+ console.error(`[MCP] Session validation fallback triggered for: ${sessionId.substring(0, 8)}...`);
1688
+ return [{
1689
+ jsonrpc: '2.0',
1690
+ id,
1691
+ error: { code: -32603, message: 'Invalid or expired session' }
1692
+ }, sessionId];
1693
+ }
1694
+ }
1695
+ // Handle other MCP methods
1696
+ if (method === 'ping') {
1697
+ // MCP spec: ping method for keepalive/health checks
1698
+ result = {};
1699
+ }
1700
+ else if (method === 'roots/list') {
1701
+ result = { roots: [] };
1702
+ }
1703
+ else if (method === 'prompts/list') {
1704
+ result = { prompts: getPromptsList() };
1705
+ }
1706
+ else if (method === 'resources/list') {
1707
+ result = { resources: getResourcesList() };
1708
+ }
1709
+ else if (method === 'resources/templates/list') {
1710
+ result = { resourceTemplates: getResourceTemplatesList() };
1711
+ }
1712
+ else if (method === 'prompts/get') {
1713
+ const promptParams = params;
1714
+ if (!promptParams.name) {
1715
+ throw new InvalidParamsError('Missing required parameter: name');
1716
+ }
1717
+ try {
1718
+ result = getPrompt(promptParams.name, promptParams.arguments || {});
1719
+ }
1720
+ catch (err) {
1721
+ throw new InvalidParamsError(err instanceof Error ? err.message : 'Unknown prompt error');
1722
+ }
1723
+ }
1724
+ else if (method === 'resources/read') {
1725
+ const resourceParams = params;
1726
+ if (!resourceParams.uri) {
1727
+ throw new InvalidParamsError('Missing required parameter: uri');
1728
+ }
1729
+ try {
1730
+ result = await readResource(resourceParams.uri, this.serverManager, this.configLoader);
1731
+ }
1732
+ catch (err) {
1733
+ throw new InvalidParamsError(err instanceof Error ? err.message : 'Unknown resource error');
1734
+ }
1735
+ }
1736
+ else if (method === 'tools/list') {
1737
+ // Auto-reinitialize session if not initialized (transparent to client)
1738
+ // This handles stale sessions from server restarts without client needing to retry
1739
+ if (!session?.initialized) {
1740
+ const newSessionId = this.createSession({ name: 'auto-reinit', version: '1.0' });
1741
+ const newSession = this.getSession(newSessionId);
1742
+ newSession.initialized = true;
1743
+ newSession.protocolVersion = '2025-06-18';
1744
+ responseSessionId = newSessionId;
1745
+ console.log(`[MCP] Auto-reinitialized session for tools/list: ${newSessionId}`);
1746
+ }
1747
+ result = await this.mcpListTools();
1748
+ }
1749
+ else if (method === 'tools/call') {
1750
+ // Auto-reinitialize session if not initialized (transparent to client)
1751
+ // This handles stale sessions from server restarts without client needing to retry
1752
+ let effectiveSessionId = sessionId;
1753
+ if (!session?.initialized) {
1754
+ const newSessionId = this.createSession({ name: 'auto-reinit', version: '1.0' });
1755
+ const newSession = this.getSession(newSessionId);
1756
+ newSession.initialized = true;
1757
+ newSession.protocolVersion = '2025-06-18';
1758
+ responseSessionId = newSessionId;
1759
+ effectiveSessionId = newSessionId;
1760
+ console.log(`[MCP] Auto-reinitialized session for tools/call: ${newSessionId}`);
1761
+ }
1762
+ const callParams = params;
1763
+ if (!callParams.name) {
1764
+ throw new InvalidParamsError('Missing required parameter: name');
1765
+ }
1766
+ result = await this.mcpCallTool(callParams.name, callParams.arguments, effectiveSessionId);
1767
+ }
1768
+ else {
1769
+ return [{
1770
+ jsonrpc: '2.0',
1771
+ id,
1772
+ error: { code: -32601, message: `Unknown method: ${method}` }
1773
+ }, sessionId];
1774
+ }
1775
+ }
1776
+ return [{
1777
+ jsonrpc: '2.0',
1778
+ id,
1779
+ result
1780
+ }, responseSessionId];
1781
+ }
1782
+ catch (error) {
1783
+ // SessionNotInitializedError must bubble up to trigger HTTP 404
1784
+ // This allows client auto-reinitialize handlers to work per MCP spec
1785
+ if (error instanceof SessionNotInitializedError) {
1786
+ throw error;
1787
+ }
1788
+ // Check if this is an invalid parameters error (JSON-RPC -32602)
1789
+ const isInvalidParams = error instanceof InvalidParamsError;
1790
+ return [{
1791
+ jsonrpc: '2.0',
1792
+ id,
1793
+ error: {
1794
+ code: isInvalidParams ? -32602 : -32603,
1795
+ message: error instanceof Error ? error.message : 'Internal error'
1796
+ }
1797
+ }, sessionId];
1798
+ }
1799
+ }
1800
+ /**
1801
+ * Handle MCP JSON-RPC requests over HTTP
1802
+ */
1803
+ async handleMcpRequest(req, res) {
1804
+ const requestStartTime = Date.now();
1805
+ const userAgent = req.headers['user-agent'] || 'unknown';
1806
+ const sessionId = req.headers['mcp-session-id'];
1807
+ const requestId = req.requestId || generateRequestId();
1808
+ // Create child logger with request context
1809
+ const reqLogger = logger.child({
1810
+ requestId,
1811
+ sessionId,
1812
+ method: req.method,
1813
+ userAgent,
1814
+ });
1815
+ // Log incoming MCP request
1816
+ reqLogger.info('MCP request received', {
1817
+ path: req.path,
1818
+ protocol: req.headers['mcp-protocol-version'],
1819
+ });
1820
+ try {
1821
+ // Handle GET requests per MCP spec 2025-06-18 - SSE streaming for server-sent events
1822
+ if (req.method === 'GET') {
1823
+ const sessionId = req.headers['mcp-session-id'];
1824
+ // If no session ID, handle health check with JSON response
1825
+ if (!sessionId) {
1826
+ const responseTime = Date.now() - requestStartTime;
1827
+ reqLogger.info('Health check request', {
1828
+ accept: req.headers.accept,
1829
+ protocolVersion: req.headers['mcp-protocol-version'],
1830
+ });
1831
+ // Health check - always return JSON regardless of Accept header
1832
+ res.setHeader('Content-Type', 'application/json');
1833
+ res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
1834
+ res.setHeader('X-Request-Id', requestId);
1835
+ res.status(200).json({ status: 'ready', protocol: MCP_PROTOCOL_VERSION });
1836
+ reqLogger.info('Health check response sent', {
1837
+ status: 200,
1838
+ duration_ms: responseTime,
1839
+ });
1840
+ return;
1841
+ }
1842
+ // Session-based requests - set up SSE streaming with Last-Event-ID resumption support
1843
+ res.setHeader('Content-Type', 'text/event-stream');
1844
+ res.setHeader('Cache-Control', 'no-cache');
1845
+ res.setHeader('Connection', 'keep-alive');
1846
+ res.setHeader('X-Accel-Buffering', 'no');
1847
+ res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
1848
+ const session = this.getSession(sessionId);
1849
+ if (!session?.initialized) {
1850
+ res.status(404).json({ error: 'Session not found or not initialized' });
1851
+ return;
1852
+ }
1853
+ res.setHeader('Mcp-Session-Id', sessionId);
1854
+ // Check for Last-Event-ID header (per MCP 2025-06-18 spec for resumption)
1855
+ const lastEventIdHeader = req.headers['last-event-id'];
1856
+ const lastEventId = lastEventIdHeader ? parseInt(String(lastEventIdHeader), 10) : undefined;
1857
+ if (lastEventId !== undefined) {
1858
+ reqLogger.info('SSE stream resumption requested', {
1859
+ lastEventId,
1860
+ transport: 'sse',
1861
+ });
1862
+ }
1863
+ else {
1864
+ reqLogger.info('New SSE stream connection', {
1865
+ transport: 'sse',
1866
+ });
1867
+ }
1868
+ // Track SSE connection for server-sent messages
1869
+ this.sseConnections.set(sessionId, res);
1870
+ // Send buffered events for resumption (if Last-Event-ID was provided)
1871
+ if (lastEventId !== undefined) {
1872
+ const bufferedEvents = this.getEventsForResumption(sessionId, lastEventId);
1873
+ if (bufferedEvents.length > 0) {
1874
+ reqLogger.debug('Sending buffered events for resumption', {
1875
+ eventCount: bufferedEvents.length,
1876
+ transport: 'sse',
1877
+ });
1878
+ for (const event of bufferedEvents) {
1879
+ res.write(`id: ${event.id}\ndata: ${JSON.stringify(event.data)}\n\n`);
1880
+ }
1881
+ }
1882
+ }
1883
+ else {
1884
+ // Send initial endpoint event ONLY on new connection (not on resumption)
1885
+ // Resuming connections should only receive buffered events, not duplicate endpoint events
1886
+ const endpointEventId = this.trackSseEvent(sessionId, { type: 'endpoint' });
1887
+ res.write(`id: ${endpointEventId}\ndata: ${JSON.stringify({ type: 'endpoint' })}\n\n`);
1888
+ // Update session's last event ID
1889
+ if (session) {
1890
+ session.lastEventId = endpointEventId;
1891
+ }
1892
+ }
1893
+ // Send periodic keepalive comments to prevent connection timeout
1894
+ // SSE comments (lines starting with :) are ignored by clients but keep stream active
1895
+ // This is critical for clients like mcp-remote that rely on async iterators
1896
+ const keepaliveInterval = setInterval(() => {
1897
+ try {
1898
+ res.write(':keepalive\n\n');
1899
+ }
1900
+ catch (error) {
1901
+ reqLogger.debug('SSE keepalive write failed', {
1902
+ error: error instanceof Error ? error.message : 'Unknown error',
1903
+ transport: 'sse',
1904
+ });
1905
+ clearInterval(keepaliveInterval);
1906
+ }
1907
+ }, 15000); // Send keepalive every 15 seconds
1908
+ // Clean up on disconnect
1909
+ req.on('close', () => {
1910
+ clearInterval(keepaliveInterval);
1911
+ this.sseConnections.delete(sessionId);
1912
+ reqLogger.info('SSE connection closed', {
1913
+ transport: 'sse',
1914
+ });
1915
+ });
1916
+ // Keep connection open for server-sent events
1917
+ return;
1918
+ }
1919
+ // Check Accept header for SSE support
1920
+ // Per HTTP spec, Accept header values left-to-right indicate preference
1921
+ // "application/json, text/event-stream" means JSON is preferred (primary), SSE is fallback
1922
+ // Only use SSE if:
1923
+ // 1. Client explicitly requests ONLY SSE ("text/event-stream")
1924
+ // 2. OR client doesn't offer JSON as an option
1925
+ const acceptHeader = req.headers.accept || 'application/json';
1926
+ const hasJSON = acceptHeader.includes('application/json');
1927
+ const hasSSE = acceptHeader.includes('text/event-stream');
1928
+ // Use SSE only if SSE is available AND JSON is NOT preferred
1929
+ const useSSE = hasSSE && !hasJSON;
1930
+ // Debug: Log all headers to understand mcp-remote communication
1931
+ console.log('[MCP] POST request headers:', {
1932
+ 'x-callback-url': req.headers['x-callback-url'],
1933
+ 'x-callback-port': req.headers['x-callback-port'],
1934
+ 'mcp-session-id': req.headers['mcp-session-id'],
1935
+ 'user-agent': req.headers['user-agent'],
1936
+ 'content-type': req.headers['content-type'],
1937
+ 'accept': req.headers['accept']
1938
+ });
1939
+ console.log(`[MCP] SSE mode: ${useSSE} (based on Accept header: "${acceptHeader}")`);
1940
+ // Parse raw body with robust error handling
1941
+ let text = '';
1942
+ try {
1943
+ if (!req.body) {
1944
+ // Empty body - valid for some requests
1945
+ text = '';
1946
+ }
1947
+ else if (typeof req.body === 'string') {
1948
+ text = req.body;
1949
+ }
1950
+ else if (Buffer.isBuffer(req.body)) {
1951
+ text = req.body.toString('utf-8');
1952
+ }
1953
+ else {
1954
+ // Unknown body type - try to convert
1955
+ text = String(req.body);
1956
+ }
1957
+ }
1958
+ catch (parseError) {
1959
+ console.error('[MCP] Body parsing error:', parseError);
1960
+ res.status(400).json({
1961
+ jsonrpc: '2.0',
1962
+ error: {
1963
+ code: -32700,
1964
+ message: 'Invalid request body'
1965
+ },
1966
+ id: null
1967
+ });
1968
+ return;
1969
+ }
1970
+ if (!text || text.trim().length === 0) {
1971
+ res.setHeader('Content-Type', 'application/json');
1972
+ res.send('');
1973
+ return;
1974
+ }
1975
+ // Parse JSON-RPC requests (support both batch arrays and newline-delimited)
1976
+ const responses = [];
1977
+ let lastSessionId;
1978
+ let requests = [];
1979
+ let isBatchRequest = false;
1980
+ // Try to parse as JSON array (batch request) first
1981
+ try {
1982
+ const parsed = JSON.parse(text.trim());
1983
+ if (Array.isArray(parsed)) {
1984
+ // JSON-RPC 2.0 batch request
1985
+ console.log(`[MCP Request] Batch request with ${parsed.length} requests`);
1986
+ requests = parsed;
1987
+ isBatchRequest = true;
1988
+ }
1989
+ else {
1990
+ // Single request (most common case)
1991
+ requests = [parsed];
1992
+ }
1993
+ }
1994
+ catch (parseError) {
1995
+ // Fallback to newline-delimited format
1996
+ const lines = text.split('\n').filter((line) => line.trim());
1997
+ console.log(`[MCP Request] Newline-delimited format with ${lines.length} lines`);
1998
+ for (const line of lines) {
1999
+ try {
2000
+ requests.push(JSON.parse(line));
2001
+ }
2002
+ catch (lineError) {
2003
+ responses.push({
2004
+ jsonrpc: '2.0',
2005
+ error: { code: -32700, message: 'Parse error' },
2006
+ id: null
2007
+ });
2008
+ }
2009
+ }
2010
+ }
2011
+ // Log request body for debugging
2012
+ if (requests.length > 0) {
2013
+ console.log(`[MCP Request] Processing ${requests.length} request(s)`);
2014
+ requests.forEach((req, idx) => {
2015
+ const truncatedParams = JSON.stringify(req.params || {}).substring(0, 200);
2016
+ console.log(`[MCP Request] ${idx + 1}: method="${req.method}" id="${req.id}" params=${truncatedParams}${JSON.stringify(req.params || {}).length > 200 ? '...' : ''}`);
2017
+ });
2018
+ }
2019
+ for (const request of requests) {
2020
+ try {
2021
+ // Detect protocol version and route appropriately
2022
+ if (request.method === 'initialize') {
2023
+ const protocolVersion = request.params?.protocolVersion;
2024
+ if (protocolVersion === '2024-11-05') {
2025
+ // Legacy protocol path
2026
+ const response = await this.handleJsonRpcRequest(request);
2027
+ if (response)
2028
+ responses.push(response);
2029
+ }
2030
+ else {
2031
+ // New Streamable HTTP protocol (default to 2025-06-18)
2032
+ const [response, sId] = await this.handleStreamableRequest(request, req);
2033
+ if (response) {
2034
+ responses.push(response);
2035
+ if (sId)
2036
+ lastSessionId = sId;
2037
+ }
2038
+ }
2039
+ }
2040
+ else {
2041
+ // For non-initialize requests, detect by session presence in header
2042
+ const headerSessionId = req.headers['mcp-session-id'];
2043
+ if (headerSessionId) {
2044
+ // Session ID provided - check if it exists
2045
+ if (!this.getSession(headerSessionId)) {
2046
+ // Session doesn't exist - auto-create and initialize (transparent recovery)
2047
+ // This handles stale sessions from server restarts without client needing to reinitialize
2048
+ const newSessionId = this.createSession({ name: 'auto-reinit', version: '1.0' });
2049
+ const newSession = this.getSession(newSessionId);
2050
+ newSession.initialized = true;
2051
+ newSession.protocolVersion = '2025-06-18';
2052
+ console.log(`[MCP] Auto-reinitialized stale session ${headerSessionId.substring(0, 8)}... → ${newSessionId.substring(0, 8)}...`);
2053
+ // Update header for downstream processing (hacky but works)
2054
+ req.headers['mcp-session-id'] = newSessionId;
2055
+ }
2056
+ // Has valid session (original or auto-created) → use Streamable HTTP
2057
+ const [response, sId] = await this.handleStreamableRequest(request, req);
2058
+ if (response) {
2059
+ responses.push(response);
2060
+ if (sId)
2061
+ lastSessionId = sId;
2062
+ }
2063
+ }
2064
+ else {
2065
+ // No session → use legacy protocol
2066
+ const response = await this.handleJsonRpcRequest(request);
2067
+ if (response)
2068
+ responses.push(response);
2069
+ }
2070
+ }
2071
+ }
2072
+ catch (parseError) {
2073
+ responses.push({
2074
+ jsonrpc: '2.0',
2075
+ error: { code: -32700, message: 'Parse error' },
2076
+ id: null
2077
+ });
2078
+ }
2079
+ }
2080
+ // Send responses
2081
+ if (responses.length === 0) {
2082
+ const responseTime = Date.now() - requestStartTime;
2083
+ console.log(`[MCP Response] ${new Date().toISOString()} | Empty response (202) | Time: ${responseTime}ms | User-Agent: ${userAgent}`);
2084
+ res.status(202).end();
2085
+ return;
2086
+ }
2087
+ // Respect the Accept header: if client requests SSE, send SSE
2088
+ // Callback port is independent - it's for fallback/bidirectional, not transport selection
2089
+ const shouldUseSSE = useSSE;
2090
+ // Check if session has callback URL registered
2091
+ const session = lastSessionId ? this.getSession(lastSessionId) : undefined;
2092
+ const hasCallback = session?.callbackUrl !== undefined;
2093
+ const responseTime = Date.now() - requestStartTime;
2094
+ if (shouldUseSSE) {
2095
+ // Server-Sent Events format
2096
+ console.log(`[MCP] Sending ${responses.length} response(s) in SSE format`);
2097
+ res.setHeader('Content-Type', 'text/event-stream');
2098
+ res.setHeader('Cache-Control', 'no-cache');
2099
+ res.setHeader('Connection', 'keep-alive');
2100
+ res.setHeader('X-Accel-Buffering', 'no');
2101
+ res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
2102
+ for (const response of responses) {
2103
+ const dataStr = `data: ${JSON.stringify(response)}\n\n`;
2104
+ console.log(`[MCP] Writing SSE data: ${dataStr.substring(0, 100)}...`);
2105
+ res.write(dataStr);
2106
+ }
2107
+ console.log(`[MCP] Ending SSE response`);
2108
+ res.end();
2109
+ // Log SSE response summary
2110
+ console.log(`[MCP Response] ${new Date().toISOString()} | SSE | Responses: ${responses.length} | Time: ${responseTime}ms | User-Agent: ${userAgent}`);
2111
+ responses.forEach((resp, idx) => {
2112
+ const truncated = JSON.stringify(resp).substring(0, 300);
2113
+ console.log(`[MCP Response] SSE ${idx + 1}: ${truncated}${JSON.stringify(resp).length > 300 ? '...' : ''}`);
2114
+ });
2115
+ }
2116
+ else {
2117
+ // Standard JSON (array for batch, newline-delimited for multiple single requests)
2118
+ reqLogger.debug('Sending JSON response', {
2119
+ responseCount: responses.length,
2120
+ batch: isBatchRequest,
2121
+ transport: 'json',
2122
+ });
2123
+ res.setHeader('Content-Type', 'application/json');
2124
+ res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
2125
+ res.setHeader('X-Request-Id', requestId);
2126
+ // Set Mcp-Session-Id header if session exists
2127
+ if (lastSessionId) {
2128
+ res.setHeader('Mcp-Session-Id', lastSessionId);
2129
+ }
2130
+ let responseText;
2131
+ if (isBatchRequest) {
2132
+ // JSON-RPC 2.0 batch response: return as JSON array
2133
+ responseText = JSON.stringify(responses);
2134
+ }
2135
+ else {
2136
+ // Newline-delimited format (backward compatibility)
2137
+ responseText = responses.map(r => JSON.stringify(r)).join('\n');
2138
+ }
2139
+ res.write(responseText);
2140
+ res.end();
2141
+ reqLogger.info('MCP response sent', {
2142
+ responseCount: responses.length,
2143
+ duration_ms: responseTime,
2144
+ transport: 'json',
2145
+ batch: isBatchRequest,
2146
+ });
2147
+ }
2148
+ }
2149
+ catch (error) {
2150
+ const responseTime = Date.now() - requestStartTime;
2151
+ // SessionNotInitializedError returns HTTP 404 per MCP spec
2152
+ // This triggers client auto-reinitialize handlers
2153
+ if (error instanceof SessionNotInitializedError) {
2154
+ reqLogger.info('Session not initialized - returning HTTP 404 for client auto-reinitialize', {
2155
+ error: error.message,
2156
+ duration_ms: responseTime,
2157
+ });
2158
+ res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
2159
+ res.setHeader('X-Request-Id', requestId);
2160
+ res.status(404).json({ error: error.message });
2161
+ return;
2162
+ }
2163
+ reqLogger.error('MCP request failed', {
2164
+ error: error instanceof Error ? error.message : 'Internal error',
2165
+ stack: error instanceof Error ? error.stack : undefined,
2166
+ duration_ms: responseTime,
2167
+ });
2168
+ res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
2169
+ res.setHeader('X-Request-Id', requestId);
2170
+ res.status(500).json({ error: error instanceof Error ? error.message : 'Internal error' });
2171
+ }
2172
+ }
2173
+ /**
2174
+ * Handle JSON-RPC method routing
2175
+ */
2176
+ async handleJsonRpcRequest(request) {
2177
+ const { method, params, id } = request;
2178
+ // Validate JSON-RPC 2.0 structure
2179
+ if (!request.jsonrpc || request.jsonrpc !== '2.0') {
2180
+ return {
2181
+ jsonrpc: '2.0',
2182
+ id,
2183
+ error: { code: -32600, message: 'Invalid Request: jsonrpc field must be "2.0"' }
2184
+ };
2185
+ }
2186
+ try {
2187
+ let result;
2188
+ // Handle MCP methods
2189
+ if (method === 'notifications/initialized') {
2190
+ return null; // No response for notifications
2191
+ }
2192
+ if (method === 'initialize') {
2193
+ result = {
2194
+ protocolVersion: '2024-11-05',
2195
+ capabilities: {
2196
+ tools: { listChanged: true },
2197
+ prompts: { listChanged: false },
2198
+ resources: { listChanged: false },
2199
+ },
2200
+ serverInfo: {
2201
+ name: 'metalink',
2202
+ version,
2203
+ },
2204
+ };
2205
+ }
2206
+ else if (method === 'ping') {
2207
+ // MCP spec: ping method for keepalive/health checks
2208
+ result = {};
2209
+ }
2210
+ else if (method === 'roots/list') {
2211
+ result = { roots: [] };
2212
+ }
2213
+ else if (method === 'prompts/list') {
2214
+ result = { prompts: getPromptsList() };
2215
+ }
2216
+ else if (method === 'resources/list') {
2217
+ result = { resources: getResourcesList() };
2218
+ }
2219
+ else if (method === 'resources/templates/list') {
2220
+ result = { resourceTemplates: getResourceTemplatesList() };
2221
+ }
2222
+ else if (method === 'prompts/get') {
2223
+ const promptParams = params;
2224
+ if (!promptParams.name) {
2225
+ throw new InvalidParamsError('Missing required parameter: name');
2226
+ }
2227
+ try {
2228
+ result = getPrompt(promptParams.name, promptParams.arguments || {});
2229
+ }
2230
+ catch (err) {
2231
+ throw new InvalidParamsError(err instanceof Error ? err.message : 'Unknown prompt error');
2232
+ }
2233
+ }
2234
+ else if (method === 'resources/read') {
2235
+ const resourceParams = params;
2236
+ if (!resourceParams.uri) {
2237
+ throw new InvalidParamsError('Missing required parameter: uri');
2238
+ }
2239
+ try {
2240
+ result = await readResource(resourceParams.uri, this.serverManager, this.configLoader);
2241
+ }
2242
+ catch (err) {
2243
+ throw new InvalidParamsError(err instanceof Error ? err.message : 'Unknown resource error');
2244
+ }
2245
+ }
2246
+ else if (method === 'tools/list') {
2247
+ result = await this.mcpListTools();
2248
+ }
2249
+ else if (method === 'tools/call') {
2250
+ const callParams = params;
2251
+ if (!callParams.name) {
2252
+ throw new InvalidParamsError('Missing required parameter: name');
2253
+ }
2254
+ result = await this.mcpCallTool(callParams.name, callParams.arguments);
2255
+ }
2256
+ else {
2257
+ throw new MethodNotFoundError(method);
2258
+ }
2259
+ return {
2260
+ jsonrpc: '2.0',
2261
+ id,
2262
+ result,
2263
+ };
2264
+ }
2265
+ catch (error) {
2266
+ console.error(`[MCP] Error handling ${method}:`, error);
2267
+ // Check for specific JSON-RPC error types
2268
+ const isMethodNotFound = error instanceof MethodNotFoundError;
2269
+ const isInvalidParams = error instanceof InvalidParamsError;
2270
+ // Determine appropriate error code
2271
+ let errorCode = -32603; // Default: Internal error
2272
+ if (isMethodNotFound) {
2273
+ errorCode = -32601; // Method not found
2274
+ }
2275
+ else if (isInvalidParams) {
2276
+ errorCode = -32602; // Invalid params
2277
+ }
2278
+ return {
2279
+ jsonrpc: '2.0',
2280
+ id,
2281
+ error: {
2282
+ code: errorCode,
2283
+ message: error instanceof Error ? error.message : 'Internal error',
2284
+ },
2285
+ };
2286
+ }
2287
+ }
2288
+ /**
2289
+ * Generate execute_tool description
2290
+ * v1.3.x: Simplified - removed "Safe by default" list that caused confusion.
2291
+ * Now relies solely on annotations.safety from search_tools results.
2292
+ */
2293
+ getExecuteToolDescription() {
2294
+ // v1.3.x: Removed dynamic server list - was causing Claude to ignore annotations.safety
2295
+ // The safety classification is now ONLY determined by checking annotations.safety in search_tools results
2296
+ return 'Execute tool (auto-approved for safe tools). BLOCKS risky tools - use execute_tool_confirm for risky tools. IMPORTANT: Check annotations.safety in search_tools results - use execute_tool for "safe" tools, execute_tool_confirm for "risky" tools. Safety annotation is authoritative.';
2297
+ }
2298
+ /**
2299
+ * MCP: tools/list - return available tools
2300
+ *
2301
+ * SPEAKEASY-INSPIRED 4-TOOL APPROACH (v1.3.52):
2302
+ * 1. search_tools - Fuzzy search across all servers
2303
+ * 2. describe_tool - Get full schema for specific tool
2304
+ * 3. execute_tool - Execute safe/read-only tools (BLOCKS risky tools)
2305
+ * 4. execute_tool_confirm - Execute risky/write tools (user confirmed)
2306
+ *
2307
+ * Optional: Base server tools (if dynamicToolExposure=true in config)
2308
+ */
2309
+ async mcpListTools() {
2310
+ const tools = [];
2311
+ // NO HARDCODING: Read base servers from config
2312
+ const baseServerNames = this.configLoader.getBaseServers();
2313
+ const dynamicToolExposure = this.config.dynamicToolExposure ?? false;
2314
+ /**
2315
+ * SPEAKEASY-INSPIRED 4-TOOL APPROACH (v1.3.52)
2316
+ *
2317
+ * Inspired by Speakeasy's token optimization strategy, we expose only 4 essential meta-tools:
2318
+ *
2319
+ * Discovery Tools (2):
2320
+ * 1. search_tools - Fuzzy search across all available tools
2321
+ * 2. describe_tool - Get full schema for a specific tool
2322
+ *
2323
+ * Execution Tools (2):
2324
+ * 3. execute_tool - Execute safe tools (auto-approved based on annotations.safety)
2325
+ * - BLOCKS risky tools with error
2326
+ * - Safety annotation overrides read/write heuristics
2327
+ * 4. execute_tool_confirm - Execute risky tools (requires user confirmation)
2328
+ * - ALLOWS any tool execution (user already confirmed)
2329
+ * - Logs classification but doesn't block
2330
+ *
2331
+ * Token usage: ~280 tokens (vs 1,300+ with full base server exposure)
2332
+ * See: https://www.speakeasy.com/blog/how-we-reduced-token-usage-by-100x-dynamic-toolsets-v2
2333
+ *
2334
+ * Legacy mode: Set dynamicToolExposure=true to expose all base server tools
2335
+ */
2336
+ tools.push({
2337
+ name: 'search_tools',
2338
+ description: 'Search tools by keyword across all available servers, or list all tools from a specific server. Can also search by server name to find all tools from matching servers. When exactly 1 tool matches, automatically includes full schema (inputSchema, requiredParams, example) to save a describe_tool call. Each result includes annotations.safety ("safe" or "risky") - use execute_tool for safe, execute_tool_confirm for risky.',
2339
+ inputSchema: {
2340
+ type: 'object',
2341
+ properties: {
2342
+ query: {
2343
+ type: 'string',
2344
+ description: 'Optional keyword to search for in server names, tool names, and descriptions. If query matches a server name, returns all tools from that server.',
2345
+ },
2346
+ server_name: {
2347
+ type: 'string',
2348
+ description: 'Optional server name to filter results. If provided, only returns tools from this specific server.',
2349
+ },
2350
+ },
2351
+ required: [],
2352
+ },
2353
+ }, {
2354
+ name: 'describe_tool',
2355
+ description: 'Get detailed schema for a specific tool including validation hints. Returns inputSchema, requiredParams, and optional validation warnings/suggestions when schema is incomplete or incorrect.',
2356
+ inputSchema: {
2357
+ type: 'object',
2358
+ properties: {
2359
+ server_name: {
2360
+ type: 'string',
2361
+ description: 'Server name (e.g., "memory", "jira-basic-auth")',
2362
+ },
2363
+ tool_name: {
2364
+ type: 'string',
2365
+ description: 'Tool name (e.g., "create_entities", "search_issues")',
2366
+ },
2367
+ },
2368
+ required: ['server_name', 'tool_name'],
2369
+ },
2370
+ }, {
2371
+ name: 'execute_tool',
2372
+ description: this.getExecuteToolDescription(),
2373
+ inputSchema: {
2374
+ type: 'object',
2375
+ properties: {
2376
+ server_name: {
2377
+ type: 'string',
2378
+ description: 'Server name (e.g., "jira-basic-auth", "memory")',
2379
+ example: 'jira-basic-auth'
2380
+ },
2381
+ tool_name: {
2382
+ type: 'string',
2383
+ description: 'Tool name (e.g., "confluence_search", "create_entities")',
2384
+ example: 'confluence_search'
2385
+ },
2386
+ arguments: {
2387
+ type: 'object',
2388
+ description: 'Tool-specific parameters. Call describe_tool(server_name, tool_name) first to discover required parameters, then nest those parameters here.',
2389
+ example: {
2390
+ cql: 'type=page ORDER BY created DESC',
2391
+ limit: 10
2392
+ },
2393
+ additionalProperties: true
2394
+ },
2395
+ max_results: {
2396
+ type: 'number',
2397
+ description: 'Optional: Limit array results to N items (for pagination)',
2398
+ example: 50
2399
+ },
2400
+ max_result_chars: {
2401
+ type: 'number',
2402
+ description: 'Optional: Limit total response size to N characters (default: 50000)',
2403
+ example: 10000
2404
+ },
2405
+ cursor: {
2406
+ type: 'string',
2407
+ description: 'Optional: Opaque cursor from previous response to continue pagination'
2408
+ }
2409
+ },
2410
+ required: ['server_name', 'tool_name', 'arguments'],
2411
+ additionalProperties: false
2412
+ }
2413
+ }, {
2414
+ name: 'execute_tool_confirm',
2415
+ description: 'Execute risky tool (requires user confirmation). IMPORTANT: Check annotations.safety in search_tools results - only use this for "risky" tools, use execute_tool for "safe" tools. Safety annotation is authoritative. Required for external services without explicit safe rules.',
2416
+ inputSchema: {
2417
+ type: 'object',
2418
+ properties: {
2419
+ server_name: {
2420
+ type: 'string',
2421
+ description: 'Server name (e.g., "jira-basic-auth", "memory")',
2422
+ example: 'memory'
2423
+ },
2424
+ tool_name: {
2425
+ type: 'string',
2426
+ description: 'Tool name (e.g., "create_issue", "delete_entities")',
2427
+ example: 'create_entities'
2428
+ },
2429
+ arguments: {
2430
+ type: 'object',
2431
+ description: 'Tool arguments object. REQUIRED. All tool-specific parameters MUST be nested inside this object, not at the top level.',
2432
+ example: {
2433
+ entities: [{ name: 'test', entityType: 'concept', observations: ['example data'] }]
2434
+ },
2435
+ additionalProperties: true
2436
+ },
2437
+ max_results: {
2438
+ type: 'number',
2439
+ description: 'Optional: Limit array results to N items (for pagination)',
2440
+ example: 50
2441
+ },
2442
+ max_result_chars: {
2443
+ type: 'number',
2444
+ description: 'Optional: Limit total response size to N characters (default: 50000)',
2445
+ example: 10000
2446
+ },
2447
+ cursor: {
2448
+ type: 'string',
2449
+ description: 'Optional: Opaque cursor from previous response to continue pagination'
2450
+ }
2451
+ },
2452
+ required: ['server_name', 'tool_name', 'arguments'],
2453
+ additionalProperties: false
2454
+ }
2455
+ });
2456
+ // OPTIONAL: Base server tools - controlled by config options (v1.3.57+)
2457
+ // Three modes:
2458
+ // 1. Speakeasy mode (default): base_servers_auto_expose_tools = false → only 4 meta-tools (~280 tokens)
2459
+ // 2. Base server exposure: base_servers_auto_expose_tools = true → expose base server tools (~1,300+ tokens)
2460
+ // 3. Legacy mode: dynamicToolExposure = true → expose all discovered tools
2461
+ const baseServersAutoExposeTools = this.config.base_servers_auto_expose_tools ?? false;
2462
+ if (dynamicToolExposure || baseServersAutoExposeTools) {
2463
+ const activeServers = this.serverManager.getActiveServers();
2464
+ for (const [serverName, serverData] of activeServers) {
2465
+ // Only expose base servers if baseServersAutoExposeTools is true
2466
+ // Or expose all servers if dynamicToolExposure is true (legacy mode)
2467
+ const isBaseServer = baseServerNames.includes(serverName);
2468
+ const shouldExpose = dynamicToolExposure || (isBaseServer && baseServersAutoExposeTools);
2469
+ if (shouldExpose && serverData.toolsReady && serverData.tools) {
2470
+ for (const tool of serverData.tools) {
2471
+ tools.push({
2472
+ name: `${serverName}-${tool.name}`,
2473
+ description: tool.description || `Tool from ${serverName} server`,
2474
+ inputSchema: tool.inputSchema || { type: 'object', properties: {} },
2475
+ });
2476
+ }
2477
+ }
2478
+ }
2479
+ }
2480
+ // OLD TOOLS (DEPRECATED in v1.3.0 - commented out for reference)
2481
+ // These have been replaced by discovery tools to save tokens
2482
+ // tools.push(
2483
+ // { name: 'list_available_servers', ... },
2484
+ // { name: 'enable_server', ... },
2485
+ // { name: 'disable_server', ... },
2486
+ // { name: 'list_servers', ... },
2487
+ // { name: 'list_tools', ... },
2488
+ // { name: 'execute_tool', ... },
2489
+ // { name: 'help', ... }
2490
+ // );
2491
+ return { tools };
2492
+ }
2493
+ /**
2494
+ * MCP: tools/call - route tool calls
2495
+ *
2496
+ * SECURITY: Includes per-server rate limiting to prevent DOS attacks.
2497
+ * Rate limit: 100 calls per server per minute (configurable).
2498
+ * Discovery endpoints (search_tools, describe_tool) have separate rate limiting.
2499
+ */
2500
+ async mcpCallTool(name, args, sessionId) {
2501
+ // Track metrics for tool call (Phase 4 - v1.4.0)
2502
+ const startTime = Date.now();
2503
+ globalMetrics.incrementCounter(`tool_calls_${name}`, 'calls');
2504
+ // SECURITY: Extract server name for rate limiting
2505
+ // For meta-tools (execute_tool, search_tools), extract from args
2506
+ // For direct calls (server-tool), extract from name
2507
+ const serverName = this.extractServerNameForRateLimit(name, args);
2508
+ // Record tool call for error rate tracking
2509
+ globalMetrics.recordToolCall(name, serverName || undefined);
2510
+ // Log tool call with structured context
2511
+ logger.info('Tool call initiated', {
2512
+ tool: name,
2513
+ server: serverName || undefined,
2514
+ sessionId,
2515
+ });
2516
+ // SECURITY: Check rate limit before proceeding
2517
+ if (serverName) {
2518
+ this.checkToolRateLimit(serverName);
2519
+ }
2520
+ // SECURITY: Discovery endpoint rate limiting (P1)
2521
+ // search_tools and describe_tool have separate rate limits per session
2522
+ if (name === 'search_tools' || name === 'describe_tool') {
2523
+ const rateLimitKey = sessionId || 'anonymous';
2524
+ this.checkDiscoveryRateLimit(rateLimitKey);
2525
+ }
2526
+ try {
2527
+ const result = await this.executeToolCall(name, args);
2528
+ // Record successful execution metrics
2529
+ const latency = Date.now() - startTime;
2530
+ globalMetrics.setGauge(`tool_latency_${name}`, latency, 'ms');
2531
+ // Record granular tool-specific metrics (Test 187)
2532
+ if (serverName) {
2533
+ globalMetrics.recordToolExecution(serverName, name.replace(`${serverName}-`, ''), latency);
2534
+ }
2535
+ // Log successful tool execution
2536
+ logger.info('Tool call completed', {
2537
+ tool: name,
2538
+ server: serverName || undefined,
2539
+ duration_ms: latency,
2540
+ });
2541
+ return result;
2542
+ }
2543
+ catch (error) {
2544
+ // Record error metrics with detailed tracking
2545
+ globalMetrics.incrementCounter(`tool_errors_${name}`, 'errors');
2546
+ globalMetrics.recordToolError(error, name, serverName || undefined);
2547
+ // Record granular tool-specific error (Test 187)
2548
+ if (serverName) {
2549
+ const errorMessage = error instanceof Error ? error.message : String(error);
2550
+ globalMetrics.recordToolFailure(serverName, name.replace(`${serverName}-`, ''), errorMessage);
2551
+ }
2552
+ // Log tool execution error
2553
+ const latency = Date.now() - startTime;
2554
+ logger.error('Tool call failed', {
2555
+ tool: name,
2556
+ server: serverName || undefined,
2557
+ error: error instanceof Error ? error.message : String(error),
2558
+ duration_ms: latency,
2559
+ });
2560
+ throw error;
2561
+ }
2562
+ }
2563
+ /**
2564
+ * SECURITY: Extract server name from tool call for rate limiting.
2565
+ *
2566
+ * @param name - Tool name (e.g., "execute_tool", "memory-search_nodes")
2567
+ * @param args - Tool arguments
2568
+ * @returns Server name or null for meta-tools without server context
2569
+ */
2570
+ extractServerNameForRateLimit(name, args) {
2571
+ // Meta-tools with explicit server_name argument
2572
+ if (name === 'execute_tool' || name === 'execute_tool_confirm' ||
2573
+ name === 'describe_tool' || name === 'list_tools') {
2574
+ const callArgs = args;
2575
+ return callArgs?.server_name || null;
2576
+ }
2577
+ // Discovery tools - no server-specific rate limiting
2578
+ if (name === 'search_tools' || name === 'list_available_servers' || name === 'list_servers') {
2579
+ return null;
2580
+ }
2581
+ // Direct tool calls: "server-toolName" format
2582
+ if (name.includes('-')) {
2583
+ const parts = name.split('-');
2584
+ return parts[0];
2585
+ }
2586
+ return null;
2587
+ }
2588
+ /**
2589
+ * SECURITY: Check and enforce per-server rate limiting.
2590
+ *
2591
+ * OWASP Reference: A6:2017-Security Misconfiguration - DOS Prevention
2592
+ *
2593
+ * @param serverName - Name of the server to check rate limit for
2594
+ * @throws Error if rate limit exceeded
2595
+ */
2596
+ checkToolRateLimit(serverName) {
2597
+ const now = Date.now();
2598
+ const limiter = this.toolExecutionRateLimiter.get(serverName);
2599
+ if (!limiter || now > limiter.resetTime) {
2600
+ // First call or window expired - reset counter
2601
+ this.toolExecutionRateLimiter.set(serverName, {
2602
+ count: 1,
2603
+ resetTime: now + this.TOOL_RATE_LIMIT_WINDOW_MS,
2604
+ });
2605
+ return;
2606
+ }
2607
+ // Increment counter
2608
+ limiter.count++;
2609
+ // Check if rate limit exceeded
2610
+ if (limiter.count > this.TOOL_RATE_LIMIT_MAX_CALLS) {
2611
+ const waitTime = Math.ceil((limiter.resetTime - now) / 1000);
2612
+ console.warn(`[SECURITY] Rate limit exceeded for server '${serverName}': ` +
2613
+ `${limiter.count} calls in window, max ${this.TOOL_RATE_LIMIT_MAX_CALLS}`);
2614
+ globalMetrics.incrementCounter(`rate_limit_exceeded_${serverName}`, 'rate_limits');
2615
+ throw new Error(`Rate limit exceeded for server '${serverName}'. ` +
2616
+ `Maximum ${this.TOOL_RATE_LIMIT_MAX_CALLS} tool calls per minute. ` +
2617
+ `Please wait ${waitTime} seconds before retrying.`);
2618
+ }
2619
+ this.toolExecutionRateLimiter.set(serverName, limiter);
2620
+ }
2621
+ /**
2622
+ * SECURITY: Check discovery endpoint rate limit (P1 - OWASP DOS Prevention)
2623
+ * Prevents abuse of search_tools and describe_tool endpoints
2624
+ *
2625
+ * @param sessionId - Session ID to track rate limit for
2626
+ * @throws Error if rate limit exceeded
2627
+ */
2628
+ checkDiscoveryRateLimit(sessionId) {
2629
+ const now = Date.now();
2630
+ const limiter = this.discoveryRateLimiter.get(sessionId);
2631
+ if (!limiter || now > limiter.resetTime) {
2632
+ // First call or window expired - reset counter
2633
+ this.discoveryRateLimiter.set(sessionId, {
2634
+ count: 1,
2635
+ resetTime: now + this.DISCOVERY_RATE_LIMIT_WINDOW_MS,
2636
+ });
2637
+ return;
2638
+ }
2639
+ // Increment counter
2640
+ limiter.count++;
2641
+ // Check if rate limit exceeded
2642
+ if (limiter.count > this.DISCOVERY_RATE_LIMIT_MAX_CALLS) {
2643
+ const waitTime = Math.ceil((limiter.resetTime - now) / 1000);
2644
+ console.warn(`[SECURITY] Discovery rate limit exceeded for session '${sessionId}': ` +
2645
+ `${limiter.count} calls in window, max ${this.DISCOVERY_RATE_LIMIT_MAX_CALLS}`);
2646
+ globalMetrics.incrementCounter('discovery_rate_limit_exceeded', 'rate_limits');
2647
+ throw new Error(`Discovery rate limit exceeded. ` +
2648
+ `Maximum ${this.DISCOVERY_RATE_LIMIT_MAX_CALLS} discovery calls per minute. ` +
2649
+ `Please wait ${waitTime} seconds before retrying.`);
2650
+ }
2651
+ this.discoveryRateLimiter.set(sessionId, limiter);
2652
+ }
2653
+ /**
2654
+ * Execute tool call logic (extracted for metrics tracking)
2655
+ */
2656
+ async executeToolCall(name, args) {
2657
+ const callArgs = args;
2658
+ switch (name) {
2659
+ case 'list_available_servers': {
2660
+ const allServers = this.configLoader.getAllServers();
2661
+ const enabledServers = this.configLoader.getServers();
2662
+ const enabledNames = new Set(enabledServers.map(s => s.name));
2663
+ return {
2664
+ content: [
2665
+ {
2666
+ type: 'text',
2667
+ text: JSON.stringify({
2668
+ servers: allServers.map(s => {
2669
+ const isStdio = s.transport === 'stdio' || s.transport === undefined;
2670
+ return {
2671
+ name: s.name,
2672
+ ...(isStdio ? { command: s.command } : { url: s.url }),
2673
+ enabled: enabledNames.has(s.name),
2674
+ };
2675
+ }),
2676
+ }, null, 2)
2677
+ }
2678
+ ]
2679
+ };
2680
+ }
2681
+ case 'list_servers': {
2682
+ const servers = this.configLoader.getServers();
2683
+ return {
2684
+ content: [
2685
+ {
2686
+ type: 'text',
2687
+ text: JSON.stringify({
2688
+ servers: servers.map(s => {
2689
+ const isStdio = s.transport === 'stdio' || s.transport === undefined;
2690
+ return {
2691
+ name: s.name,
2692
+ ...(isStdio ? { command: s.command } : { url: s.url }),
2693
+ status: this.serverManager.getServerStatus(s.name)?.status || 'stopped',
2694
+ toolCount: this.serverManager.getServerTools(s.name).length || 0,
2695
+ };
2696
+ }),
2697
+ }, null, 2)
2698
+ }
2699
+ ]
2700
+ };
2701
+ }
2702
+ case 'list_tools': {
2703
+ const serverName = callArgs?.server_name;
2704
+ if (!serverName)
2705
+ throw new InvalidParamsError('server_name required');
2706
+ // Get tools from server manager
2707
+ const tools = this.serverManager.getServerTools(serverName);
2708
+ return {
2709
+ content: [
2710
+ {
2711
+ type: 'text',
2712
+ text: JSON.stringify({
2713
+ server: serverName,
2714
+ tools: tools || [],
2715
+ }, null, 2)
2716
+ }
2717
+ ]
2718
+ };
2719
+ }
2720
+ case 'execute_tool': {
2721
+ const args = callArgs;
2722
+ // DEBUG: Log raw incoming args to diagnose Raycast/Grok issues
2723
+ console.log(`[DEBUG execute_tool] RAW INCOMING ARGS: ${JSON.stringify(args)}`);
2724
+ // Phase 2a: Detect and fix Raycast format issues
2725
+ const fixedArgs = this.serverManager.detectAndFixRaycastFormat(args);
2726
+ const serverName = fixedArgs.server_name;
2727
+ const toolName = fixedArgs.tool_name;
2728
+ if (!serverName || !toolName) {
2729
+ throw new InvalidParamsError('server_name and tool_name are required. Format: {"server_name": "memory", "tool_name": "search_nodes", "arguments": {...}}');
2730
+ }
2731
+ // v1.1.29: Extract tool arguments for argument-level safety inspection
2732
+ const toolArgs = fixedArgs.arguments || {};
2733
+ // SAFETY CHECK: Verify this tool is classified as 'safe' (with argument inspection)
2734
+ const safetyResult = this.serverManager.classifyToolSafety(serverName, toolName, toolArgs);
2735
+ if (safetyResult.safety === 'risky') {
2736
+ throw new InvalidParamsError(`Tool ${serverName}:${toolName} is classified as RISKY and requires user confirmation.\n` +
2737
+ `Use 'execute_tool_confirm' instead for this tool.\n` +
2738
+ `Classification reason: ${safetyResult.reason}`);
2739
+ }
2740
+ // Phase 2b: Get tool schema for validation
2741
+ const toolSchema = this.serverManager.getToolSchema(serverName, toolName);
2742
+ if (!toolSchema) {
2743
+ throw new InvalidParamsError(`Tool '${toolName}' not found in server '${serverName}'. Use describe_tool to get the tool schema or search_tools to find available tools.`);
2744
+ }
2745
+ // Phase 2c: Detect missing arguments
2746
+ const missingArgs = this.serverManager.detectMissingArguments(fixedArgs, toolSchema);
2747
+ if (missingArgs) {
2748
+ const inputSchema = toolSchema.inputSchema;
2749
+ const requiredParams = inputSchema?.required?.join(', ') || 'unknown';
2750
+ const errorMsg = `❌ Missing required parameters for ${serverName}:${toolName}\n` +
2751
+ `\n` +
2752
+ `Required: ${requiredParams}\n` +
2753
+ `\n` +
2754
+ `✅ CORRECT FORMAT:\n` +
2755
+ ` {"server_name": "${serverName}", "tool_name": "${toolName}", "arguments": {...}}\n` +
2756
+ `\n` +
2757
+ `For full schema: describe_tool('${serverName}', '${toolName}')`;
2758
+ throw new InvalidParamsError(errorMsg);
2759
+ }
2760
+ // Phase 2d: Validate required params exist inside arguments
2761
+ const args2 = fixedArgs.arguments || {};
2762
+ const inputSchema2 = toolSchema.inputSchema;
2763
+ const requiredParams2 = inputSchema2?.required || [];
2764
+ // Check if required params are missing from arguments
2765
+ const missingRequiredParams = requiredParams2.filter(param => !(param in args2));
2766
+ if (missingRequiredParams.length > 0) {
2767
+ // Generate example values for each required param
2768
+ const exampleArgs = {};
2769
+ for (const param of requiredParams2) {
2770
+ const propSchema = inputSchema2?.properties?.[param];
2771
+ if (propSchema?.example)
2772
+ exampleArgs[param] = propSchema.example;
2773
+ else if (propSchema?.default)
2774
+ exampleArgs[param] = propSchema.default;
2775
+ else if (propSchema?.type === 'string')
2776
+ exampleArgs[param] = `<${param}>`;
2777
+ else if (propSchema?.type === 'number')
2778
+ exampleArgs[param] = 10;
2779
+ else
2780
+ exampleArgs[param] = `<${param}>`;
2781
+ }
2782
+ const errorMsg = `❌ Missing required parameters for ${serverName}:${toolName}\n` +
2783
+ `\n` +
2784
+ `You provided: ${JSON.stringify(args2)}\n` +
2785
+ `Missing: ${missingRequiredParams.join(', ')}\n` +
2786
+ `\n` +
2787
+ `✅ CORRECT FORMAT:\n` +
2788
+ ` {"server_name": "${serverName}", "tool_name": "${toolName}", "arguments": ${JSON.stringify(exampleArgs)}}\n` +
2789
+ `\n` +
2790
+ `For full schema: describe_tool('${serverName}', '${toolName}')`;
2791
+ throw new InvalidParamsError(errorMsg);
2792
+ }
2793
+ console.log(`[MetaLink] execute_tool (SAFE): ${serverName}:${toolName} with args ${JSON.stringify(args2)}`);
2794
+ // Phase 2e: Auto-start server if not running
2795
+ const serverConfig = this.configLoader.getServer(serverName);
2796
+ if (serverConfig) {
2797
+ await this.serverManager.ensureServerStarted(serverName, serverConfig);
2798
+ }
2799
+ // Phase 3: Call the actual tool via response router
2800
+ try {
2801
+ const result = await this.serverManager.callTool(serverName, toolName, args2);
2802
+ // Phase 4: Apply pagination if parameters provided
2803
+ const paginationParams = {
2804
+ max_results: fixedArgs.max_results,
2805
+ max_result_chars: fixedArgs.max_result_chars,
2806
+ cursor: fixedArgs.cursor
2807
+ };
2808
+ // Only paginate if at least one pagination param is provided
2809
+ if (paginationParams.max_results || paginationParams.max_result_chars || paginationParams.cursor) {
2810
+ const paginated = this.paginateResult(result, paginationParams);
2811
+ // Return result with pagination metadata merged in
2812
+ if (typeof result === 'object' && result !== null && !Array.isArray(result)) {
2813
+ return {
2814
+ ...result,
2815
+ result: paginated.result,
2816
+ _pagination: paginated._pagination
2817
+ };
2818
+ }
2819
+ else {
2820
+ // If result is not an object (e.g., primitive or array), wrap it
2821
+ return {
2822
+ result: paginated.result,
2823
+ _pagination: paginated._pagination
2824
+ };
2825
+ }
2826
+ }
2827
+ return result;
2828
+ }
2829
+ catch (toolError) {
2830
+ // Enhanced error with inputSchema for debugging
2831
+ const errorMsg = `Tool execution failed for ${serverName}:${toolName}: ${toolError instanceof Error ? toolError.message : String(toolError)}`;
2832
+ const inputSchema = toolSchema.inputSchema;
2833
+ // Check if error is parameter-related
2834
+ const errorStr = (toolError instanceof Error ? toolError.message : String(toolError)).toLowerCase();
2835
+ const isParamError = errorStr.includes('null') || errorStr.includes('undefined') ||
2836
+ errorStr.includes('required') || errorStr.includes('missing');
2837
+ if (isParamError && inputSchema) {
2838
+ const requiredParams = inputSchema.required || [];
2839
+ const availableParams = Object.keys(inputSchema.properties || {});
2840
+ const optionalParams = availableParams.filter(p => !requiredParams.includes(p));
2841
+ const requiredList = requiredParams.length > 0 ? requiredParams.join(', ') : 'none';
2842
+ const optionalList = optionalParams.length > 0 ? optionalParams.join(', ') : 'none';
2843
+ const hint = `\n\n💡 Hint: This tool expects:\n` +
2844
+ ` Required: ${requiredList}\n` +
2845
+ ` Optional: ${optionalList}\n` +
2846
+ ` Use describe_tool('${serverName}', '${toolName}') for full schema.`;
2847
+ throw new Error(errorMsg + hint);
2848
+ }
2849
+ throw new Error(errorMsg);
2850
+ }
2851
+ }
2852
+ case 'execute_tool_confirm': {
2853
+ const args = callArgs;
2854
+ // Phase 2a: Detect and fix Raycast format issues
2855
+ const fixedArgs = this.serverManager.detectAndFixRaycastFormat(args);
2856
+ const serverName = fixedArgs.server_name;
2857
+ const toolName = fixedArgs.tool_name;
2858
+ if (!serverName || !toolName) {
2859
+ throw new InvalidParamsError('server_name and tool_name are required. Format: {"server_name": "task-master-ai", "tool_name": "set_task_status", "arguments": {...}}');
2860
+ }
2861
+ // v1.1.29: Extract tool arguments for argument-level safety inspection
2862
+ const toolArgs = fixedArgs.arguments || {};
2863
+ // SAFETY CHECK: Log classification (but allow regardless - user confirmed)
2864
+ const safetyResult = this.serverManager.classifyToolSafety(serverName, toolName, toolArgs);
2865
+ console.log(`[MetaLink] execute_tool_confirm (${safetyResult.safety.toUpperCase()}): ${serverName}:${toolName} - ${safetyResult.reason}`);
2866
+ // Phase 2b: Get tool schema for validation
2867
+ const toolSchema = this.serverManager.getToolSchema(serverName, toolName);
2868
+ if (!toolSchema) {
2869
+ throw new InvalidParamsError(`Tool '${toolName}' not found in server '${serverName}'. Use describe_tool to get the tool schema or search_tools to find available tools.`);
2870
+ }
2871
+ // Phase 2c: Detect missing arguments
2872
+ const missingArgs = this.serverManager.detectMissingArguments(fixedArgs, toolSchema);
2873
+ if (missingArgs) {
2874
+ const inputSchema = toolSchema.inputSchema;
2875
+ const requiredParams = inputSchema?.required?.join(', ') || 'unknown';
2876
+ const errorMsg = `❌ Missing required parameters for ${serverName}:${toolName}\n` +
2877
+ `\n` +
2878
+ `Required: ${requiredParams}\n` +
2879
+ `\n` +
2880
+ `✅ CORRECT FORMAT:\n` +
2881
+ ` {"server_name": "${serverName}", "tool_name": "${toolName}", "arguments": {...}}\n` +
2882
+ `\n` +
2883
+ `For full schema: describe_tool('${serverName}', '${toolName}')`;
2884
+ throw new InvalidParamsError(errorMsg);
2885
+ }
2886
+ // Phase 2d: Lenient mode - if arguments missing, default to empty object
2887
+ const args2 = fixedArgs.arguments || {};
2888
+ console.log(`[MetaLink] execute_tool_confirm: ${serverName}:${toolName} with args ${JSON.stringify(args2)}`);
2889
+ // Phase 2e: Auto-start server if not running
2890
+ const serverConfig = this.configLoader.getServer(serverName);
2891
+ if (serverConfig) {
2892
+ await this.serverManager.ensureServerStarted(serverName, serverConfig);
2893
+ }
2894
+ // Phase 3: Call the actual tool via response router
2895
+ try {
2896
+ const result = await this.serverManager.callTool(serverName, toolName, args2);
2897
+ // Phase 4: Apply pagination if parameters provided
2898
+ const paginationParams = {
2899
+ max_results: fixedArgs.max_results,
2900
+ max_result_chars: fixedArgs.max_result_chars,
2901
+ cursor: fixedArgs.cursor
2902
+ };
2903
+ // Only paginate if at least one pagination param is provided
2904
+ if (paginationParams.max_results || paginationParams.max_result_chars || paginationParams.cursor) {
2905
+ const paginated = this.paginateResult(result, paginationParams);
2906
+ // Return result with pagination metadata merged in
2907
+ if (typeof result === 'object' && result !== null && !Array.isArray(result)) {
2908
+ return {
2909
+ ...result,
2910
+ result: paginated.result,
2911
+ _pagination: paginated._pagination
2912
+ };
2913
+ }
2914
+ else {
2915
+ // If result is not an object (e.g., primitive or array), wrap it
2916
+ return {
2917
+ result: paginated.result,
2918
+ _pagination: paginated._pagination
2919
+ };
2920
+ }
2921
+ }
2922
+ return result;
2923
+ }
2924
+ catch (toolError) {
2925
+ // Enhanced error with inputSchema for debugging
2926
+ const errorMsg = `Tool execution failed for ${serverName}:${toolName}: ${toolError instanceof Error ? toolError.message : String(toolError)}`;
2927
+ const inputSchema = toolSchema.inputSchema;
2928
+ // Check if error is parameter-related
2929
+ const errorStr = (toolError instanceof Error ? toolError.message : String(toolError)).toLowerCase();
2930
+ const isParamError = errorStr.includes('null') || errorStr.includes('undefined') ||
2931
+ errorStr.includes('required') || errorStr.includes('missing');
2932
+ if (isParamError && inputSchema) {
2933
+ const requiredParams = inputSchema.required || [];
2934
+ const availableParams = Object.keys(inputSchema.properties || {});
2935
+ const optionalParams = availableParams.filter(p => !requiredParams.includes(p));
2936
+ const requiredList = requiredParams.length > 0 ? requiredParams.join(', ') : 'none';
2937
+ const optionalList = optionalParams.length > 0 ? optionalParams.join(', ') : 'none';
2938
+ const hint = `\n\n💡 Hint: This tool expects:\n` +
2939
+ ` Required: ${requiredList}\n` +
2940
+ ` Optional: ${optionalList}\n` +
2941
+ ` Use describe_tool('${serverName}', '${toolName}') for full schema.`;
2942
+ throw new Error(errorMsg + hint);
2943
+ }
2944
+ throw new Error(errorMsg);
2945
+ }
2946
+ }
2947
+ // ===== 4-TOOL SPEAKEASY APPROACH (v1.3.52) =====
2948
+ // Discovery tools: search_tools, describe_tool
2949
+ // Execution tools: execute_tool, execute_tool_confirm
2950
+ case 'search_tools': {
2951
+ // FIX v1.3.52: Search ALL servers using CACHED schemas (no auto-start)
2952
+ // UPDATE v1.3.56: Support optional server_name filter and server name matching
2953
+ // - query (optional): Search term for server names, tool names, and descriptions
2954
+ // - server_name (optional): Filter to specific server
2955
+ // - At least one parameter required
2956
+ try {
2957
+ const query = callArgs?.query || '';
2958
+ const serverNameFilter = callArgs?.server_name || '';
2959
+ // Validate: at least one parameter provided
2960
+ if (!query && !serverNameFilter) {
2961
+ throw new InvalidParamsError('At least one parameter required: query or server_name');
2962
+ }
2963
+ const results = [];
2964
+ // Get all servers or filter to specific server
2965
+ const allServers = this.configLoader.getAllServers();
2966
+ let serversToSearch = serverNameFilter
2967
+ ? allServers.filter((s) => s.name === serverNameFilter)
2968
+ : allServers;
2969
+ // Validate server_name if provided
2970
+ if (serverNameFilter && serversToSearch.length === 0) {
2971
+ throw new InvalidParamsError(`Server '${serverNameFilter}' not found in registry`);
2972
+ }
2973
+ // Multi-keyword matching: split query into individual words
2974
+ const keywords = query.toLowerCase().split(/\s+/).filter(k => k.length > 0);
2975
+ // Step 1: Check if any keyword matches a server name
2976
+ let serverKeyword;
2977
+ if (keywords.length > 0 && !serverNameFilter) {
2978
+ for (const keyword of keywords) {
2979
+ const matchedServer = allServers.find((s) => s.name.toLowerCase().includes(keyword) || keyword.includes(s.name.toLowerCase()));
2980
+ if (matchedServer) {
2981
+ serverKeyword = keyword;
2982
+ serversToSearch = [matchedServer];
2983
+ break;
2984
+ }
2985
+ }
2986
+ }
2987
+ // Step 2: Get remaining keywords (exclude server name keyword)
2988
+ const searchKeywords = serverKeyword
2989
+ ? keywords.filter(k => k !== serverKeyword)
2990
+ : keywords;
2991
+ for (const serverConfig of serversToSearch) {
2992
+ const serverName = serverConfig.name.toLowerCase();
2993
+ const tools = this.serverManager.getServerTools(serverConfig.name);
2994
+ for (const tool of tools) {
2995
+ const toolName = tool.name.toLowerCase();
2996
+ const toolDesc = (tool.description || '').toLowerCase();
2997
+ // Determine match type
2998
+ let matchType = 'all';
2999
+ let shouldInclude = false;
3000
+ if (keywords.length === 0) {
3001
+ // No query, just listing all tools from filtered server
3002
+ matchType = 'all';
3003
+ shouldInclude = true;
3004
+ }
3005
+ else if (serverKeyword && searchKeywords.length === 0) {
3006
+ // Only server name in query - return ALL tools from this server
3007
+ matchType = 'server';
3008
+ shouldInclude = true;
3009
+ }
3010
+ else if (searchKeywords.length > 0) {
3011
+ // Multi-keyword matching: check if ANY keyword matches tool name or description
3012
+ const nameMatches = searchKeywords.some(keyword => toolName.includes(keyword));
3013
+ const descMatches = searchKeywords.some(keyword => toolDesc.includes(keyword));
3014
+ if (nameMatches && descMatches) {
3015
+ matchType = 'all';
3016
+ shouldInclude = true;
3017
+ }
3018
+ else if (nameMatches) {
3019
+ matchType = 'name';
3020
+ shouldInclude = true;
3021
+ }
3022
+ else if (descMatches) {
3023
+ matchType = 'description';
3024
+ shouldInclude = true;
3025
+ }
3026
+ else if (serverKeyword) {
3027
+ // Server matched but no tool/description keywords matched
3028
+ // Still include if we're filtering by server (be permissive)
3029
+ matchType = 'server';
3030
+ shouldInclude = true;
3031
+ }
3032
+ }
3033
+ if (shouldInclude) {
3034
+ // Extract required and optional params from inputSchema
3035
+ const inputSchema = tool.inputSchema;
3036
+ const properties = inputSchema?.properties;
3037
+ const requiredArray = inputSchema?.required;
3038
+ const allParams = properties ? Object.keys(properties) : [];
3039
+ const requiredParams = requiredArray || [];
3040
+ const optionalParams = allParams.filter(p => !requiredParams.includes(p));
3041
+ // Generate example arguments from inputSchema
3042
+ const exampleArgs = {};
3043
+ if (properties) {
3044
+ for (const [key, value] of Object.entries(properties)) {
3045
+ const prop = value;
3046
+ // Use example from schema if available, otherwise generate placeholder
3047
+ if (prop.example !== undefined) {
3048
+ exampleArgs[key] = prop.example;
3049
+ }
3050
+ else if (prop.default !== undefined) {
3051
+ exampleArgs[key] = prop.default;
3052
+ }
3053
+ else if (prop.type === 'string') {
3054
+ // Generate meaningful examples for common param names
3055
+ if (key === 'query')
3056
+ exampleArgs[key] = 'search term';
3057
+ else if (key === 'cql')
3058
+ exampleArgs[key] = 'type=page ORDER BY created DESC';
3059
+ else if (key === 'jql')
3060
+ exampleArgs[key] = 'project = PROJ ORDER BY created DESC';
3061
+ else if (key === 'timezone')
3062
+ exampleArgs[key] = 'UTC';
3063
+ else if (key === 'url')
3064
+ exampleArgs[key] = 'https://example.com';
3065
+ else
3066
+ exampleArgs[key] = `<${key}>`;
3067
+ }
3068
+ else if (prop.type === 'number' || prop.type === 'integer') {
3069
+ if (key === 'limit' || key === 'max_results' || key === 'maxResults')
3070
+ exampleArgs[key] = 10;
3071
+ else
3072
+ exampleArgs[key] = 1;
3073
+ }
3074
+ else if (prop.type === 'boolean') {
3075
+ exampleArgs[key] = true;
3076
+ }
3077
+ else if (prop.type === 'array') {
3078
+ exampleArgs[key] = [];
3079
+ }
3080
+ else if (prop.type === 'object') {
3081
+ exampleArgs[key] = {};
3082
+ }
3083
+ }
3084
+ }
3085
+ // Store schema metadata for potential auto-describe
3086
+ // v1.1.31: Include safety annotations in search results
3087
+ // v1.3.x: Add argument inspection hints for risky tools with safe patterns
3088
+ let safetyAnnotation;
3089
+ if (tool.annotations) {
3090
+ safetyAnnotation = tool.annotations;
3091
+ }
3092
+ else {
3093
+ const classification = this.serverManager.classifyToolSafety(serverConfig.name, tool.name);
3094
+ safetyAnnotation = {
3095
+ safety: classification.safety,
3096
+ safetyReason: classification.reason,
3097
+ requiresConfirmation: classification.safety === 'risky',
3098
+ };
3099
+ }
3100
+ // v1.3.x: Check for argument inspection rules
3101
+ const toolSafetyRules = this.configLoader.getToolSafetyRules();
3102
+ const argInspectionRules = toolSafetyRules?.argumentInspectionRules || [];
3103
+ const fullToolName = `${serverConfig.name}:${tool.name}`;
3104
+ const argInspectionRule = argInspectionRules.find((rule) => rule.tool === fullToolName);
3105
+ let toolNote = "Use describe_tool to get full schema before execution";
3106
+ if (argInspectionRule && safetyAnnotation.safety === 'risky') {
3107
+ // Add argument inspection hint for risky tools with auto-approval patterns
3108
+ safetyAnnotation.argumentInspection = {
3109
+ enabled: true,
3110
+ field: argInspectionRule.argumentField,
3111
+ safePatterns: argInspectionRule.safeCommandPatterns || [],
3112
+ note: `Auto-approved when ${argInspectionRule.argumentField} matches safe patterns (e.g., ${(argInspectionRule.safeCommandPatterns || []).slice(0, 3).join(', ')})`
3113
+ };
3114
+ toolNote = `Risky by default, but AUTO-APPROVED for read-only operations. Check ${argInspectionRule.argumentField} - patterns like ${(argInspectionRule.safeCommandPatterns || []).slice(0, 3).join(', ')} are safe.`;
3115
+ }
3116
+ results.push({
3117
+ server: serverConfig.name,
3118
+ tool: tool.name,
3119
+ name: `${serverConfig.name}-${tool.name}`,
3120
+ description: tool.description,
3121
+ note: toolNote,
3122
+ matchType,
3123
+ // v1.3.x: Top-level safety fields for easier access
3124
+ safety: safetyAnnotation.safety,
3125
+ execute_with: safetyAnnotation.safety === 'safe' ? 'execute_tool' : 'execute_tool_confirm',
3126
+ annotations: safetyAnnotation,
3127
+ _schema: {
3128
+ inputSchema,
3129
+ requiredParams,
3130
+ optionalParams,
3131
+ exampleArgs
3132
+ }
3133
+ });
3134
+ }
3135
+ }
3136
+ }
3137
+ // Auto-describe for single tool results
3138
+ let autoDescribed = false;
3139
+ if (results.length === 1 && results[0]._schema) {
3140
+ const result = results[0];
3141
+ const schema = result._schema;
3142
+ if (schema) {
3143
+ // Add full schema details to the single result
3144
+ result.inputSchema = schema.inputSchema;
3145
+ result.requiredParams = schema.requiredParams;
3146
+ result.optionalParams = schema.optionalParams;
3147
+ result.example = {
3148
+ server_name: result.server,
3149
+ tool_name: result.tool,
3150
+ arguments: schema.exampleArgs
3151
+ };
3152
+ // Anthropic Advanced Tool Use: Include tool use examples if available
3153
+ // https://www.anthropic.com/engineering/advanced-tool-use
3154
+ const tool = this.serverManager.getServerTools(result.server).find(t => t.name === result.tool);
3155
+ if (tool?.inputExamples && Array.isArray(tool.inputExamples) && tool.inputExamples.length > 0) {
3156
+ result.inputExamples = tool.inputExamples;
3157
+ }
3158
+ // Remove the note since we're providing full schema
3159
+ delete result.note;
3160
+ delete result._schema;
3161
+ autoDescribed = true;
3162
+ }
3163
+ }
3164
+ else {
3165
+ // Include inputSchema in all results for Raycast compatibility
3166
+ // v1.1.67: Always include inputSchema even when not auto-describing
3167
+ results.forEach(r => {
3168
+ if (r._schema) {
3169
+ r.inputSchema = r._schema.inputSchema;
3170
+ r.requiredParams = r._schema.requiredParams;
3171
+ r.optionalParams = r._schema.optionalParams;
3172
+ delete r._schema;
3173
+ }
3174
+ });
3175
+ }
3176
+ return {
3177
+ content: [
3178
+ {
3179
+ type: 'text',
3180
+ text: JSON.stringify({
3181
+ query: query || undefined,
3182
+ server_name: serverNameFilter || undefined,
3183
+ count: results.length,
3184
+ autoDescribed,
3185
+ tools: results.slice(0, 50)
3186
+ }, null, 2)
3187
+ }
3188
+ ]
3189
+ };
3190
+ }
3191
+ catch (error) {
3192
+ throw new Error(`Failed to search tools: ${error instanceof Error ? error.message : String(error)}`);
3193
+ }
3194
+ }
3195
+ case 'describe_tool': {
3196
+ try {
3197
+ const serverName = callArgs?.server_name;
3198
+ const toolName = callArgs?.tool_name;
3199
+ // RAYCAST DEBUG: Log what client is requesting
3200
+ console.log(`[describe_tool] REQUEST: server="${serverName}" tool="${toolName}"`);
3201
+ if (!serverName || !toolName) {
3202
+ throw new InvalidParamsError('server_name and tool_name are required');
3203
+ }
3204
+ // Get server config
3205
+ const allServers = this.configLoader.getAllServers();
3206
+ const serverConfig = allServers.find((s) => s.name === serverName);
3207
+ if (!serverConfig) {
3208
+ throw new InvalidParamsError(`Server '${serverName}' not found in registry. Use search_tools to find available tools.`);
3209
+ }
3210
+ // Discover tools from the server
3211
+ const toolSchemas = await this.serverManager.discoverToolSchemas(serverName, serverConfig);
3212
+ const toolSchema = toolSchemas.find(t => t.name === toolName);
3213
+ if (!toolSchema) {
3214
+ const availableTools = toolSchemas.map(t => t.name).join(', ');
3215
+ throw new Error(`Tool '${toolName}' not found in server '${serverName}'.\n` +
3216
+ `Available tools: ${availableTools}\n` +
3217
+ `Use search_tools with a keyword to find tools across all servers.`);
3218
+ }
3219
+ // v1.4.0: Validate schema and collect hints
3220
+ const { SchemaValidator } = await import('./schema-validator.js');
3221
+ const validator = new SchemaValidator();
3222
+ const validationResult = validator.validateToolSchema(toolSchema);
3223
+ // Generate example arguments from inputSchema
3224
+ const inputSchema = toolSchema.inputSchema;
3225
+ const properties = inputSchema?.properties;
3226
+ const requiredArray = inputSchema?.required;
3227
+ const exampleArgs = {};
3228
+ if (properties) {
3229
+ for (const [key, value] of Object.entries(properties)) {
3230
+ const prop = value;
3231
+ // Use example from schema if available, otherwise generate placeholder
3232
+ if (prop.example !== undefined) {
3233
+ exampleArgs[key] = prop.example;
3234
+ }
3235
+ else if (prop.default !== undefined) {
3236
+ exampleArgs[key] = prop.default;
3237
+ }
3238
+ else if (prop.type === 'string') {
3239
+ exampleArgs[key] = prop.description ? `<${key}>` : 'example';
3240
+ }
3241
+ else if (prop.type === 'number') {
3242
+ exampleArgs[key] = 10;
3243
+ }
3244
+ else if (prop.type === 'boolean') {
3245
+ exampleArgs[key] = true;
3246
+ }
3247
+ else if (prop.type === 'array') {
3248
+ exampleArgs[key] = [];
3249
+ }
3250
+ else if (prop.type === 'object') {
3251
+ exampleArgs[key] = {};
3252
+ }
3253
+ }
3254
+ }
3255
+ // Calculate optional params (all params not in required array)
3256
+ const allParams = properties ? Object.keys(properties) : [];
3257
+ const optionalParams = allParams.filter(p => !(requiredArray || []).includes(p));
3258
+ // Build tool object matching search_tools auto-describe format for Raycast compatibility
3259
+ const toolObj = {
3260
+ server: serverName,
3261
+ tool: toolName,
3262
+ name: `${serverName}-${toolName}`,
3263
+ description: toolSchema.description || '',
3264
+ inputSchema: toolSchema.inputSchema || {},
3265
+ requiredParams: requiredArray || [],
3266
+ optionalParams: optionalParams,
3267
+ example: {
3268
+ server_name: serverName,
3269
+ tool_name: toolName,
3270
+ arguments: exampleArgs
3271
+ }
3272
+ };
3273
+ // Anthropic Advanced Tool Use: Include tool use examples if available
3274
+ // https://www.anthropic.com/engineering/advanced-tool-use
3275
+ if (toolSchema.inputExamples && Array.isArray(toolSchema.inputExamples) && toolSchema.inputExamples.length > 0) {
3276
+ toolObj.inputExamples = toolSchema.inputExamples;
3277
+ }
3278
+ // Phase 2: Include safety annotations if available
3279
+ if (toolSchema.annotations) {
3280
+ toolObj.annotations = toolSchema.annotations;
3281
+ }
3282
+ // Build response in discover_tools format (array with single tool)
3283
+ const responseObj = {
3284
+ server: serverName,
3285
+ tools: [toolObj],
3286
+ count: 1,
3287
+ detailLevel: 'full' // Always full for describe_tool
3288
+ };
3289
+ // RAYCAST DEBUG: Log what schema is returned
3290
+ console.log(`[describe_tool] RESPONSE: ${serverName}:${toolName} - requiredParams=${JSON.stringify(requiredArray || [])} - hasInputSchema=${!!toolSchema.inputSchema}`);
3291
+ return {
3292
+ content: [
3293
+ {
3294
+ type: 'text',
3295
+ text: JSON.stringify(responseObj, null, 2)
3296
+ }
3297
+ ]
3298
+ };
3299
+ }
3300
+ catch (error) {
3301
+ throw new Error(`Failed to describe tool: ${error instanceof Error ? error.message : String(error)}`);
3302
+ }
3303
+ }
3304
+ // ===== END 4-TOOL SPEAKEASY APPROACH =====
3305
+ // ===== OLD MANAGEMENT TOOLS (DEPRECATED v1.3.0) =====
3306
+ // These have been replaced by discovery tools to reduce token usage
3307
+ // Commented out but kept for reference. Can be removed in v2.0.0
3308
+ /*
3309
+ case 'enable_server': {
3310
+ const serverName = callArgs?.server_name;
3311
+ if (!serverName) throw new Error('server_name required');
3312
+
3313
+ const timestamp = new Date().toISOString();
3314
+ console.log(`[metalink] Enabling server: ${serverName} at ${timestamp}`);
3315
+
3316
+ try {
3317
+ // Get server config from registry
3318
+ const allServers = this.configLoader.getAllServers();
3319
+ const serverConfig = allServers.find((s: any) => s.name === serverName);
3320
+
3321
+ if (!serverConfig) {
3322
+ const availableNames = allServers.map((s: any) => s.name).join(', ');
3323
+ throw new Error(
3324
+ `Server '${serverName}' not found in registry. Available: ${availableNames}`
3325
+ );
3326
+ }
3327
+
3328
+ // Check if already enabled
3329
+ const activeServers = this.serverManager.getActiveServers();
3330
+ if (activeServers.has(serverName)) {
3331
+ const serverData = activeServers.get(serverName);
3332
+ const tools = serverData?.tools || [];
3333
+ return {
3334
+ content: [
3335
+ {
3336
+ type: 'text',
3337
+ text: JSON.stringify({
3338
+ status: 'already_enabled',
3339
+ server: serverName,
3340
+ tools_count: tools.length,
3341
+ tools: tools.map((t: any) => ({
3342
+ name: t.name,
3343
+ description: t.description || '',
3344
+ })),
3345
+ }, null, 2)
3346
+ }
3347
+ ]
3348
+ };
3349
+ }
3350
+
3351
+ // Start server process
3352
+ console.log(`[metalink] Using mcpm to run ${serverName}`);
3353
+ await this.serverManager.startServer(serverConfig);
3354
+
3355
+ // Get process and fetch tools
3356
+ console.log(`[metalink] Retrieving tools from ${serverName}...`);
3357
+ const serverProcess = this.serverManager.getProcess(serverName);
3358
+ if (!serverProcess) {
3359
+ throw new Error(`Failed to start ${serverName} - no process found`);
3360
+ }
3361
+
3362
+ // Fetch tools from the process
3363
+ const tools = await this.serverManager.fetchToolsFromProcess(serverProcess);
3364
+ this.serverManager.setServerTools(serverName, tools);
3365
+
3366
+ console.log(`[metalink] Successfully retrieved ${tools.length} tools from ${serverName}`);
3367
+ console.log(`[metalink] Response router set up for ${serverName}`);
3368
+
3369
+ return {
3370
+ content: [
3371
+ {
3372
+ type: 'text',
3373
+ text: JSON.stringify({
3374
+ status: 'enabled',
3375
+ server: serverName,
3376
+ tools_count: tools.length,
3377
+ tools: tools.map((t: any) => ({
3378
+ name: t.name,
3379
+ description: t.description || '',
3380
+ })),
3381
+ }, null, 2)
3382
+ }
3383
+ ]
3384
+ };
3385
+ } catch (error: any) {
3386
+ console.error(`[metalink] Failed to enable ${serverName}:`, error);
3387
+ throw new Error(`Failed to enable server '${serverName}': ${error.message}`);
3388
+ }
3389
+ }
3390
+
3391
+ case 'disable_server': {
3392
+ const serverName = callArgs?.server_name;
3393
+ if (!serverName) throw new InvalidParamsError('server_name required');
3394
+ // This would disable the server at runtime
3395
+ return {
3396
+ content: [
3397
+ {
3398
+ type: 'text',
3399
+ text: JSON.stringify({
3400
+ disabled: true,
3401
+ server: serverName,
3402
+ }, null, 2)
3403
+ }
3404
+ ]
3405
+ };
3406
+ }
3407
+
3408
+ case 'help': {
3409
+ const topic = (callArgs?.topic as string) || 'all';
3410
+
3411
+ const help = {
3412
+ overview: 'MetaLink provides three ways to interact with MCP servers and management tools',
3413
+
3414
+ management_tools: {
3415
+ description: 'Control server lifecycle and discover tools',
3416
+ tools: [
3417
+ {
3418
+ name: 'list_available_servers',
3419
+ description: 'List all servers that can be dynamically loaded',
3420
+ usage: 'No arguments needed',
3421
+ example: '{"name": "list_available_servers", "arguments": {}}'
3422
+ },
3423
+ {
3424
+ name: 'enable_server',
3425
+ description: 'Enable a server and load its tools',
3426
+ usage: 'Provide server_name (e.g., "memory", "docs-server")',
3427
+ example: '{"name": "enable_server", "arguments": {"server_name": "task-master-ai"}}'
3428
+ },
3429
+ {
3430
+ name: 'disable_server',
3431
+ description: 'Disable an enabled server',
3432
+ usage: 'Provide server_name',
3433
+ example: '{"name": "disable_server", "arguments": {"server_name": "memory"}}'
3434
+ },
3435
+ {
3436
+ name: 'list_servers',
3437
+ description: 'Show all currently enabled servers with tool counts',
3438
+ usage: 'No arguments needed',
3439
+ example: '{"name": "list_servers", "arguments": {}}'
3440
+ },
3441
+ {
3442
+ name: 'list_tools',
3443
+ description: 'List tools available on a specific server',
3444
+ usage: 'Provide server_name',
3445
+ example: '{"name": "list_tools", "arguments": {"server_name": "memory"}}'
3446
+ }
3447
+ ]
3448
+ },
3449
+
3450
+ direct_tool_calls: {
3451
+ description: 'Call tools using hyphenated format: server-toolName',
3452
+ format: 'server-toolName (e.g., memory-search_nodes, time-get_current_time, docs-server-list_libraries)',
3453
+ example: '{"name": "memory-search_nodes", "arguments": {"query": "metalink"}}',
3454
+ note: 'Preferred method for direct tool access when you know the server and tool names'
3455
+ },
3456
+
3457
+ execute_tool_method: {
3458
+ description: 'Generic tool executor with explicit parameters',
3459
+ format: 'Use execute_tool with server_name, tool_name, and arguments (nested)',
3460
+ example: '{"name": "execute_tool", "arguments": {"server_name": "memory", "tool_name": "search_nodes", "arguments": {"query": "metalink"}}}',
3461
+ critical: 'ALWAYS include "arguments" key in the outer structure, even if the tool takes no args: {"arguments": {}}'
3462
+ },
3463
+
3464
+ execute_tool_confirm: {
3465
+ description: 'Execute tools with user confirmation workflow',
3466
+ format: 'Same as execute_tool, but returns confirmation request before execution',
3467
+ example: '{"name": "execute_tool_confirm", "arguments": {"server_name": "memory", "tool_name": "delete_entities", "arguments": {"entityNames": ["test"]}}}',
3468
+ use_case: 'For sensitive/destructive operations that require explicit user approval'
3469
+ },
3470
+
3471
+ workflow: {
3472
+ step1_discover: 'list_available_servers → see what servers can be loaded',
3473
+ step2_enable: 'enable_server → load a server and fetch its tools',
3474
+ step3_explore: 'list_tools → see all tools available on that server',
3475
+ step4_execute: 'Use direct calls (server-toolName) OR execute_tool to call tools',
3476
+ step5_search: 'search_tools → find tools by keyword across all servers'
3477
+ }
3478
+ };
3479
+
3480
+ // Filter by topic
3481
+ if (topic === 'management') {
3482
+ return {
3483
+ content: [
3484
+ {
3485
+ type: 'text',
3486
+ text: JSON.stringify({ help: { overview: help.overview, management_tools: help.management_tools } }, null, 2)
3487
+ }
3488
+ ]
3489
+ };
3490
+ } else if (topic === 'direct') {
3491
+ return {
3492
+ content: [
3493
+ {
3494
+ type: 'text',
3495
+ text: JSON.stringify({ help: { overview: help.overview, direct_tool_calls: help.direct_tool_calls } }, null, 2)
3496
+ }
3497
+ ]
3498
+ };
3499
+ } else if (topic === 'execute_tool') {
3500
+ return {
3501
+ content: [
3502
+ {
3503
+ type: 'text',
3504
+ text: JSON.stringify({ help: { overview: help.overview, execute_tool_method: help.execute_tool_method, execute_tool_confirm: help.execute_tool_confirm } }, null, 2)
3505
+ }
3506
+ ]
3507
+ };
3508
+ } else if (topic === 'workflow') {
3509
+ return {
3510
+ content: [
3511
+ {
3512
+ type: 'text',
3513
+ text: JSON.stringify({ help: { overview: help.overview, workflow: help.workflow } }, null, 2)
3514
+ }
3515
+ ]
3516
+ };
3517
+ }
3518
+
3519
+ return {
3520
+ content: [
3521
+ {
3522
+ type: 'text',
3523
+ text: JSON.stringify({ help }, null, 2)
3524
+ }
3525
+ ]
3526
+ };
3527
+ }
3528
+ */
3529
+ // ===== END OLD MANAGEMENT TOOLS =====
3530
+ default: {
3531
+ // Handle base server tools in format "server-toolName"
3532
+ // Support hyphenated server names like "duckduckgo-mcp"
3533
+ if (name.includes('-')) {
3534
+ const lastHyphenIndex = name.lastIndexOf('-');
3535
+ if (lastHyphenIndex === -1) {
3536
+ throw new InvalidParamsError(`Invalid tool name format: ${name}`);
3537
+ }
3538
+ const serverName = name.substring(0, lastHyphenIndex);
3539
+ const toolName = name.substring(lastHyphenIndex + 1);
3540
+ if (serverName && toolName) {
3541
+ // v1.4.0: Refresh discovery timer on tool activity
3542
+ if (this.serverManager.isDiscovered(serverName)) {
3543
+ this.serverManager.refreshDiscoveryTimer(serverName);
3544
+ }
3545
+ // Phase 3: Auto-start servers on first tool call (including base servers as fallback)
3546
+ console.log(`[MetaLink] Ensuring server is started: ${serverName}`);
3547
+ try {
3548
+ // Get server config and ensure it's started
3549
+ const allServers = this.configLoader.getAllServers();
3550
+ const serverConfig = allServers.find((s) => s.name === serverName);
3551
+ if (!serverConfig) {
3552
+ throw new InvalidParamsError(`Server '${serverName}' not found in registry`);
3553
+ }
3554
+ // Always call ensureServerStarted - it's idempotent and handles base servers too
3555
+ await this.serverManager.ensureServerStarted(serverName, serverConfig);
3556
+ // Notify connected clients that tools/list has changed
3557
+ this.notifyToolsListChanged();
3558
+ }
3559
+ catch (autoStartError) {
3560
+ throw new Error(`Failed to auto-start server ${serverName}: ${autoStartError instanceof Error ? autoStartError.message : String(autoStartError)}`);
3561
+ }
3562
+ // Phase 2b: Get tool schema for validation
3563
+ const toolSchema = this.serverManager.getToolSchema(serverName, toolName);
3564
+ if (!toolSchema) {
3565
+ throw new InvalidParamsError(`Tool '${toolName}' not found in server '${serverName}'. Use describe_tool to get the tool schema or search_tools to find available tools.`);
3566
+ }
3567
+ // Phase 2d: Use args passed from mcpCallTool (from the 'arguments' parameter in the request)
3568
+ // args is the 'arguments' object from the MCP request, not callArgs
3569
+ const args2 = args || {};
3570
+ console.log(`[MetaLink] direct tool call: ${name} with args ${JSON.stringify(args2)}`);
3571
+ // Phase 3: Special handling for memory-search_nodes - use fallback directly
3572
+ if (serverName === 'memory' && toolName === 'search_nodes') {
3573
+ console.log(`[MetaLink] Intercepting memory-search_nodes - using read_graph with client-side filtering`);
3574
+ try {
3575
+ const graphResult = await this.serverManager.callTool('memory', 'read_graph', {});
3576
+ const query = (args2.query || '').toLowerCase();
3577
+ // Parse the nested response
3578
+ if (graphResult && typeof graphResult === 'object') {
3579
+ const resultObj = graphResult;
3580
+ if (resultObj.content && Array.isArray(resultObj.content)) {
3581
+ const textContent = resultObj.content[0]?.text;
3582
+ if (textContent) {
3583
+ const graphData = JSON.parse(textContent);
3584
+ // Filter entities by query
3585
+ const filteredEntities = graphData.entities?.filter((e) => query === '' ||
3586
+ e.name.toLowerCase().includes(query) ||
3587
+ e.observations?.some((obs) => obs.toLowerCase().includes(query))) || [];
3588
+ return {
3589
+ content: [{
3590
+ type: 'text',
3591
+ text: JSON.stringify({
3592
+ entities: filteredEntities,
3593
+ relations: graphData.relations || []
3594
+ }, null, 2)
3595
+ }]
3596
+ };
3597
+ }
3598
+ }
3599
+ }
3600
+ }
3601
+ catch (fallbackError) {
3602
+ console.error(`[MetaLink] Fallback for memory:search_nodes failed:`, fallbackError);
3603
+ throw new Error(`Tool execution failed for ${name}: memory:search_nodes fallback failed - ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
3604
+ }
3605
+ }
3606
+ // Phase 3: Call the actual tool via response router
3607
+ try {
3608
+ const result = await this.serverManager.callTool(serverName, toolName, args2);
3609
+ return result;
3610
+ }
3611
+ catch (toolError) {
3612
+ // Enhanced error with inputSchema for debugging
3613
+ const errorMsg = `Tool execution failed for ${name}: ${toolError instanceof Error ? toolError.message : String(toolError)}`;
3614
+ // Check if error is parameter-related
3615
+ const errorStr = (toolError instanceof Error ? toolError.message : String(toolError)).toLowerCase();
3616
+ const isParamError = errorStr.includes('null') || errorStr.includes('undefined') ||
3617
+ errorStr.includes('required') || errorStr.includes('missing');
3618
+ if (isParamError && toolSchema) {
3619
+ const inputSchema = toolSchema.inputSchema;
3620
+ if (inputSchema) {
3621
+ const requiredParams = inputSchema.required || [];
3622
+ const availableParams = Object.keys(inputSchema.properties || {});
3623
+ const optionalParams = availableParams.filter(p => !requiredParams.includes(p));
3624
+ const requiredList = requiredParams.length > 0 ? requiredParams.join(', ') : 'none';
3625
+ const optionalList = optionalParams.length > 0 ? optionalParams.join(', ') : 'none';
3626
+ const hint = `\n\n💡 Hint: This tool expects:\n` +
3627
+ ` Required: ${requiredList}\n` +
3628
+ ` Optional: ${optionalList}\n` +
3629
+ ` Use describe_tool('${serverName}', '${toolName}') for full schema.`;
3630
+ throw new Error(errorMsg + hint);
3631
+ }
3632
+ }
3633
+ throw new Error(errorMsg);
3634
+ }
3635
+ }
3636
+ }
3637
+ throw new InvalidParamsError(`Unknown tool: ${name}`);
3638
+ }
3639
+ }
3640
+ }
3641
+ /**
3642
+ * Add a new server to the registry
3643
+ */
3644
+ async addServer(req, res) {
3645
+ try {
3646
+ const serverConfig = req.body;
3647
+ // Validate and add server to registry
3648
+ const server = await this.configLoader.saveServerToRegistry(serverConfig, {
3649
+ allowAnyCommand: false,
3650
+ timeout: 5000,
3651
+ });
3652
+ // v1.1.29: Trigger immediate tool discovery
3653
+ let discoveredToolCount = 0;
3654
+ try {
3655
+ console.log(`[AddServer] Starting immediate tool discovery for '${server.name}'`);
3656
+ await this.serverManager.ensureServerStarted(server.name, server);
3657
+ const tools = this.serverManager.getServerTools(server.name);
3658
+ discoveredToolCount = tools.length;
3659
+ console.log(`[AddServer] Discovered ${discoveredToolCount} tools for '${server.name}': ${tools.map(t => t.name).join(', ')}`);
3660
+ }
3661
+ catch (discoveryError) {
3662
+ console.warn(`[AddServer] Tool discovery failed for '${server.name}' (non-fatal):`, discoveryError);
3663
+ // Don't fail the add operation if discovery fails
3664
+ }
3665
+ // Broadcast server:added event
3666
+ this.broadcastEvent({
3667
+ type: 'server:added',
3668
+ data: {
3669
+ server,
3670
+ timestamp: Date.now(),
3671
+ },
3672
+ });
3673
+ res.status(201).json({
3674
+ success: true,
3675
+ message: `Server '${server.name}' added to registry`,
3676
+ server,
3677
+ discoveredTools: discoveredToolCount,
3678
+ });
3679
+ }
3680
+ catch (error) {
3681
+ const message = error instanceof Error ? error.message : 'Failed to add server';
3682
+ res.status(400).json({
3683
+ error: message,
3684
+ });
3685
+ }
3686
+ }
3687
+ /**
3688
+ * Remove a server from the registry
3689
+ */
3690
+ async removeServer(req, res) {
3691
+ try {
3692
+ const { name } = req.params;
3693
+ // Check if server exists
3694
+ const server = this.configLoader.getServer(name);
3695
+ if (!server) {
3696
+ res.status(404).json({
3697
+ error: `Server '${name}' not found in registry`,
3698
+ });
3699
+ return;
3700
+ }
3701
+ // Check if server is running and require force if so
3702
+ const status = this.serverManager.getServerStatus(name);
3703
+ if (status?.status === 'running') {
3704
+ // Check for force flag in query params
3705
+ const force = req.query.force === 'true';
3706
+ if (!force) {
3707
+ res.status(409).json({
3708
+ error: `Server '${name}' is currently running. Use ?force=true to remove it anyway.`,
3709
+ running: true,
3710
+ });
3711
+ return;
3712
+ }
3713
+ // Clean up running server before removing
3714
+ try {
3715
+ await this.serverManager.removeServer(name);
3716
+ }
3717
+ catch (error) {
3718
+ // Continue anyway - cleanup happened
3719
+ }
3720
+ }
3721
+ // Remove from registry
3722
+ await this.configLoader.removeServerFromRegistry(name, { timeout: 5000 });
3723
+ // Broadcast server:removed event
3724
+ this.broadcastEvent({
3725
+ type: 'server:removed',
3726
+ data: {
3727
+ name,
3728
+ timestamp: Date.now(),
3729
+ },
3730
+ });
3731
+ res.json({
3732
+ success: true,
3733
+ message: `Server '${name}' removed from registry`,
3734
+ });
3735
+ }
3736
+ catch (error) {
3737
+ const message = error instanceof Error ? error.message : 'Failed to remove server';
3738
+ res.status(400).json({
3739
+ error: message,
3740
+ });
3741
+ }
3742
+ }
3743
+ /**
3744
+ * Validate a server configuration without saving
3745
+ */
3746
+ async validateServer(req, res) {
3747
+ try {
3748
+ const { RegistryManager } = await import('../config/registry.js');
3749
+ const registry = new RegistryManager();
3750
+ // Validate configuration
3751
+ const validation = await registry.validateServer(req.body, {
3752
+ allowAnyCommand: false,
3753
+ });
3754
+ if (validation.valid) {
3755
+ res.json({
3756
+ valid: true,
3757
+ message: 'Server configuration is valid',
3758
+ });
3759
+ }
3760
+ else {
3761
+ res.status(400).json({
3762
+ valid: false,
3763
+ errors: validation.errors || [],
3764
+ });
3765
+ }
3766
+ }
3767
+ catch (error) {
3768
+ const message = error instanceof Error ? error.message : 'Validation error';
3769
+ res.status(400).json({
3770
+ error: message,
3771
+ });
3772
+ }
3773
+ }
3774
+ /**
3775
+ * Start the HTTP server
3776
+ * NOTE: This Promise never resolves - it keeps the event loop alive for background mode
3777
+ */
3778
+ async listen(port, host) {
3779
+ return new Promise((_resolve, reject) => {
3780
+ this.server = this.app.listen(port, host, () => {
3781
+ console.log(`[MetaLink] HTTP server listening on http://${host}:${port}`);
3782
+ console.log(`[MetaLink] Dashboard available at http://${host}:${port}`);
3783
+ console.log(`[MetaLink] API available at http://${host}:${port}/api/v1`);
3784
+ console.log(`[MetaLink] Events available at http://${host}:${port}/api/v1/events`);
3785
+ console.log(`[MetaLink] MCP endpoint available at http://${host}:${port}/mcp`);
3786
+ // NOTE: Intentionally NOT resolving - keeps event loop alive for background mode
3787
+ });
3788
+ // Handle server errors
3789
+ this.server.on('error', (err) => {
3790
+ reject(err);
3791
+ });
3792
+ });
3793
+ }
3794
+ /**
3795
+ * Send message via SSE to connected client
3796
+ */
3797
+ sendSSEMessage(sessionId, message) {
3798
+ const conn = this.sseConnections.get(sessionId);
3799
+ if (conn && !conn.destroyed) {
3800
+ try {
3801
+ conn.write(`data: ${JSON.stringify(message)}\n\n`);
3802
+ }
3803
+ catch (err) {
3804
+ console.log(`[SSE] Error sending message to session ${sessionId}:`, err instanceof Error ? err.message : err);
3805
+ this.sseConnections.delete(sessionId);
3806
+ }
3807
+ }
3808
+ }
3809
+ notifyToolsListChanged() {
3810
+ const now = Date.now();
3811
+ if (now - this.lastToolsListNotification < this.TOOLS_LIST_NOTIFICATION_THROTTLE_MS) {
3812
+ console.log('[MCP] Throttling tools/list_changed notification');
3813
+ return;
3814
+ }
3815
+ this.lastToolsListNotification = now;
3816
+ const notification = {
3817
+ jsonrpc: '2.0',
3818
+ method: 'notifications/tools/list_changed'
3819
+ };
3820
+ let sentCount = 0;
3821
+ for (const [sessionId, conn] of this.sseConnections) {
3822
+ if (!conn.destroyed) {
3823
+ try {
3824
+ const eventId = this.trackSseEvent(sessionId, notification);
3825
+ conn.write(`id: ${eventId}\ndata: ${JSON.stringify(notification)}\n\n`);
3826
+ sentCount++;
3827
+ }
3828
+ catch (error) {
3829
+ console.warn(`[MCP] Failed to send notification to session ${sessionId}:`, error);
3830
+ this.sseConnections.delete(sessionId);
3831
+ }
3832
+ }
3833
+ }
3834
+ if (sentCount > 0) {
3835
+ console.log(`[MCP] Sent notifications/tools/list_changed to ${sentCount} client(s)`);
3836
+ }
3837
+ }
3838
+ /**
3839
+ * Send response via callback URL (MCP callback protocol for bidirectional communication)
3840
+ */
3841
+ async sendViaCallback(session, response) {
3842
+ if (!session.callbackUrl) {
3843
+ return false;
3844
+ }
3845
+ try {
3846
+ console.log(`[CALLBACK] Posting response to ${session.callbackUrl}`);
3847
+ // POST response to callback URL
3848
+ const callbackResponse = await fetch(session.callbackUrl, {
3849
+ method: 'POST',
3850
+ headers: {
3851
+ 'Content-Type': 'application/json',
3852
+ 'Mcp-Session-Id': session.id,
3853
+ 'MCP-Protocol-Version': MCP_PROTOCOL_VERSION
3854
+ },
3855
+ body: JSON.stringify(response),
3856
+ signal: AbortSignal.timeout(10000) // 10 second timeout
3857
+ });
3858
+ if (!callbackResponse.ok) {
3859
+ console.warn(`[CALLBACK] Warning: callback returned ${callbackResponse.status}`);
3860
+ }
3861
+ return true;
3862
+ }
3863
+ catch (error) {
3864
+ console.error(`[CALLBACK] Error sending to ${session.callbackUrl}:`, error instanceof Error ? error.message : error);
3865
+ return false;
3866
+ }
3867
+ }
3868
+ /**
3869
+ * Cleanup
3870
+ */
3871
+ /**
3872
+ * Get all safety rules
3873
+ */
3874
+ async getSafetyRules(req, res) {
3875
+ try {
3876
+ const includePatterns = req.query.include_patterns === 'true';
3877
+ const rules = this.configLoader.getToolSafetyRules();
3878
+ const response = {
3879
+ safeToolOverrides: rules.safeToolOverrides || [],
3880
+ riskyToolOverrides: rules.riskyToolOverrides || [],
3881
+ };
3882
+ if (includePatterns) {
3883
+ response.safePatterns = rules.safePatterns || [];
3884
+ response.riskyPatterns = rules.riskyPatterns || [];
3885
+ }
3886
+ // Always include argumentInspectionRules (v1.1.29+)
3887
+ response.argumentInspectionRules = rules.argumentInspectionRules || [];
3888
+ res.json(response);
3889
+ }
3890
+ catch (error) {
3891
+ res.status(500).json({
3892
+ error: error instanceof Error ? error.message : 'Failed to get safety rules',
3893
+ });
3894
+ }
3895
+ }
3896
+ /**
3897
+ * Check if a specific tool is safe or risky
3898
+ */
3899
+ async checkToolSafety(req, res) {
3900
+ try {
3901
+ const { server, tool } = req.params;
3902
+ if (!server || !tool) {
3903
+ res.status(400).json({
3904
+ error: 'Missing required parameters: server and tool',
3905
+ });
3906
+ return;
3907
+ }
3908
+ const result = this.serverManager.classifyToolSafety(server, tool);
3909
+ res.json({
3910
+ server,
3911
+ tool,
3912
+ fullName: `${server}:${tool}`,
3913
+ safety: result.safety,
3914
+ reason: result.reason,
3915
+ requiresConfirmation: result.safety === 'risky',
3916
+ });
3917
+ }
3918
+ catch (error) {
3919
+ res.status(500).json({
3920
+ error: error instanceof Error ? error.message : 'Failed to check tool safety',
3921
+ });
3922
+ }
3923
+ }
3924
+ /**
3925
+ * Add a safe tool override
3926
+ */
3927
+ async addSafeToolOverride(req, res) {
3928
+ try {
3929
+ const { tool, reason } = req.body;
3930
+ if (!tool || typeof tool !== 'string') {
3931
+ res.status(400).json({
3932
+ error: 'Missing or invalid required parameter: tool (string)',
3933
+ });
3934
+ return;
3935
+ }
3936
+ // Validate tool format
3937
+ const toolPattern = /^[a-zA-Z0-9_-]+:[a-zA-Z0-9_*-]+$/;
3938
+ if (!toolPattern.test(tool)) {
3939
+ res.status(400).json({
3940
+ error: 'Invalid tool format. Expected: server:tool or server:*',
3941
+ });
3942
+ return;
3943
+ }
3944
+ await this.configLoader.addSafeToolOverride(tool);
3945
+ res.status(201).json({
3946
+ success: true,
3947
+ message: `Added ${tool} to safe tools`,
3948
+ tool,
3949
+ reason: reason || undefined,
3950
+ });
3951
+ }
3952
+ catch (error) {
3953
+ res.status(500).json({
3954
+ error: error instanceof Error ? error.message : 'Failed to add safe tool override',
3955
+ });
3956
+ }
3957
+ }
3958
+ /**
3959
+ * Add a risky tool override
3960
+ */
3961
+ async addRiskyToolOverride(req, res) {
3962
+ try {
3963
+ const { tool, reason } = req.body;
3964
+ if (!tool || typeof tool !== 'string') {
3965
+ res.status(400).json({
3966
+ error: 'Missing or invalid required parameter: tool (string)',
3967
+ });
3968
+ return;
3969
+ }
3970
+ // Validate tool format
3971
+ const toolPattern = /^[a-zA-Z0-9_-]+:[a-zA-Z0-9_*-]+$/;
3972
+ if (!toolPattern.test(tool)) {
3973
+ res.status(400).json({
3974
+ error: 'Invalid tool format. Expected: server:tool or server:*',
3975
+ });
3976
+ return;
3977
+ }
3978
+ await this.configLoader.addRiskyToolOverride(tool);
3979
+ res.status(201).json({
3980
+ success: true,
3981
+ message: `Added ${tool} to risky tools`,
3982
+ tool,
3983
+ reason: reason || undefined,
3984
+ });
3985
+ }
3986
+ catch (error) {
3987
+ res.status(500).json({
3988
+ error: error instanceof Error ? error.message : 'Failed to add risky tool override',
3989
+ });
3990
+ }
3991
+ }
3992
+ /**
3993
+ * Add a safe pattern
3994
+ */
3995
+ async addSafePattern(req, res) {
3996
+ try {
3997
+ const { pattern, reason } = req.body;
3998
+ if (!pattern || typeof pattern !== 'string') {
3999
+ res.status(400).json({
4000
+ error: 'Missing or invalid required parameter: pattern (string)',
4001
+ });
4002
+ return;
4003
+ }
4004
+ // Validate regex pattern
4005
+ try {
4006
+ new RegExp(pattern);
4007
+ }
4008
+ catch (e) {
4009
+ res.status(400).json({
4010
+ error: `Invalid regex pattern: ${pattern}`,
4011
+ });
4012
+ return;
4013
+ }
4014
+ await this.configLoader.addSafePattern(pattern);
4015
+ res.status(201).json({
4016
+ success: true,
4017
+ message: `Added pattern ${pattern} to safe patterns`,
4018
+ pattern,
4019
+ reason: reason || undefined,
4020
+ });
4021
+ }
4022
+ catch (error) {
4023
+ res.status(500).json({
4024
+ error: error instanceof Error ? error.message : 'Failed to add safe pattern',
4025
+ });
4026
+ }
4027
+ }
4028
+ /**
4029
+ * Add a risky pattern
4030
+ */
4031
+ async addRiskyPattern(req, res) {
4032
+ try {
4033
+ const { pattern, reason } = req.body;
4034
+ if (!pattern || typeof pattern !== 'string') {
4035
+ res.status(400).json({
4036
+ error: 'Missing or invalid required parameter: pattern (string)',
4037
+ });
4038
+ return;
4039
+ }
4040
+ // Validate regex pattern
4041
+ try {
4042
+ new RegExp(pattern);
4043
+ }
4044
+ catch (e) {
4045
+ res.status(400).json({
4046
+ error: `Invalid regex pattern: ${pattern}`,
4047
+ });
4048
+ return;
4049
+ }
4050
+ await this.configLoader.addRiskyPattern(pattern);
4051
+ res.status(201).json({
4052
+ success: true,
4053
+ message: `Added pattern ${pattern} to risky patterns`,
4054
+ pattern,
4055
+ reason: reason || undefined,
4056
+ });
4057
+ }
4058
+ catch (error) {
4059
+ res.status(500).json({
4060
+ error: error instanceof Error ? error.message : 'Failed to add risky pattern',
4061
+ });
4062
+ }
4063
+ }
4064
+ /**
4065
+ * Remove a rule (tool override or pattern)
4066
+ */
4067
+ async removeRule(req, res) {
4068
+ try {
4069
+ const { rule } = req.params;
4070
+ if (!rule) {
4071
+ res.status(400).json({
4072
+ error: 'Missing required parameter: rule',
4073
+ });
4074
+ return;
4075
+ }
4076
+ // URL decode the rule
4077
+ const decodedRule = decodeURIComponent(rule);
4078
+ // Auto-detect rule type
4079
+ const rules = this.configLoader.getToolSafetyRules();
4080
+ let removed = false;
4081
+ let type = '';
4082
+ if (rules.safeToolOverrides?.includes(decodedRule)) {
4083
+ await this.configLoader.removeSafeToolOverride(decodedRule);
4084
+ removed = true;
4085
+ type = 'safe_tool_override';
4086
+ }
4087
+ else if (rules.riskyToolOverrides?.includes(decodedRule)) {
4088
+ await this.configLoader.removeRiskyToolOverride(decodedRule);
4089
+ removed = true;
4090
+ type = 'risky_tool_override';
4091
+ }
4092
+ else if (rules.safePatterns?.includes(decodedRule)) {
4093
+ await this.configLoader.removeSafePattern(decodedRule);
4094
+ removed = true;
4095
+ type = 'safe_pattern';
4096
+ }
4097
+ else if (rules.riskyPatterns?.includes(decodedRule)) {
4098
+ await this.configLoader.removeRiskyPattern(decodedRule);
4099
+ removed = true;
4100
+ type = 'risky_pattern';
4101
+ }
4102
+ if (!removed) {
4103
+ res.status(404).json({
4104
+ error: `Rule not found: ${decodedRule}`,
4105
+ });
4106
+ return;
4107
+ }
4108
+ res.json({
4109
+ success: true,
4110
+ message: `Removed ${type}: ${decodedRule}`,
4111
+ rule: decodedRule,
4112
+ type,
4113
+ });
4114
+ }
4115
+ catch (error) {
4116
+ res.status(500).json({
4117
+ error: error instanceof Error ? error.message : 'Failed to remove rule',
4118
+ });
4119
+ }
4120
+ }
4121
+ /**
4122
+ * Reset safety rules to defaults
4123
+ */
4124
+ async resetSafetyRules(req, res) {
4125
+ try {
4126
+ const { force } = req.body;
4127
+ if (!force) {
4128
+ res.status(400).json({
4129
+ error: 'Reset requires explicit confirmation. Set force: true',
4130
+ });
4131
+ return;
4132
+ }
4133
+ await this.configLoader.resetToDefaults();
4134
+ res.json({
4135
+ success: true,
4136
+ message: 'Reset all safety rules to defaults',
4137
+ });
4138
+ }
4139
+ catch (error) {
4140
+ res.status(500).json({
4141
+ error: error instanceof Error ? error.message : 'Failed to reset safety rules',
4142
+ });
4143
+ }
4144
+ }
4145
+ /**
4146
+ * Import safety rules from JSON
4147
+ */
4148
+ async importSafetyRules(req, res) {
4149
+ try {
4150
+ const { rules, merge = true } = req.body; // Default to merge mode
4151
+ if (!rules || typeof rules !== 'object') {
4152
+ res.status(400).json({
4153
+ error: 'Missing or invalid required parameter: rules (object)',
4154
+ });
4155
+ return;
4156
+ }
4157
+ // Get current rules to preserve argumentInspectionRules
4158
+ const currentRules = this.configLoader.getToolSafetyRules();
4159
+ // Extract arrays from the rules object
4160
+ const safeToolOverrides = Array.isArray(rules.safeToolOverrides) ? rules.safeToolOverrides : [];
4161
+ const riskyToolOverrides = Array.isArray(rules.riskyToolOverrides) ? rules.riskyToolOverrides : [];
4162
+ const safePatterns = Array.isArray(rules.safePatterns) ? rules.safePatterns : [];
4163
+ const riskyPatterns = Array.isArray(rules.riskyPatterns) ? rules.riskyPatterns : [];
4164
+ // Preserve argumentInspectionRules unless explicitly provided in import
4165
+ const argumentInspectionRules = Array.isArray(rules.argumentInspectionRules)
4166
+ ? rules.argumentInspectionRules
4167
+ : (currentRules.argumentInspectionRules || []);
4168
+ if (merge) {
4169
+ // Merge mode: Add new rules to existing ones (default)
4170
+ // Merge tool overrides (avoid duplicates)
4171
+ const mergedSafeTools = [...new Set([...(currentRules.safeToolOverrides || []), ...safeToolOverrides])];
4172
+ const mergedRiskyTools = [...new Set([...(currentRules.riskyToolOverrides || []), ...riskyToolOverrides])];
4173
+ const mergedSafePatterns = [...new Set([...(currentRules.safePatterns || []), ...safePatterns])];
4174
+ const mergedRiskyPatterns = [...new Set([...(currentRules.riskyPatterns || []), ...riskyPatterns])];
4175
+ await this.configLoader.setToolSafetyRules({
4176
+ safeToolOverrides: mergedSafeTools,
4177
+ riskyToolOverrides: mergedRiskyTools,
4178
+ safePatterns: mergedSafePatterns,
4179
+ riskyPatterns: mergedRiskyPatterns,
4180
+ argumentInspectionRules, // Always preserve
4181
+ });
4182
+ res.json({
4183
+ success: true,
4184
+ message: 'Safety rules imported and merged successfully',
4185
+ imported: {
4186
+ safeTools: mergedSafeTools.length,
4187
+ riskyTools: mergedRiskyTools.length,
4188
+ safePatterns: mergedSafePatterns.length,
4189
+ riskyPatterns: mergedRiskyPatterns.length,
4190
+ },
4191
+ });
4192
+ }
4193
+ else {
4194
+ // Replace mode: Replace all rules with imported ones
4195
+ await this.configLoader.setToolSafetyRules({
4196
+ safeToolOverrides,
4197
+ riskyToolOverrides,
4198
+ safePatterns,
4199
+ riskyPatterns,
4200
+ argumentInspectionRules, // Still preserve if not provided
4201
+ });
4202
+ res.json({
4203
+ success: true,
4204
+ message: 'Safety rules imported successfully (replace mode)',
4205
+ imported: {
4206
+ safeTools: safeToolOverrides.length,
4207
+ riskyTools: riskyToolOverrides.length,
4208
+ safePatterns: safePatterns.length,
4209
+ riskyPatterns: riskyPatterns.length,
4210
+ },
4211
+ });
4212
+ }
4213
+ }
4214
+ catch (error) {
4215
+ res.status(500).json({
4216
+ error: error instanceof Error ? error.message : 'Failed to import safety rules',
4217
+ });
4218
+ }
4219
+ }
4220
+ async cleanup() {
4221
+ // Save metrics before shutdown (Phase 4 - v1.4.0)
4222
+ try {
4223
+ this.metricsPersistence.stopPeriodicWrites();
4224
+ const finalMetrics = {
4225
+ timestamp: Date.now(),
4226
+ metrics: globalMetrics.getMetrics(),
4227
+ serverMetrics: globalMetrics.getAllServerMetrics(),
4228
+ apiMetrics: globalMetrics.getApiMetrics(),
4229
+ };
4230
+ await this.metricsPersistence.save(finalMetrics);
4231
+ console.log('[MetricsPersistence] Saved final metrics on shutdown');
4232
+ }
4233
+ catch (error) {
4234
+ console.error('[MetricsPersistence] Failed to save metrics on shutdown:', error);
4235
+ }
4236
+ await this.serverManager.cleanup();
4237
+ for (const client of this.eventClients) {
4238
+ client.end();
4239
+ }
4240
+ this.eventClients.clear();
4241
+ // Close all SSE connections
4242
+ for (const [sessionId, conn] of this.sseConnections) {
4243
+ try {
4244
+ conn.end();
4245
+ }
4246
+ catch (err) {
4247
+ console.log(`[SSE] Error closing connection for session ${sessionId}`);
4248
+ }
4249
+ }
4250
+ this.sseConnections.clear();
4251
+ }
4252
+ }
4253
+ //# sourceMappingURL=http.js.map