@mxf-dev/core 2.0.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (466) hide show
  1. package/dist/config/ModelContextLimits.d.ts.map +1 -1
  2. package/dist/config/ModelContextLimits.js +7 -1
  3. package/dist/config/ModelContextLimits.js.map +1 -1
  4. package/dist/constants/CoreTools.d.ts +6 -5
  5. package/dist/constants/CoreTools.d.ts.map +1 -1
  6. package/dist/constants/CoreTools.js +6 -5
  7. package/dist/constants/CoreTools.js.map +1 -1
  8. package/dist/constants/DefaultPhaseProfiles.d.ts.map +1 -1
  9. package/dist/constants/DefaultPhaseProfiles.js +29 -0
  10. package/dist/constants/DefaultPhaseProfiles.js.map +1 -1
  11. package/dist/constants/ReservedIdentities.d.ts +14 -0
  12. package/dist/constants/ReservedIdentities.d.ts.map +1 -0
  13. package/dist/constants/ReservedIdentities.js +42 -0
  14. package/dist/constants/ReservedIdentities.js.map +1 -0
  15. package/dist/database/adapters/mongodb/MongoKnowledgeGraphRepository.d.ts +16 -16
  16. package/dist/database/adapters/mongodb/MongoKnowledgeGraphRepository.d.ts.map +1 -1
  17. package/dist/database/adapters/mongodb/MongoKnowledgeGraphRepository.js +98 -36
  18. package/dist/database/adapters/mongodb/MongoKnowledgeGraphRepository.js.map +1 -1
  19. package/dist/events/ClientEventBus.d.ts +28 -0
  20. package/dist/events/ClientEventBus.d.ts.map +1 -1
  21. package/dist/events/ClientEventBus.js +87 -40
  22. package/dist/events/ClientEventBus.js.map +1 -1
  23. package/dist/events/EventBus.d.ts +19 -1
  24. package/dist/events/EventBus.d.ts.map +1 -1
  25. package/dist/events/EventBus.js +23 -0
  26. package/dist/events/EventBus.js.map +1 -1
  27. package/dist/events/EventBusBase.d.ts +37 -6
  28. package/dist/events/EventBusBase.d.ts.map +1 -1
  29. package/dist/events/EventBusBase.js +75 -27
  30. package/dist/events/EventBusBase.js.map +1 -1
  31. package/dist/events/EventNames.d.ts +49 -36
  32. package/dist/events/EventNames.d.ts.map +1 -1
  33. package/dist/events/EventNames.js +38 -36
  34. package/dist/events/EventNames.js.map +1 -1
  35. package/dist/events/PublicEvents.d.ts +5 -1
  36. package/dist/events/PublicEvents.d.ts.map +1 -1
  37. package/dist/events/PublicEvents.js +7 -0
  38. package/dist/events/PublicEvents.js.map +1 -1
  39. package/dist/events/ServerEventBus.d.ts +7 -54
  40. package/dist/events/ServerEventBus.d.ts.map +1 -1
  41. package/dist/events/ServerEventBus.js +10 -96
  42. package/dist/events/ServerEventBus.js.map +1 -1
  43. package/dist/events/event-definitions/ControlLoopEvents.d.ts +35 -0
  44. package/dist/events/event-definitions/ControlLoopEvents.d.ts.map +1 -1
  45. package/dist/events/event-definitions/ControlLoopEvents.js +6 -0
  46. package/dist/events/event-definitions/ControlLoopEvents.js.map +1 -1
  47. package/dist/events/event-definitions/SdkEvents.d.ts +55 -0
  48. package/dist/events/event-definitions/SdkEvents.d.ts.map +1 -0
  49. package/dist/events/event-definitions/SdkEvents.js +36 -0
  50. package/dist/events/event-definitions/SdkEvents.js.map +1 -0
  51. package/dist/events/event-definitions/TaskEvents.d.ts +2 -7
  52. package/dist/events/event-definitions/TaskEvents.d.ts.map +1 -1
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +2 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/interfaces/AgentInterfaces.d.ts +5 -0
  58. package/dist/interfaces/AgentInterfaces.d.ts.map +1 -1
  59. package/dist/interfaces/ChannelConfig.d.ts +2 -2
  60. package/dist/interfaces/ChannelConfig.d.ts.map +1 -1
  61. package/dist/interfaces/ConversationMessage.d.ts +1 -1
  62. package/dist/interfaces/ConversationMessage.d.ts.map +1 -1
  63. package/dist/interfaces/IMemoryPersistence.d.ts +47 -1
  64. package/dist/interfaces/IMemoryPersistence.d.ts.map +1 -1
  65. package/dist/interfaces/SocketServiceInterface.d.ts +3 -2
  66. package/dist/interfaces/SocketServiceInterface.d.ts.map +1 -1
  67. package/dist/interfaces/TaskInterfaces.d.ts +14 -1
  68. package/dist/interfaces/TaskInterfaces.d.ts.map +1 -1
  69. package/dist/models/agent.d.ts +0 -1
  70. package/dist/models/agent.d.ts.map +1 -1
  71. package/dist/models/agent.js +0 -5
  72. package/dist/models/agent.js.map +1 -1
  73. package/dist/models/agentIdentityReservation.d.ts +42 -0
  74. package/dist/models/agentIdentityReservation.d.ts.map +1 -0
  75. package/dist/models/agentIdentityReservation.js +52 -0
  76. package/dist/models/agentIdentityReservation.js.map +1 -0
  77. package/dist/models/channel.d.ts +2 -0
  78. package/dist/models/channel.d.ts.map +1 -1
  79. package/dist/models/channel.js.map +1 -1
  80. package/dist/models/channelKey.d.ts +1 -0
  81. package/dist/models/channelKey.d.ts.map +1 -1
  82. package/dist/models/channelKey.js +5 -0
  83. package/dist/models/channelKey.js.map +1 -1
  84. package/dist/models/memory.d.ts +1 -1
  85. package/dist/models/memory.d.ts.map +1 -1
  86. package/dist/models/memory.js +9 -12
  87. package/dist/models/memory.js.map +1 -1
  88. package/dist/models/memoryStrata.d.ts +3 -16
  89. package/dist/models/memoryStrata.d.ts.map +1 -1
  90. package/dist/models/memoryStrata.js +3 -5
  91. package/dist/models/memoryStrata.js.map +1 -1
  92. package/dist/models/plan.d.ts +2 -2
  93. package/dist/models/plan.d.ts.map +1 -1
  94. package/dist/models/plan.js +1 -1
  95. package/dist/models/plan.js.map +1 -1
  96. package/dist/models/taskEffectiveness.d.ts +1 -0
  97. package/dist/models/taskEffectiveness.d.ts.map +1 -1
  98. package/dist/models/taskEffectiveness.js +7 -1
  99. package/dist/models/taskEffectiveness.js.map +1 -1
  100. package/dist/models/user.d.ts +4 -0
  101. package/dist/models/user.d.ts.map +1 -1
  102. package/dist/models/user.js +8 -0
  103. package/dist/models/user.js.map +1 -1
  104. package/dist/protocols/mcp/LlmTestEnvironmentGuard.d.ts +16 -0
  105. package/dist/protocols/mcp/LlmTestEnvironmentGuard.d.ts.map +1 -0
  106. package/dist/protocols/mcp/LlmTestEnvironmentGuard.js +25 -0
  107. package/dist/protocols/mcp/LlmTestEnvironmentGuard.js.map +1 -0
  108. package/dist/protocols/mcp/McpServerTypes.d.ts +9 -0
  109. package/dist/protocols/mcp/McpServerTypes.d.ts.map +1 -1
  110. package/dist/protocols/mcp/providers/AnthropicMcpClient.d.ts.map +1 -1
  111. package/dist/protocols/mcp/providers/AnthropicMcpClient.js +1 -0
  112. package/dist/protocols/mcp/providers/AnthropicMcpClient.js.map +1 -1
  113. package/dist/protocols/mcp/providers/AzureOpenAiMcpClient.d.ts.map +1 -1
  114. package/dist/protocols/mcp/providers/AzureOpenAiMcpClient.js +1 -0
  115. package/dist/protocols/mcp/providers/AzureOpenAiMcpClient.js.map +1 -1
  116. package/dist/protocols/mcp/providers/BaseMcpClient.d.ts +4 -0
  117. package/dist/protocols/mcp/providers/BaseMcpClient.d.ts.map +1 -1
  118. package/dist/protocols/mcp/providers/BaseMcpClient.js +10 -0
  119. package/dist/protocols/mcp/providers/BaseMcpClient.js.map +1 -1
  120. package/dist/protocols/mcp/providers/CustomMcpClient.d.ts.map +1 -1
  121. package/dist/protocols/mcp/providers/CustomMcpClient.js +7 -0
  122. package/dist/protocols/mcp/providers/CustomMcpClient.js.map +1 -1
  123. package/dist/protocols/mcp/providers/GeminiMcpClient.d.ts.map +1 -1
  124. package/dist/protocols/mcp/providers/GeminiMcpClient.js +1 -0
  125. package/dist/protocols/mcp/providers/GeminiMcpClient.js.map +1 -1
  126. package/dist/protocols/mcp/providers/OllamaMcpClient.d.ts +1 -0
  127. package/dist/protocols/mcp/providers/OllamaMcpClient.d.ts.map +1 -1
  128. package/dist/protocols/mcp/providers/OllamaMcpClient.js +1 -0
  129. package/dist/protocols/mcp/providers/OllamaMcpClient.js.map +1 -1
  130. package/dist/protocols/mcp/providers/OpenAiMcpClient.d.ts.map +1 -1
  131. package/dist/protocols/mcp/providers/OpenAiMcpClient.js +1 -0
  132. package/dist/protocols/mcp/providers/OpenAiMcpClient.js.map +1 -1
  133. package/dist/protocols/mcp/providers/OpenRouterMcpClient.d.ts.map +1 -1
  134. package/dist/protocols/mcp/providers/OpenRouterMcpClient.js +2 -0
  135. package/dist/protocols/mcp/providers/OpenRouterMcpClient.js.map +1 -1
  136. package/dist/protocols/mcp/providers/XaiMcpClient.d.ts.map +1 -1
  137. package/dist/protocols/mcp/providers/XaiMcpClient.js +1 -0
  138. package/dist/protocols/mcp/providers/XaiMcpClient.js.map +1 -1
  139. package/dist/protocols/mcp/security/BrowserTargetGuard.d.ts +14 -0
  140. package/dist/protocols/mcp/security/BrowserTargetGuard.d.ts.map +1 -0
  141. package/dist/protocols/mcp/security/BrowserTargetGuard.js +55 -0
  142. package/dist/protocols/mcp/security/BrowserTargetGuard.js.map +1 -0
  143. package/dist/protocols/mcp/security/ExternalMcpRegistrationPolicy.d.ts +24 -0
  144. package/dist/protocols/mcp/security/ExternalMcpRegistrationPolicy.d.ts.map +1 -0
  145. package/dist/protocols/mcp/security/ExternalMcpRegistrationPolicy.js +33 -0
  146. package/dist/protocols/mcp/security/ExternalMcpRegistrationPolicy.js.map +1 -0
  147. package/dist/protocols/mcp/security/HttpTargetGuard.d.ts.map +1 -1
  148. package/dist/protocols/mcp/security/HttpTargetGuard.js +68 -6
  149. package/dist/protocols/mcp/security/HttpTargetGuard.js.map +1 -1
  150. package/dist/protocols/mcp/security/McpSecurityGuard.d.ts.map +1 -1
  151. package/dist/protocols/mcp/security/McpSecurityGuard.js +7 -5
  152. package/dist/protocols/mcp/security/McpSecurityGuard.js.map +1 -1
  153. package/dist/protocols/mcp/security/McpToolPolicy.d.ts +18 -0
  154. package/dist/protocols/mcp/security/McpToolPolicy.d.ts.map +1 -1
  155. package/dist/protocols/mcp/security/McpToolPolicy.js +60 -0
  156. package/dist/protocols/mcp/security/McpToolPolicy.js.map +1 -1
  157. package/dist/protocols/mcp/services/ExternalMcpServerManager.d.ts +118 -5
  158. package/dist/protocols/mcp/services/ExternalMcpServerManager.d.ts.map +1 -1
  159. package/dist/protocols/mcp/services/ExternalMcpServerManager.js +499 -98
  160. package/dist/protocols/mcp/services/ExternalMcpServerManager.js.map +1 -1
  161. package/dist/protocols/mcp/services/ExternalServerConfigs.d.ts +2 -2
  162. package/dist/protocols/mcp/services/ExternalServerConfigs.d.ts.map +1 -1
  163. package/dist/protocols/mcp/services/ExternalServerConfigs.js +25 -8
  164. package/dist/protocols/mcp/services/ExternalServerConfigs.js.map +1 -1
  165. package/dist/protocols/mcp/tools/AnalyticsTools.d.ts +3 -37
  166. package/dist/protocols/mcp/tools/AnalyticsTools.d.ts.map +1 -1
  167. package/dist/protocols/mcp/tools/AnalyticsTools.js +134 -614
  168. package/dist/protocols/mcp/tools/AnalyticsTools.js.map +1 -1
  169. package/dist/protocols/mcp/tools/CodeAnalysisTools.d.ts.map +1 -1
  170. package/dist/protocols/mcp/tools/CodeAnalysisTools.js +16 -13
  171. package/dist/protocols/mcp/tools/CodeAnalysisTools.js.map +1 -1
  172. package/dist/protocols/mcp/tools/EffectivenessTools.d.ts.map +1 -1
  173. package/dist/protocols/mcp/tools/EffectivenessTools.js +94 -30
  174. package/dist/protocols/mcp/tools/EffectivenessTools.js.map +1 -1
  175. package/dist/protocols/mcp/tools/GitTools.d.ts.map +1 -1
  176. package/dist/protocols/mcp/tools/GitTools.js +9 -5
  177. package/dist/protocols/mcp/tools/GitTools.js.map +1 -1
  178. package/dist/protocols/mcp/tools/InfrastructureTools.d.ts +10 -40
  179. package/dist/protocols/mcp/tools/InfrastructureTools.d.ts.map +1 -1
  180. package/dist/protocols/mcp/tools/InfrastructureTools.js +29 -70
  181. package/dist/protocols/mcp/tools/InfrastructureTools.js.map +1 -1
  182. package/dist/protocols/mcp/tools/JsonTools.d.ts.map +1 -1
  183. package/dist/protocols/mcp/tools/JsonTools.js +4 -4
  184. package/dist/protocols/mcp/tools/JsonTools.js.map +1 -1
  185. package/dist/protocols/mcp/tools/KnowledgeGraphTools.d.ts +55 -592
  186. package/dist/protocols/mcp/tools/KnowledgeGraphTools.d.ts.map +1 -1
  187. package/dist/protocols/mcp/tools/KnowledgeGraphTools.js +169 -119
  188. package/dist/protocols/mcp/tools/KnowledgeGraphTools.js.map +1 -1
  189. package/dist/protocols/mcp/tools/MemorySearchTools.d.ts +6 -6
  190. package/dist/protocols/mcp/tools/MemorySearchTools.d.ts.map +1 -1
  191. package/dist/protocols/mcp/tools/MemorySearchTools.js +107 -39
  192. package/dist/protocols/mcp/tools/MemorySearchTools.js.map +1 -1
  193. package/dist/protocols/mcp/tools/MemoryUtilityTools.d.ts +4 -5
  194. package/dist/protocols/mcp/tools/MemoryUtilityTools.d.ts.map +1 -1
  195. package/dist/protocols/mcp/tools/MemoryUtilityTools.js +59 -208
  196. package/dist/protocols/mcp/tools/MemoryUtilityTools.js.map +1 -1
  197. package/dist/protocols/mcp/tools/PlanningTools.d.ts +2 -1
  198. package/dist/protocols/mcp/tools/PlanningTools.d.ts.map +1 -1
  199. package/dist/protocols/mcp/tools/PlanningTools.js +111 -49
  200. package/dist/protocols/mcp/tools/PlanningTools.js.map +1 -1
  201. package/dist/protocols/mcp/tools/ProjectContextTools.d.ts.map +1 -1
  202. package/dist/protocols/mcp/tools/ProjectContextTools.js +18 -12
  203. package/dist/protocols/mcp/tools/ProjectContextTools.js.map +1 -1
  204. package/dist/protocols/mcp/tools/SafetyTools.d.ts.map +1 -1
  205. package/dist/protocols/mcp/tools/SafetyTools.js +16 -13
  206. package/dist/protocols/mcp/tools/SafetyTools.js.map +1 -1
  207. package/dist/protocols/mcp/tools/SearchProjectTools.d.ts +24 -7
  208. package/dist/protocols/mcp/tools/SearchProjectTools.d.ts.map +1 -1
  209. package/dist/protocols/mcp/tools/SearchProjectTools.js +226 -126
  210. package/dist/protocols/mcp/tools/SearchProjectTools.js.map +1 -1
  211. package/dist/protocols/mcp/tools/TaskPlanningTools.d.ts +0 -32
  212. package/dist/protocols/mcp/tools/TaskPlanningTools.d.ts.map +1 -1
  213. package/dist/protocols/mcp/tools/TaskPlanningTools.js +363 -450
  214. package/dist/protocols/mcp/tools/TaskPlanningTools.js.map +1 -1
  215. package/dist/protocols/mcp/tools/TestTools.d.ts.map +1 -1
  216. package/dist/protocols/mcp/tools/TestTools.js +18 -11
  217. package/dist/protocols/mcp/tools/TestTools.js.map +1 -1
  218. package/dist/protocols/mcp/tools/TypeScriptTools.d.ts.map +1 -1
  219. package/dist/protocols/mcp/tools/TypeScriptTools.js +22 -12
  220. package/dist/protocols/mcp/tools/TypeScriptTools.js.map +1 -1
  221. package/dist/protocols/mcp/tools/UserInputTools.d.ts.map +1 -1
  222. package/dist/protocols/mcp/tools/UserInputTools.js +14 -1
  223. package/dist/protocols/mcp/tools/UserInputTools.js.map +1 -1
  224. package/dist/protocols/mcp/tools/UserMemoryTools.d.ts +3 -1
  225. package/dist/protocols/mcp/tools/UserMemoryTools.d.ts.map +1 -1
  226. package/dist/protocols/mcp/tools/UserMemoryTools.js +27 -62
  227. package/dist/protocols/mcp/tools/UserMemoryTools.js.map +1 -1
  228. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.d.ts.map +1 -1
  229. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.js +14 -60
  230. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.js.map +1 -1
  231. package/dist/protocols/mcp/tools/shell/ShellSandbox.d.ts.map +1 -1
  232. package/dist/protocols/mcp/tools/shell/ShellSandbox.js +9 -27
  233. package/dist/protocols/mcp/tools/shell/ShellSandbox.js.map +1 -1
  234. package/dist/repositories/interfaces/IKnowledgeGraphRepository.d.ts +40 -30
  235. package/dist/repositories/interfaces/IKnowledgeGraphRepository.d.ts.map +1 -1
  236. package/dist/schemas/EventPayloadSchema.d.ts +28 -0
  237. package/dist/schemas/EventPayloadSchema.d.ts.map +1 -1
  238. package/dist/schemas/EventPayloadSchema.js +30 -1
  239. package/dist/schemas/EventPayloadSchema.js.map +1 -1
  240. package/dist/schemas/MxpProtocolSchemas.d.ts +6 -6
  241. package/dist/services/AgentPerformanceService.d.ts +11 -0
  242. package/dist/services/AgentPerformanceService.d.ts.map +1 -1
  243. package/dist/services/AgentPerformanceService.js +59 -33
  244. package/dist/services/AgentPerformanceService.js.map +1 -1
  245. package/dist/services/AutoCorrectionService.d.ts +3 -0
  246. package/dist/services/AutoCorrectionService.d.ts.map +1 -1
  247. package/dist/services/AutoCorrectionService.js +19 -2
  248. package/dist/services/AutoCorrectionService.js.map +1 -1
  249. package/dist/services/BackgroundTaskManager.d.ts +23 -7
  250. package/dist/services/BackgroundTaskManager.d.ts.map +1 -1
  251. package/dist/services/BackgroundTaskManager.js +137 -57
  252. package/dist/services/BackgroundTaskManager.js.map +1 -1
  253. package/dist/services/BrowserManager.d.ts +2 -0
  254. package/dist/services/BrowserManager.d.ts.map +1 -1
  255. package/dist/services/BrowserManager.js +18 -15
  256. package/dist/services/BrowserManager.js.map +1 -1
  257. package/dist/services/ChannelContextMemoryOperations.d.ts +8 -1
  258. package/dist/services/ChannelContextMemoryOperations.d.ts.map +1 -1
  259. package/dist/services/ChannelContextMemoryOperations.js +89 -165
  260. package/dist/services/ChannelContextMemoryOperations.js.map +1 -1
  261. package/dist/services/ChannelContextMessageOperations.d.ts +4 -3
  262. package/dist/services/ChannelContextMessageOperations.d.ts.map +1 -1
  263. package/dist/services/ChannelContextMessageOperations.js +72 -212
  264. package/dist/services/ChannelContextMessageOperations.js.map +1 -1
  265. package/dist/services/MemoryService.d.ts +52 -21
  266. package/dist/services/MemoryService.d.ts.map +1 -1
  267. package/dist/services/MemoryService.js +775 -548
  268. package/dist/services/MemoryService.js.map +1 -1
  269. package/dist/services/PatternLearningService.d.ts +5 -0
  270. package/dist/services/PatternLearningService.d.ts.map +1 -1
  271. package/dist/services/PatternLearningService.js +26 -3
  272. package/dist/services/PatternLearningService.js.map +1 -1
  273. package/dist/services/PerformanceOptimizationService.d.ts +2 -1
  274. package/dist/services/PerformanceOptimizationService.d.ts.map +1 -1
  275. package/dist/services/PerformanceOptimizationService.js +21 -6
  276. package/dist/services/PerformanceOptimizationService.js.map +1 -1
  277. package/dist/services/PredictiveAnalyticsService.d.ts +1 -0
  278. package/dist/services/PredictiveAnalyticsService.d.ts.map +1 -1
  279. package/dist/services/PredictiveAnalyticsService.js +18 -6
  280. package/dist/services/PredictiveAnalyticsService.js.map +1 -1
  281. package/dist/services/ProactiveValidationService.d.ts +6 -0
  282. package/dist/services/ProactiveValidationService.d.ts.map +1 -1
  283. package/dist/services/ProactiveValidationService.js +58 -39
  284. package/dist/services/ProactiveValidationService.js.map +1 -1
  285. package/dist/services/TaskEffectivenessService.d.ts +55 -31
  286. package/dist/services/TaskEffectivenessService.d.ts.map +1 -1
  287. package/dist/services/TaskEffectivenessService.js +243 -125
  288. package/dist/services/TaskEffectivenessService.js.map +1 -1
  289. package/dist/services/ToolExecutionInterceptor.d.ts +4 -0
  290. package/dist/services/ToolExecutionInterceptor.d.ts.map +1 -1
  291. package/dist/services/ToolExecutionInterceptor.js +31 -5
  292. package/dist/services/ToolExecutionInterceptor.js.map +1 -1
  293. package/dist/services/UserInputRequestManager.d.ts +13 -3
  294. package/dist/services/UserInputRequestManager.d.ts.map +1 -1
  295. package/dist/services/UserInputRequestManager.js +42 -6
  296. package/dist/services/UserInputRequestManager.js.map +1 -1
  297. package/dist/services/UserMemoryService.d.ts +2 -0
  298. package/dist/services/UserMemoryService.d.ts.map +1 -1
  299. package/dist/services/UserMemoryService.js +9 -4
  300. package/dist/services/UserMemoryService.js.map +1 -1
  301. package/dist/services/ValidationAnalyticsService.d.ts +4 -0
  302. package/dist/services/ValidationAnalyticsService.d.ts.map +1 -1
  303. package/dist/services/ValidationAnalyticsService.js +30 -8
  304. package/dist/services/ValidationAnalyticsService.js.map +1 -1
  305. package/dist/services/ValidationCacheService.d.ts +3 -1
  306. package/dist/services/ValidationCacheService.d.ts.map +1 -1
  307. package/dist/services/ValidationCacheService.js +25 -4
  308. package/dist/services/ValidationCacheService.js.map +1 -1
  309. package/dist/services/ValidationMiddleware.d.ts +7 -0
  310. package/dist/services/ValidationMiddleware.d.ts.map +1 -1
  311. package/dist/services/ValidationMiddleware.js +53 -10
  312. package/dist/services/ValidationMiddleware.js.map +1 -1
  313. package/dist/services/ValidationPerformanceService.d.ts +3 -0
  314. package/dist/services/ValidationPerformanceService.d.ts.map +1 -1
  315. package/dist/services/ValidationPerformanceService.js +28 -11
  316. package/dist/services/ValidationPerformanceService.js.map +1 -1
  317. package/dist/services/WebSearchService.d.ts.map +1 -1
  318. package/dist/services/WebSearchService.js +9 -0
  319. package/dist/services/WebSearchService.js.map +1 -1
  320. package/dist/services/dag/TaskDagService.d.ts +3 -0
  321. package/dist/services/dag/TaskDagService.d.ts.map +1 -1
  322. package/dist/services/dag/TaskDagService.js +18 -0
  323. package/dist/services/dag/TaskDagService.js.map +1 -1
  324. package/dist/services/kg/EntityQValueManager.d.ts +3 -1
  325. package/dist/services/kg/EntityQValueManager.d.ts.map +1 -1
  326. package/dist/services/kg/EntityQValueManager.js +10 -10
  327. package/dist/services/kg/EntityQValueManager.js.map +1 -1
  328. package/dist/services/kg/GraphSurpriseCalculator.d.ts +0 -4
  329. package/dist/services/kg/GraphSurpriseCalculator.d.ts.map +1 -1
  330. package/dist/services/kg/GraphSurpriseCalculator.js +0 -9
  331. package/dist/services/kg/GraphSurpriseCalculator.js.map +1 -1
  332. package/dist/services/kg/KnowledgeGraphService.d.ts +12 -12
  333. package/dist/services/kg/KnowledgeGraphService.d.ts.map +1 -1
  334. package/dist/services/kg/KnowledgeGraphService.js +43 -51
  335. package/dist/services/kg/KnowledgeGraphService.js.map +1 -1
  336. package/dist/services/kg/OrparGraphIntegration.d.ts.map +1 -1
  337. package/dist/services/kg/OrparGraphIntegration.js +6 -5
  338. package/dist/services/kg/OrparGraphIntegration.js.map +1 -1
  339. package/dist/services/orpar-memory/CycleConsolidationTrigger.d.ts +1 -0
  340. package/dist/services/orpar-memory/CycleConsolidationTrigger.d.ts.map +1 -1
  341. package/dist/services/orpar-memory/CycleConsolidationTrigger.js +8 -1
  342. package/dist/services/orpar-memory/CycleConsolidationTrigger.js.map +1 -1
  343. package/dist/services/orpar-memory/OrparMemoryCoordinator.d.ts +8 -1
  344. package/dist/services/orpar-memory/OrparMemoryCoordinator.d.ts.map +1 -1
  345. package/dist/services/orpar-memory/OrparMemoryCoordinator.js +93 -45
  346. package/dist/services/orpar-memory/OrparMemoryCoordinator.js.map +1 -1
  347. package/dist/services/orpar-memory/SurpriseOrparAdapter.d.ts +1 -0
  348. package/dist/services/orpar-memory/SurpriseOrparAdapter.d.ts.map +1 -1
  349. package/dist/services/orpar-memory/SurpriseOrparAdapter.js +8 -1
  350. package/dist/services/orpar-memory/SurpriseOrparAdapter.js.map +1 -1
  351. package/dist/types/ChannelContext.d.ts +2 -1
  352. package/dist/types/ChannelContext.d.ts.map +1 -1
  353. package/dist/types/EffectivenessTypes.d.ts +4 -1
  354. package/dist/types/EffectivenessTypes.d.ts.map +1 -1
  355. package/dist/types/TaskCompletionTypes.d.ts +7 -5
  356. package/dist/types/TaskCompletionTypes.d.ts.map +1 -1
  357. package/dist/types/TaskTypes.d.ts +8 -1
  358. package/dist/types/TaskTypes.d.ts.map +1 -1
  359. package/dist/utils/GlobMatcher.d.ts +37 -0
  360. package/dist/utils/GlobMatcher.d.ts.map +1 -0
  361. package/dist/utils/GlobMatcher.js +131 -0
  362. package/dist/utils/GlobMatcher.js.map +1 -0
  363. package/package.json +1 -1
  364. package/src/config/ModelContextLimits.ts +7 -1
  365. package/src/constants/CoreTools.ts +6 -5
  366. package/src/constants/DefaultPhaseProfiles.ts +29 -0
  367. package/src/constants/ReservedIdentities.ts +51 -0
  368. package/src/database/adapters/mongodb/MongoKnowledgeGraphRepository.ts +115 -36
  369. package/src/events/ClientEventBus.ts +117 -48
  370. package/src/events/EventBus.ts +32 -2
  371. package/src/events/EventBusBase.ts +132 -39
  372. package/src/events/EventNames.ts +43 -36
  373. package/src/events/PublicEvents.ts +8 -0
  374. package/src/events/ServerEventBus.ts +9 -128
  375. package/src/events/event-definitions/ControlLoopEvents.ts +12 -0
  376. package/src/events/event-definitions/SdkEvents.ts +58 -0
  377. package/src/events/event-definitions/TaskEvents.ts +2 -2
  378. package/src/index.ts +15 -0
  379. package/src/interfaces/AgentInterfaces.ts +5 -0
  380. package/src/interfaces/ChannelConfig.ts +2 -2
  381. package/src/interfaces/ConversationMessage.ts +5 -0
  382. package/src/interfaces/IMemoryPersistence.ts +55 -1
  383. package/src/interfaces/SocketServiceInterface.ts +3 -2
  384. package/src/interfaces/TaskInterfaces.ts +16 -1
  385. package/src/models/agent.ts +0 -6
  386. package/src/models/agentIdentityReservation.ts +77 -0
  387. package/src/models/channel.ts +2 -0
  388. package/src/models/channelKey.ts +10 -0
  389. package/src/models/memory.ts +18 -19
  390. package/src/models/memoryStrata.ts +9 -23
  391. package/src/models/plan.ts +3 -3
  392. package/src/models/taskEffectiveness.ts +9 -2
  393. package/src/models/user.ts +12 -0
  394. package/src/protocols/mcp/LlmTestEnvironmentGuard.ts +30 -0
  395. package/src/protocols/mcp/McpServerTypes.ts +9 -0
  396. package/src/protocols/mcp/providers/AnthropicMcpClient.ts +2 -0
  397. package/src/protocols/mcp/providers/AzureOpenAiMcpClient.ts +1 -0
  398. package/src/protocols/mcp/providers/BaseMcpClient.ts +13 -0
  399. package/src/protocols/mcp/providers/CustomMcpClient.ts +7 -0
  400. package/src/protocols/mcp/providers/GeminiMcpClient.ts +2 -0
  401. package/src/protocols/mcp/providers/OllamaMcpClient.ts +3 -1
  402. package/src/protocols/mcp/providers/OpenAiMcpClient.ts +2 -0
  403. package/src/protocols/mcp/providers/OpenRouterMcpClient.ts +3 -0
  404. package/src/protocols/mcp/providers/XaiMcpClient.ts +2 -0
  405. package/src/protocols/mcp/security/BrowserTargetGuard.ts +64 -0
  406. package/src/protocols/mcp/security/ExternalMcpRegistrationPolicy.ts +38 -0
  407. package/src/protocols/mcp/security/HttpTargetGuard.ts +77 -6
  408. package/src/protocols/mcp/security/McpSecurityGuard.ts +7 -5
  409. package/src/protocols/mcp/security/McpToolPolicy.ts +74 -0
  410. package/src/protocols/mcp/services/ExternalMcpServerManager.ts +656 -111
  411. package/src/protocols/mcp/services/ExternalServerConfigs.ts +26 -8
  412. package/src/protocols/mcp/tools/AnalyticsTools.ts +167 -641
  413. package/src/protocols/mcp/tools/CodeAnalysisTools.ts +44 -14
  414. package/src/protocols/mcp/tools/EffectivenessTools.ts +185 -45
  415. package/src/protocols/mcp/tools/GitTools.ts +15 -5
  416. package/src/protocols/mcp/tools/InfrastructureTools.ts +42 -90
  417. package/src/protocols/mcp/tools/JsonTools.ts +7 -4
  418. package/src/protocols/mcp/tools/KnowledgeGraphTools.ts +313 -164
  419. package/src/protocols/mcp/tools/MemorySearchTools.ts +158 -46
  420. package/src/protocols/mcp/tools/MemoryUtilityTools.ts +85 -232
  421. package/src/protocols/mcp/tools/PlanningTools.ts +137 -59
  422. package/src/protocols/mcp/tools/ProjectContextTools.ts +33 -12
  423. package/src/protocols/mcp/tools/SafetyTools.ts +49 -13
  424. package/src/protocols/mcp/tools/SearchProjectTools.ts +294 -153
  425. package/src/protocols/mcp/tools/TaskPlanningTools.ts +437 -514
  426. package/src/protocols/mcp/tools/TestTools.ts +41 -11
  427. package/src/protocols/mcp/tools/TypeScriptTools.ts +61 -12
  428. package/src/protocols/mcp/tools/UserInputTools.ts +15 -1
  429. package/src/protocols/mcp/tools/UserMemoryTools.ts +27 -62
  430. package/src/protocols/mcp/tools/shell/ShellExecuteHandler.ts +26 -84
  431. package/src/protocols/mcp/tools/shell/ShellSandbox.ts +13 -31
  432. package/src/repositories/interfaces/IKnowledgeGraphRepository.ts +45 -27
  433. package/src/schemas/EventPayloadSchema.ts +45 -1
  434. package/src/services/AgentPerformanceService.ts +85 -36
  435. package/src/services/AutoCorrectionService.ts +25 -6
  436. package/src/services/BackgroundTaskManager.ts +183 -76
  437. package/src/services/BrowserManager.ts +20 -16
  438. package/src/services/ChannelContextMemoryOperations.ts +169 -202
  439. package/src/services/ChannelContextMessageOperations.ts +112 -274
  440. package/src/services/MemoryService.ts +1053 -760
  441. package/src/services/PatternLearningService.ts +32 -7
  442. package/src/services/PerformanceOptimizationService.ts +26 -9
  443. package/src/services/PredictiveAnalyticsService.ts +23 -10
  444. package/src/services/ProactiveValidationService.ts +66 -40
  445. package/src/services/TaskEffectivenessService.ts +421 -206
  446. package/src/services/ToolExecutionInterceptor.ts +40 -9
  447. package/src/services/UserInputRequestManager.ts +51 -6
  448. package/src/services/UserMemoryService.ts +12 -3
  449. package/src/services/ValidationAnalyticsService.ts +36 -11
  450. package/src/services/ValidationCacheService.ts +30 -7
  451. package/src/services/ValidationMiddleware.ts +62 -14
  452. package/src/services/ValidationPerformanceService.ts +47 -25
  453. package/src/services/WebSearchService.ts +16 -4
  454. package/src/services/dag/TaskDagService.ts +20 -1
  455. package/src/services/kg/EntityQValueManager.ts +21 -10
  456. package/src/services/kg/GraphSurpriseCalculator.ts +0 -14
  457. package/src/services/kg/KnowledgeGraphService.ts +56 -48
  458. package/src/services/kg/OrparGraphIntegration.ts +11 -5
  459. package/src/services/orpar-memory/CycleConsolidationTrigger.ts +10 -1
  460. package/src/services/orpar-memory/OrparMemoryCoordinator.ts +142 -71
  461. package/src/services/orpar-memory/SurpriseOrparAdapter.ts +10 -1
  462. package/src/types/ChannelContext.ts +3 -1
  463. package/src/types/EffectivenessTypes.ts +6 -2
  464. package/src/types/TaskCompletionTypes.ts +9 -9
  465. package/src/types/TaskTypes.ts +12 -1
  466. package/src/utils/GlobMatcher.ts +157 -0
