@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,3255 @@
1
+ /**
2
+ * Server Manager - Handles spawning and managing MCP server processes and HTTP clients
3
+ */
4
+ import { spawn, spawnSync } from "child_process";
5
+ import path from "path";
6
+ import { EventEmitter } from "events";
7
+ import { LRUCache } from "lru-cache";
8
+ import { version } from "../index.js";
9
+ import { ProcessManager, HttpConnectionManager, SchemaCacheManager, } from "./managers/index.js";
10
+ import { SchemaStore } from "./schema-store.js";
11
+ import { SchemaValidator } from "./schema-validator.js";
12
+ import { globalMetrics } from "../metrics/index.js";
13
+ import { CircuitBreakerManager } from "./circuit-breaker.js";
14
+ export class ServerManager extends EventEmitter {
15
+ /**
16
+ * Constructor - Initialize with configurable schema cache TTL and persistent storage
17
+ */
18
+ constructor(config) {
19
+ super();
20
+ this.processes = new Map();
21
+ this.httpClients = new Map(); // Track HTTP clients
22
+ this.state = {};
23
+ this.healthCheckIntervals = new Map();
24
+ this.activeServers = new Map(); // Track server tools
25
+ this.baseServers = [];
26
+ this.baseServersEnabled = false;
27
+ this.schemasCacheTTL = 300000; // Default 5 minutes in ms
28
+ // Persistent schema storage
29
+ this.schemaStore = null;
30
+ this.schemasBackgroundRefresh = true;
31
+ this.backgroundRefreshInterval = null;
32
+ this.BACKGROUND_REFRESH_INTERVAL_MS = 60000; // 1 minute
33
+ // Adaptive TTL configuration
34
+ this.schemaStabilityMetrics = new Map();
35
+ this.ADAPTIVE_TTL_ENABLED = true;
36
+ this.TTL_STABLE = 3600000; // 60 minutes for stable schemas
37
+ this.TTL_NORMAL = 300000; // 5 minutes for normal schemas (default)
38
+ this.TTL_UNSTABLE = 300000; // 5 minutes for unstable schemas (same as normal)
39
+ this.STABILITY_WINDOW = 5; // Look at last 5 refreshes
40
+ this.UNSTABLE_THRESHOLD = 2; // Changed in last 2 refreshes = unstable
41
+ this.discoveredServers = new Set();
42
+ // Discovery TTL properties (v1.4.0)
43
+ this.discoveryTimers = new Map();
44
+ this.discoveryTTL = 600000; // 10 minutes (configurable)
45
+ // v1.3.74: Failed discovery tracking - skip servers that fail repeatedly
46
+ this.failedDiscoveryAttempts = new Map();
47
+ this.MAX_DISCOVERY_FAILURES = 3; // Skip after this many failures
48
+ this.DISCOVERY_FAILURE_BACKOFF_MS = 3600000; // 1 hour backoff
49
+ // v1.1.8: Startup Mutex - Prevents race condition in ensureServerStarted()
50
+ this.startupMutex = new Map();
51
+ // Auto-restart tracking for failed servers
52
+ this.autoRestartConfig = {
53
+ enabled: true,
54
+ maxRetries: 3,
55
+ baseDelay: 5000, // 5 seconds
56
+ maxDelay: 300000, // 5 minutes
57
+ backoffMultiplier: 2,
58
+ };
59
+ this.restartAttempts = new Map();
60
+ // Phase 3: Response router state
61
+ this.stdoutBuffers = new Map(); // Buffer per server
62
+ this.stdoutListeners = new Map(); // Listener references for cleanup
63
+ this.pendingRequests = new Map(); // Pending requests per server
64
+ // v1.1.49: Monotonic request ID counters per server (fixes ID collision bug)
65
+ this.requestIdCounters = new Map();
66
+ // Timeout configuration (in milliseconds)
67
+ this.timeouts = {
68
+ serverInit: 10000, // 10s - Server initialization timeout
69
+ toolFetch: 60000, // 60s - Tool fetch timeout
70
+ toolExecution: 300000, // 5min - Tool execution timeout
71
+ gracefulShutdown: 5000, // 5s - Graceful shutdown timeout
72
+ };
73
+ // Initialize circuit breaker manager with configuration
74
+ this.circuitBreakerManager = new CircuitBreakerManager({
75
+ failureThreshold: config?.circuitBreaker?.failureThreshold ?? 3,
76
+ resetTimeout: config?.circuitBreaker?.resetTimeout ?? 30000,
77
+ halfOpenSuccessThreshold: config?.circuitBreaker?.halfOpenSuccessThreshold ?? 1,
78
+ onStateChange: (oldState, newState, serverName) => {
79
+ console.log(`[CircuitBreaker] ${serverName}: ${oldState} → ${newState}`);
80
+ this.emit("circuit-breaker-state-change", {
81
+ serverName,
82
+ oldState,
83
+ newState,
84
+ });
85
+ },
86
+ });
87
+ // Phase 1-3: Initialize extracted managers
88
+ this.processManager = new ProcessManager(this.circuitBreakerManager, {
89
+ autoRestart: this.autoRestartConfig,
90
+ });
91
+ this.httpConnectionManager = new HttpConnectionManager({
92
+ timeouts: {
93
+ gracefulShutdown: this.timeouts.gracefulShutdown,
94
+ },
95
+ });
96
+ this.schemaCacheManager = new SchemaCacheManager({
97
+ cacheTTL: config?.schemasCacheTTL ?? 300000,
98
+ maxCacheEntries: config?.maxSchemaCacheEntries ?? 100,
99
+ backgroundRefresh: config?.schemasBackgroundRefresh ?? true,
100
+ cacheDir: config?.schemasCachePath ?? "~/.config/metalink/schemas",
101
+ });
102
+ // Forward events from managers
103
+ this.processManager.on("server:started", ({ name, pid }) => {
104
+ this.emit("server:started", { name, pid });
105
+ });
106
+ this.processManager.on("server:stopped", ({ name }) => {
107
+ this.emit("server:stopped", { name });
108
+ });
109
+ this.processManager.on("health:check", (result) => {
110
+ this.emit("health:check", result);
111
+ });
112
+ this.httpConnectionManager.on("server:started", ({ name, type, url }) => {
113
+ this.emit("server:started", { name, type, url });
114
+ });
115
+ this.httpConnectionManager.on("server:stopped", ({ name }) => {
116
+ this.emit("server:stopped", { name });
117
+ });
118
+ this.httpConnectionManager.on("health:check", (result) => {
119
+ this.emit("health:check", result);
120
+ });
121
+ // Initialize auto-restart configuration
122
+ if (config?.autoRestart) {
123
+ if (config.autoRestart.enabled !== undefined) {
124
+ this.autoRestartConfig.enabled = config.autoRestart.enabled;
125
+ }
126
+ if (config.autoRestart.maxRetries !== undefined) {
127
+ this.autoRestartConfig.maxRetries = config.autoRestart.maxRetries;
128
+ }
129
+ if (config.autoRestart.baseDelay !== undefined) {
130
+ this.autoRestartConfig.baseDelay = config.autoRestart.baseDelay;
131
+ }
132
+ if (config.autoRestart.maxDelay !== undefined) {
133
+ this.autoRestartConfig.maxDelay = config.autoRestart.maxDelay;
134
+ }
135
+ if (config.autoRestart.backoffMultiplier !== undefined) {
136
+ this.autoRestartConfig.backoffMultiplier =
137
+ config.autoRestart.backoffMultiplier;
138
+ }
139
+ console.log(`[ServerManager] Auto-restart configured:`, this.autoRestartConfig);
140
+ }
141
+ // Load base servers from config (empty array if not configured)
142
+ this.baseServers = config?.base_servers || [];
143
+ if (this.baseServers.length > 0) {
144
+ console.log(`[ServerManager] Base servers configured:`, this.baseServers);
145
+ }
146
+ if (config?.schemasCacheTTL) {
147
+ this.schemasCacheTTL = config.schemasCacheTTL;
148
+ console.log(`[ServerManager] Schema cache TTL configured to ${config.schemasCacheTTL}ms`);
149
+ }
150
+ // Initialize LRU cache for schemas with bounded memory
151
+ const maxCacheEntries = config?.maxSchemaCacheEntries ?? 100;
152
+ this.toolSchemaCache = new LRUCache({
153
+ max: maxCacheEntries,
154
+ dispose: (value, key) => {
155
+ console.log(`[ServerManager] LRU eviction: ${key} (${value.tools.length} tools, age: ${Date.now() - value.timestamp}ms)`);
156
+ },
157
+ });
158
+ console.log(`[ServerManager] Schema cache initialized with max ${maxCacheEntries} entries`);
159
+ // Initialize persistent schema store
160
+ const cachePath = config?.schemasCachePath || "~/.config/metalink/schemas";
161
+ // Expand ~ to home directory (v1.3.60: global cache across versions)
162
+ const expandedCachePath = cachePath.startsWith("~/")
163
+ ? cachePath.replace("~", process.env.HOME || process.env.USERPROFILE || "~")
164
+ : cachePath;
165
+ this.schemaStore = new SchemaStore(expandedCachePath, this.schemasCacheTTL);
166
+ // Load cached schemas from disk on startup
167
+ this.loadSchemasFromDisk();
168
+ // Initialize background refresh setting
169
+ if (config?.schemasBackgroundRefresh !== undefined) {
170
+ this.schemasBackgroundRefresh = config.schemasBackgroundRefresh;
171
+ }
172
+ // Initialize discovery TTL (v1.4.0)
173
+ if (config?.discoveryTTL !== undefined) {
174
+ this.discoveryTTL = config.discoveryTTL;
175
+ console.log(`[ServerManager] Discovery TTL configured to ${config.discoveryTTL}ms`);
176
+ }
177
+ else if (process.env.METALINK_DISCOVERY_TTL) {
178
+ this.discoveryTTL = parseInt(process.env.METALINK_DISCOVERY_TTL);
179
+ console.log(`[ServerManager] Discovery TTL from env: ${this.discoveryTTL}ms`);
180
+ }
181
+ // Initialize timeout configuration
182
+ if (config?.timeouts) {
183
+ if (config.timeouts.serverInit !== undefined) {
184
+ this.timeouts.serverInit = config.timeouts.serverInit;
185
+ }
186
+ if (config.timeouts.toolFetch !== undefined) {
187
+ this.timeouts.toolFetch = config.timeouts.toolFetch;
188
+ }
189
+ if (config.timeouts.toolExecution !== undefined) {
190
+ this.timeouts.toolExecution = config.timeouts.toolExecution;
191
+ }
192
+ if (config.timeouts.gracefulShutdown !== undefined) {
193
+ this.timeouts.gracefulShutdown = config.timeouts.gracefulShutdown;
194
+ }
195
+ console.log(`[ServerManager] Timeouts configured:`, this.timeouts);
196
+ }
197
+ // Override with environment variables if present
198
+ if (process.env.METALINK_TIMEOUT_INIT) {
199
+ this.timeouts.serverInit = parseInt(process.env.METALINK_TIMEOUT_INIT);
200
+ console.log(`[ServerManager] Server init timeout from env: ${this.timeouts.serverInit}ms`);
201
+ }
202
+ if (process.env.METALINK_TIMEOUT_TOOL_FETCH) {
203
+ this.timeouts.toolFetch = parseInt(process.env.METALINK_TIMEOUT_TOOL_FETCH);
204
+ console.log(`[ServerManager] Tool fetch timeout from env: ${this.timeouts.toolFetch}ms`);
205
+ }
206
+ if (process.env.METALINK_TIMEOUT_TOOL_EXECUTION) {
207
+ this.timeouts.toolExecution = parseInt(process.env.METALINK_TIMEOUT_TOOL_EXECUTION);
208
+ console.log(`[ServerManager] Tool execution timeout from env: ${this.timeouts.toolExecution}ms`);
209
+ }
210
+ if (process.env.METALINK_TIMEOUT_SHUTDOWN) {
211
+ this.timeouts.gracefulShutdown = parseInt(process.env.METALINK_TIMEOUT_SHUTDOWN);
212
+ console.log(`[ServerManager] Graceful shutdown timeout from env: ${this.timeouts.gracefulShutdown}ms`);
213
+ }
214
+ // Start background refresh if enabled
215
+ if (this.schemasBackgroundRefresh) {
216
+ this.startBackgroundRefresh();
217
+ }
218
+ }
219
+ /**
220
+ * Load cached schemas from disk into memory on startup
221
+ * Restores schemas across daemon restarts
222
+ */
223
+ async loadSchemasFromDisk() {
224
+ if (!this.schemaStore)
225
+ return;
226
+ try {
227
+ const diskSchemas = await this.schemaStore.loadFromDisk();
228
+ let loadedCount = 0;
229
+ for (const [serverName, schema] of diskSchemas) {
230
+ // Load all schemas from disk regardless of TTL (persist across restarts)
231
+ // Background refresh will update expired schemas for running servers
232
+ this.toolSchemaCache.set(serverName, schema);
233
+ this.schemaStore.updateMemoryCache(serverName, schema);
234
+ this.discoveredServers.add(serverName);
235
+ // Restore stability metrics from disk
236
+ if (schema.stability) {
237
+ this.schemaStabilityMetrics.set(serverName, {
238
+ serverName,
239
+ refreshCount: schema.stability.refreshCount,
240
+ changeCount: schema.stability.changeCount,
241
+ lastChangeTimestamp: schema.stability.lastChangeTimestamp,
242
+ currentTTL: schema.stability.currentTTL,
243
+ stability: schema.stability.stability,
244
+ });
245
+ }
246
+ loadedCount++;
247
+ }
248
+ console.log(`[SchemaStore] Loaded ${loadedCount} valid schemas from disk (with stability metrics)`);
249
+ }
250
+ catch (error) {
251
+ console.warn("[SchemaStore] Failed to load schemas from disk:", error);
252
+ // Continue without disk cache - will fetch on demand
253
+ }
254
+ }
255
+ /**
256
+ * Calculate adaptive TTL based on schema stability
257
+ * Returns TTL in milliseconds
258
+ */
259
+ calculateAdaptiveTTL(serverName) {
260
+ if (!this.ADAPTIVE_TTL_ENABLED) {
261
+ return this.schemasCacheTTL; // Use default if disabled
262
+ }
263
+ const metrics = this.schemaStabilityMetrics.get(serverName);
264
+ if (!metrics) {
265
+ return this.TTL_NORMAL; // Default for new servers
266
+ }
267
+ // Stable: 0 changes in last 5 refreshes → 60 minute TTL
268
+ if (metrics.refreshCount >= this.STABILITY_WINDOW &&
269
+ metrics.changeCount === 0) {
270
+ return this.TTL_STABLE;
271
+ }
272
+ // Unstable: changed in last 2 refreshes → 5 minute TTL (same as normal)
273
+ if (metrics.changeCount >= this.UNSTABLE_THRESHOLD) {
274
+ return this.TTL_UNSTABLE;
275
+ }
276
+ // Normal: between stable and unstable → 5 minute TTL
277
+ return this.TTL_NORMAL;
278
+ }
279
+ /**
280
+ * Classify stability level for logging
281
+ */
282
+ getStabilityLevel(serverName) {
283
+ const metrics = this.schemaStabilityMetrics.get(serverName);
284
+ if (!metrics)
285
+ return "normal";
286
+ if (metrics.refreshCount >= this.STABILITY_WINDOW &&
287
+ metrics.changeCount === 0) {
288
+ return "stable";
289
+ }
290
+ if (metrics.changeCount >= this.UNSTABLE_THRESHOLD) {
291
+ return "unstable";
292
+ }
293
+ return "normal";
294
+ }
295
+ /**
296
+ * Detect if schema has changed (compare tool names and counts)
297
+ */
298
+ hasSchemaChanged(oldTools, newTools) {
299
+ if (oldTools.length !== newTools.length) {
300
+ return true;
301
+ }
302
+ // Compare tool names (sorted)
303
+ const oldNames = oldTools.map((t) => t.name).sort();
304
+ const newNames = newTools.map((t) => t.name).sort();
305
+ for (let i = 0; i < oldNames.length; i++) {
306
+ if (oldNames[i] !== newNames[i]) {
307
+ return true;
308
+ }
309
+ }
310
+ return false; // No changes detected
311
+ }
312
+ /**
313
+ * Update stability metrics after refresh
314
+ */
315
+ updateStabilityMetrics(serverName, schemaChanged) {
316
+ let metrics = this.schemaStabilityMetrics.get(serverName);
317
+ if (!metrics) {
318
+ // Initialize new metrics
319
+ metrics = {
320
+ serverName,
321
+ refreshCount: 0,
322
+ changeCount: 0,
323
+ lastChangeTimestamp: 0,
324
+ currentTTL: this.TTL_NORMAL,
325
+ stability: "normal",
326
+ };
327
+ this.schemaStabilityMetrics.set(serverName, metrics);
328
+ }
329
+ // Update counters
330
+ metrics.refreshCount++;
331
+ if (schemaChanged) {
332
+ metrics.changeCount++;
333
+ metrics.lastChangeTimestamp = Date.now();
334
+ // Reset stability window when schema changes
335
+ // Only keep last STABILITY_WINDOW refreshes
336
+ if (metrics.refreshCount > this.STABILITY_WINDOW) {
337
+ metrics.refreshCount = this.STABILITY_WINDOW;
338
+ metrics.changeCount = Math.min(metrics.changeCount, this.UNSTABLE_THRESHOLD);
339
+ }
340
+ }
341
+ else {
342
+ // Schema stable - decay change count over time
343
+ if (metrics.refreshCount >= this.STABILITY_WINDOW) {
344
+ metrics.changeCount = Math.max(0, metrics.changeCount - 1);
345
+ }
346
+ }
347
+ // Recalculate TTL
348
+ metrics.currentTTL = this.calculateAdaptiveTTL(serverName);
349
+ metrics.stability = this.getStabilityLevel(serverName);
350
+ }
351
+ /**
352
+ * Start background refresh interval
353
+ * Periodically refreshes schemas for servers near TTL expiration
354
+ */
355
+ startBackgroundRefresh() {
356
+ if (this.backgroundRefreshInterval) {
357
+ return; // Already running
358
+ }
359
+ this.backgroundRefreshInterval = setInterval(async () => {
360
+ await this.refreshExpiredSchemas();
361
+ // v1.3.72: Also proactively discover schemas for uncached servers
362
+ await this.discoverUncachedServers();
363
+ }, this.BACKGROUND_REFRESH_INTERVAL_MS);
364
+ const adaptiveMsg = this.ADAPTIVE_TTL_ENABLED
365
+ ? ` (adaptive TTL: stable=${this.TTL_STABLE}ms, normal=${this.TTL_NORMAL}ms)`
366
+ : "";
367
+ console.log(`[SchemaStore] Background refresh enabled (interval: 1min, refresh at 80% TTL${adaptiveMsg})`);
368
+ }
369
+ /**
370
+ * Refresh schemas that are expiring soon (>80% of adaptive TTL)
371
+ * Only refreshes servers that are already running
372
+ */
373
+ async refreshExpiredSchemas() {
374
+ const now = Date.now();
375
+ for (const [serverName, schema] of this.toolSchemaCache) {
376
+ // Use adaptive TTL per server
377
+ const serverTTL = this.calculateAdaptiveTTL(serverName);
378
+ const expireThreshold = serverTTL * 0.8; // 80% of adaptive TTL
379
+ const age = now - schema.timestamp;
380
+ // Skip if not near expiration
381
+ if (age < expireThreshold) {
382
+ continue;
383
+ }
384
+ // Skip if server not running (no need to refresh)
385
+ if (!this.isServerActive(serverName)) {
386
+ continue;
387
+ }
388
+ try {
389
+ const oldTools = schema.tools;
390
+ const process = this.getProcess(serverName);
391
+ if (!process)
392
+ continue;
393
+ const tools = await this.fetchToolsFromProcess(process, serverName);
394
+ // Detect schema change
395
+ const schemaChanged = this.hasSchemaChanged(oldTools, tools);
396
+ // Update stability metrics
397
+ this.updateStabilityMetrics(serverName, schemaChanged);
398
+ // Get updated metrics
399
+ const metrics = this.schemaStabilityMetrics.get(serverName);
400
+ // Log refresh with stability info
401
+ const changeMsg = schemaChanged ? " (schema changed)" : " (no changes)";
402
+ const stabilityMsg = metrics
403
+ ? ` [${metrics.stability}, TTL: ${metrics.currentTTL}ms]`
404
+ : "";
405
+ console.log(`[SchemaStore] Background refresh for ${serverName}${changeMsg}${stabilityMsg}`);
406
+ // Prepare new schema with stability metrics
407
+ const newSchema = {
408
+ tools,
409
+ timestamp: now,
410
+ stability: metrics
411
+ ? {
412
+ refreshCount: metrics.refreshCount,
413
+ changeCount: metrics.changeCount,
414
+ lastChangeTimestamp: metrics.lastChangeTimestamp,
415
+ currentTTL: metrics.currentTTL,
416
+ stability: metrics.stability,
417
+ }
418
+ : undefined,
419
+ };
420
+ // Update memory cache
421
+ this.toolSchemaCache.set(serverName, { tools, timestamp: now });
422
+ // Persist to disk (non-blocking)
423
+ if (this.schemaStore) {
424
+ this.schemaStore.saveToDisk(serverName, newSchema).catch((error) => {
425
+ console.warn(`[SchemaStore] Failed to persist refreshed schema for ${serverName}:`, error);
426
+ });
427
+ }
428
+ }
429
+ catch (error) {
430
+ console.warn(`[SchemaStore] Background refresh failed for ${serverName}:`, error);
431
+ }
432
+ }
433
+ }
434
+ /**
435
+ * v1.3.72: Proactively discover schemas for servers without cache
436
+ * Starts servers one at a time to avoid overload, fetches schemas, then stops them
437
+ * Only discovers ONE server per refresh cycle to avoid overwhelming the system
438
+ */
439
+ async discoverUncachedServers() {
440
+ if (!this.serverListProvider) {
441
+ return; // No provider set, can't discover
442
+ }
443
+ try {
444
+ const allServers = this.serverListProvider();
445
+ // Find first server without cached schema (disk or memory)
446
+ for (const config of allServers) {
447
+ const serverName = config.name;
448
+ // Skip if already has cached schema in memory
449
+ if (this.toolSchemaCache.has(serverName)) {
450
+ continue;
451
+ }
452
+ // Skip if already has cached schema on disk
453
+ if (this.schemaStore) {
454
+ const diskCached = this.schemaStore.getSyncFromDisk(serverName);
455
+ if (diskCached && diskCached.length > 0) {
456
+ // Also populate memory cache
457
+ this.toolSchemaCache.set(serverName, {
458
+ tools: diskCached,
459
+ timestamp: Date.now(),
460
+ });
461
+ continue;
462
+ }
463
+ }
464
+ // Skip if already running (will be discovered via normal flow)
465
+ if (this.isServerActive(serverName)) {
466
+ continue;
467
+ }
468
+ // v1.3.74: Skip if server has failed too many times recently
469
+ const failedAttempt = this.failedDiscoveryAttempts.get(serverName);
470
+ if (failedAttempt) {
471
+ const timeSinceLastAttempt = Date.now() - failedAttempt.lastAttempt;
472
+ if (failedAttempt.count >= this.MAX_DISCOVERY_FAILURES &&
473
+ timeSinceLastAttempt < this.DISCOVERY_FAILURE_BACKOFF_MS) {
474
+ // Skip this server, still in backoff period
475
+ continue;
476
+ }
477
+ // Reset if backoff period has passed
478
+ if (timeSinceLastAttempt >= this.DISCOVERY_FAILURE_BACKOFF_MS) {
479
+ this.failedDiscoveryAttempts.delete(serverName);
480
+ console.log(`[SchemaStore] Proactive discovery: ${serverName} backoff expired, will retry`);
481
+ }
482
+ }
483
+ // Discover this server's schema
484
+ console.log(`[SchemaStore] Proactive discovery: starting ${serverName} to fetch schemas...`);
485
+ try {
486
+ // Start server
487
+ await this.startServer(config);
488
+ let tools = [];
489
+ // Check if HTTP or stdio server
490
+ const isHttpServer = this.httpClients.has(serverName);
491
+ if (isHttpServer) {
492
+ const client = this.httpClients.get(serverName);
493
+ const response = await client.call("tools/list", {});
494
+ if (response.result &&
495
+ typeof response.result === "object" &&
496
+ "tools" in response.result) {
497
+ tools = response.result.tools;
498
+ }
499
+ }
500
+ else {
501
+ const process = this.getProcess(serverName);
502
+ if (process) {
503
+ tools = await this.fetchToolsFromProcess(process, serverName);
504
+ }
505
+ }
506
+ // Cache the schema
507
+ const schema = { tools, timestamp: Date.now() };
508
+ this.toolSchemaCache.set(serverName, schema);
509
+ // Persist to disk
510
+ if (this.schemaStore) {
511
+ await this.schemaStore.saveToDisk(serverName, schema);
512
+ }
513
+ console.log(`[SchemaStore] Proactive discovery: ${serverName} cached (${tools.length} tools)`);
514
+ // v1.1.33: Don't kill servers after caching - let them stay running
515
+ // This prevents "process has been killed" errors and improves latency
516
+ // await this.stopServer(serverName);
517
+ // console.log(`[SchemaStore] Proactive discovery: ${serverName} stopped after caching`);
518
+ // Only discover ONE server per cycle to avoid overwhelming the system
519
+ return;
520
+ }
521
+ catch (error) {
522
+ console.warn(`[SchemaStore] Proactive discovery failed for ${serverName}:`, error);
523
+ // v1.3.74: Track failure for backoff
524
+ const existing = this.failedDiscoveryAttempts.get(serverName);
525
+ const failureCount = (existing?.count || 0) + 1;
526
+ this.failedDiscoveryAttempts.set(serverName, {
527
+ count: failureCount,
528
+ lastAttempt: Date.now(),
529
+ });
530
+ if (failureCount >= this.MAX_DISCOVERY_FAILURES) {
531
+ console.log(`[SchemaStore] Proactive discovery: ${serverName} failed ${failureCount} times, skipping for 1 hour`);
532
+ }
533
+ // Try to stop if it was started
534
+ try {
535
+ await this.stopServer(serverName);
536
+ }
537
+ catch {
538
+ // Ignore stop errors
539
+ }
540
+ // Continue to next server in next cycle
541
+ return;
542
+ }
543
+ }
544
+ }
545
+ catch (error) {
546
+ console.warn("[SchemaStore] Proactive discovery error:", error);
547
+ }
548
+ }
549
+ /**
550
+ * Start a server process (stdio or HTTP)
551
+ */
552
+ async startServer(config) {
553
+ if (this.processes.has(config.name) || this.httpClients.has(config.name)) {
554
+ throw new Error(`Server ${config.name} is already running`);
555
+ }
556
+ // Route to appropriate handler based on transport
557
+ if (config.transport === "stdio" || config.transport === undefined) {
558
+ return this.startStdioServer(config);
559
+ }
560
+ else {
561
+ return this.startHttpServer(config);
562
+ }
563
+ }
564
+ /**
565
+ * Start a stdio-based server (spawns process)
566
+ */
567
+ async startStdioServer(config) {
568
+ try {
569
+ // Helper to expand env var syntax: ${VAR} and ~/path
570
+ const expandEnvVar = (value, env) => {
571
+ // Expand ${VAR_NAME} syntax
572
+ let expanded = value.replace(/\$\{([^}]+)\}/g, (_, key) => env[key] || value);
573
+ // Expand ~ to home directory
574
+ if (expanded.startsWith("~")) {
575
+ expanded = expanded.replace(/^~/, env.HOME || "/root");
576
+ }
577
+ return expanded;
578
+ };
579
+ // Replace environment variables in args and env values
580
+ const processEnv = {
581
+ ...process.env,
582
+ };
583
+ // Expand and add config.env
584
+ if (config.env) {
585
+ for (const [key, value] of Object.entries(config.env)) {
586
+ processEnv[key] = expandEnvVar(value, processEnv);
587
+ }
588
+ }
589
+ // Resolve env vars in args
590
+ const args = (config.args || []).map((arg) => expandEnvVar(arg, processEnv));
591
+ // Prepare spawn options with optional cwd
592
+ const spawnOptions = {
593
+ env: processEnv,
594
+ stdio: "pipe",
595
+ };
596
+ // Add cwd if configured
597
+ if (config.cwd) {
598
+ // Expand ~ and env variables in cwd path
599
+ let cwdPath = config.cwd;
600
+ if (cwdPath.startsWith("~")) {
601
+ cwdPath = cwdPath.replace(/^~/, processEnv.HOME || "/root");
602
+ }
603
+ // Resolve to absolute path
604
+ spawnOptions.cwd = path.resolve(cwdPath);
605
+ console.log(`[${config.name}] Starting with cwd: ${spawnOptions.cwd}`);
606
+ }
607
+ // v1.1.52: Robust orphan process cleanup with security fixes
608
+ // Addresses: command injection, false positives, cross-user matching, parent process exclusion
609
+ // Step 1: Kill any in-memory tracked process (from this ServerManager instance)
610
+ const existingProcess = this.processes.get(config.name);
611
+ if (existingProcess && !existingProcess.killed) {
612
+ console.log(`[${config.name}] Killing tracked orphan process (PID: ${existingProcess.pid}) before starting new instance`);
613
+ try {
614
+ existingProcess.kill("SIGTERM");
615
+ await new Promise((resolve) => setTimeout(resolve, 300));
616
+ if (!existingProcess.killed) {
617
+ existingProcess.kill("SIGKILL");
618
+ }
619
+ }
620
+ catch (killError) {
621
+ console.warn(`[${config.name}] Failed to kill tracked process:`, killError);
622
+ }
623
+ this.processes.delete(config.name);
624
+ this.activeServers.delete(config.name);
625
+ }
626
+ // Step 2: Kill system orphan processes using METALINK_SERVER_NAME env var
627
+ // This is the unique identifier we inject into spawned processes
628
+ const serverNamePattern = `METALINK_SERVER_NAME=${config.name}`;
629
+ // Also create fallback pattern from args (for processes from older versions)
630
+ // Use scoped package names (@org/pkg) which are unique, avoid generic patterns
631
+ const packageIdentifier = args.find((arg) => arg.includes("@") && arg.includes("/")) ||
632
+ (config.command !== "npx" && config.command !== "uvx"
633
+ ? config.command
634
+ : null);
635
+ // Get PIDs to exclude: current process + parent + grandparent
636
+ const excludePids = new Set();
637
+ excludePids.add(String(process.pid));
638
+ try {
639
+ const ppidResult = spawnSync("ps", ["-o", "ppid=", "-p", String(process.pid)], { encoding: "utf8", timeout: 2000 });
640
+ const ppid = ppidResult.stdout?.trim();
641
+ if (ppid) {
642
+ excludePids.add(ppid);
643
+ // Get grandparent too
644
+ const gppidResult = spawnSync("ps", ["-o", "ppid=", "-p", ppid], {
645
+ encoding: "utf8",
646
+ timeout: 2000,
647
+ });
648
+ const gppid = gppidResult.stdout?.trim();
649
+ if (gppid)
650
+ excludePids.add(gppid);
651
+ }
652
+ }
653
+ catch {
654
+ // Non-fatal - continue with just current PID excluded
655
+ }
656
+ // Get current user ID for filtering
657
+ let currentUid = "";
658
+ try {
659
+ const idResult = spawnSync("id", ["-u"], {
660
+ encoding: "utf8",
661
+ timeout: 2000,
662
+ });
663
+ currentUid = idResult.stdout?.trim() || "";
664
+ }
665
+ catch {
666
+ // Non-fatal - will skip user filtering
667
+ }
668
+ // Search for orphans using server name pattern (primary) or package identifier (fallback)
669
+ const patternsToSearch = [serverNamePattern];
670
+ if (packageIdentifier) {
671
+ patternsToSearch.push(packageIdentifier);
672
+ }
673
+ const orphanPids = new Set();
674
+ for (const pattern of patternsToSearch) {
675
+ try {
676
+ // Use spawnSync to avoid shell injection - no string interpolation
677
+ const pgrepArgs = currentUid
678
+ ? ["-f", "-u", currentUid, pattern]
679
+ : ["-f", pattern];
680
+ const pgrepResult = spawnSync("pgrep", pgrepArgs, {
681
+ encoding: "utf8",
682
+ timeout: 5000,
683
+ });
684
+ // pgrep returns exit code 1 if no matches (not an error)
685
+ const pids = (pgrepResult.stdout || "")
686
+ .trim()
687
+ .split("\n")
688
+ .filter((pid) => pid && !excludePids.has(pid));
689
+ pids.forEach((pid) => orphanPids.add(pid));
690
+ }
691
+ catch (pgrepError) {
692
+ const errMsg = pgrepError instanceof Error
693
+ ? pgrepError.message
694
+ : String(pgrepError);
695
+ if (errMsg.includes("command not found") ||
696
+ errMsg.includes("ENOENT")) {
697
+ console.error(`[${config.name}] pgrep not found - cannot detect orphan processes`);
698
+ }
699
+ // Other errors are non-fatal
700
+ }
701
+ }
702
+ if (orphanPids.size > 0) {
703
+ const pidList = Array.from(orphanPids);
704
+ console.log(`[${config.name}] Found ${pidList.length} orphan process(es): PIDs ${pidList.join(", ")}`);
705
+ // Send SIGTERM to all orphans (using spawnSync for safety)
706
+ for (const pid of pidList) {
707
+ try {
708
+ const killResult = spawnSync("kill", ["-TERM", pid], {
709
+ timeout: 1000,
710
+ });
711
+ if (killResult.status === 0) {
712
+ console.log(`[${config.name}] Sent SIGTERM to orphan PID: ${pid}`);
713
+ }
714
+ }
715
+ catch {
716
+ // Process may have already exited
717
+ }
718
+ }
719
+ // Wait for graceful termination
720
+ await new Promise((resolve) => setTimeout(resolve, 800));
721
+ // Force kill any remaining with SIGKILL
722
+ for (const pid of pidList) {
723
+ try {
724
+ // Check if still alive
725
+ const checkResult = spawnSync("kill", ["-0", pid], {
726
+ timeout: 500,
727
+ });
728
+ if (checkResult.status === 0) {
729
+ // Still alive - force kill
730
+ spawnSync("kill", ["-KILL", pid], { timeout: 1000 });
731
+ console.log(`[${config.name}] Sent SIGKILL to orphan PID: ${pid}`);
732
+ }
733
+ }
734
+ catch {
735
+ // Process already dead
736
+ }
737
+ }
738
+ }
739
+ // Inject METALINK_SERVER_NAME into process env for future orphan detection
740
+ processEnv.METALINK_SERVER_NAME = config.name;
741
+ processEnv.METALINK_VERSION = version;
742
+ const childProcess = spawn(config.command, args, spawnOptions);
743
+ this.processes.set(config.name, childProcess);
744
+ this.state[config.name] = {
745
+ pid: childProcess.pid || 0,
746
+ status: "running",
747
+ uptime: Date.now(),
748
+ lastHealthCheck: Date.now(),
749
+ errorCount: 0,
750
+ };
751
+ // Track server start in metrics (Phase 4 - v1.4.0)
752
+ globalMetrics.recordServerStart(config.name);
753
+ // Capture stdout and stderr for debugging
754
+ let stdoutData = "";
755
+ let stderrData = "";
756
+ const debugMode = process.env.METALINK_DEBUG === "true";
757
+ if (childProcess.stdout) {
758
+ childProcess.stdout.on("data", (chunk) => {
759
+ const str = chunk.toString();
760
+ stdoutData += str;
761
+ if (debugMode) {
762
+ console.log(`[DIAG] [${config.name}] Listener #1 (startStdioServer debug) received data:`, str.substring(0, 100));
763
+ }
764
+ console.log(`[${config.name}] stdout: ${str}`);
765
+ });
766
+ }
767
+ if (childProcess.stderr) {
768
+ childProcess.stderr.on("data", (chunk) => {
769
+ const str = chunk.toString();
770
+ stderrData += str;
771
+ console.error(`[${config.name}] stderr: ${str}`);
772
+ });
773
+ }
774
+ // Handle process errors
775
+ childProcess.on("error", (error) => {
776
+ this.handleServerError(config.name, error);
777
+ });
778
+ childProcess.on("exit", (code) => {
779
+ if (code !== 0 && (stdoutData || stderrData)) {
780
+ console.error(`[MetaLink] Server '${config.name}' output on crash:`);
781
+ if (stdoutData)
782
+ console.error(` stdout: ${stdoutData}`);
783
+ if (stderrData)
784
+ console.error(` stderr: ${stderrData}`);
785
+ }
786
+ this.handleServerExit(config.name, code);
787
+ });
788
+ // Start health checks if enabled
789
+ if (config.healthCheck?.enabled) {
790
+ this.startHealthCheck(config.name, config.healthCheck.interval || 30000);
791
+ }
792
+ this.emit("server:started", { name: config.name, pid: childProcess.pid });
793
+ return this.state[config.name];
794
+ }
795
+ catch (error) {
796
+ throw new Error(`Failed to start stdio server ${config.name}: ${error instanceof Error ? error.message : String(error)}`);
797
+ }
798
+ }
799
+ /**
800
+ * Start an HTTP-based server (connects via HTTP client)
801
+ */
802
+ async startHttpServer(config) {
803
+ try {
804
+ // Phase 6: Delegate to HttpConnectionManager
805
+ const serverProcess = await this.httpConnectionManager.startHttpServer(config);
806
+ // Keep local state for backward compatibility
807
+ const client = this.httpConnectionManager.getClient(config.name);
808
+ if (client) {
809
+ this.httpClients.set(config.name, client);
810
+ }
811
+ // Update local state
812
+ this.state[config.name] = serverProcess;
813
+ // Track server start in metrics (Phase 4 - v1.4.0)
814
+ globalMetrics.recordServerStart(config.name);
815
+ // Forward events
816
+ this.httpConnectionManager.on("server:started", ({ name, type, url }) => {
817
+ this.emit("server:started", { name, type, url });
818
+ });
819
+ return serverProcess;
820
+ }
821
+ catch (error) {
822
+ throw new Error(`Failed to start HTTP server ${config.name}: ${error instanceof Error ? error.message : String(error)}`);
823
+ }
824
+ }
825
+ /**
826
+ * Stop a server process (stdio or HTTP)
827
+ */
828
+ async stopServer(serverName) {
829
+ // Phase 5: Check ProcessManager first, then fall back to local maps
830
+ const isStdioInManager = this.processManager.isProcessRunning(serverName);
831
+ const isStdioInLocal = this.processes.has(serverName);
832
+ const isHttp = this.httpClients.has(serverName);
833
+ if (!isStdioInManager && !isStdioInLocal && !isHttp) {
834
+ throw new Error(`Server ${serverName} is not running`);
835
+ }
836
+ // Stop health checks
837
+ const healthCheckInterval = this.healthCheckIntervals.get(serverName);
838
+ if (healthCheckInterval) {
839
+ clearInterval(healthCheckInterval);
840
+ this.healthCheckIntervals.delete(serverName);
841
+ }
842
+ if (isStdioInManager) {
843
+ // Phase 5: Delegate to ProcessManager
844
+ await this.processManager.stopServer(serverName);
845
+ // Also clean up local map if present
846
+ if (isStdioInLocal) {
847
+ this.processes.delete(serverName);
848
+ }
849
+ }
850
+ else if (isStdioInLocal) {
851
+ // Legacy: Direct process termination
852
+ const childProcess = this.processes.get(serverName);
853
+ // v1.1.27: Clean up all event listeners to prevent memory leaks
854
+ if (childProcess.stdout) {
855
+ childProcess.stdout.removeAllListeners();
856
+ }
857
+ if (childProcess.stderr) {
858
+ childProcess.stderr.removeAllListeners();
859
+ }
860
+ childProcess.removeAllListeners();
861
+ childProcess.kill("SIGTERM");
862
+ // Wait for graceful shutdown
863
+ await new Promise((resolve) => {
864
+ const timeout = setTimeout(() => {
865
+ childProcess.kill("SIGKILL");
866
+ resolve();
867
+ }, this.timeouts.gracefulShutdown);
868
+ childProcess.once("exit", () => {
869
+ clearTimeout(timeout);
870
+ resolve();
871
+ });
872
+ });
873
+ this.processes.delete(serverName);
874
+ }
875
+ else if (isHttp) {
876
+ // Phase 6: Delegate to HttpConnectionManager
877
+ if (this.httpConnectionManager.hasClient(serverName)) {
878
+ await this.httpConnectionManager.stopServer(serverName);
879
+ }
880
+ // Clean up local map
881
+ this.httpClients.delete(serverName);
882
+ }
883
+ if (this.state[serverName]) {
884
+ this.state[serverName].status = "stopped";
885
+ }
886
+ this.emit("server:stopped", { name: serverName });
887
+ }
888
+ /**
889
+ * Remove a server from the manager
890
+ * Stops the process if running and cleans up all associated state
891
+ * Called when a server is removed from the registry
892
+ */
893
+ async removeServer(serverName) {
894
+ // 1. Stop process if running
895
+ try {
896
+ await this.stopServer(serverName);
897
+ }
898
+ catch (error) {
899
+ // Server might not be running, continue cleanup anyway
900
+ }
901
+ // 2. Clean up active servers tracking
902
+ this.activeServers.delete(serverName);
903
+ // 3. Clean up response router listeners
904
+ const listener = this.stdoutListeners.get(serverName);
905
+ if (listener) {
906
+ // Remove listeners from the process if it still exists
907
+ const process = this.processes.get(serverName);
908
+ if (process && process.stdout) {
909
+ process.stdout.removeListener("data", listener.onStdoutData);
910
+ process.removeListener("error", listener.onError);
911
+ process.removeListener("exit", listener.onExit);
912
+ }
913
+ this.stdoutListeners.delete(serverName);
914
+ }
915
+ // 4. Clear pending requests for this server
916
+ const pendingRequests = this.pendingRequests.get(serverName);
917
+ if (pendingRequests) {
918
+ // Reject all pending requests with informative error
919
+ for (const [requestId, request] of pendingRequests) {
920
+ clearTimeout(request.timeout);
921
+ request.reject(new Error(`Server '${serverName}' was removed from registry (pending request ${requestId})`));
922
+ }
923
+ this.pendingRequests.delete(serverName);
924
+ }
925
+ // 5. Clear stdout buffers
926
+ this.stdoutBuffers.delete(serverName);
927
+ // 6. Delete state
928
+ delete this.state[serverName];
929
+ // 7. Ensure process is fully removed
930
+ this.processes.delete(serverName);
931
+ this.emit("server:removed", { name: serverName });
932
+ }
933
+ /**
934
+ * Restart a server
935
+ * v1.4.2: Clears schema cache to ensure fresh tool discovery
936
+ */
937
+ async restartServer(config) {
938
+ try {
939
+ await this.stopServer(config.name);
940
+ }
941
+ catch {
942
+ // Server might not be running, continue anyway
943
+ }
944
+ // v1.4.2: Clear schema cache to force fresh discovery on restart
945
+ this.invalidateServerSchema(config.name);
946
+ return this.startServer(config);
947
+ }
948
+ /**
949
+ * Invalidate schema cache for a server
950
+ * Forces fresh discovery on next access
951
+ * v1.4.2: Added to support force-refresh of tool lists
952
+ */
953
+ invalidateServerSchema(serverName) {
954
+ // Phase 7: Delegate to SchemaCacheManager
955
+ this.schemaCacheManager.invalidate(serverName);
956
+ // Legacy: Keep local cache invalidation for backward compatibility
957
+ const hadCache = this.toolSchemaCache.has(serverName);
958
+ this.toolSchemaCache.delete(serverName);
959
+ // Clear from discovered servers set
960
+ this.discoveredServers.delete(serverName);
961
+ // Clear discovery timer if exists
962
+ const timer = this.discoveryTimers.get(serverName);
963
+ if (timer) {
964
+ clearTimeout(timer);
965
+ this.discoveryTimers.delete(serverName);
966
+ }
967
+ // Clear stability metrics
968
+ this.schemaStabilityMetrics.delete(serverName);
969
+ if (hadCache) {
970
+ console.log(`[ServerManager] Schema cache invalidated for ${serverName}`);
971
+ }
972
+ }
973
+ /**
974
+ * Force refresh schema for a server
975
+ * Invalidates cache and re-discovers tools from running server
976
+ * v1.4.2: Added to support force-refresh of tool lists
977
+ */
978
+ async forceRefreshSchema(serverName, config) {
979
+ console.log(`[ServerManager] Force refreshing schema for ${serverName}...`);
980
+ // 1. Invalidate all caches
981
+ this.invalidateServerSchema(serverName);
982
+ // 2. Also delete from disk cache
983
+ if (this.schemaStore) {
984
+ try {
985
+ const schemaPath = `${this.schemaStore["cacheDir"]}/${serverName}.json`;
986
+ const fs = await import("fs/promises");
987
+ await fs.unlink(schemaPath).catch(() => { }); // Ignore if not exists
988
+ console.log(`[ServerManager] Deleted disk cache for ${serverName}`);
989
+ }
990
+ catch (error) {
991
+ // Disk cache deletion is best-effort
992
+ }
993
+ }
994
+ // 3. If server is running, fetch tools directly
995
+ if (this.isServerActive(serverName)) {
996
+ const process = this.getProcess(serverName);
997
+ if (process) {
998
+ try {
999
+ const tools = await this.fetchToolsFromProcess(process, serverName);
1000
+ const schema = { tools, timestamp: Date.now() };
1001
+ // Update cache
1002
+ this.toolSchemaCache.set(serverName, schema);
1003
+ this.discoveredServers.add(serverName);
1004
+ // Persist to disk
1005
+ if (this.schemaStore) {
1006
+ await this.schemaStore.saveToDisk(serverName, schema);
1007
+ }
1008
+ console.log(`[ServerManager] Force refreshed ${serverName}: ${tools.length} tools`);
1009
+ return tools;
1010
+ }
1011
+ catch (error) {
1012
+ console.warn(`[ServerManager] Failed to fetch tools from running server ${serverName}:`, error);
1013
+ }
1014
+ }
1015
+ }
1016
+ // 4. If not running or fetch failed, do full discovery
1017
+ return this.discoverToolSchemas(serverName, config);
1018
+ }
1019
+ /**
1020
+ * Get server status
1021
+ */
1022
+ getServerStatus(serverName) {
1023
+ return this.state[serverName];
1024
+ }
1025
+ /**
1026
+ * Get all server statuses
1027
+ */
1028
+ getAllServers() {
1029
+ return { ...this.state };
1030
+ }
1031
+ /**
1032
+ * Start health check for a server
1033
+ */
1034
+ startHealthCheck(serverName, interval) {
1035
+ const healthCheckInterval = setInterval(async () => {
1036
+ const result = await this.healthCheck(serverName);
1037
+ this.emit("health:check", result);
1038
+ if (!result.healthy && this.state[serverName]) {
1039
+ this.state[serverName].errorCount++;
1040
+ }
1041
+ }, interval);
1042
+ this.healthCheckIntervals.set(serverName, healthCheckInterval);
1043
+ }
1044
+ /**
1045
+ * Perform health check on a server
1046
+ */
1047
+ async healthCheck(serverName) {
1048
+ const childProcess = this.processes.get(serverName);
1049
+ const state = this.state[serverName];
1050
+ if (!childProcess || !state) {
1051
+ return {
1052
+ serverName,
1053
+ healthy: false,
1054
+ error: "Server not found",
1055
+ timestamp: Date.now(),
1056
+ };
1057
+ }
1058
+ // Simple health check: check if process is still running
1059
+ const healthy = !childProcess.killed;
1060
+ if (state) {
1061
+ state.lastHealthCheck = Date.now();
1062
+ }
1063
+ return {
1064
+ serverName,
1065
+ healthy,
1066
+ timestamp: Date.now(),
1067
+ };
1068
+ }
1069
+ /**
1070
+ * Start health checks for HTTP server
1071
+ */
1072
+ startHttpHealthCheck(serverName, interval) {
1073
+ const healthCheckInterval = setInterval(async () => {
1074
+ const result = await this.httpHealthCheck(serverName);
1075
+ this.emit("health:check", result);
1076
+ if (!result.healthy && this.state[serverName]) {
1077
+ this.state[serverName].errorCount++;
1078
+ }
1079
+ }, interval);
1080
+ this.healthCheckIntervals.set(serverName, healthCheckInterval);
1081
+ }
1082
+ /**
1083
+ * Perform health check on an HTTP server
1084
+ */
1085
+ async httpHealthCheck(serverName) {
1086
+ const client = this.httpClients.get(serverName);
1087
+ const state = this.state[serverName];
1088
+ if (!client || !state) {
1089
+ return {
1090
+ serverName,
1091
+ healthy: false,
1092
+ error: "HTTP client not found",
1093
+ timestamp: Date.now(),
1094
+ };
1095
+ }
1096
+ try {
1097
+ // Use HTTP client's built-in health check
1098
+ const healthy = await client.healthCheck();
1099
+ if (state) {
1100
+ state.lastHealthCheck = Date.now();
1101
+ }
1102
+ return {
1103
+ serverName,
1104
+ healthy,
1105
+ timestamp: Date.now(),
1106
+ };
1107
+ }
1108
+ catch (error) {
1109
+ return {
1110
+ serverName,
1111
+ healthy: false,
1112
+ error: error instanceof Error ? error.message : "Unknown error",
1113
+ timestamp: Date.now(),
1114
+ };
1115
+ }
1116
+ }
1117
+ /**
1118
+ * Handle server errors
1119
+ */
1120
+ handleServerError(serverName, error) {
1121
+ console.error(`[MetaLink] Server '${serverName}' error:`, error.message);
1122
+ if (this.state[serverName]) {
1123
+ this.state[serverName].status = "crashed";
1124
+ this.state[serverName].errorCount++;
1125
+ }
1126
+ // Track server error in metrics with classification (Phase 4 - v1.4.0)
1127
+ const errorType = globalMetrics.classifyErrorType(error);
1128
+ globalMetrics.recordServerError(serverName, error.message, errorType);
1129
+ this.emit("server:error", { name: serverName, error: error.message });
1130
+ }
1131
+ /**
1132
+ * Handle server exit
1133
+ */
1134
+ handleServerExit(serverName, code) {
1135
+ this.processes.delete(serverName);
1136
+ this.activeServers.delete(serverName); // v1.1.33: Clean up stale server data to prevent "process killed" errors
1137
+ if (code !== 0) {
1138
+ console.error(`[MetaLink] Server '${serverName}' exited with code ${code}`);
1139
+ }
1140
+ if (this.state[serverName]) {
1141
+ this.state[serverName].status = code === 0 ? "stopped" : "crashed";
1142
+ }
1143
+ this.emit("server:exited", { name: serverName, code });
1144
+ // Trigger auto-restart if server crashed (non-zero exit code)
1145
+ if (code !== 0) {
1146
+ this.scheduleServerRestart(serverName);
1147
+ }
1148
+ }
1149
+ /**
1150
+ * Get tools for a server
1151
+ * v1.3.60: Check both active servers AND schema cache (for stopped servers)
1152
+ * v1.3.71: Also check disk cache for tools that persist across daemon restarts
1153
+ */
1154
+ getServerTools(serverName) {
1155
+ // Phase 7: Delegate to SchemaCacheManager first
1156
+ const cachedTools = this.schemaCacheManager.getCachedTools(serverName);
1157
+ if (cachedTools && cachedTools.length > 0) {
1158
+ return cachedTools;
1159
+ }
1160
+ // Fallback to active servers (running)
1161
+ const serverData = this.activeServers.get(serverName);
1162
+ if (serverData?.tools && serverData.tools.length > 0) {
1163
+ return serverData.tools;
1164
+ }
1165
+ // Legacy fallback to local caches (will be removed in Phase 8)
1166
+ const cached = this.toolSchemaCache.get(serverName);
1167
+ if (cached?.tools && cached.tools.length > 0) {
1168
+ return cached.tools;
1169
+ }
1170
+ // Fallback to disk cache via SchemaStore
1171
+ if (this.schemaStore) {
1172
+ const diskCached = this.schemaStore.getSyncFromDisk(serverName);
1173
+ if (diskCached && diskCached.length > 0) {
1174
+ // Also populate memory cache for faster subsequent lookups
1175
+ this.toolSchemaCache.set(serverName, {
1176
+ tools: diskCached,
1177
+ timestamp: Date.now(),
1178
+ });
1179
+ return diskCached;
1180
+ }
1181
+ }
1182
+ return [];
1183
+ }
1184
+ /**
1185
+ * Set tools for a server
1186
+ * v1.3.62: Persist schemas to disk whenever tools are updated
1187
+ */
1188
+ setServerTools(serverName, tools, process) {
1189
+ let serverData = this.activeServers.get(serverName);
1190
+ if (!serverData) {
1191
+ serverData = {
1192
+ process: process || this.processes.get(serverName),
1193
+ tools: [],
1194
+ toolsReady: false,
1195
+ };
1196
+ this.activeServers.set(serverName, serverData);
1197
+ }
1198
+ else if (process) {
1199
+ // Update process if provided
1200
+ serverData.process = process;
1201
+ }
1202
+ else if (!serverData.process) {
1203
+ // Try to retrieve from processes map if not already set
1204
+ serverData.process = this.processes.get(serverName);
1205
+ }
1206
+ // Phase 2: Enrich tools with safety annotations
1207
+ const enrichedTools = this.enrichToolsWithAnnotations(serverName, tools);
1208
+ serverData.tools = enrichedTools;
1209
+ serverData.toolsReady = true;
1210
+ // v1.3.62: Persist schemas to disk (global cache across versions)
1211
+ // This ensures schemas are saved regardless of how server started:
1212
+ // - Base server initialization
1213
+ // - Direct tool execution (auto-start)
1214
+ // - search_tools (when server already running)
1215
+ // - discover_tools (explicit discovery)
1216
+ const schema = { tools: enrichedTools, timestamp: Date.now() };
1217
+ // Phase 7: Delegate to SchemaCacheManager
1218
+ this.schemaCacheManager.setCachedTools(serverName, enrichedTools, "discovery");
1219
+ // Legacy: Keep local caches for backward compatibility (will be removed in Phase 8)
1220
+ this.toolSchemaCache.set(serverName, schema);
1221
+ if (this.schemaStore) {
1222
+ this.schemaStore.saveToDisk(serverName, schema).catch((error) => {
1223
+ console.warn(`[ServerManager] Failed to persist schema for ${serverName}:`, error);
1224
+ });
1225
+ }
1226
+ }
1227
+ /**
1228
+ * Get all active servers with their tools
1229
+ */
1230
+ getActiveServers() {
1231
+ return new Map(this.activeServers);
1232
+ }
1233
+ /**
1234
+ * Get the process for a specific server
1235
+ * Phase 5: Delegate to ProcessManager first, fallback to local map
1236
+ */
1237
+ getProcess(serverName) {
1238
+ // Try ProcessManager first (Phase 5 delegation)
1239
+ const managerProcess = this.processManager.getProcess(serverName);
1240
+ if (managerProcess) {
1241
+ return managerProcess;
1242
+ }
1243
+ // Legacy fallback to local map
1244
+ return this.processes.get(serverName);
1245
+ }
1246
+ /**
1247
+ * Check if a server is currently active (running and tools loaded)
1248
+ */
1249
+ isServerActive(serverName) {
1250
+ // v1.1.36: Check process.killed flag to ensure dead processes trigger auto-restart
1251
+ const process = this.processes.get(serverName);
1252
+ const hasValidProcess = process && !process.killed;
1253
+ const hasHttpClient = this.httpClients.has(serverName);
1254
+ const hasTools = this.activeServers.has(serverName);
1255
+ return (hasValidProcess || hasHttpClient) && hasTools;
1256
+ }
1257
+ /**
1258
+ * Wait for MCP initialization handshake to complete (without fetching tools)
1259
+ */
1260
+ async waitForInitialization(childProcess, serverName) {
1261
+ return new Promise((resolve, reject) => {
1262
+ const debugMode = process.env.METALINK_DEBUG === "true";
1263
+ let settled = false; // Guard against double-rejection
1264
+ const timeout = setTimeout(() => {
1265
+ if (settled)
1266
+ return; // Already resolved/rejected, ignore timeout
1267
+ settled = true;
1268
+ const serverInfo = serverName ? ` for ${serverName}` : "";
1269
+ console.error(`[MetaLink] Server initialization timeout after ${this.timeouts.serverInit}ms${serverInfo}`);
1270
+ if (debugMode) {
1271
+ console.log(`[DIAG] waitForInitialization${serverInfo}: TIMEOUT (no initialize response in ${this.timeouts.serverInit}ms)`);
1272
+ }
1273
+ childProcess.stdout?.removeListener("data", onData);
1274
+ reject(new Error(`Server initialization timeout after ${this.timeouts.serverInit}ms${serverInfo}`));
1275
+ }, this.timeouts.serverInit);
1276
+ let buffer = "";
1277
+ let initialized = false;
1278
+ const handleLines = () => {
1279
+ const lines = buffer.split("\n");
1280
+ for (let i = 0; i < lines.length - 1; i++) {
1281
+ const line = lines[i].trim();
1282
+ if (!line || !line.startsWith("{"))
1283
+ continue;
1284
+ let response;
1285
+ try {
1286
+ response = JSON.parse(line);
1287
+ }
1288
+ catch (parseError) {
1289
+ continue;
1290
+ }
1291
+ // Wait for initialize response only
1292
+ if (!initialized &&
1293
+ (response.result || response.error) &&
1294
+ response.id === 0) {
1295
+ if (settled)
1296
+ return; // Already timed out or settled
1297
+ settled = true;
1298
+ if (debugMode) {
1299
+ console.log(`[DIAG] waitForInitialization: Detected initialize response (id=0), success=${!!response.result}`);
1300
+ }
1301
+ clearTimeout(timeout);
1302
+ childProcess.stdout?.removeListener("data", onData);
1303
+ if (response.error) {
1304
+ reject(new Error(`Server initialization failed: ${JSON.stringify(response.error)}`));
1305
+ }
1306
+ else {
1307
+ console.log(`[MetaLink] Server ${serverName || "unknown"} initialized successfully`);
1308
+ // Send initialized notification (MCP spec requirement)
1309
+ const initializedNotif = JSON.stringify({
1310
+ jsonrpc: "2.0",
1311
+ method: "notifications/initialized",
1312
+ params: {},
1313
+ });
1314
+ childProcess.stdin?.write(initializedNotif + "\n");
1315
+ resolve();
1316
+ }
1317
+ return;
1318
+ }
1319
+ }
1320
+ buffer = lines[lines.length - 1];
1321
+ };
1322
+ const onData = (data) => {
1323
+ buffer += data.toString();
1324
+ handleLines();
1325
+ };
1326
+ childProcess.stdout?.on("data", onData);
1327
+ // Send initialize request
1328
+ const initReq = JSON.stringify({
1329
+ jsonrpc: "2.0",
1330
+ id: 0,
1331
+ method: "initialize",
1332
+ params: {
1333
+ protocolVersion: "2025-06-18",
1334
+ capabilities: { roots: { listChanged: true } },
1335
+ clientInfo: { name: "metalink", version: "1.3.49" },
1336
+ },
1337
+ });
1338
+ childProcess.stdin?.write(initReq + "\n");
1339
+ });
1340
+ }
1341
+ /**
1342
+ * Fetch tools from a spawned MCP server via JSON-RPC (PUBLIC)
1343
+ */
1344
+ async fetchToolsFromProcess(childProcess, serverName) {
1345
+ return new Promise((resolve, reject) => {
1346
+ const debugMode = process.env.METALINK_DEBUG === "true";
1347
+ let settled = false; // CRITICAL FIX: Guard against double-rejection
1348
+ // Capture stderr for debugging (especially for npm-based servers)
1349
+ let stderrOutput = "";
1350
+ if (childProcess.stderr) {
1351
+ childProcess.stderr.on("data", (chunk) => {
1352
+ stderrOutput += chunk.toString();
1353
+ });
1354
+ }
1355
+ // v1.1.27: Track SIGKILL timeout to prevent race condition
1356
+ let killTimeout = null;
1357
+ // Timeout: 60s for npm-based servers (download + install + startup)
1358
+ const timeout = setTimeout(() => {
1359
+ if (settled)
1360
+ return; // CRITICAL FIX: Already resolved/rejected, ignore timeout
1361
+ settled = true;
1362
+ const serverInfo = serverName ? ` for ${serverName}` : "";
1363
+ console.error(`[MetaLink] Server initialization timeout after 60s${serverInfo}`);
1364
+ if (debugMode) {
1365
+ console.log(`[DIAG] fetchToolsFromProcess${serverInfo}: TIMEOUT (no tools/list response in 60s)`);
1366
+ }
1367
+ if (stderrOutput) {
1368
+ console.error(`[MetaLink] Server stderr${serverInfo}:\n${stderrOutput}`);
1369
+ }
1370
+ try {
1371
+ childProcess.kill("SIGTERM");
1372
+ // v1.1.27: Store SIGKILL timeout reference
1373
+ killTimeout = setTimeout(() => {
1374
+ if (!childProcess.killed) {
1375
+ childProcess.kill("SIGKILL");
1376
+ }
1377
+ }, 2000);
1378
+ }
1379
+ catch (killError) {
1380
+ console.error(`[MetaLink] Error killing timed-out process: ${killError instanceof Error ? killError.message : String(killError)}`);
1381
+ }
1382
+ reject(new Error(`Server initialization timeout after ${this.timeouts.toolFetch}ms${serverInfo}`));
1383
+ }, this.timeouts.toolFetch);
1384
+ // v1.1.27: Helper to clear all timeouts
1385
+ const clearAllTimeouts = () => {
1386
+ clearTimeout(timeout);
1387
+ if (killTimeout) {
1388
+ clearTimeout(killTimeout);
1389
+ killTimeout = null;
1390
+ }
1391
+ };
1392
+ let buffer = "";
1393
+ let initialized = false;
1394
+ const handleLines = () => {
1395
+ const lines = buffer.split("\n");
1396
+ for (let i = 0; i < lines.length - 1; i++) {
1397
+ const line = lines[i].trim();
1398
+ if (!line || !line.startsWith("{"))
1399
+ continue;
1400
+ let response;
1401
+ try {
1402
+ response = JSON.parse(line);
1403
+ }
1404
+ catch (parseError) {
1405
+ console.error(`[MetaLink] Failed to parse JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
1406
+ continue;
1407
+ }
1408
+ // Wait for initialize response first
1409
+ if (!initialized &&
1410
+ (response.result || response.error) &&
1411
+ response.id === 0) {
1412
+ if (response.error) {
1413
+ if (settled)
1414
+ return; // CRITICAL FIX: Already resolved/rejected
1415
+ settled = true;
1416
+ clearAllTimeouts(); // v1.1.27: Use helper to clear all timeouts
1417
+ childProcess.stdout?.removeListener("data", onData);
1418
+ reject(new Error(`Server initialization failed: ${JSON.stringify(response.error)}`));
1419
+ return;
1420
+ }
1421
+ else {
1422
+ initialized = true;
1423
+ console.error(`[MetaLink] Server initialized successfully, requesting tools...`);
1424
+ // Send initialized notification (MCP spec requirement)
1425
+ const initializedNotif = JSON.stringify({
1426
+ jsonrpc: "2.0",
1427
+ method: "notifications/initialized",
1428
+ params: {},
1429
+ });
1430
+ childProcess.stdin?.write(initializedNotif + "\n");
1431
+ // Request tools/list
1432
+ const listReq = JSON.stringify({
1433
+ jsonrpc: "2.0",
1434
+ id: 1,
1435
+ method: "tools/list",
1436
+ params: {},
1437
+ });
1438
+ childProcess.stdin?.write(listReq + "\n");
1439
+ continue;
1440
+ }
1441
+ }
1442
+ // Handle tools/list response
1443
+ if (response.id === 1) {
1444
+ if (debugMode) {
1445
+ console.log(`[DIAG] fetchToolsFromProcess: Detected tools/list response (id=1), has_error=${!!response.error}, has_result=${!!response.result}`);
1446
+ }
1447
+ if (response.error) {
1448
+ if (settled)
1449
+ return; // CRITICAL FIX: Already resolved/rejected
1450
+ settled = true;
1451
+ clearAllTimeouts(); // v1.1.27: Use helper to clear all timeouts
1452
+ childProcess.stdout?.removeListener("data", onData);
1453
+ reject(new Error(`Failed to get tools list: ${JSON.stringify(response.error)}`));
1454
+ return;
1455
+ }
1456
+ const resultTools = response.result
1457
+ ?.tools;
1458
+ if (resultTools && Array.isArray(resultTools)) {
1459
+ if (settled)
1460
+ return; // CRITICAL FIX: Already resolved/rejected
1461
+ settled = true;
1462
+ clearAllTimeouts(); // v1.1.27: Use helper to clear all timeouts
1463
+ childProcess.stdout?.removeListener("data", onData);
1464
+ console.error(`[MetaLink] Successfully received ${resultTools.length} tools`);
1465
+ if (debugMode) {
1466
+ console.log(`[DIAG] fetchToolsFromProcess: Returning ${resultTools.length} tools to caller`);
1467
+ }
1468
+ // v1.3.58: Health check - warn if server returns 0 tools
1469
+ if (resultTools.length === 0) {
1470
+ console.error(`[MetaLink] ⚠️ WARNING: Server returned 0 tools!`);
1471
+ console.error(`[MetaLink] This may indicate:`);
1472
+ console.error(`[MetaLink] • Server still initializing (may need more time)`);
1473
+ console.error(`[MetaLink] • Backend service connection failed`);
1474
+ console.error(`[MetaLink] • Environment variables (API keys, URLs) incorrect`);
1475
+ console.error(`[MetaLink] • Server does not implement tools/list correctly`);
1476
+ console.error(`[MetaLink] Debug with: metalink server debug <server-name>`);
1477
+ }
1478
+ // v1.4.0: Schema validation
1479
+ this.validateToolSchemas(resultTools, serverName);
1480
+ resolve(resultTools);
1481
+ return;
1482
+ }
1483
+ }
1484
+ }
1485
+ buffer = lines[lines.length - 1];
1486
+ };
1487
+ const onData = (data) => {
1488
+ buffer += data.toString();
1489
+ handleLines();
1490
+ };
1491
+ childProcess.stdout?.on("data", onData);
1492
+ childProcess.stderr?.on("data", (data) => {
1493
+ const msg = data.toString().trim();
1494
+ if (msg)
1495
+ console.error(`[MetaLink] [${childProcess.pid}] stderr: ${msg}`);
1496
+ });
1497
+ // Send initialize request
1498
+ const initReq = JSON.stringify({
1499
+ jsonrpc: "2.0",
1500
+ id: 0,
1501
+ method: "initialize",
1502
+ params: {
1503
+ protocolVersion: "2025-06-18",
1504
+ capabilities: { roots: { listChanged: true } },
1505
+ clientInfo: { name: "metalink", version },
1506
+ },
1507
+ });
1508
+ childProcess.stdin?.write(initReq + "\n");
1509
+ });
1510
+ }
1511
+ /**
1512
+ * Initialize base servers before daemon startup
1513
+ * Starts all configured base servers unconditionally for immediate availability
1514
+ */
1515
+ async initializeBaseServers(configLoader) {
1516
+ if (this.baseServersEnabled) {
1517
+ console.log("[MetaLink] Base servers already initialized");
1518
+ return;
1519
+ }
1520
+ if (this.baseServers.length === 0) {
1521
+ console.log("[MetaLink] No base servers configured, skipping initialization");
1522
+ this.baseServersEnabled = true;
1523
+ return;
1524
+ }
1525
+ console.log(`[MetaLink] Initializing base servers: ${this.baseServers.join(", ")}`);
1526
+ // Get all available servers (config + registry)
1527
+ const allServers = configLoader.getAllServers();
1528
+ const serverConfigs = new Map(allServers.map((s) => [s.name, s]));
1529
+ // Start base servers in parallel for faster startup (80% improvement: 5-15s → <2s)
1530
+ const startResults = await Promise.allSettled(this.baseServers.map(async (baseServerName) => {
1531
+ const config = serverConfigs.get(baseServerName);
1532
+ if (!config) {
1533
+ console.warn(`[MetaLink] Base server '${baseServerName}' not found in available servers, skipping`);
1534
+ return { server: baseServerName, status: "skipped" };
1535
+ }
1536
+ try {
1537
+ console.log(`[MetaLink] Starting base server: ${baseServerName}`);
1538
+ await this.startServer(config);
1539
+ // Fetch tools from the started server
1540
+ const serverProcess = this.processes.get(baseServerName);
1541
+ if (serverProcess) {
1542
+ const tools = await this.fetchToolsFromProcess(serverProcess, baseServerName);
1543
+ this.setServerTools(baseServerName, tools);
1544
+ console.log(`[MetaLink] Base server '${baseServerName}' ready with ${tools.length} tools`);
1545
+ return {
1546
+ server: baseServerName,
1547
+ status: "success",
1548
+ toolCount: tools.length,
1549
+ };
1550
+ }
1551
+ return { server: baseServerName, status: "no-process" };
1552
+ }
1553
+ catch (error) {
1554
+ console.error(`[MetaLink] Failed to start base server '${baseServerName}': ${error instanceof Error ? error.message : String(error)}`);
1555
+ return {
1556
+ server: baseServerName,
1557
+ status: "failed",
1558
+ error: error instanceof Error ? error.message : String(error),
1559
+ };
1560
+ }
1561
+ }));
1562
+ // Count successful starts
1563
+ const startedCount = startResults.filter((r) => r.status === "fulfilled" && r.value?.status === "success").length;
1564
+ this.baseServersEnabled = true;
1565
+ console.log(`[MetaLink] Base servers initialization complete (${startedCount}/${this.baseServers.length} started)`);
1566
+ }
1567
+ /**
1568
+ * Check if base servers are enabled
1569
+ */
1570
+ areBaseServersEnabled() {
1571
+ return this.baseServersEnabled;
1572
+ }
1573
+ /**
1574
+ * Background population of missing schemas (v1.3.62)
1575
+ * Compares registry servers with cached schemas and populates missing ones
1576
+ * Runs in background, one server at a time, non-blocking
1577
+ */
1578
+ async populateMissingSchemas(configLoader) {
1579
+ console.log("[SchemaPopulation] Starting background schema population...");
1580
+ // Get all servers from registry
1581
+ const allServers = configLoader.getAllServers();
1582
+ const serverNames = Array.from(allServers.keys());
1583
+ // Find servers missing from cache
1584
+ const missingServers = serverNames.filter((name) => !this.toolSchemaCache.has(name));
1585
+ if (missingServers.length === 0) {
1586
+ console.log("[SchemaPopulation] All servers already have cached schemas");
1587
+ return;
1588
+ }
1589
+ console.log(`[SchemaPopulation] Found ${missingServers.length} servers with missing schemas:`, missingServers);
1590
+ // Start servers one by one in background
1591
+ let populated = 0;
1592
+ for (const serverName of missingServers) {
1593
+ try {
1594
+ const config = allServers.get(serverName);
1595
+ if (!config)
1596
+ continue;
1597
+ console.log(`[SchemaPopulation] [${populated + 1}/${missingServers.length}] Populating schema for ${serverName}...`);
1598
+ // Start server and fetch tools
1599
+ await this.ensureServerStarted(serverName, config);
1600
+ const tools = this.getServerTools(serverName);
1601
+ if (tools.length > 0) {
1602
+ console.log(`[SchemaPopulation] ✅ Cached ${tools.length} tools from ${serverName}`);
1603
+ populated++;
1604
+ // v1.1.33: Don't kill servers after caching - improves reliability
1605
+ // Previously would stop server here to save resources, but this caused
1606
+ // "process has been killed" errors when tools were called
1607
+ // this.stopServer(serverName);
1608
+ // console.log(`[SchemaPopulation] Stopped ${serverName} (schema cached)`);
1609
+ }
1610
+ else {
1611
+ console.warn(`[SchemaPopulation] ⚠️ ${serverName} returned 0 tools`);
1612
+ }
1613
+ }
1614
+ catch (error) {
1615
+ console.error(`[SchemaPopulation] Failed to populate ${serverName}:`, error instanceof Error ? error.message : String(error));
1616
+ }
1617
+ }
1618
+ console.log(`[SchemaPopulation] Complete: ${populated}/${missingServers.length} schemas populated`);
1619
+ }
1620
+ /**
1621
+ * Get schema for a specific tool
1622
+ * v1.1.47: Add disk fallback to match getServerTools() behavior
1623
+ * This fixes the bug where CLI shows tools but MCP can't find them
1624
+ */
1625
+ getToolSchema(serverName, toolName) {
1626
+ // First check active servers (running)
1627
+ const serverData = this.activeServers.get(serverName);
1628
+ if (serverData?.tools) {
1629
+ const tool = serverData.tools.find((t) => t.name === toolName);
1630
+ if (tool)
1631
+ return tool;
1632
+ }
1633
+ // Fallback to schema cache (persistent cache for stopped servers)
1634
+ const cached = this.toolSchemaCache.get(serverName);
1635
+ if (cached?.tools) {
1636
+ const tool = cached.tools.find((t) => t.name === toolName);
1637
+ if (tool)
1638
+ return tool;
1639
+ }
1640
+ // v1.1.47: Fallback to disk cache (persists across daemon restarts)
1641
+ // This ensures getToolSchema() has same fallback chain as getServerTools()
1642
+ if (this.schemaStore) {
1643
+ const diskCached = this.schemaStore.getSyncFromDisk(serverName);
1644
+ if (diskCached && diskCached.length > 0) {
1645
+ // Populate memory cache for faster subsequent lookups
1646
+ this.toolSchemaCache.set(serverName, {
1647
+ tools: diskCached,
1648
+ timestamp: Date.now(),
1649
+ });
1650
+ const tool = diskCached.find((t) => t.name === toolName);
1651
+ if (tool)
1652
+ return tool;
1653
+ }
1654
+ }
1655
+ return null;
1656
+ }
1657
+ /**
1658
+ * Detect if arguments are missing when tool requires them
1659
+ */
1660
+ detectMissingArguments(args, toolSchema) {
1661
+ if (!args || typeof args !== "object") {
1662
+ return false;
1663
+ }
1664
+ const hasArguments = "arguments" in args;
1665
+ const argumentsValue = args.arguments;
1666
+ const inputSchema = toolSchema?.inputSchema;
1667
+ const requiresArguments = inputSchema?.required && inputSchema.required.length > 0;
1668
+ if (!hasArguments && requiresArguments && inputSchema?.required) {
1669
+ console.error(`[MetaLink] Missing 'arguments' parameter. Tool: ${args.server_name || "unknown"}-${args.tool_name || "unknown"}, ` +
1670
+ `Required params: ${inputSchema.required.join(", ")}`);
1671
+ return true;
1672
+ }
1673
+ if (hasArguments &&
1674
+ (argumentsValue === null || argumentsValue === undefined) &&
1675
+ requiresArguments &&
1676
+ inputSchema?.required) {
1677
+ console.error(`[MetaLink] 'arguments' parameter is null/undefined. Tool: ${args.server_name || "unknown"}-${args.tool_name || "unknown"}, ` +
1678
+ `Required params: ${inputSchema.required.join(", ")}`);
1679
+ return true;
1680
+ }
1681
+ return false;
1682
+ }
1683
+ /**
1684
+ * Detect and fix Raycast's malformed argument format
1685
+ * Raycast sometimes wraps tool_name and server_name inside the arguments object
1686
+ */
1687
+ detectAndFixRaycastFormat(args) {
1688
+ if (!args || typeof args !== "object" || Array.isArray(args)) {
1689
+ return args;
1690
+ }
1691
+ // Check if args.arguments exists and contains tool_name or server_name (malformed)
1692
+ const argObj = args.arguments;
1693
+ if (argObj && typeof argObj === "object" && !Array.isArray(argObj)) {
1694
+ const hasToolName = "tool_name" in argObj;
1695
+ const hasServerName = "server_name" in argObj;
1696
+ if (hasToolName || hasServerName) {
1697
+ // This is Raycast's malformed format - fix it
1698
+ const toolName = argObj.tool_name;
1699
+ const serverName = argObj.server_name;
1700
+ // Extract tool parameters (anything that's not tool_name or server_name)
1701
+ const toolParams = {};
1702
+ for (const [key, value] of Object.entries(argObj)) {
1703
+ if (key !== "tool_name" && key !== "server_name") {
1704
+ toolParams[key] = value;
1705
+ }
1706
+ }
1707
+ // Determine final arguments - unwrap if toolParams contains nested 'arguments'
1708
+ let finalArguments;
1709
+ if ("arguments" in toolParams &&
1710
+ typeof toolParams.arguments === "object") {
1711
+ // Raycast sent nested arguments - unwrap it
1712
+ finalArguments = toolParams.arguments;
1713
+ }
1714
+ else if (Object.keys(toolParams).length > 0) {
1715
+ // Tool params at this level - use them directly
1716
+ finalArguments = toolParams;
1717
+ }
1718
+ else {
1719
+ // No params found - use empty object
1720
+ finalArguments = {};
1721
+ }
1722
+ // Reconstruct correct structure
1723
+ const fixedArgs = {
1724
+ ...args,
1725
+ server_name: serverName || args.server_name,
1726
+ tool_name: toolName || args.tool_name,
1727
+ arguments: finalArguments,
1728
+ };
1729
+ console.error(`[MetaLink] [Raycast Workaround] Fixed malformed argument format for ${fixedArgs.server_name}-${fixedArgs.tool_name}` +
1730
+ ` | Unwrapped nested arguments: ${Object.keys(finalArguments).length} params`);
1731
+ if (Object.keys(finalArguments).length === 0) {
1732
+ console.error(`[MetaLink] [Raycast Issue] WARNING: No tool parameters found after fixing malformed format. ` +
1733
+ `Tool: ${fixedArgs.server_name}-${fixedArgs.tool_name} | ` +
1734
+ `This may indicate Raycast failed to populate arguments from describe_tool response.`);
1735
+ }
1736
+ return fixedArgs;
1737
+ }
1738
+ }
1739
+ // Case B: Check if tool params are at TOP level instead of in arguments
1740
+ // This happens when Raycast passes: {"server_name": "X", "tool_name": "Y", "cql": "..."}
1741
+ // instead of: {"server_name": "X", "tool_name": "Y", "arguments": {"cql": "..."}}
1742
+ if (!args.arguments) {
1743
+ // Extract tool parameters (anything that's not server_name, tool_name, or arguments)
1744
+ const topLevelParams = {};
1745
+ for (const [key, value] of Object.entries(args)) {
1746
+ if (key !== "tool_name" &&
1747
+ key !== "server_name" &&
1748
+ key !== "arguments") {
1749
+ topLevelParams[key] = value;
1750
+ }
1751
+ }
1752
+ // If we found top-level params, wrap them in arguments
1753
+ if (Object.keys(topLevelParams).length > 0) {
1754
+ console.error(`[MetaLink] [Raycast Workaround] Fixed flat argument format for ${args.server_name}-${args.tool_name}. ` +
1755
+ `Moved top-level params to arguments: ${Object.keys(topLevelParams).join(", ")}`);
1756
+ return {
1757
+ server_name: args.server_name,
1758
+ tool_name: args.tool_name,
1759
+ arguments: topLevelParams,
1760
+ };
1761
+ }
1762
+ }
1763
+ return args;
1764
+ }
1765
+ /**
1766
+ * Phase 3: Set up response router for a server
1767
+ * Listens to stdout, parses JSON-RPC responses, and matches them to pending requests
1768
+ */
1769
+ setupServerResponseRouter(serverName) {
1770
+ const serverData = this.activeServers.get(serverName);
1771
+ if (!serverData || !serverData.process) {
1772
+ return; // No process to set up router for
1773
+ }
1774
+ // Check if router already exists - but verify it's for the CURRENT process
1775
+ // After server restart, old router points to dead process and must be reset
1776
+ if (this.stdoutListeners.has(serverName)) {
1777
+ const currentPid = serverData.process.pid;
1778
+ const storedPid = this.stdoutListeners.get(serverName)?.processPid;
1779
+ if (storedPid === currentPid) {
1780
+ console.log(`[MetaLink] Response router already exists for ${serverName} (pid=${currentPid})`);
1781
+ return;
1782
+ }
1783
+ // Process changed - clean up stale router and set up fresh one
1784
+ console.log(`[MetaLink] Response router stale for ${serverName} (old pid=${storedPid}, new pid=${currentPid}), resetting...`);
1785
+ this.cleanupServerResponseRouter(serverName);
1786
+ }
1787
+ // Initialize pending requests map for this server
1788
+ if (!this.pendingRequests.has(serverName)) {
1789
+ this.pendingRequests.set(serverName, new Map());
1790
+ }
1791
+ // Initialize buffer for this server
1792
+ if (!this.stdoutBuffers.has(serverName)) {
1793
+ this.stdoutBuffers.set(serverName, "");
1794
+ }
1795
+ const maxBufferSize = 10 * 1024 * 1024; // 10MB limit per server (increased from 1MB for large results like read_graph)
1796
+ // Create shared listener function
1797
+ const onStdoutData = (data) => {
1798
+ let buffer = this.stdoutBuffers.get(serverName) || "";
1799
+ buffer += data.toString();
1800
+ // Warn if buffer is getting large (50% of max)
1801
+ const warnThreshold = maxBufferSize * 0.5;
1802
+ if (buffer.length > warnThreshold && buffer.length <= maxBufferSize) {
1803
+ console.warn(`[MetaLink] Large buffer for ${serverName}: ${(buffer.length / 1024 / 1024).toFixed(2)}MB (${((buffer.length / maxBufferSize) * 100).toFixed(0)}% of max). Consider pagination for large results.`);
1804
+ }
1805
+ // Check buffer size limit
1806
+ if (buffer.length > maxBufferSize) {
1807
+ console.error(`[MetaLink] Buffer exceeded max size (${(buffer.length / 1024 / 1024).toFixed(2)}MB > ${(maxBufferSize / 1024 / 1024).toFixed(2)}MB) for ${serverName}, rejecting pending requests`);
1808
+ const pending = this.pendingRequests.get(serverName);
1809
+ if (pending) {
1810
+ for (const [_requestId, requestInfo] of pending) {
1811
+ clearTimeout(requestInfo.timeout);
1812
+ requestInfo.reject(new Error(`Buffer exceeded max size: ${(buffer.length / 1024 / 1024).toFixed(2)}MB > ${(maxBufferSize / 1024 / 1024).toFixed(2)}MB. Consider using pagination or filtering for large results.`));
1813
+ }
1814
+ pending.clear();
1815
+ }
1816
+ this.stdoutBuffers.set(serverName, "");
1817
+ this.cleanupServerResponseRouter(serverName);
1818
+ return;
1819
+ }
1820
+ // Extract complete JSON objects using brace-depth tracking
1821
+ // This handles both single-line and multiline JSON-RPC responses
1822
+ const extractCompleteJsonObjects = (buf) => {
1823
+ const objects = [];
1824
+ let position = 0;
1825
+ while (position < buf.length) {
1826
+ // Skip whitespace
1827
+ while (position < buf.length && /\s/.test(buf[position])) {
1828
+ position++;
1829
+ }
1830
+ if (position >= buf.length)
1831
+ break;
1832
+ // Must start with '{'
1833
+ if (buf[position] !== "{") {
1834
+ // Skip non-JSON content (e.g., log messages from server)
1835
+ const nextBrace = buf.indexOf("{", position);
1836
+ if (nextBrace === -1) {
1837
+ // No more JSON objects, keep remainder
1838
+ break;
1839
+ }
1840
+ position = nextBrace;
1841
+ continue;
1842
+ }
1843
+ // Track brace depth and extract complete object
1844
+ let braceDepth = 0;
1845
+ let inString = false;
1846
+ let escaped = false;
1847
+ const objectStart = position;
1848
+ while (position < buf.length) {
1849
+ const char = buf[position];
1850
+ // Handle escape sequences
1851
+ if (escaped) {
1852
+ escaped = false;
1853
+ position++;
1854
+ continue;
1855
+ }
1856
+ if (char === "\\" && inString) {
1857
+ escaped = true;
1858
+ position++;
1859
+ continue;
1860
+ }
1861
+ // Track string state
1862
+ if (char === '"' && !escaped) {
1863
+ inString = !inString;
1864
+ position++;
1865
+ continue;
1866
+ }
1867
+ if (inString) {
1868
+ position++;
1869
+ continue;
1870
+ }
1871
+ // Track brace depth (outside strings)
1872
+ if (char === "{") {
1873
+ braceDepth++;
1874
+ }
1875
+ else if (char === "}") {
1876
+ braceDepth--;
1877
+ }
1878
+ position++;
1879
+ // Complete object found
1880
+ if (braceDepth === 0) {
1881
+ const jsonStr = buf.substring(objectStart, position).trim();
1882
+ try {
1883
+ const obj = JSON.parse(jsonStr);
1884
+ if (obj.id !== undefined ||
1885
+ obj.error !== undefined ||
1886
+ obj.result !== undefined) {
1887
+ objects.push(obj);
1888
+ console.log(`[MetaLink] Extracted complete JSON object for ${serverName}: id=${obj.id}`);
1889
+ }
1890
+ }
1891
+ catch (parseError) {
1892
+ console.error(`[MetaLink] Failed to parse JSON object in ${serverName}:`, `Position ${objectStart}-${position}:`, jsonStr.substring(0, 100) +
1893
+ (jsonStr.length > 100 ? "..." : ""), parseError instanceof Error
1894
+ ? parseError.message
1895
+ : String(parseError));
1896
+ }
1897
+ break;
1898
+ }
1899
+ }
1900
+ // If we didn't complete the object, break and keep remainder in buffer
1901
+ if (braceDepth > 0) {
1902
+ position = objectStart; // Reset to start of incomplete object
1903
+ break;
1904
+ }
1905
+ }
1906
+ return {
1907
+ objects,
1908
+ remaining: buf.substring(position).trim(),
1909
+ };
1910
+ };
1911
+ // Extract complete JSON objects from buffer
1912
+ const { objects, remaining } = extractCompleteJsonObjects(buffer);
1913
+ this.stdoutBuffers.set(serverName, remaining);
1914
+ const pending = this.pendingRequests.get(serverName);
1915
+ if (!pending || pending.size === 0) {
1916
+ return; // No pending requests
1917
+ }
1918
+ // Process extracted JSON objects
1919
+ for (const response of objects) {
1920
+ const requestId = response.id;
1921
+ // Check if this response matches any pending request
1922
+ if (requestId !== undefined && pending.has(requestId)) {
1923
+ const requestInfo = pending.get(requestId);
1924
+ if (requestInfo) {
1925
+ clearTimeout(requestInfo.timeout);
1926
+ pending.delete(requestId);
1927
+ console.log(`[MetaLink] Response router: matched requestId=${requestId} for ${serverName}-${requestInfo.toolName} (${pending.size} remaining)`);
1928
+ // Route response to the correct promise
1929
+ if (response.error) {
1930
+ requestInfo.reject(new Error(JSON.stringify(response.error)));
1931
+ }
1932
+ else {
1933
+ requestInfo.resolve({
1934
+ content: [
1935
+ {
1936
+ type: "text",
1937
+ text: JSON.stringify(response.result),
1938
+ },
1939
+ ],
1940
+ });
1941
+ }
1942
+ }
1943
+ }
1944
+ }
1945
+ };
1946
+ // Set up error and exit handlers
1947
+ const onError = (error) => {
1948
+ console.error(`[MetaLink] Process error for ${serverName}: ${error.message}`);
1949
+ const pending = this.pendingRequests.get(serverName);
1950
+ if (pending) {
1951
+ for (const [_requestId, requestInfo] of pending) {
1952
+ clearTimeout(requestInfo.timeout);
1953
+ requestInfo.reject(new Error(`Server error: ${error.message}`));
1954
+ }
1955
+ pending.clear();
1956
+ }
1957
+ this.cleanupServerResponseRouter(serverName);
1958
+ };
1959
+ const onExit = (code) => {
1960
+ console.error(`[MetaLink] Process exited for ${serverName} (code=${code})`);
1961
+ const pending = this.pendingRequests.get(serverName);
1962
+ if (pending) {
1963
+ for (const [_requestId, requestInfo] of pending) {
1964
+ clearTimeout(requestInfo.timeout);
1965
+ requestInfo.reject(new Error(`Server exited unexpectedly (code=${code})`));
1966
+ }
1967
+ pending.clear();
1968
+ }
1969
+ this.activeServers.delete(serverName);
1970
+ this.cleanupServerResponseRouter(serverName);
1971
+ };
1972
+ // Attach listeners
1973
+ serverData.process.stdout?.on("data", onStdoutData);
1974
+ serverData.process.on("error", onError);
1975
+ serverData.process.on("exit", onExit);
1976
+ // Store listener references for cleanup (including PID to detect stale routers)
1977
+ this.stdoutListeners.set(serverName, {
1978
+ onStdoutData,
1979
+ onError,
1980
+ onExit,
1981
+ processPid: serverData.process.pid,
1982
+ });
1983
+ console.log(`[MetaLink] Response router set up for ${serverName} (using JSON boundary detection)`);
1984
+ }
1985
+ /**
1986
+ * Phase 3: Clean up response router for a server
1987
+ */
1988
+ cleanupServerResponseRouter(serverName) {
1989
+ const serverData = this.activeServers.get(serverName);
1990
+ const listenerInfo = this.stdoutListeners.get(serverName);
1991
+ if (listenerInfo && serverData?.process) {
1992
+ // Remove listeners
1993
+ serverData.process.stdout?.removeListener("data", listenerInfo.onStdoutData);
1994
+ serverData.process.removeListener("error", listenerInfo.onError);
1995
+ serverData.process.removeListener("exit", listenerInfo.onExit);
1996
+ console.log(`[MetaLink] Response router cleaned up for ${serverName}`);
1997
+ }
1998
+ // Clear state
1999
+ this.stdoutListeners.delete(serverName);
2000
+ this.stdoutBuffers.delete(serverName);
2001
+ }
2002
+ /**
2003
+ * Phase 3: Call a tool on a server and wait for response
2004
+ * Supports both stdio (process) and HTTP (httpClient) servers
2005
+ */
2006
+ async callTool(serverName, toolName, toolArgs) {
2007
+ if (!serverName || !toolName) {
2008
+ throw new Error(`Invalid tool call: server_name='${serverName}', tool_name='${toolName}'`);
2009
+ }
2010
+ // Check circuit breaker before attempting tool call
2011
+ const breaker = this.circuitBreakerManager.getBreaker(serverName);
2012
+ if (!breaker.canExecute()) {
2013
+ const metrics = breaker.getMetrics();
2014
+ const timeUntilReset = metrics.lastFailureTime
2015
+ ? Math.max(0, 30000 - (Date.now() - metrics.lastFailureTime))
2016
+ : 0;
2017
+ throw new Error(`Circuit breaker is OPEN for ${serverName}. ` +
2018
+ `Server has failed ${metrics.failures} consecutive times. ` +
2019
+ `Retry in ${Math.ceil(timeUntilReset / 1000)}s.`);
2020
+ }
2021
+ let serverData = this.activeServers.get(serverName);
2022
+ // v1.1.56: State recovery - if server process exists but not in activeServers, attempt recovery
2023
+ if (!serverData) {
2024
+ const process = this.processes.get(serverName);
2025
+ const httpClient = this.httpClients.get(serverName);
2026
+ // Check if process/client exists but activeServers is out of sync
2027
+ if (process && !process.killed) {
2028
+ console.warn(`[MetaLink] State corruption detected: ${serverName} has process (PID ${process.pid}) but not in activeServers. Attempting recovery...`);
2029
+ // Try to recover from schema cache
2030
+ const cached = this.toolSchemaCache.get(serverName);
2031
+ if (cached?.tools && cached.tools.length > 0) {
2032
+ this.setServerTools(serverName, cached.tools, process);
2033
+ serverData = this.activeServers.get(serverName);
2034
+ console.log(`[MetaLink] State recovered for ${serverName}: ${cached.tools.length} tools restored`);
2035
+ }
2036
+ else {
2037
+ console.error(`[MetaLink] Cannot recover ${serverName}: no cached schema. Fetching tools...`);
2038
+ try {
2039
+ const tools = await this.fetchToolsFromProcess(process, serverName);
2040
+ this.setServerTools(serverName, tools, process);
2041
+ serverData = this.activeServers.get(serverName);
2042
+ console.log(`[MetaLink] State recovered for ${serverName}: fetched ${tools.length} tools`);
2043
+ }
2044
+ catch (error) {
2045
+ console.error(`[MetaLink] Failed to fetch tools for recovery:`, error);
2046
+ throw new Error(`Server '${serverName}' state corrupted and recovery failed: ${error instanceof Error ? error.message : String(error)}`);
2047
+ }
2048
+ }
2049
+ }
2050
+ else if (httpClient) {
2051
+ console.warn(`[MetaLink] State corruption detected: ${serverName} has HTTP client but not in activeServers. Attempting recovery...`);
2052
+ // Try to recover from schema cache for HTTP servers
2053
+ const cached = this.toolSchemaCache.get(serverName);
2054
+ if (cached?.tools && cached.tools.length > 0) {
2055
+ this.setServerTools(serverName, cached.tools);
2056
+ serverData = this.activeServers.get(serverName);
2057
+ console.log(`[MetaLink] State recovered for ${serverName}: ${cached.tools.length} tools restored`);
2058
+ }
2059
+ else {
2060
+ console.error(`[MetaLink] Cannot recover ${serverName}: no cached schema for HTTP server`);
2061
+ throw new Error(`Server '${serverName}' HTTP client state corrupted and no cached schema available`);
2062
+ }
2063
+ }
2064
+ else {
2065
+ // No process or client exists - server truly not active
2066
+ const activeList = Array.from(this.activeServers.keys()).join(", ") || "none";
2067
+ throw new Error(`Server '${serverName}' is not active. Active servers: ${activeList}. Enable it first with enable_server.`);
2068
+ }
2069
+ }
2070
+ // Final type assertion after recovery
2071
+ if (!serverData) {
2072
+ throw new Error(`Server '${serverName}' state recovery failed - activeServers is still empty`);
2073
+ }
2074
+ // Check if this is an HTTP server
2075
+ const httpClient = this.httpClients.get(serverName);
2076
+ if (httpClient) {
2077
+ // HTTP server: use httpClient.call()
2078
+ console.log(`[MetaLink] Calling tool '${toolName}' on HTTP server '${serverName}'`);
2079
+ try {
2080
+ const response = await httpClient.call("tools/call", {
2081
+ name: toolName,
2082
+ arguments: toolArgs,
2083
+ });
2084
+ if (response.error) {
2085
+ breaker.recordFailure();
2086
+ throw new Error(`Tool error: ${response.error.message}`);
2087
+ }
2088
+ breaker.recordSuccess();
2089
+ // Reset restart tracking on successful tool execution
2090
+ this.resetRestartTracking(serverName);
2091
+ return response.result;
2092
+ }
2093
+ catch (error) {
2094
+ breaker.recordFailure();
2095
+ throw new Error(`HTTP tool call failed: ${error instanceof Error ? error.message : String(error)}`);
2096
+ }
2097
+ }
2098
+ // stdio server: use process stdin/stdout
2099
+ if (!serverData.process) {
2100
+ throw new Error(`Server '${serverName}' has no process associated`);
2101
+ }
2102
+ // v1.4.1: Auto-restart killed servers instead of throwing
2103
+ // This fixes "process has been killed" errors when server crashes mid-session
2104
+ if (serverData.process.killed) {
2105
+ console.log(`[MetaLink] Server '${serverName}' process was killed, auto-restarting...`);
2106
+ // Get server config for restart
2107
+ if (!this.serverListProvider) {
2108
+ throw new Error(`Server '${serverName}' process has been killed and cannot auto-restart (no config provider)`);
2109
+ }
2110
+ const allConfigs = this.serverListProvider();
2111
+ const config = allConfigs.find((c) => c.name === serverName);
2112
+ if (!config) {
2113
+ throw new Error(`Server '${serverName}' process has been killed and cannot auto-restart (config not found)`);
2114
+ }
2115
+ // Clean up dead process state
2116
+ this.processes.delete(serverName);
2117
+ this.activeServers.delete(serverName);
2118
+ // Restart the server synchronously before tool call
2119
+ await this.ensureServerStarted(serverName, config);
2120
+ // Get refreshed server data after restart
2121
+ serverData = this.activeServers.get(serverName);
2122
+ if (!serverData?.process || serverData.process.killed) {
2123
+ throw new Error(`Server '${serverName}' failed to restart`);
2124
+ }
2125
+ console.log(`[MetaLink] Server '${serverName}' auto-restarted successfully`);
2126
+ }
2127
+ console.log(`[MetaLink] Calling tool '${toolName}' on server '${serverName}'`);
2128
+ // Ensure response router is set up for this server
2129
+ this.setupServerResponseRouter(serverName);
2130
+ // Generate unique request ID using monotonic counter (v1.1.49: fixes collision bug)
2131
+ const counter = (this.requestIdCounters.get(serverName) || 0) + 1;
2132
+ this.requestIdCounters.set(serverName, counter);
2133
+ const requestId = counter;
2134
+ return new Promise((resolve, reject) => {
2135
+ // Set up timeout (5 minutes)
2136
+ const timeout = setTimeout(() => {
2137
+ const pending = this.pendingRequests.get(serverName);
2138
+ if (pending && pending.has(requestId)) {
2139
+ pending.delete(requestId);
2140
+ console.error(`[MetaLink] Tool call timeout after ${this.timeouts.toolExecution}ms: ${serverName}-${toolName} (requestId=${requestId})`);
2141
+ }
2142
+ breaker.recordFailure();
2143
+ reject(new Error(`Tool call timeout after ${this.timeouts.toolExecution}ms: ${serverName}-${toolName}`));
2144
+ }, this.timeouts.toolExecution);
2145
+ // Register this request in pendingRequests
2146
+ if (!this.pendingRequests.has(serverName)) {
2147
+ this.pendingRequests.set(serverName, new Map());
2148
+ }
2149
+ const pending = this.pendingRequests.get(serverName);
2150
+ // Create wrapped reject that cleans up and records failure
2151
+ const wrappedReject = (error) => {
2152
+ clearTimeout(timeout);
2153
+ if (pending.has(requestId)) {
2154
+ pending.delete(requestId);
2155
+ console.log(`[MetaLink] Request ${requestId} rejected for ${serverName}-${toolName}`);
2156
+ }
2157
+ breaker.recordFailure();
2158
+ reject(error);
2159
+ };
2160
+ // Create wrapped resolve that records success
2161
+ const wrappedResolve = (result) => {
2162
+ clearTimeout(timeout);
2163
+ if (pending.has(requestId)) {
2164
+ pending.delete(requestId);
2165
+ }
2166
+ breaker.recordSuccess();
2167
+ // Reset restart tracking on successful tool execution
2168
+ this.resetRestartTracking(serverName);
2169
+ resolve(result);
2170
+ };
2171
+ pending.set(requestId, {
2172
+ resolve: wrappedResolve,
2173
+ reject: wrappedReject,
2174
+ timeout,
2175
+ toolName,
2176
+ startTime: Date.now(),
2177
+ });
2178
+ console.log(`[MetaLink] Registered request ${requestId} for ${serverName}-${toolName} (${pending.size} total pending)`);
2179
+ // Send the request
2180
+ const request = JSON.stringify({
2181
+ jsonrpc: "2.0",
2182
+ id: requestId,
2183
+ method: "tools/call",
2184
+ params: {
2185
+ name: toolName,
2186
+ arguments: toolArgs,
2187
+ },
2188
+ });
2189
+ try {
2190
+ console.log(`[MetaLink] Writing tool call request (id=${requestId}) to ${serverName}-${toolName}`);
2191
+ if (!serverData.process) {
2192
+ throw new Error(`Server process not available for ${serverName}`);
2193
+ }
2194
+ serverData.process.stdin?.write(request + "\n");
2195
+ }
2196
+ catch (writeError) {
2197
+ // Clean up on write error
2198
+ if (pending.has(requestId)) {
2199
+ clearTimeout(timeout);
2200
+ pending.delete(requestId);
2201
+ }
2202
+ breaker.recordFailure();
2203
+ reject(new Error(`Failed to write tool call request: ${writeError instanceof Error ? writeError.message : String(writeError)}`));
2204
+ }
2205
+ });
2206
+ }
2207
+ /**
2208
+ * Discover tool schemas from a server with auto-start capability
2209
+ * Uses cache (in-memory first, then disk) to avoid repeated restarts
2210
+ * Auto-starts servers on first discovery (transparent to caller)
2211
+ * Persists schemas to disk for faster discovery across daemon restarts
2212
+ */
2213
+ async discoverToolSchemas(serverName, config) {
2214
+ // 1. Check in-memory cache first (fastest)
2215
+ const memCached = this.toolSchemaCache.get(serverName);
2216
+ if (memCached && Date.now() - memCached.timestamp < this.schemasCacheTTL) {
2217
+ console.log(`[Discovery] Using in-memory cache for ${serverName} (${memCached.tools.length} tools, TTL: ${this.schemasCacheTTL}ms)`);
2218
+ this.discoveredServers.add(serverName);
2219
+ this.resetDiscoveryTimer(serverName); // v1.4.0: Start/reset expiration timer
2220
+ // Trigger background refresh if nearing expiration
2221
+ if (this.schemasBackgroundRefresh &&
2222
+ Date.now() - memCached.timestamp > this.schemasCacheTTL * 0.8) {
2223
+ this.refreshSchemaForServer(serverName).catch((error) => {
2224
+ console.warn(`[Discovery] Background refresh failed for ${serverName}:`, error);
2225
+ });
2226
+ }
2227
+ // CRITICAL FIX: Ensure annotations are present in cached tools
2228
+ // The cache should already have enriched tools, but verify
2229
+ const hasAnnotations = memCached.tools.length > 0 && memCached.tools[0].annotations;
2230
+ if (!hasAnnotations) {
2231
+ console.log(`[Discovery] Cache missing annotations for ${serverName}, enriching...`);
2232
+ return this.enrichToolsWithAnnotations(serverName, memCached.tools);
2233
+ }
2234
+ return memCached.tools;
2235
+ }
2236
+ // 2. Check disk cache if memory miss (Phase 1: v1.3.9+)
2237
+ if (this.schemaStore) {
2238
+ try {
2239
+ const diskSchemas = await this.schemaStore.loadFromDisk();
2240
+ const diskCached = diskSchemas.get(serverName);
2241
+ if (diskCached &&
2242
+ Date.now() - diskCached.timestamp < this.schemasCacheTTL) {
2243
+ console.log(`[Discovery] Using disk cache for ${serverName} (${diskCached.tools.length} tools)`);
2244
+ this.toolSchemaCache.set(serverName, diskCached);
2245
+ this.discoveredServers.add(serverName);
2246
+ this.resetDiscoveryTimer(serverName); // v1.4.0: Start/reset expiration timer
2247
+ // Trigger background refresh if nearing expiration
2248
+ if (this.schemasBackgroundRefresh &&
2249
+ Date.now() - diskCached.timestamp > this.schemasCacheTTL * 0.8) {
2250
+ this.refreshSchemaForServer(serverName).catch((error) => {
2251
+ console.warn(`[Discovery] Background refresh failed for ${serverName}:`, error);
2252
+ });
2253
+ }
2254
+ // CRITICAL FIX: Ensure annotations are present in disk cached tools
2255
+ const hasAnnotations = diskCached.tools.length > 0 && diskCached.tools[0].annotations;
2256
+ if (!hasAnnotations) {
2257
+ console.log(`[Discovery] Disk cache missing annotations for ${serverName}, enriching...`);
2258
+ return this.enrichToolsWithAnnotations(serverName, diskCached.tools);
2259
+ }
2260
+ return diskCached.tools;
2261
+ }
2262
+ }
2263
+ catch (error) {
2264
+ console.warn(`[Discovery] Failed to check disk cache for ${serverName}:`, error);
2265
+ // Fall through to server startup
2266
+ }
2267
+ }
2268
+ // 3. If server is already running, use its tools
2269
+ if (this.isServerActive(serverName)) {
2270
+ const tools = this.getServerTools(serverName);
2271
+ console.log(`[Discovery] Using running server tools for ${serverName} (${tools.length} tools)`);
2272
+ const schema = { tools, timestamp: Date.now() };
2273
+ this.toolSchemaCache.set(serverName, schema);
2274
+ this.discoveredServers.add(serverName);
2275
+ this.resetDiscoveryTimer(serverName); // v1.4.0: Start/reset expiration timer
2276
+ // Persist to disk asynchronously
2277
+ if (this.schemaStore) {
2278
+ this.schemaStore.saveToDisk(serverName, schema).catch((error) => {
2279
+ console.warn(`[Discovery] Failed to persist schema for ${serverName}:`, error);
2280
+ });
2281
+ }
2282
+ return tools;
2283
+ }
2284
+ // 4. Auto-start server for discovery if not running (v1.3.8+)
2285
+ console.log(`[Discovery] Auto-starting ${serverName} for schema discovery...`);
2286
+ try {
2287
+ await this.ensureServerStarted(serverName, config);
2288
+ const tools = this.getServerTools(serverName);
2289
+ console.log(`[Discovery] Discovered ${tools.length} tools from ${serverName}`);
2290
+ const schema = { tools, timestamp: Date.now() };
2291
+ this.toolSchemaCache.set(serverName, schema);
2292
+ this.discoveredServers.add(serverName);
2293
+ this.resetDiscoveryTimer(serverName); // v1.4.0: Start/reset expiration timer
2294
+ // Persist to disk asynchronously (Phase 1: v1.3.9+)
2295
+ if (this.schemaStore) {
2296
+ this.schemaStore.saveToDisk(serverName, schema).catch((error) => {
2297
+ console.warn(`[Discovery] Failed to persist schema for ${serverName}:`, error);
2298
+ });
2299
+ }
2300
+ return tools;
2301
+ }
2302
+ catch (error) {
2303
+ console.error(`[Discovery] Failed to auto-start and discover ${serverName}:`, error);
2304
+ throw new Error(`Server ${serverName} not found and failed to auto-start. Details: ${error instanceof Error ? error.message : String(error)}`);
2305
+ }
2306
+ }
2307
+ /**
2308
+ * Refresh schema for a specific server
2309
+ * Used by discovery to keep schemas fresh
2310
+ */
2311
+ async refreshSchemaForServer(serverName) {
2312
+ if (!this.isServerActive(serverName)) {
2313
+ // Server not running, skip refresh
2314
+ return;
2315
+ }
2316
+ try {
2317
+ const process = this.getProcess(serverName);
2318
+ if (!process)
2319
+ return;
2320
+ const tools = await this.fetchToolsFromProcess(process, serverName);
2321
+ const schema = { tools, timestamp: Date.now() };
2322
+ // Update cache
2323
+ this.toolSchemaCache.set(serverName, schema);
2324
+ // Persist to disk
2325
+ if (this.schemaStore) {
2326
+ await this.schemaStore.saveToDisk(serverName, schema);
2327
+ }
2328
+ console.log(`[Discovery] Refreshed schema for ${serverName}`);
2329
+ }
2330
+ catch (error) {
2331
+ console.warn(`[Discovery] Failed to refresh schema for ${serverName}:`, error);
2332
+ }
2333
+ }
2334
+ /**
2335
+ * Ensure server is started, auto-starting if needed
2336
+ * Used when direct tool call happens and server isn't running
2337
+ */
2338
+ async ensureServerStarted(serverName, config) {
2339
+ // Check circuit breaker before attempting startup
2340
+ const breaker = this.circuitBreakerManager.getBreaker(serverName);
2341
+ if (!breaker.canExecute()) {
2342
+ const metrics = breaker.getMetrics();
2343
+ const timeUntilReset = metrics.lastFailureTime
2344
+ ? Math.max(0, 30000 - (Date.now() - metrics.lastFailureTime))
2345
+ : 0;
2346
+ throw new Error(`Circuit breaker is OPEN for ${serverName}. ` +
2347
+ `Server has failed ${metrics.failures} consecutive times. ` +
2348
+ `Retry in ${Math.ceil(timeUntilReset / 1000)}s.`);
2349
+ }
2350
+ // v1.1.8: Check if startup already in progress (mutex pattern)
2351
+ if (this.startupMutex.has(serverName)) {
2352
+ const mutex = this.startupMutex.get(serverName);
2353
+ mutex.callerCount++;
2354
+ console.log(`[Auto-Start] ${serverName} startup in progress, waiting... (caller ${mutex.callerCount})`);
2355
+ return mutex.promise;
2356
+ }
2357
+ // v1.1.8: Check if already started
2358
+ if (this.processes.has(serverName) || this.httpClients.has(serverName)) {
2359
+ this.discoveredServers.add(serverName);
2360
+ // v1.1.56: Always ensure server is in activeServers if process exists
2361
+ if (!this.activeServers.has(serverName)) {
2362
+ console.warn(`[Auto-Start] ${serverName} has process but not in activeServers. Recovering state...`);
2363
+ // Try to load from cache first
2364
+ const cached = this.toolSchemaCache.get(serverName);
2365
+ if (cached?.tools && cached.tools.length > 0) {
2366
+ const process = this.processes.get(serverName);
2367
+ this.setServerTools(serverName, cached.tools, process);
2368
+ console.log(`[Auto-Start] Recovered ${serverName} from cache: ${cached.tools.length} tools`);
2369
+ }
2370
+ else {
2371
+ // No cache - fetch tools from running process
2372
+ const process = this.processes.get(serverName);
2373
+ if (process && !process.killed) {
2374
+ console.log(`[Auto-Start] No cache for ${serverName}, fetching tools from process...`);
2375
+ try {
2376
+ const tools = await this.fetchToolsFromProcess(process, serverName);
2377
+ this.setServerTools(serverName, tools, process);
2378
+ console.log(`[Auto-Start] Recovered ${serverName}: fetched ${tools.length} tools`);
2379
+ }
2380
+ catch (error) {
2381
+ console.error(`[Auto-Start] Failed to fetch tools for ${serverName}:`, error);
2382
+ // Process exists but can't get tools - kill it and restart
2383
+ console.warn(`[Auto-Start] Killing corrupted process for ${serverName} and restarting...`);
2384
+ await this.stopServer(serverName);
2385
+ await this.startServer(config);
2386
+ }
2387
+ }
2388
+ else {
2389
+ console.log(`[Auto-Start] HTTP server ${serverName} exists, using cached schema`);
2390
+ }
2391
+ }
2392
+ }
2393
+ return;
2394
+ }
2395
+ // v1.1.8: Create mutex entry before starting
2396
+ const startPromise = this.performServerStartup(serverName, config);
2397
+ this.startupMutex.set(serverName, {
2398
+ promise: startPromise,
2399
+ startTime: Date.now(),
2400
+ callerCount: 1,
2401
+ });
2402
+ // v1.1.27: Enhanced mutex cleanup with error logging
2403
+ startPromise
2404
+ .catch((error) => {
2405
+ console.error(`[Auto-Start] ${serverName} startup failed:`, error instanceof Error ? error.message : String(error));
2406
+ })
2407
+ .finally(() => {
2408
+ const mutex = this.startupMutex.get(serverName);
2409
+ if (mutex) {
2410
+ const duration = Date.now() - mutex.startTime;
2411
+ console.log(`[Auto-Start] ${serverName} completed in ${duration}ms (${mutex.callerCount} waiters)`);
2412
+ this.startupMutex.delete(serverName);
2413
+ }
2414
+ });
2415
+ return startPromise;
2416
+ }
2417
+ /**
2418
+ * v1.1.8: Actual server startup logic (called only by first concurrent request)
2419
+ * Other concurrent requests wait for this promise via mutex
2420
+ */
2421
+ async performServerStartup(serverName, config) {
2422
+ const breaker = this.circuitBreakerManager.getBreaker(serverName);
2423
+ try {
2424
+ if (this.isServerActive(serverName)) {
2425
+ this.discoveredServers.add(serverName);
2426
+ breaker.recordSuccess();
2427
+ return;
2428
+ }
2429
+ console.log(`[Auto-Start] Starting ${serverName} for first tool call...`);
2430
+ await this.startServer(config);
2431
+ // Use cached schemas if available, otherwise fetch
2432
+ if (this.toolSchemaCache.has(serverName)) {
2433
+ const cached = this.toolSchemaCache.get(serverName);
2434
+ // Even with cached schemas, wait for server initialization to complete
2435
+ const process = this.getProcess(serverName);
2436
+ if (process) {
2437
+ try {
2438
+ await this.waitForInitialization(process, serverName);
2439
+ }
2440
+ catch (error) {
2441
+ console.error(`[Auto-Start] Initialization failed for ${serverName}:`, error);
2442
+ breaker.recordFailure();
2443
+ throw error;
2444
+ }
2445
+ }
2446
+ this.setServerTools(serverName, cached.tools);
2447
+ this.discoveredServers.add(serverName);
2448
+ console.log(`[Auto-Start] Using cached schemas for ${serverName} (${cached.tools.length} tools)`);
2449
+ }
2450
+ else {
2451
+ let tools = [];
2452
+ const isHttpServer = this.httpClients.has(serverName);
2453
+ if (isHttpServer) {
2454
+ const client = this.httpClients.get(serverName);
2455
+ try {
2456
+ const response = await client.call("tools/list", {});
2457
+ if (response.result &&
2458
+ typeof response.result === "object" &&
2459
+ "tools" in response.result) {
2460
+ tools = response.result.tools;
2461
+ }
2462
+ }
2463
+ catch (error) {
2464
+ console.warn(`[Auto-Start] Failed to fetch tools from HTTP server ${serverName}:`, error);
2465
+ breaker.recordFailure();
2466
+ throw error;
2467
+ }
2468
+ }
2469
+ else {
2470
+ tools = await this.fetchToolsFromProcess(this.getProcess(serverName), serverName);
2471
+ }
2472
+ this.setServerTools(serverName, tools);
2473
+ const schema = { tools, timestamp: Date.now() };
2474
+ this.toolSchemaCache.set(serverName, schema);
2475
+ this.discoveredServers.add(serverName);
2476
+ // Persist to disk asynchronously
2477
+ if (this.schemaStore) {
2478
+ this.schemaStore.saveToDisk(serverName, schema).catch((error) => {
2479
+ console.warn(`[Auto-Start] Failed to persist schema for ${serverName}:`, error);
2480
+ });
2481
+ }
2482
+ console.log(`[Auto-Start] Fetched schemas from ${serverName} (${tools.length} tools)`);
2483
+ }
2484
+ this.setupServerResponseRouter(serverName);
2485
+ // Record success for circuit breaker
2486
+ breaker.recordSuccess();
2487
+ }
2488
+ catch (error) {
2489
+ // Record failure for circuit breaker
2490
+ breaker.recordFailure();
2491
+ throw error;
2492
+ }
2493
+ }
2494
+ /**
2495
+ * Get discovered servers list
2496
+ */
2497
+ getDiscoveredServers() {
2498
+ return Array.from(this.discoveredServers);
2499
+ }
2500
+ /**
2501
+ * Get server tools from cache (if discovered)
2502
+ */
2503
+ getServerToolsFromCache(serverName) {
2504
+ const cached = this.toolSchemaCache.get(serverName);
2505
+ if (cached) {
2506
+ this.discoveredServers.add(serverName);
2507
+ return cached.tools;
2508
+ }
2509
+ return null;
2510
+ }
2511
+ /**
2512
+ * Get circuit breaker metrics for a specific server
2513
+ *
2514
+ * @param serverName - Name of the server
2515
+ * @returns Circuit breaker metrics or null if server not found
2516
+ */
2517
+ getCircuitBreakerMetrics(serverName) {
2518
+ const breaker = this.circuitBreakerManager.getAllBreakers().get(serverName);
2519
+ if (breaker) {
2520
+ return breaker.getMetrics();
2521
+ }
2522
+ return null;
2523
+ }
2524
+ /**
2525
+ * Get circuit breaker metrics for all servers
2526
+ *
2527
+ * @returns Array of metrics for all servers with circuit breakers
2528
+ */
2529
+ getAllCircuitBreakerMetrics() {
2530
+ return this.circuitBreakerManager.getAllMetrics();
2531
+ }
2532
+ /**
2533
+ * Manually reset circuit breaker for a server
2534
+ *
2535
+ * @param serverName - Name of the server
2536
+ */
2537
+ resetCircuitBreaker(serverName) {
2538
+ this.circuitBreakerManager.reset(serverName);
2539
+ }
2540
+ /**
2541
+ * Reset all circuit breakers
2542
+ */
2543
+ resetAllCircuitBreakers() {
2544
+ this.circuitBreakerManager.resetAll();
2545
+ }
2546
+ /**
2547
+ * Set callback for tools/list changes (v1.4.0)
2548
+ * Called when discovery expires
2549
+ */
2550
+ setToolsListChangedCallback(callback) {
2551
+ this.onToolsListChanged = callback;
2552
+ }
2553
+ /**
2554
+ * v1.3.72: Set server list provider for proactive schema caching
2555
+ * This allows background refresh to discover all servers, not just running ones
2556
+ */
2557
+ setServerListProvider(provider) {
2558
+ this.serverListProvider = provider;
2559
+ console.log("[ServerManager] Server list provider set for proactive schema caching");
2560
+ }
2561
+ /**
2562
+ * Phase 2: Set config loader for tool safety classification
2563
+ */
2564
+ setConfigLoader(configLoader) {
2565
+ this.configLoader = configLoader;
2566
+ }
2567
+ /**
2568
+ * Phase 2: Classify tool safety level: 'safe' (auto-approve) or 'risky' (requires confirmation)
2569
+ * Dynamic classification based on config rules
2570
+ *
2571
+ * @param serverName - Server name (e.g., "memory", "jira-basic-auth")
2572
+ * @param toolName - Tool name (e.g., "create_entities", "search_issues")
2573
+ * @returns Object with safety classification and reason
2574
+ */
2575
+ classifyToolSafety(serverName, toolName, toolArguments) {
2576
+ if (!this.configLoader) {
2577
+ console.log(`[SafetyClassify] No configLoader set, defaulting to risky for ${serverName}:${toolName}`);
2578
+ return {
2579
+ safety: "risky",
2580
+ reason: "No safety rules configured (default: risky)",
2581
+ };
2582
+ }
2583
+ const rules = this.configLoader.getToolSafetyRules();
2584
+ const fullName = `${serverName}:${toolName}`;
2585
+ // Debug logging
2586
+ console.log(`[SafetyClassify] Checking ${fullName}${toolArguments ? " (with arguments)" : ""}`);
2587
+ console.log(`[SafetyClassify] safeToolOverrides:`, rules.safeToolOverrides);
2588
+ // v1.3.x: Check server-level safety first (highest priority)
2589
+ const serverConfig = this.configLoader.getServer(serverName);
2590
+ if (serverConfig) {
2591
+ const serverSafety = serverConfig.safety;
2592
+ if (serverSafety === "safe") {
2593
+ console.log(`[SafetyClassify] ${fullName} => SAFE (server-level: ${serverName} is trusted)`);
2594
+ return {
2595
+ safety: "safe",
2596
+ reason: `Server '${serverName}' is configured as trusted (all tools safe)`,
2597
+ };
2598
+ }
2599
+ else if (serverSafety === "risky") {
2600
+ console.log(`[SafetyClassify] ${fullName} => RISKY (server-level: ${serverName} is untrusted)`);
2601
+ return {
2602
+ safety: "risky",
2603
+ reason: `Server '${serverName}' is configured as untrusted (all tools risky)`,
2604
+ };
2605
+ }
2606
+ // undefined or 'default' falls through to tool-level classification
2607
+ }
2608
+ // Check explicit risky overrides first (highest priority)
2609
+ if (rules.riskyToolOverrides?.some((pattern) => {
2610
+ // Escape special regex chars, then replace * with .* for wildcard support
2611
+ const escapedPattern = "^" +
2612
+ pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") +
2613
+ "$";
2614
+ const regex = new RegExp(escapedPattern);
2615
+ const matches = regex.test(fullName);
2616
+ if (matches) {
2617
+ console.log(`[SafetyClassify] ${fullName} matched riskyToolOverride: ${pattern}`);
2618
+ }
2619
+ return matches;
2620
+ })) {
2621
+ console.log(`[SafetyClassify] ${fullName} => RISKY (riskyToolOverrides)`);
2622
+ return { safety: "risky", reason: "Matches risky tool override pattern" };
2623
+ }
2624
+ // Check explicit safe overrides (BEFORE argument inspection)
2625
+ const safeOverrideMatch = rules.safeToolOverrides?.some((pattern) => {
2626
+ // Escape special regex chars, then replace * with .* for wildcard support
2627
+ const escapedPattern = "^" +
2628
+ pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") +
2629
+ "$";
2630
+ const regex = new RegExp(escapedPattern);
2631
+ const matches = regex.test(fullName);
2632
+ if (matches) {
2633
+ console.log(`[SafetyClassify] ${fullName} matched safeToolOverride: ${pattern} (regex: ${regex})`);
2634
+ }
2635
+ return matches;
2636
+ });
2637
+ if (safeOverrideMatch) {
2638
+ console.log(`[SafetyClassify] ${fullName} => SAFE (safeToolOverrides)`);
2639
+ return { safety: "safe", reason: "Matches safe tool override pattern" };
2640
+ }
2641
+ // v1.1.29: Check argument-level inspection rules
2642
+ if (toolArguments && rules.argumentInspectionRules) {
2643
+ const inspectionResult = this.inspectToolArguments(fullName, toolArguments, rules.argumentInspectionRules);
2644
+ if (inspectionResult) {
2645
+ console.log(`[SafetyClassify] ${fullName} => ${inspectionResult.safety.toUpperCase()} (argument inspection: ${inspectionResult.reason})`);
2646
+ return inspectionResult;
2647
+ }
2648
+ }
2649
+ // Check risky patterns (if matches, it's risky)
2650
+ if (rules.riskyPatterns?.some((pattern) => {
2651
+ const regex = new RegExp(pattern, "i");
2652
+ const matches = regex.test(toolName);
2653
+ if (matches) {
2654
+ console.log(`[SafetyClassify] ${fullName} matched riskyPattern: ${pattern}`);
2655
+ }
2656
+ return matches;
2657
+ })) {
2658
+ console.log(`[SafetyClassify] ${fullName} => RISKY (riskyPatterns)`);
2659
+ return {
2660
+ safety: "risky",
2661
+ reason: "Tool name matches risky pattern (create, update, delete, etc.)",
2662
+ };
2663
+ }
2664
+ // Check safe patterns (if matches, it's safe)
2665
+ if (rules.safePatterns?.some((pattern) => {
2666
+ const regex = new RegExp(pattern, "i");
2667
+ return regex.test(toolName);
2668
+ })) {
2669
+ console.log(`[SafetyClassify] ${fullName} => SAFE (safePatterns)`);
2670
+ return {
2671
+ safety: "safe",
2672
+ reason: "Tool name matches safe pattern (search, get, list, etc.)",
2673
+ };
2674
+ }
2675
+ // v1.1.50: Analyze tool description for safety indicators before defaulting to risky
2676
+ const toolSchema = this.getToolSchema(serverName, toolName);
2677
+ if (toolSchema?.description) {
2678
+ const descResult = this.classifyByDescription(fullName, toolSchema.description);
2679
+ if (descResult) {
2680
+ return descResult;
2681
+ }
2682
+ }
2683
+ // Default to risky (fail-safe: require confirmation for unknown tools)
2684
+ console.log(`[SafetyClassify] ${fullName} => RISKY (default)`);
2685
+ return {
2686
+ safety: "risky",
2687
+ reason: "No matching safety pattern (default: risky)",
2688
+ };
2689
+ }
2690
+ /**
2691
+ * v1.1.50: Classify tool safety based on description analysis
2692
+ * Analyzes tool description for keywords that indicate read-only vs modifying operations
2693
+ *
2694
+ * @param fullName - Full tool name (server:tool)
2695
+ * @param description - Tool description to analyze
2696
+ * @returns Safety classification or null if inconclusive
2697
+ */
2698
+ classifyByDescription(fullName, description) {
2699
+ const desc = description.toLowerCase();
2700
+ // Keywords that strongly indicate read-only operations
2701
+ const safeKeywords = [
2702
+ /^(gets?|retrieves?|returns?|shows?|displays?|lists?|searches?|fetches?|reads?|views?)\b/,
2703
+ /\b(read[- ]?only|readonly|non[- ]?destructive)\b/,
2704
+ /\breturns?\s+(a\s+)?(list|array|object|string|number|boolean|information|data|details|status|result)/i,
2705
+ /\b(documentation|help|info|metadata|schema|version)\b/,
2706
+ ];
2707
+ // Keywords that strongly indicate modifying operations
2708
+ const riskyKeywords = [
2709
+ /^(creates?|deletes?|removes?|updates?|modifies?|changes?|sets?|writes?|adds?|inserts?|drops?|alters?|executes?|runs?|sends?|posts?|puts?)\b/,
2710
+ /\b(will\s+)?(create|delete|remove|update|modify|change|set|write|add|insert|drop|alter|execute|run|send|post|put)\s+(a\s+)?(new\s+)?/i,
2711
+ /\b(destructive|irreversible|permanent)\b/,
2712
+ ];
2713
+ // Check for safe indicators
2714
+ for (const pattern of safeKeywords) {
2715
+ if (pattern.test(desc)) {
2716
+ console.log(`[SafetyClassify] ${fullName} => SAFE (description analysis: matches "${pattern}")`);
2717
+ return {
2718
+ safety: "safe",
2719
+ reason: `Description indicates read-only operation`,
2720
+ };
2721
+ }
2722
+ }
2723
+ // Check for risky indicators
2724
+ for (const pattern of riskyKeywords) {
2725
+ if (pattern.test(desc)) {
2726
+ console.log(`[SafetyClassify] ${fullName} => RISKY (description analysis: matches "${pattern}")`);
2727
+ return {
2728
+ safety: "risky",
2729
+ reason: `Description indicates modifying operation`,
2730
+ };
2731
+ }
2732
+ }
2733
+ // Inconclusive - let caller handle default
2734
+ console.log(`[SafetyClassify] ${fullName} description analysis inconclusive`);
2735
+ return null;
2736
+ }
2737
+ /**
2738
+ * v1.3.x: Resolve nested path in object (e.g., "params.path" -> obj.params.path)
2739
+ *
2740
+ * @param obj - Object to extract value from
2741
+ * @param path - Dot-separated path (e.g., "params.path", "body.query.bool")
2742
+ * @returns Value at path or undefined if not found
2743
+ */
2744
+ resolveNestedPath(obj, path) {
2745
+ const parts = path.split(".");
2746
+ let current = obj;
2747
+ for (const part of parts) {
2748
+ if (current === null || current === undefined) {
2749
+ return undefined;
2750
+ }
2751
+ if (typeof current !== "object") {
2752
+ return undefined;
2753
+ }
2754
+ current = current[part];
2755
+ }
2756
+ return current;
2757
+ }
2758
+ /**
2759
+ * v1.1.29: Inspect tool arguments for command-level safety classification
2760
+ * v1.3.x: Enhanced to support nested paths (e.g., "params.path")
2761
+ *
2762
+ * @param fullToolName - Full tool name (e.g., "ssh:runRemoteCommand")
2763
+ * @param toolArguments - Arguments passed to the tool
2764
+ * @param inspectionRules - Argument inspection rules from config
2765
+ * @returns Safety classification result or null if no matching rule
2766
+ */
2767
+ inspectToolArguments(fullToolName, toolArguments, inspectionRules) {
2768
+ // v1.1.55: Find matching inspection rule with wildcard support
2769
+ // Supports patterns like "metabase*:execute" to match "metabase:execute", "metabase-foo:execute"
2770
+ const rule = inspectionRules.find((r) => {
2771
+ if (r.tool === fullToolName)
2772
+ return true; // Exact match
2773
+ // Convert wildcard pattern to regex (supports * and ? wildcards)
2774
+ if (r.tool.includes("*") || r.tool.includes("?")) {
2775
+ const regexPattern = r.tool
2776
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape regex special chars except * and ?
2777
+ .replace(/\*/g, ".*") // * = any characters
2778
+ .replace(/\?/g, "."); // ? = single character
2779
+ const regex = new RegExp(`^${regexPattern}$`, "i");
2780
+ return regex.test(fullToolName);
2781
+ }
2782
+ return false;
2783
+ });
2784
+ if (!rule) {
2785
+ return null; // No inspection rule for this tool
2786
+ }
2787
+ // v1.3.x: Extract the argument field value with nested path support
2788
+ // e.g., "command" for top-level, "params.path" for nested
2789
+ const argumentValue = rule.argumentField.includes(".")
2790
+ ? this.resolveNestedPath(toolArguments, rule.argumentField)
2791
+ : toolArguments[rule.argumentField];
2792
+ if (!argumentValue) {
2793
+ // v1.1.55: If field is optional, skip inspection (return null) instead of marking risky
2794
+ // This allows tools with multiple modes (SQL vs card_id) to fall through to other rules
2795
+ if (rule.optionalField) {
2796
+ console.log(`[ArgumentInspection] ${fullToolName}: Optional field "${rule.argumentField}" missing, skipping inspection`);
2797
+ return null;
2798
+ }
2799
+ console.log(`[ArgumentInspection] ${fullToolName}: Missing argument field "${rule.argumentField}"`);
2800
+ return {
2801
+ safety: "risky",
2802
+ reason: `Missing required argument field for inspection: ${rule.argumentField}`,
2803
+ };
2804
+ }
2805
+ // Handle array of commands (for batch operations)
2806
+ const commands = Array.isArray(argumentValue)
2807
+ ? argumentValue
2808
+ : [argumentValue];
2809
+ for (const cmd of commands) {
2810
+ const commandStr = String(cmd);
2811
+ console.log(`[ArgumentInspection] ${fullToolName}: Inspecting command: "${commandStr}"`);
2812
+ // Check risky command patterns first (highest priority)
2813
+ if (rule.riskyCommandPatterns) {
2814
+ for (const pattern of rule.riskyCommandPatterns) {
2815
+ const regex = new RegExp(pattern, "i");
2816
+ if (regex.test(commandStr)) {
2817
+ console.log(`[ArgumentInspection] ${fullToolName}: Command matched risky pattern: ${pattern}`);
2818
+ return {
2819
+ safety: "risky",
2820
+ reason: `Command matches risky pattern: ${pattern}`,
2821
+ };
2822
+ }
2823
+ }
2824
+ }
2825
+ // Check safe command patterns
2826
+ if (rule.safeCommandPatterns) {
2827
+ let matchedSafePattern = false;
2828
+ for (const pattern of rule.safeCommandPatterns) {
2829
+ const regex = new RegExp(pattern, "i");
2830
+ if (regex.test(commandStr)) {
2831
+ console.log(`[ArgumentInspection] ${fullToolName}: Command matched safe pattern: ${pattern}`);
2832
+ matchedSafePattern = true;
2833
+ break;
2834
+ }
2835
+ }
2836
+ if (matchedSafePattern) {
2837
+ continue; // This command is safe, check next
2838
+ }
2839
+ else if (rule.whitelistMode) {
2840
+ // Whitelist mode: reject if not in safe patterns
2841
+ console.log(`[ArgumentInspection] ${fullToolName}: Command NOT in whitelist (whitelist mode enabled)`);
2842
+ return {
2843
+ safety: "risky",
2844
+ reason: "Command not in safe command whitelist",
2845
+ };
2846
+ }
2847
+ else {
2848
+ // Blacklist mode: no match in safe or risky patterns, default to risky
2849
+ console.log(`[ArgumentInspection] ${fullToolName}: Command not matched by any pattern (defaulting to risky)`);
2850
+ return {
2851
+ safety: "risky",
2852
+ reason: "Command does not match any known safe pattern",
2853
+ };
2854
+ }
2855
+ }
2856
+ }
2857
+ // All commands passed inspection
2858
+ console.log(`[ArgumentInspection] ${fullToolName}: All commands safe`);
2859
+ return {
2860
+ safety: "safe",
2861
+ reason: "All commands match safe patterns",
2862
+ };
2863
+ }
2864
+ /**
2865
+ * Phase 2: Enrich tools with safety annotations
2866
+ * Adds annotations field to each tool with safety classification
2867
+ *
2868
+ * @param serverName - Server name
2869
+ * @param tools - Array of tools from server
2870
+ * @returns Tools enriched with annotations
2871
+ */
2872
+ enrichToolsWithAnnotations(serverName, tools) {
2873
+ return tools.map((tool) => {
2874
+ const classification = this.classifyToolSafety(serverName, tool.name);
2875
+ return {
2876
+ ...tool,
2877
+ annotations: {
2878
+ safety: classification.safety,
2879
+ safetyReason: classification.reason,
2880
+ requiresConfirmation: classification.safety === "risky",
2881
+ },
2882
+ };
2883
+ });
2884
+ }
2885
+ /**
2886
+ * Check if server is discovered (v1.4.0)
2887
+ */
2888
+ isDiscovered(serverName) {
2889
+ return this.discoveredServers.has(serverName);
2890
+ }
2891
+ /**
2892
+ * Refresh discovery timer for active server (v1.4.0)
2893
+ * Called on tool execution to keep active servers available
2894
+ */
2895
+ refreshDiscoveryTimer(serverName) {
2896
+ if (this.discoveredServers.has(serverName)) {
2897
+ this.resetDiscoveryTimer(serverName);
2898
+ }
2899
+ }
2900
+ /**
2901
+ * Reset/start discovery timer (v1.4.0 - private)
2902
+ * @param serverName Server to reset timer for
2903
+ */
2904
+ resetDiscoveryTimer(serverName) {
2905
+ // Clear existing timer
2906
+ if (this.discoveryTimers.has(serverName)) {
2907
+ clearTimeout(this.discoveryTimers.get(serverName));
2908
+ }
2909
+ // Don't set timers for base servers (never expire)
2910
+ if (this.baseServers.includes(serverName)) {
2911
+ return;
2912
+ }
2913
+ // Don't set timer if TTL is 0 (disabled)
2914
+ if (this.discoveryTTL === 0) {
2915
+ return;
2916
+ }
2917
+ // Start new timer
2918
+ const timer = setTimeout(() => {
2919
+ this.expireDiscoveredServer(serverName);
2920
+ }, this.discoveryTTL);
2921
+ this.discoveryTimers.set(serverName, timer);
2922
+ }
2923
+ /**
2924
+ * Expire discovered server (v1.4.0 - private)
2925
+ * Removes server from cache and notifies clients
2926
+ */
2927
+ expireDiscoveredServer(serverName) {
2928
+ // Skip base servers (safety check)
2929
+ if (this.baseServers.includes(serverName)) {
2930
+ return;
2931
+ }
2932
+ // Skip if server has active requests (defer expiration)
2933
+ const serverProcess = this.processes.get(serverName);
2934
+ if (serverProcess && this.hasActiveRequests(serverProcess)) {
2935
+ console.log(`[MetaLink] Deferring expiration of '${serverName}' (active requests)`);
2936
+ this.resetDiscoveryTimer(serverName); // Try again in TTL period
2937
+ return;
2938
+ }
2939
+ // Remove from cache
2940
+ console.log(`[MetaLink] Expiring discovered server: ${serverName}`);
2941
+ this.toolSchemaCache.delete(serverName);
2942
+ this.discoveredServers.delete(serverName);
2943
+ this.discoveryTimers.delete(serverName);
2944
+ // Stop server process if not base server
2945
+ if (this.processes.has(serverName)) {
2946
+ this.stopServer(serverName).catch((err) => {
2947
+ console.warn(`[MetaLink] Failed to stop expired server ${serverName}:`, err);
2948
+ });
2949
+ }
2950
+ // Notify via callback (HTTP server will broadcast)
2951
+ if (this.onToolsListChanged) {
2952
+ this.onToolsListChanged();
2953
+ }
2954
+ }
2955
+ /**
2956
+ * Check if server has active requests (v1.4.0 - private)
2957
+ * Simple heuristic: check if process started recently
2958
+ */
2959
+ hasActiveRequests(serverProcess) {
2960
+ // Check if there are pending requests for this server
2961
+ const serverName = Array.from(this.processes.entries()).find(([, proc]) => proc === serverProcess)?.[0];
2962
+ if (serverName && this.pendingRequests.has(serverName)) {
2963
+ const pending = this.pendingRequests.get(serverName);
2964
+ if (pending && pending.size > 0) {
2965
+ return true; // Has pending requests
2966
+ }
2967
+ }
2968
+ // Fallback: check if process started recently (< gracefulShutdown timeout)
2969
+ const processAge = Date.now() - (serverProcess.startTime || 0);
2970
+ return processAge < this.timeouts.gracefulShutdown;
2971
+ }
2972
+ /**
2973
+ * Validate tool schemas and log warnings/suggestions
2974
+ * v1.4.0: Schema validation for improved schema quality
2975
+ */
2976
+ validateToolSchemas(tools, serverName) {
2977
+ const validator = new SchemaValidator();
2978
+ for (const tool of tools) {
2979
+ const result = validator.validateToolSchema(tool);
2980
+ // Log errors
2981
+ if (result.errors.length > 0) {
2982
+ console.error(`[SchemaValidator] ❌ Errors in ${serverName ? `${serverName}-` : ""}${tool.name}:`);
2983
+ for (const error of result.errors) {
2984
+ console.error(` • ${error}`);
2985
+ }
2986
+ }
2987
+ // Log warnings
2988
+ if (result.warnings.length > 0) {
2989
+ console.warn(`[SchemaValidator] ⚠️ Warnings for ${serverName ? `${serverName}-` : ""}${tool.name}:`);
2990
+ for (const warning of result.warnings) {
2991
+ console.warn(` • ${warning.field}: ${warning.message}`);
2992
+ }
2993
+ }
2994
+ // Log suggestions
2995
+ if (result.suggestions && result.suggestions.length > 0) {
2996
+ console.log(`[SchemaValidator] 💡 Suggestions for ${serverName ? `${serverName}-` : ""}${tool.name}:`);
2997
+ for (const suggestion of result.suggestions) {
2998
+ console.log(` • ${suggestion}`);
2999
+ }
3000
+ }
3001
+ // Log inferred required parameters
3002
+ if (result.inferredRequired && result.inferredRequired.length > 0) {
3003
+ console.log(`[SchemaValidator] 🔍 Inferred required params for ${serverName ? `${serverName}-` : ""}${tool.name}: ${result.inferredRequired.join(", ")}`);
3004
+ }
3005
+ }
3006
+ }
3007
+ /**
3008
+ * Cleanup all servers
3009
+ */
3010
+ async cleanup() {
3011
+ // Clear all discovery timers (v1.4.0)
3012
+ for (const timer of this.discoveryTimers.values()) {
3013
+ clearTimeout(timer);
3014
+ }
3015
+ this.discoveryTimers.clear();
3016
+ // Stop background refresh
3017
+ if (this.backgroundRefreshInterval) {
3018
+ clearInterval(this.backgroundRefreshInterval);
3019
+ this.backgroundRefreshInterval = null;
3020
+ console.log("[SchemaStore] Stopped background refresh");
3021
+ }
3022
+ // Cleanup circuit breakers
3023
+ this.circuitBreakerManager.destroy();
3024
+ console.log("[CircuitBreaker] Cleaned up all circuit breakers");
3025
+ // Flush pending disk writes
3026
+ if (this.schemaStore) {
3027
+ try {
3028
+ await this.schemaStore.flush();
3029
+ }
3030
+ catch (error) {
3031
+ console.warn("[SchemaStore] Failed to flush during cleanup:", error);
3032
+ }
3033
+ }
3034
+ const servers = Array.from(this.processes.keys());
3035
+ for (const serverName of servers) {
3036
+ try {
3037
+ await this.stopServer(serverName);
3038
+ }
3039
+ catch (error) {
3040
+ console.error(`Failed to stop server ${serverName}:`, error);
3041
+ }
3042
+ }
3043
+ // Clear all health check intervals
3044
+ for (const interval of this.healthCheckIntervals.values()) {
3045
+ clearInterval(interval);
3046
+ }
3047
+ this.healthCheckIntervals.clear();
3048
+ this.activeServers.clear();
3049
+ // Clear all auto-restart timers
3050
+ for (const restartInfo of this.restartAttempts.values()) {
3051
+ if (restartInfo.restartTimer) {
3052
+ clearTimeout(restartInfo.restartTimer);
3053
+ }
3054
+ }
3055
+ this.restartAttempts.clear();
3056
+ }
3057
+ /**
3058
+ * Calculate exponential backoff delay for restart attempts
3059
+ */
3060
+ calculateBackoffDelay(attemptNumber) {
3061
+ const { baseDelay, maxDelay, backoffMultiplier } = this.autoRestartConfig;
3062
+ const exponentialDelay = baseDelay * Math.pow(backoffMultiplier, attemptNumber);
3063
+ return Math.min(exponentialDelay, maxDelay);
3064
+ }
3065
+ /**
3066
+ * Schedule a server restart with exponential backoff
3067
+ */
3068
+ scheduleServerRestart(serverName) {
3069
+ if (!this.autoRestartConfig.enabled) {
3070
+ console.log(`[AutoRestart] Auto-restart disabled for ${serverName}`);
3071
+ return;
3072
+ }
3073
+ // Get or initialize restart tracking
3074
+ let restartInfo = this.restartAttempts.get(serverName);
3075
+ if (!restartInfo) {
3076
+ restartInfo = {
3077
+ count: 0,
3078
+ lastAttempt: 0,
3079
+ nextRetryTime: 0,
3080
+ };
3081
+ this.restartAttempts.set(serverName, restartInfo);
3082
+ }
3083
+ // Check if we've exceeded max retries
3084
+ if (restartInfo.count >= this.autoRestartConfig.maxRetries) {
3085
+ console.error(`[AutoRestart] ${serverName} has failed ${restartInfo.count} times. ` +
3086
+ `Maximum retry limit (${this.autoRestartConfig.maxRetries}) reached. ` +
3087
+ `Manual intervention required.`);
3088
+ this.emit("server:restart-limit-reached", {
3089
+ serverName,
3090
+ attemptCount: restartInfo.count,
3091
+ maxRetries: this.autoRestartConfig.maxRetries,
3092
+ });
3093
+ return;
3094
+ }
3095
+ // Cancel any existing restart timer
3096
+ if (restartInfo.restartTimer) {
3097
+ clearTimeout(restartInfo.restartTimer);
3098
+ }
3099
+ // Calculate backoff delay
3100
+ const delay = this.calculateBackoffDelay(restartInfo.count);
3101
+ const nextRetryTime = Date.now() + delay;
3102
+ console.log(`[AutoRestart] Scheduling restart attempt ${restartInfo.count + 1}/${this.autoRestartConfig.maxRetries} ` +
3103
+ `for ${serverName} in ${delay}ms (${Math.round(delay / 1000)}s)`);
3104
+ // Update restart tracking
3105
+ restartInfo.count++;
3106
+ restartInfo.nextRetryTime = nextRetryTime;
3107
+ // Schedule the restart
3108
+ restartInfo.restartTimer = setTimeout(async () => {
3109
+ console.log(`[AutoRestart] Attempting restart ${restartInfo.count}/${this.autoRestartConfig.maxRetries} ` +
3110
+ `for ${serverName}`);
3111
+ restartInfo.lastAttempt = Date.now();
3112
+ try {
3113
+ // Attempt to restart the server
3114
+ await this.attemptServerRestart(serverName);
3115
+ // If successful, reset restart tracking
3116
+ console.log(`[AutoRestart] Successfully restarted ${serverName}`);
3117
+ this.resetRestartTracking(serverName);
3118
+ this.emit("server:restart-success", {
3119
+ serverName,
3120
+ attemptCount: restartInfo.count,
3121
+ });
3122
+ }
3123
+ catch (error) {
3124
+ console.error(`[AutoRestart] Restart attempt ${restartInfo.count} failed for ${serverName}: ` +
3125
+ `${error instanceof Error ? error.message : String(error)}`);
3126
+ this.emit("server:restart-failed", {
3127
+ serverName,
3128
+ attemptCount: restartInfo.count,
3129
+ error: error instanceof Error ? error.message : String(error),
3130
+ });
3131
+ // The server will crash again if restart failed, triggering another schedule
3132
+ }
3133
+ }, delay);
3134
+ this.emit("server:restart-scheduled", {
3135
+ serverName,
3136
+ attemptNumber: restartInfo.count,
3137
+ delayMs: delay,
3138
+ nextRetryTime,
3139
+ });
3140
+ }
3141
+ /**
3142
+ * Attempt to restart a failed server
3143
+ */
3144
+ async attemptServerRestart(serverName) {
3145
+ // Get server config - need to look it up from the server list provider
3146
+ if (!this.serverListProvider) {
3147
+ throw new Error("Server list provider not configured");
3148
+ }
3149
+ const allConfigs = this.serverListProvider();
3150
+ const config = allConfigs.find((c) => c.name === serverName);
3151
+ if (!config) {
3152
+ throw new Error(`No configuration found for server ${serverName}`);
3153
+ }
3154
+ // Clean up old process/client
3155
+ await this.stopServer(serverName);
3156
+ // Wait a brief moment for cleanup
3157
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3158
+ // Start the server fresh
3159
+ await this.startServer(config);
3160
+ // Verify it started successfully
3161
+ const isRunning = this.isServerActive(serverName);
3162
+ if (!isRunning) {
3163
+ throw new Error(`Server ${serverName} failed to start`);
3164
+ }
3165
+ }
3166
+ /**
3167
+ * Reset restart tracking for a server (called after successful restart)
3168
+ */
3169
+ resetRestartTracking(serverName) {
3170
+ const restartInfo = this.restartAttempts.get(serverName);
3171
+ if (restartInfo?.restartTimer) {
3172
+ clearTimeout(restartInfo.restartTimer);
3173
+ }
3174
+ this.restartAttempts.delete(serverName);
3175
+ console.log(`[AutoRestart] Reset restart tracking for ${serverName}`);
3176
+ }
3177
+ /**
3178
+ * Get restart status for a server (public API for monitoring)
3179
+ */
3180
+ getRestartStatus(serverName) {
3181
+ const restartInfo = this.restartAttempts.get(serverName);
3182
+ if (!restartInfo) {
3183
+ return {
3184
+ inProgress: false,
3185
+ attemptCount: 0,
3186
+ nextRetryTime: null,
3187
+ };
3188
+ }
3189
+ return {
3190
+ inProgress: true,
3191
+ attemptCount: restartInfo.count,
3192
+ nextRetryTime: restartInfo.nextRetryTime,
3193
+ };
3194
+ }
3195
+ /**
3196
+ * v1.1.27: Graceful shutdown - clean up all timers and stop all servers
3197
+ * Call this method before process exit to prevent memory leaks and orphaned timers
3198
+ */
3199
+ async shutdown() {
3200
+ console.log("[ServerManager] Initiating graceful shutdown...");
3201
+ // 1. Clear discovery timers
3202
+ for (const [serverName, timer] of this.discoveryTimers.entries()) {
3203
+ clearTimeout(timer);
3204
+ console.log(`[ServerManager] Cleared discovery timer for ${serverName}`);
3205
+ }
3206
+ this.discoveryTimers.clear();
3207
+ // 2. Clear background refresh interval
3208
+ if (this.backgroundRefreshInterval) {
3209
+ clearInterval(this.backgroundRefreshInterval);
3210
+ this.backgroundRefreshInterval = null;
3211
+ console.log("[ServerManager] Cleared background refresh interval");
3212
+ }
3213
+ // 3. Clear all health check intervals
3214
+ for (const [serverName, interval] of this.healthCheckIntervals.entries()) {
3215
+ clearInterval(interval);
3216
+ console.log(`[ServerManager] Cleared health check interval for ${serverName}`);
3217
+ }
3218
+ this.healthCheckIntervals.clear();
3219
+ // 4. Clear restart timers
3220
+ for (const [serverName, restartInfo] of this.restartAttempts.entries()) {
3221
+ if (restartInfo.restartTimer) {
3222
+ clearTimeout(restartInfo.restartTimer);
3223
+ console.log(`[ServerManager] Cleared restart timer for ${serverName}`);
3224
+ }
3225
+ }
3226
+ this.restartAttempts.clear();
3227
+ // 5. Clear startup mutexes
3228
+ this.startupMutex.clear();
3229
+ // 6. Stop all servers gracefully
3230
+ const serverNames = [...this.processes.keys(), ...this.httpClients.keys()];
3231
+ console.log(`[ServerManager] Stopping ${serverNames.length} servers...`);
3232
+ const stopPromises = serverNames.map(async (serverName) => {
3233
+ try {
3234
+ await this.stopServer(serverName);
3235
+ console.log(`[ServerManager] Stopped server: ${serverName}`);
3236
+ }
3237
+ catch (error) {
3238
+ console.error(`[ServerManager] Error stopping server ${serverName}:`, error);
3239
+ }
3240
+ });
3241
+ await Promise.allSettled(stopPromises);
3242
+ // 7. Clear remaining state
3243
+ this.activeServers.clear();
3244
+ this.discoveredServers.clear();
3245
+ this.toolSchemaCache.clear();
3246
+ this.schemaStabilityMetrics.clear();
3247
+ this.failedDiscoveryAttempts.clear();
3248
+ this.stdoutBuffers.clear();
3249
+ this.stdoutListeners.clear();
3250
+ this.pendingRequests.clear();
3251
+ this.requestIdCounters.clear();
3252
+ console.log("[ServerManager] Shutdown complete");
3253
+ }
3254
+ }
3255
+ //# sourceMappingURL=manager.js.map