@@ -34,6 +34,7 @@ import { createExternalMcpServerEventPayload, createExternalMcpServerErrorEventP
34
34
  import { AutoCorrectionService } from '../../../services/AutoCorrectionService.js';
35
35
  import { Events } from '../../../events/EventNames.js';
36
36
  import { v4 as uuidv4 } from 'uuid';
37
+ import { assertUnsafeStdioMcpEnabled } from '../security/ExternalMcpRegistrationPolicy.js';
37
38
  // Create logger and validator instances
38
39
  const logger = new Logger('info', 'ExternalMcpServerManager', 'server');
39
40
  const validator = createStrictValidator('ExternalMcpServerManager');
@@ -44,6 +45,15 @@ const REQUEST_TIMEOUTS_MS = {
44
45
  'tools/call': 30000,
45
46
  ping: 5000
46
47
  };
48
+ /** Delay before restarting a crashed server. */
49
+ const DEFAULT_RESTART_DELAY_MS = 2000;
50
+ /**
51
+ * Consecutive failed health probes after which a running server is declared
52
+ * dead and restarted. One failure can be a slow reply; two in a row on a
53
+ * connection that answers in milliseconds when healthy means the connection
54
+ * is gone.
55
+ */
56
+ const HEALTH_FAILURES_BEFORE_RESTART = 2;
47
57
  /** MCP protocol version this client negotiates in the initialize handshake. */
48
58
  const MCP_PROTOCOL_VERSION = '2024-11-05';
49
59
  /** Version this client reports to servers. */
@@ -55,14 +65,28 @@ export class ExternalMcpServerManager extends EventEmitter {
55
65
  servers = new Map();
56
66
  // Scope tracking for channel/agent-scoped servers
57
67
  serverScopes = new Map();
68
+ /**
69
+ * Channel ids which crossed their terminal deletion boundary in this
70
+ * process. Channel ids are reserved in persistence, so this tombstone never
71
+ * needs to be removed and closes register/delete races in both directions.
72
+ */
73
+ retiredChannelIds = new Set();
58
74
  autoCorrectionService;
59
75
  /** Injectable event emitter — decouples from direct EventBus.server dependency.
60
76
  * Server-side: uses default ServerToolEventEmitter (wraps EventBus.server).
61
77
  * Client-side: uses ClientToolEventEmitter (wraps EventBus.client + socketEmit). */
62
78
  toolEventEmitter = null;
79
+ /** EventBus subscriptions owned by this manager instance. */
80
+ eventSubscriptions = [];
81
+ /** Delay before restarting a crashed server. Overridable for tests. */
82
+ restartDelayMs = DEFAULT_RESTART_DELAY_MS;
83
+ /** Per-method JSON-RPC reply timeouts. Overridable for tests and tuning. */
84
+ requestTimeouts = { ...REQUEST_TIMEOUTS_MS };
63
85
  /**
64
86
  * @param options.toolEventEmitter - Injectable event emitter for decoupling from EventBus.server
65
87
  * @param options.skipServerEventHandlers - Skip setting up EventBus.server listeners (for client-side usage)
88
+ * @param options.restartDelayMs - Delay before restarting a crashed server (default 2000)
89
+ * @param options.requestTimeoutsMs - Per-method JSON-RPC reply timeout overrides
66
90
  */
67
91
  constructor(options) {
68
92
  super();
@@ -70,6 +94,12 @@ export class ExternalMcpServerManager extends EventEmitter {
70
94
  if (options?.toolEventEmitter) {
71
95
  this.toolEventEmitter = options.toolEventEmitter;
72
96
  }
97
+ if (options?.restartDelayMs !== undefined) {
98
+ this.restartDelayMs = options.restartDelayMs;
99
+ }
100
+ if (options?.requestTimeoutsMs) {
101
+ this.requestTimeouts = { ...this.requestTimeouts, ...options.requestTimeoutsMs };
102
+ }
73
103
  // Set up event listeners for SDK-initiated server registration
74
104
  // Skipped when running client-side (no EventBus.server listeners needed)
75
105
  if (!options?.skipServerEventHandlers) {
@@ -81,8 +111,12 @@ export class ExternalMcpServerManager extends EventEmitter {
81
111
  */
82
112
  setupEventHandlers() {
83
113
  // Handle external server registration requests from SDK
84
- EventBus.server.on(Events.Mcp.EXTERNAL_SERVER_REGISTER, async (payload) => {
114
+ this.eventSubscriptions.push(EventBus.server.on(Events.Mcp.EXTERNAL_SERVER_REGISTER, async (payload) => {
85
115
  try {
116
+ // EventBus registrations originate outside this manager (normally
117
+ // from a socket). They are caller-controlled and stdio means host
118
+ // process execution, so require the operator's explicit opt-in.
119
+ assertUnsafeStdioMcpEnabled(payload.data?.transport);
86
120
  const serverConfig = {
87
121
  id: payload.data.id,
88
122
  name: payload.data.name,
@@ -130,13 +164,12 @@ export class ExternalMcpServerManager extends EventEmitter {
130
164
  }
131
165
  });
132
166
  }
133
- });
167
+ }));
134
168
  // Handle external server unregistration requests from SDK
135
- EventBus.server.on(Events.Mcp.EXTERNAL_SERVER_UNREGISTER, async (payload) => {
169
+ this.eventSubscriptions.push(EventBus.server.on(Events.Mcp.EXTERNAL_SERVER_UNREGISTER, async (payload) => {
136
170
  try {
137
171
  const serverId = payload.data.serverId;
138
- await this.stopServer(serverId);
139
- this.servers.delete(serverId);
172
+ await this.unregisterServer(serverId);
140
173
  // Emit success response
141
174
  EventBus.server.emit(Events.Mcp.EXTERNAL_SERVER_UNREGISTERED, {
142
175
  eventId: uuidv4(),
@@ -152,7 +185,9 @@ export class ExternalMcpServerManager extends EventEmitter {
152
185
  });
153
186
  }
154
187
  catch (error) {
155
- logger.error(`Error unregistering external server:`, error);
188
+ // Interpolate the message: passing the raw error as a second console
189
+ // argument makes Bun print a source-code excerpt instead of the message.
190
+ logger.error(`Error unregistering external server: ${error instanceof Error ? error.message : String(error)}`);
156
191
  // Emit error response
157
192
  EventBus.server.emit(Events.Mcp.EXTERNAL_SERVER_UNREGISTERED, {
158
193
  eventId: uuidv4(),
@@ -167,50 +202,40 @@ export class ExternalMcpServerManager extends EventEmitter {
167
202
  }
168
203
  });
169
204
  }
170
- });
205
+ }));
171
206
  // Handle channel-scoped server registration requests
172
- EventBus.server.on(Events.Mcp.CHANNEL_SERVER_REGISTER, async (payload) => {
207
+ this.eventSubscriptions.push(EventBus.server.on(Events.Mcp.CHANNEL_SERVER_REGISTER, async (payload) => {
173
208
  try {
209
+ // Channel membership is not permission to execute a host command.
210
+ // The HTTP/socket entry point must also establish administrator
211
+ // authority; this is the defense-in-depth feature gate.
212
+ assertUnsafeStdioMcpEnabled(payload.data?.transport);
174
213
  logger.info(`[CHANNEL_SERVER_REGISTER] Received registration request: ${JSON.stringify({ agentId: payload.agentId, channelId: payload.channelId, serverId: payload.data?.id })}`);
175
214
  const channelId = payload.data.channelId || payload.channelId;
176
- const serverId = `${channelId}:${payload.data.id}`;
177
- const serverConfig = {
178
- id: serverId,
215
+ await this.registerChannelServer(channelId, {
216
+ id: payload.data.id,
179
217
  name: payload.data.name,
180
218
  version: payload.data.version || '1.0.0',
181
219
  command: payload.data.command || '',
182
220
  args: payload.data.args || [],
183
- transport: (payload.data.transport || 'stdio'),
184
- url: payload.data.url,
185
221
  autoStart: payload.data.autoStart !== false,
186
222
  restartOnCrash: payload.data.restartOnCrash !== false,
187
223
  maxRestartAttempts: payload.data.maxRestartAttempts || 3,
188
224
  healthCheckInterval: payload.data.healthCheckInterval || 30000,
189
225
  startupTimeout: payload.data.startupTimeout || 10000,
190
- environmentVariables: payload.data.environmentVariables || {}
191
- };
192
- // Track scope BEFORE registration so we can store the registration context
193
- // Note: connectedAgents starts empty - only actual game agents are counted, not the admin who registers
194
- this.serverScopes.set(serverId, {
195
- scope: 'channel',
196
- scopeId: channelId,
197
- connectedAgents: new Set(),
198
- keepAliveMinutes: payload.data.keepAliveMinutes || 5,
199
- // Store registration context for deferred success emission
200
- registrationContext: {
201
- agentId: payload.agentId,
202
- channelId,
203
- originalServerId: payload.data.id,
204
- serverName: payload.data.name
205
- }
226
+ environmentVariables: payload.data.environmentVariables || {},
227
+ keepAliveMinutes: payload.data.keepAliveMinutes
228
+ },
229
+ // Registration context for deferred success emission after tool discovery
230
+ {
231
+ agentId: payload.agentId,
232
+ channelId,
233
+ originalServerId: payload.data.id,
234
+ serverName: payload.data.name
206
235
  });
207
- // Register the server (this starts the process and tool discovery)
208
- // Success event will be emitted after tool discovery completes
209
- await this.registerServer(serverConfig);
210
- logger.info(`[CHANNEL_SERVER_REGISTER] Server ${serverId} registered, waiting for tool discovery before emitting success`);
211
236
  }
212
237
  catch (error) {
213
- logger.error(`Error registering channel server:`, error);
238
+ logger.error(`Error registering channel server: ${error instanceof Error ? error.message : String(error)}`);
214
239
  // Emit error response
215
240
  EventBus.server.emit(McpEvents.CHANNEL_SERVER_REGISTRATION_FAILED, createExternalMcpServerEventPayload(McpEvents.CHANNEL_SERVER_REGISTRATION_FAILED, payload.agentId, payload.data?.channelId || payload.channelId, {
216
241
  serverId: payload.data?.id,
@@ -220,15 +245,12 @@ export class ExternalMcpServerManager extends EventEmitter {
220
245
  error: error instanceof Error ? error.message : String(error)
221
246
  }));
222
247
  }
223
- });
248
+ }));
224
249
  // Handle channel server unregistration requests
225
- EventBus.server.on(Events.Mcp.CHANNEL_SERVER_UNREGISTER, async (payload) => {
250
+ this.eventSubscriptions.push(EventBus.server.on(Events.Mcp.CHANNEL_SERVER_UNREGISTER, async (payload) => {
226
251
  try {
227
252
  const channelId = payload.data.channelId || payload.channelId;
228
- const serverId = `${channelId}:${payload.data.serverId}`;
229
- await this.stopServer(serverId);
230
- this.servers.delete(serverId);
231
- this.serverScopes.delete(serverId);
253
+ await this.unregisterChannelServer(channelId, payload.data.serverId);
232
254
  // Emit success response
233
255
  EventBus.server.emit(McpEvents.CHANNEL_SERVER_UNREGISTERED, createExternalMcpServerEventPayload(McpEvents.CHANNEL_SERVER_UNREGISTERED, payload.agentId, channelId, {
234
256
  serverId: payload.data.serverId,
@@ -239,7 +261,7 @@ export class ExternalMcpServerManager extends EventEmitter {
239
261
  }));
240
262
  }
241
263
  catch (error) {
242
- logger.error(`Error unregistering channel server:`, error);
264
+ logger.error(`Error unregistering channel server: ${error instanceof Error ? error.message : String(error)}`);
243
265
  // Emit error response
244
266
  EventBus.server.emit(McpEvents.CHANNEL_SERVER_UNREGISTERED, createExternalMcpServerEventPayload(McpEvents.CHANNEL_SERVER_UNREGISTERED, payload.agentId, payload.data?.channelId || payload.channelId, {
245
267
  serverId: payload.data?.serverId,
@@ -249,12 +271,18 @@ export class ExternalMcpServerManager extends EventEmitter {
249
271
  error: error instanceof Error ? error.message : String(error)
250
272
  }));
251
273
  }
252
- });
274
+ }));
253
275
  }
254
276
  /**
255
277
  * Register a new external server configuration
256
278
  */
257
279
  async registerServer(config) {
280
+ // This manager currently speaks line-delimited MCP over child-process
281
+ // stdio only. Never let an HTTP-labelled registration fall through to
282
+ // spawn(config.command, ...), which would bypass the stdio feature gate.
283
+ if (config.transport === 'http') {
284
+ throw new Error('HTTP transport registration is not implemented by ExternalMcpServerManager');
285
+ }
258
286
  logger.info(`[REGISTER_SERVER] Registering server ${config.id}: command="${config.command}", args=${JSON.stringify(config.args)}, autoStart=${config.autoStart}`);
259
287
  // Validate configuration
260
288
  validator.assertIsNonEmptyString(config.id, 'Server ID must be a non-empty string');
@@ -287,7 +315,203 @@ export class ExternalMcpServerManager extends EventEmitter {
287
315
  }
288
316
  }
289
317
  /**
290
- * Start an external server process
318
+ * Register a channel-scoped server: track its scope, then register and
319
+ * (per config) start it. Resolves after the MCP handshake and tool
320
+ * discovery when autoStart is set, so a resolved promise means the tools
321
+ * are in the registry.
322
+ *
323
+ * A re-registration over an existing scope preserves the connected-agent
324
+ * set and clears any pending keepAlive timer — previously the timer
325
+ * reference was silently overwritten while the timer kept running, so a
326
+ * stale keepAlive could stop a freshly re-registered server later.
327
+ */
328
+ async registerChannelServer(channelId, config, registrationContext) {
329
+ validator.assertIsNonEmptyString(channelId, 'channelId must be a non-empty string');
330
+ const serverId = `${channelId}:${config.id}`;
331
+ const keepAliveMinutes = config.keepAliveMinutes || 5;
332
+ if (this.retiredChannelIds.has(channelId)) {
333
+ throw new Error(`Channel ${channelId} is deleted; MCP servers cannot be registered`);
334
+ }
335
+ const existingScope = this.serverScopes.get(serverId);
336
+ if (existingScope?.keepAliveTimer) {
337
+ clearTimeout(existingScope.keepAliveTimer);
338
+ }
339
+ this.serverScopes.set(serverId, {
340
+ scope: 'channel',
341
+ scopeId: channelId,
342
+ // Only actual channel agents are counted, not the admin who registers.
343
+ // On re-registration, keep whoever is already connected.
344
+ connectedAgents: existingScope?.connectedAgents ?? new Set(),
345
+ keepAliveMinutes,
346
+ registrationContext
347
+ });
348
+ logger.info(`[CHANNEL_SERVER_REGISTER] Registering channel server ${serverId} ` +
349
+ `(keepAlive ${keepAliveMinutes}min, autoStart ${config.autoStart}, restartOnCrash ${config.restartOnCrash})`);
350
+ const { keepAliveMinutes: _ignored, ...serverConfig } = config;
351
+ try {
352
+ await this.registerServer({ ...serverConfig, id: serverId });
353
+ // Deletion may have won while process startup/handshake was in
354
+ // flight. Remove the just-created runtime before returning so a
355
+ // late registration cannot resurrect a deleted channel's server.
356
+ if (this.retiredChannelIds.has(channelId)) {
357
+ await this.removeServer(serverId, 'registration raced channel deletion');
358
+ throw new Error(`Channel ${channelId} was deleted during MCP server registration`);
359
+ }
360
+ }
361
+ catch (error) {
362
+ // Registration failed before the server record existed — do not leave
363
+ // a scope entry behind for agents to "join" (unless one existed before).
364
+ if (!existingScope && !this.servers.has(serverId)) {
365
+ this.serverScopes.delete(serverId);
366
+ }
367
+ throw error;
368
+ }
369
+ }
370
+ /**
371
+ * Unregister a channel-scoped server: stop the process and remove both the
372
+ * server record and the scope tracking, including any keepAlive timer.
373
+ *
374
+ * Idempotent: unregistering a server that is partially or fully gone cleans
375
+ * up whatever remains and resolves. Production showed the half-removed
376
+ * state — record deleted, scope alive — and an unregister that throws
377
+ * "not found" against it leaves the zombie in place forever.
378
+ */
379
+ async unregisterChannelServer(channelId, serverId) {
380
+ validator.assertIsNonEmptyString(channelId, 'channelId must be a non-empty string');
381
+ validator.assertIsNonEmptyString(serverId, 'serverId must be a non-empty string');
382
+ await this.removeServer(`${channelId}:${serverId}`, 'channel server unregistration');
383
+ }
384
+ /**
385
+ * Permanently retire a channel's MCP runtime in this process.
386
+ *
387
+ * The tombstone is installed before process cleanup, which makes concurrent
388
+ * registrations fail both before and after their asynchronous startup.
389
+ * Every known scoped record is removed, including orphaned records whose
390
+ * scope metadata or server entry is only partially present.
391
+ */
392
+ async retireChannel(channelId) {
393
+ validator.assertIsNonEmptyString(channelId, 'channelId must be a non-empty string');
394
+ this.retiredChannelIds.add(channelId);
395
+ const serverIds = new Set();
396
+ for (const [serverId, scopeData] of this.serverScopes.entries()) {
397
+ if (scopeData.scope === 'channel' && scopeData.scopeId === channelId) {
398
+ serverIds.add(serverId);
399
+ }
400
+ }
401
+ for (const serverId of this.servers.keys()) {
402
+ if (serverId.startsWith(`${channelId}:`)) {
403
+ serverIds.add(serverId);
404
+ }
405
+ }
406
+ const results = await Promise.allSettled(Array.from(serverIds, serverId => (this.removeServer(serverId, 'channel deletion'))));
407
+ const failures = results.filter((result) => result.status === 'rejected');
408
+ if (failures.length > 0) {
409
+ throw new Error(`Failed to retire ${failures.length} MCP server(s) for channel ${channelId}`);
410
+ }
411
+ }
412
+ /**
413
+ * Unregister a server by its full id (global servers, and the SDK-facing
414
+ * EXTERNAL_SERVER_UNREGISTER path). Also removes scope tracking — this
415
+ * path used to delete only the server record, which was exactly the
416
+ * zombie state observed in production: agents kept "joining" a scope
417
+ * whose server no longer existed.
418
+ */
419
+ async unregisterServer(serverId) {
420
+ validator.assertIsNonEmptyString(serverId, 'serverId must be a non-empty string');
421
+ await this.removeServer(serverId, 'server unregistration');
422
+ }
423
+ /**
424
+ * Stop a server (if running) and remove every trace of it: server record,
425
+ * scope entry, keepAlive timer. Never throws for a missing record — it
426
+ * removes what exists and says what it did.
427
+ */
428
+ async removeServer(serverId, reason) {
429
+ const serverData = this.servers.get(serverId);
430
+ const scopeData = this.serverScopes.get(serverId);
431
+ if (!serverData && !scopeData) {
432
+ logger.warn(`Nothing to unregister for ${serverId} (${reason}) — no record, no scope`);
433
+ return;
434
+ }
435
+ if (scopeData?.keepAliveTimer) {
436
+ clearTimeout(scopeData.keepAliveTimer);
437
+ scopeData.keepAliveTimer = undefined;
438
+ }
439
+ if (serverData) {
440
+ try {
441
+ await this.stopServer(serverId, undefined, undefined, reason);
442
+ }
443
+ catch (error) {
444
+ logger.error(`Error stopping ${serverId} during ${reason}: ` +
445
+ `${error instanceof Error ? error.message : String(error)} — removing its record anyway`);
446
+ }
447
+ }
448
+ else {
449
+ logger.warn(`Unregistering ${serverId} (${reason}): server record was already gone, ` +
450
+ `removing the orphaned scope entry`);
451
+ }
452
+ this.servers.delete(serverId);
453
+ this.serverScopes.delete(serverId);
454
+ logger.info(`Unregistered server ${serverId} (${reason})`);
455
+ }
456
+ /**
457
+ * Remove a server that cannot be kept alive (restart budget exhausted,
458
+ * unrecoverable spawn failure). Loud by design: this is the path that
459
+ * prevents zombies, so it reports at error level.
460
+ */
461
+ removeServerAfterFailure(serverId, reason) {
462
+ const serverData = this.servers.get(serverId);
463
+ if (serverData) {
464
+ if (serverData.healthCheckTimer) {
465
+ clearInterval(serverData.healthCheckTimer);
466
+ serverData.healthCheckTimer = undefined;
467
+ }
468
+ if (serverData.startupTimer) {
469
+ clearTimeout(serverData.startupTimer);
470
+ serverData.startupTimer = undefined;
471
+ }
472
+ this.rejectPendingRequests(serverId, reason);
473
+ this.settleReadyWaiters(serverId, new Error(reason));
474
+ if (serverData.process && !serverData.process.killed) {
475
+ serverData.process.kill('SIGKILL');
476
+ }
477
+ }
478
+ const scopeData = this.serverScopes.get(serverId);
479
+ if (scopeData?.keepAliveTimer) {
480
+ clearTimeout(scopeData.keepAliveTimer);
481
+ }
482
+ this.servers.delete(serverId);
483
+ this.serverScopes.delete(serverId);
484
+ logger.error(`Server ${serverId} unregistered: ${reason}. ` +
485
+ `Its tools are removed from the registry; re-register the server to restore them.`);
486
+ this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STOPPED, serverId);
487
+ }
488
+ /**
489
+ * Settle every caller waiting for a server to become ready.
490
+ */
491
+ settleReadyWaiters(serverId, error) {
492
+ const serverData = this.servers.get(serverId);
493
+ if (!serverData?.readyWaiters?.length) {
494
+ return;
495
+ }
496
+ const waiters = serverData.readyWaiters;
497
+ serverData.readyWaiters = [];
498
+ for (const waiter of waiters) {
499
+ if (error) {
500
+ waiter.reject(error);
501
+ }
502
+ else {
503
+ waiter.resolve();
504
+ }
505
+ }
506
+ }
507
+ /**
508
+ * Start an external server process.
509
+ *
510
+ * Resolves once the MCP handshake AND tool discovery have completed — a
511
+ * resolved startServer() means the server's tools are in the registry.
512
+ * It used to resolve right after spawn, which let callers (agent join,
513
+ * restart) proceed against a server that had not finished — or would
514
+ * never finish — its handshake.
291
515
  */
292
516
  async startServer(serverId, agentId, channelId) {
293
517
  logger.info(`[START_SERVER] Starting server ${serverId}`);
@@ -302,8 +526,19 @@ export class ExternalMcpServerManager extends EventEmitter {
302
526
  logger.info(`[START_SERVER] Server ${serverId} already running`);
303
527
  return;
304
528
  }
529
+ if (status.status === 'starting') {
530
+ // Another caller is already starting this server — wait for that
531
+ // startup instead of spawning a second process.
532
+ logger.info(`[START_SERVER] Server ${serverId} already starting, awaiting readiness`);
533
+ await new Promise((resolve, reject) => {
534
+ serverData.readyWaiters = serverData.readyWaiters ?? [];
535
+ serverData.readyWaiters.push({ resolve, reject });
536
+ });
537
+ return;
538
+ }
305
539
  // Update status
306
540
  status.status = 'starting';
541
+ serverData.expectedExit = false;
307
542
  this.emitServerEvent(McpEvents.EXTERNAL_SERVER_SPAWN, serverId, agentId, channelId);
308
543
  try {
309
544
  // Spawn the process
@@ -351,31 +586,85 @@ export class ExternalMcpServerManager extends EventEmitter {
351
586
  const errorMessage = error instanceof Error ? error.message : String(error);
352
587
  logger.error(`❌ Failed to start server ${config.name}: ${errorMessage}`);
353
588
  this.handleServerError(serverId, errorMessage, agentId, channelId);
589
+ throw error instanceof Error ? error : new Error(errorMessage);
354
590
  }
591
+ // Wait for the handshake and tool discovery. Settled by
592
+ // initializeMcpConnection() on success, and by handleServerError()
593
+ // or the exit handler on failure — those already report the cause,
594
+ // so a rejection here propagates without being re-handled.
595
+ await new Promise((resolve, reject) => {
596
+ serverData.readyWaiters = serverData.readyWaiters ?? [];
597
+ serverData.readyWaiters.push({ resolve, reject });
598
+ });
355
599
  }
356
600
  /**
357
- * Handle agent joining a channel - start channel servers and track connection
601
+ * Handle agent joining a channel verify each channel server is actually
602
+ * alive before counting the agent as connected.
603
+ *
604
+ * This used to be a blind reference-count bump: a missing server record was
605
+ * silently skipped and a dead process was never probed, so agents "joined"
606
+ * servers that could not serve a single tool call, and the only downstream
607
+ * signal was a NOT FOUND warning when their allowlist resolved.
358
608
  */
359
609
  async onAgentJoinChannel(agentId, channelId) {
360
610
  logger.info(`Agent ${agentId} joining channel ${channelId} - checking for channel servers`);
361
611
  // Find all channel-scoped servers for this channel
362
612
  for (const [serverId, scopeData] of this.serverScopes.entries()) {
363
- if (scopeData.scope === 'channel' && scopeData.scopeId === channelId) {
364
- // Add agent to connected agents
365
- scopeData.connectedAgents.add(agentId);
366
- // Clear any pending keepAlive timer
613
+ if (scopeData.scope !== 'channel' || scopeData.scopeId !== channelId) {
614
+ continue;
615
+ }
616
+ const serverData = this.servers.get(serverId);
617
+ if (!serverData) {
618
+ // The production zombie state: a scope entry whose server record is
619
+ // gone. There is no config left to restart from, so remove the
620
+ // orphan loudly instead of pretending the agent connected.
621
+ logger.error(`Agent ${agentId} tried to join channel server ${serverId}, but its server record is gone ` +
622
+ `(scope entry was orphaned). Removing the orphaned scope — the server must be re-registered.`);
367
623
  if (scopeData.keepAliveTimer) {
368
624
  clearTimeout(scopeData.keepAliveTimer);
369
- scopeData.keepAliveTimer = undefined;
370
625
  }
371
- // Start server if not already running
372
- const serverData = this.servers.get(serverId);
373
- if (serverData && serverData.status.status !== 'running') {
374
- logger.info(`Starting channel server ${serverId} for agent ${agentId}`);
626
+ this.serverScopes.delete(serverId);
627
+ continue;
628
+ }
629
+ try {
630
+ if (serverData.status.status !== 'running') {
631
+ logger.info(`Starting channel server ${serverId} for agent ${agentId} (status: ${serverData.status.status})`);
375
632
  await this.startServer(serverId);
376
633
  }
377
- logger.info(`Agent ${agentId} connected to channel server ${serverId} (${scopeData.connectedAgents.size} agents)`);
634
+ else {
635
+ // Status says running — prove it. A live entry with a dead child
636
+ // (or a wedged MCP connection) must trigger recovery at join
637
+ // time, not a silent ref-count bump.
638
+ const alive = serverData.process && !serverData.process.killed && serverData.process.exitCode === null;
639
+ if (!alive) {
640
+ logger.warn(`Channel server ${serverId} has no live process at agent join — restarting`);
641
+ await this.restartServer(serverId);
642
+ }
643
+ else {
644
+ try {
645
+ await this.sendRequest(serverId, 'tools/list');
646
+ }
647
+ catch (probeError) {
648
+ logger.warn(`Channel server ${serverId} did not answer the liveness probe at agent join ` +
649
+ `(${probeError instanceof Error ? probeError.message : String(probeError)}) — restarting`);
650
+ await this.restartServer(serverId);
651
+ }
652
+ }
653
+ }
654
+ }
655
+ catch (error) {
656
+ logger.error(`Channel server ${serverId} could not be made available for agent ${agentId}: ` +
657
+ `${error instanceof Error ? error.message : String(error)}`);
658
+ continue;
659
+ }
660
+ // Only a server that is verifiably up counts the agent as connected
661
+ scopeData.connectedAgents.add(agentId);
662
+ // Clear any pending keepAlive timer
663
+ if (scopeData.keepAliveTimer) {
664
+ clearTimeout(scopeData.keepAliveTimer);
665
+ scopeData.keepAliveTimer = undefined;
378
666
  }
667
+ logger.info(`Agent ${agentId} connected to channel server ${serverId} (${scopeData.connectedAgents.size} agents)`);
379
668
  }
380
669
  }
381
670
  /**
@@ -394,12 +683,18 @@ export class ExternalMcpServerManager extends EventEmitter {
394
683
  const keepAliveMs = (scopeData.keepAliveMinutes || 5) * 60 * 1000;
395
684
  logger.info(`Last agent left channel server ${serverId}, starting ${scopeData.keepAliveMinutes}min keepAlive timer`);
396
685
  scopeData.keepAliveTimer = setTimeout(async () => {
686
+ scopeData.keepAliveTimer = undefined;
687
+ if (!this.servers.has(serverId)) {
688
+ logger.warn(`KeepAlive expired for ${serverId}, but its server record is already gone — removing the orphaned scope`);
689
+ this.serverScopes.delete(serverId);
690
+ return;
691
+ }
397
692
  logger.info(`KeepAlive expired for server ${serverId}, stopping server`);
398
693
  try {
399
- await this.stopServer(serverId);
694
+ await this.stopServer(serverId, undefined, undefined, 'keepAlive expired');
400
695
  }
401
696
  catch (error) {
402
- logger.error(`Error stopping server ${serverId} after keepAlive:`, error);
697
+ logger.error(`Error stopping server ${serverId} after keepAlive: ${error instanceof Error ? error.message : String(error)}`);
403
698
  }
404
699
  }, keepAliveMs);
405
700
  }
@@ -411,24 +706,29 @@ export class ExternalMcpServerManager extends EventEmitter {
411
706
  */
412
707
  getServersByScope(scope, scopeId) {
413
708
  const servers = [];
414
- for (const [serverId, scopeData] of this.serverScopes.entries()) {
415
- if (scopeData.scope === scope) {
709
+ for (const [serverId, serverData] of this.servers.entries()) {
710
+ // Servers registered through the original global registration path
711
+ // predate serverScopes and are explicitly global, matching the tool
712
+ // snapshot contract in getAllExternalTools().
713
+ const scopeData = this.serverScopes.get(serverId);
714
+ const actualScope = scopeData?.scope ?? 'global';
715
+ if (actualScope === scope) {
416
716
  // For channel/agent scope, match scopeId
417
- if ((scope === 'channel' || scope === 'agent') && scopeData.scopeId !== scopeId) {
717
+ if ((scope === 'channel' || scope === 'agent') && scopeData?.scopeId !== scopeId) {
418
718
  continue;
419
719
  }
420
- const serverData = this.servers.get(serverId);
421
- if (serverData) {
422
- servers.push(serverData.status);
423
- }
720
+ servers.push(serverData.status);
424
721
  }
425
722
  }
426
- return servers;
723
+ return servers.sort((a, b) => a.id.localeCompare(b.id));
427
724
  }
428
725
  /**
429
- * Stop an external server process
726
+ * Stop an external server process.
727
+ *
728
+ * An intentional stop: the exit handler will see `expectedExit` and will
729
+ * neither log the exit as a crash nor restart the process.
430
730
  */
431
- async stopServer(serverId, agentId, channelId) {
731
+ async stopServer(serverId, agentId, channelId, reason) {
432
732
  const serverData = this.servers.get(serverId);
433
733
  if (!serverData) {
434
734
  throw new Error(`Server ${serverId} not found`);
@@ -437,6 +737,9 @@ export class ExternalMcpServerManager extends EventEmitter {
437
737
  if (status.status === 'stopped') {
438
738
  return;
439
739
  }
740
+ const droppedTools = status.tools.length;
741
+ logger.info(`Stopping server ${serverId} (${reason ?? 'no reason given'})` +
742
+ (droppedTools > 0 ? ` — removing its ${droppedTools} tool(s) from the registry` : ''));
440
743
  // Emit stop event
441
744
  this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STOP, serverId, agentId, channelId);
442
745
  // Clear timers
@@ -451,14 +754,19 @@ export class ExternalMcpServerManager extends EventEmitter {
451
754
  // Fail anything still in flight before we kill the process, so callers get
452
755
  // a clear error rather than waiting out their own timeouts.
453
756
  this.rejectPendingRequests(serverId, 'server is stopping');
757
+ this.settleReadyWaiters(serverId, new Error(`Server ${serverId} was stopped (${reason ?? 'no reason given'})`));
454
758
  // Terminate process
455
759
  if (serverData.process) {
456
- serverData.process.kill('SIGTERM');
457
- // Force kill after timeout
458
- setTimeout(() => {
459
- if (serverData.process && !serverData.process.killed) {
760
+ serverData.expectedExit = true;
761
+ const stoppingProcess = serverData.process;
762
+ stoppingProcess.kill('SIGTERM');
763
+ // Force kill after timeout; the exit handler clears this when the
764
+ // process goes down on its own.
765
+ serverData.forceKillTimer = setTimeout(() => {
766
+ serverData.forceKillTimer = undefined;
767
+ if (!stoppingProcess.killed && stoppingProcess.exitCode === null) {
460
768
  logger.warn(`Force killing server ${config.name}`);
461
- serverData.process.kill('SIGKILL');
769
+ stoppingProcess.kill('SIGKILL');
462
770
  }
463
771
  }, 5000);
464
772
  }
@@ -494,10 +802,15 @@ export class ExternalMcpServerManager extends EventEmitter {
494
802
  getAllExternalTools() {
495
803
  const tools = [];
496
804
  for (const [serverId, serverData] of this.servers) {
805
+ const scopeData = this.serverScopes.get(serverId);
806
+ const scope = scopeData?.scope ?? 'global';
807
+ const scopeId = scopeData?.scopeId;
497
808
  for (const tool of serverData.status.tools) {
498
809
  tools.push({
499
810
  ...tool,
500
- serverId
811
+ serverId,
812
+ scope,
813
+ scopeId
501
814
  });
502
815
  }
503
816
  }
@@ -513,29 +826,79 @@ export class ExternalMcpServerManager extends EventEmitter {
513
826
  const { config, status } = serverData;
514
827
  // Handle process exit
515
828
  process.on('exit', (code, signal) => {
829
+ // This process is down — its SIGKILL escalation timer is moot.
830
+ if (serverData.forceKillTimer) {
831
+ clearTimeout(serverData.forceKillTimer);
832
+ serverData.forceKillTimer = undefined;
833
+ }
834
+ const currentData = this.servers.get(serverId);
835
+ if (!currentData || currentData.process !== process) {
836
+ // Exit of a process instance that has already been replaced (restart)
837
+ // or whose server record is gone (unregistered). Not this record's
838
+ // state to change.
839
+ logger.debug(`Ignoring exit of a superseded process for ${serverId} (code ${code ?? 'null'}, signal ${signal ?? 'none'})`);
840
+ return;
841
+ }
842
+ const wasExpected = currentData.expectedExit === true;
843
+ currentData.expectedExit = false;
844
+ const droppedTools = status.tools.length;
516
845
  status.status = 'stopped';
517
846
  status.pid = undefined;
518
847
  // The handshake does not survive the process. A restarted server has to
519
848
  // perform it again before it can be marked running.
520
849
  status.initialized = false;
521
850
  status.initializing = false;
851
+ status.tools = [];
522
852
  // Anything still waiting on this process will never get a reply.
523
853
  this.rejectPendingRequests(serverId, `server exited (code ${code ?? 'null'}, signal ${signal ?? 'none'})`);
854
+ this.settleReadyWaiters(serverId, new Error(`Server ${serverId} exited during startup (code ${code ?? 'null'}, signal ${signal ?? 'none'})`));
524
855
  // Clear timers
525
856
  if (serverData.healthCheckTimer) {
526
857
  clearInterval(serverData.healthCheckTimer);
527
858
  serverData.healthCheckTimer = undefined;
528
859
  }
529
- // Handle restart if configured
530
- if (config.restartOnCrash && code !== 0 && status.restartCount < config.maxRestartAttempts) {
531
- status.restartCount++;
532
- setTimeout(() => {
533
- // Check if server still exists before attempting restart
534
- // (it may have been unregistered during the delay)
535
- if (this.servers.has(serverId)) {
536
- this.startServer(serverId);
537
- }
538
- }, 2000); // Wait 2 seconds before restart
860
+ if (serverData.startupTimer) {
861
+ clearTimeout(serverData.startupTimer);
862
+ serverData.startupTimer = undefined;
863
+ }
864
+ if (wasExpected) {
865
+ logger.info(`Server ${serverId} exited after stop (code ${code ?? 'null'}, signal ${signal ?? 'none'})`);
866
+ return;
867
+ }
868
+ // Unexpected death. This used to happen in complete silence — no log
869
+ // line at any level which is how production servers vanished from
870
+ // the tool registry with nothing to grep for.
871
+ logger.error(`Server ${serverId} exited unexpectedly (code ${code ?? 'null'}, signal ${signal ?? 'none'})` +
872
+ (droppedTools > 0 ? ` — its ${droppedTools} tool(s) are removed from the registry` : ''));
873
+ // Restart on ANY unexpected exit when configured — including a clean
874
+ // exit code. `code !== 0` used to gate this, leaving a child that
875
+ // exited 0 dead forever with no restart and no log.
876
+ if (config.restartOnCrash) {
877
+ if (status.restartCount < config.maxRestartAttempts) {
878
+ status.restartCount++;
879
+ logger.warn(`Restarting server ${serverId} in ${this.restartDelayMs}ms ` +
880
+ `(attempt ${status.restartCount}/${config.maxRestartAttempts})`);
881
+ setTimeout(() => {
882
+ // Check if server still exists before attempting restart
883
+ // (it may have been unregistered during the delay)
884
+ if (this.servers.has(serverId)) {
885
+ this.startServer(serverId).catch(error => {
886
+ logger.error(`Restart of server ${serverId} failed: ` +
887
+ `${error instanceof Error ? error.message : String(error)}`);
888
+ });
889
+ }
890
+ }, this.restartDelayMs);
891
+ }
892
+ else {
893
+ // Out of restart budget: remove the server entirely rather than
894
+ // leaving a zombie record + scope that agents can "join".
895
+ this.removeServerAfterFailure(serverId, `crashed and exhausted its ${config.maxRestartAttempts} restart attempt(s)`);
896
+ return;
897
+ }
898
+ }
899
+ else {
900
+ logger.error(`Server ${serverId} will not be restarted (restartOnCrash is off). ` +
901
+ `An agent joining its channel will start it again on demand.`);
539
902
  }
540
903
  this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STOPPED, serverId);
541
904
  });
@@ -642,7 +1005,7 @@ export class ExternalMcpServerManager extends EventEmitter {
642
1005
  }
643
1006
  const stdin = serverData.process.stdin;
644
1007
  const requestId = uuidv4();
645
- const timeoutMs = REQUEST_TIMEOUTS_MS[method];
1008
+ const timeoutMs = this.requestTimeouts[method];
646
1009
  return new Promise((resolve, reject) => {
647
1010
  const timer = setTimeout(() => {
648
1011
  serverData.pending.delete(requestId);
@@ -735,12 +1098,27 @@ export class ExternalMcpServerManager extends EventEmitter {
735
1098
  }
736
1099
  try {
737
1100
  await this.sendRequest(serverId, 'tools/list');
1101
+ serverData.consecutiveHealthFailures = 0;
738
1102
  this.emitServerHealthStatus(serverId, 'healthy');
739
1103
  }
740
1104
  catch (error) {
741
1105
  const message = error instanceof Error ? error.message : String(error);
742
- logger.warn(`Health check failed for ${serverId}: ${message}`);
1106
+ serverData.consecutiveHealthFailures = (serverData.consecutiveHealthFailures ?? 0) + 1;
1107
+ logger.warn(`Health check failed for ${serverId} ` +
1108
+ `(${serverData.consecutiveHealthFailures} consecutive): ${message}`);
743
1109
  this.emitServerHealthStatus(serverId, 'unhealthy');
1110
+ // A process that is alive but no longer answering MCP is as dead as a
1111
+ // crashed one — the exit handler will never fire for it. Recover here.
1112
+ if (serverData.consecutiveHealthFailures >= HEALTH_FAILURES_BEFORE_RESTART &&
1113
+ serverData.config.restartOnCrash &&
1114
+ status.status === 'running') {
1115
+ serverData.consecutiveHealthFailures = 0;
1116
+ logger.error(`Server ${serverId} failed ${HEALTH_FAILURES_BEFORE_RESTART} consecutive health checks — restarting it`);
1117
+ this.restartServer(serverId).catch(restartError => {
1118
+ logger.error(`Health-check restart of ${serverId} failed: ` +
1119
+ `${restartError instanceof Error ? restartError.message : String(restartError)}`);
1120
+ });
1121
+ }
744
1122
  }
745
1123
  }
746
1124
  /**
@@ -859,6 +1237,14 @@ export class ExternalMcpServerManager extends EventEmitter {
859
1237
  this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STARTED, serverId);
860
1238
  // Now that the connection is live, find out what the server offers.
861
1239
  await this.discoverServerTools(serverId);
1240
+ // A server that came up healthy earns back its full restart budget.
1241
+ // restartCount used to only ever grow, so a server that crashed a few
1242
+ // times over its lifetime — days apart, each recovered — permanently
1243
+ // exhausted its budget and the next crash left it down for good.
1244
+ serverData.status.restartCount = 0;
1245
+ serverData.consecutiveHealthFailures = 0;
1246
+ // Whoever awaited startServer() can proceed: the tools are discovered.
1247
+ this.settleReadyWaiters(serverId);
862
1248
  }
863
1249
  /**
864
1250
  * Ask a server what tools it has.
@@ -891,6 +1277,8 @@ export class ExternalMcpServerManager extends EventEmitter {
891
1277
  status.status = 'error';
892
1278
  status.lastError = error;
893
1279
  logger.error(`❌ Server ${serverId} error: ${error}`);
1280
+ // Anyone awaiting this server's startup gets the failure now.
1281
+ this.settleReadyWaiters(serverId, new Error(`Server ${serverId} failed: ${error}`));
894
1282
  // Emit error event
895
1283
  this.emitServerErrorEvent(serverId, error, agentId, channelId);
896
1284
  }
@@ -986,9 +1374,24 @@ export class ExternalMcpServerManager extends EventEmitter {
986
1374
  * Cleanup and shutdown all servers
987
1375
  */
988
1376
  async shutdown() {
1377
+ // Stop accepting lifecycle requests before child-process teardown. Node's
1378
+ // removeAllListeners() below only affects this class's EventEmitter; it
1379
+ // does not detach RxJS EventBus subscriptions.
1380
+ for (const subscription of this.eventSubscriptions) {
1381
+ subscription.unsubscribe();
1382
+ }
1383
+ this.eventSubscriptions = [];
989
1384
  const shutdownPromises = Array.from(this.servers.keys()).map(serverId => this.stopServer(serverId));
990
1385
  await Promise.allSettled(shutdownPromises);
1386
+ // Clear pending keepAlive timers so nothing fires against cleared maps
1387
+ for (const scopeData of this.serverScopes.values()) {
1388
+ if (scopeData.keepAliveTimer) {
1389
+ clearTimeout(scopeData.keepAliveTimer);
1390
+ }
1391
+ }
991
1392
  this.servers.clear();
1393
+ this.serverScopes.clear();
1394
+ this.retiredChannelIds.clear();
992
1395
  this.removeAllListeners();
993
1396
  }
994
1397
  /**
@@ -1044,22 +1447,20 @@ export class ExternalMcpServerManager extends EventEmitter {
1044
1447
  }
1045
1448
  }
1046
1449
  /**
1047
- * Restart a server by ID
1450
+ * Restart a server by ID. Resolves after the restarted server has finished
1451
+ * its handshake and tool discovery (see startServer). Throws when the
1452
+ * restart fails, so callers can react instead of proceeding against a dead
1453
+ * server.
1048
1454
  */
1049
1455
  async restartServer(serverId) {
1050
- try {
1051
- // Stop the server first
1052
- await this.stopServer(serverId);
1053
- // Wait a bit before restarting
1054
- await new Promise(resolve => setTimeout(resolve, 1000));
1055
- // Start the server again
1056
- await this.startServer(serverId);
1057
- return true;
1058
- }
1059
- catch (error) {
1060
- logger.error(`Failed to restart server ${serverId}: ${error instanceof Error ? error.message : String(error)}`);
1061
- return false;
1062
- }
1456
+ logger.info(`Restarting server ${serverId}`);
1457
+ // Stop the server first
1458
+ await this.stopServer(serverId, undefined, undefined, 'restart');
1459
+ // Give the old process a moment to release stdio
1460
+ await new Promise(resolve => setTimeout(resolve, this.restartDelayMs));
1461
+ // Start the server again — resolves after handshake + tool discovery
1462
+ await this.startServer(serverId);
1463
+ return true;
1063
1464
  }
1064
1465
  /**
1065
1466
  * Execute a tool on an external MCP server via JSON-RPC with auto-correction support