@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
@@ -43,11 +43,30 @@ import { AutoCorrectionService } from '../../../services/AutoCorrectionService.j
43
43
  import { IToolEventEmitter } from './IToolEventEmitter.js';
44
44
  import { Events } from '../../../events/EventNames.js';
45
45
  import { v4 as uuidv4 } from 'uuid';
46
+ import { assertUnsafeStdioMcpEnabled } from '../security/ExternalMcpRegistrationPolicy.js';
46
47
 
47
48
  // Create logger and validator instances
48
49
  const logger = new Logger('info', 'ExternalMcpServerManager', 'server');
49
50
  const validator = createStrictValidator('ExternalMcpServerManager');
50
51
 
52
+ interface ExternalMcpRegistrationRequestData extends Partial<ExternalServerConfig> {
53
+ id: string;
54
+ name: string;
55
+ channelId?: string;
56
+ keepAliveMinutes?: number;
57
+ }
58
+
59
+ interface ExternalMcpUnregistrationRequestData {
60
+ serverId: string;
61
+ channelId?: string;
62
+ }
63
+
64
+ interface ExternalMcpLifecycleRequestPayload<TData> {
65
+ agentId: AgentId;
66
+ channelId: ChannelId;
67
+ data: TData;
68
+ }
69
+
51
70
  /**
52
71
  * Configuration for an external MCP server
53
72
  */
@@ -64,6 +83,10 @@ export interface ExternalServerConfig {
64
83
  command: string;
65
84
  /** Arguments for the command */
66
85
  args: string[];
86
+ /** Transport requested by a remote registration. Omitted means stdio. */
87
+ transport?: 'stdio' | 'http';
88
+ /** Endpoint for an HTTP transport registration. */
89
+ url?: string;
67
90
  /** Working directory for the process */
68
91
  workingDirectory?: string;
69
92
  /** Environment variables for the process */
@@ -109,6 +132,10 @@ export interface ExternalMcpTool {
109
132
  description: string;
110
133
  inputSchema: Record<string, any>;
111
134
  serverId: string;
135
+ /** Authoritative visibility scope assigned when the server was registered. */
136
+ scope: 'global' | 'channel' | 'agent';
137
+ /** Channel or agent identifier for non-global scopes. */
138
+ scopeId?: string;
112
139
  }
113
140
 
114
141
  /**
@@ -129,6 +156,23 @@ const REQUEST_TIMEOUTS_MS = {
129
156
  ping: 5000
130
157
  } as const;
131
158
 
159
+ /** Delay before restarting a crashed server. */
160
+ const DEFAULT_RESTART_DELAY_MS = 2000;
161
+
162
+ /**
163
+ * Consecutive failed health probes after which a running server is declared
164
+ * dead and restarted. One failure can be a slow reply; two in a row on a
165
+ * connection that answers in milliseconds when healthy means the connection
166
+ * is gone.
167
+ */
168
+ const HEALTH_FAILURES_BEFORE_RESTART = 2;
169
+
170
+ /** A caller waiting for a starting server to finish its handshake and discovery. */
171
+ interface ReadyWaiter {
172
+ resolve: () => void;
173
+ reject: (error: Error) => void;
174
+ }
175
+
132
176
  /** MCP protocol version this client negotiates in the initialize handshake. */
133
177
  const MCP_PROTOCOL_VERSION = '2024-11-05';
134
178
 
@@ -158,6 +202,18 @@ export class ExternalMcpServerManager extends EventEmitter {
158
202
  pending: Map<string, PendingRequest>;
159
203
  /** Partial line left over from the last stdout chunk. */
160
204
  stdoutBuffer: string;
205
+ /**
206
+ * Set by stopServer() before it kills the process, so the exit handler
207
+ * can tell an intentional stop from a crash. A crash restarts (when
208
+ * configured); an intentional stop never does.
209
+ */
210
+ expectedExit?: boolean;
211
+ /** SIGKILL escalation timer set by stopServer; cleared when the process exits. */
212
+ forceKillTimer?: NodeJS.Timeout;
213
+ /** Callers awaiting handshake + tool discovery of a starting server. */
214
+ readyWaiters?: ReadyWaiter[];
215
+ /** Consecutive failed health probes; reset on any successful probe. */
216
+ consecutiveHealthFailures?: number;
161
217
  }> = new Map();
162
218
 
163
219
  // Scope tracking for channel/agent-scoped servers
@@ -176,6 +232,13 @@ export class ExternalMcpServerManager extends EventEmitter {
176
232
  };
177
233
  }> = new Map();
178
234
 
235
+ /**
236
+ * Channel ids which crossed their terminal deletion boundary in this
237
+ * process. Channel ids are reserved in persistence, so this tombstone never
238
+ * needs to be removed and closes register/delete races in both directions.
239
+ */
240
+ private retiredChannelIds = new Set<string>();
241
+
179
242
  private autoCorrectionService: AutoCorrectionService;
180
243
 
181
244
  /** Injectable event emitter — decouples from direct EventBus.server dependency.
@@ -183,17 +246,39 @@ export class ExternalMcpServerManager extends EventEmitter {
183
246
  * Client-side: uses ClientToolEventEmitter (wraps EventBus.client + socketEmit). */
184
247
  private toolEventEmitter: IToolEventEmitter | null = null;
185
248
 
249
+ /** EventBus subscriptions owned by this manager instance. */
250
+ private eventSubscriptions: Array<{ unsubscribe: () => void }> = [];
251
+
252
+ /** Delay before restarting a crashed server. Overridable for tests. */
253
+ private restartDelayMs: number = DEFAULT_RESTART_DELAY_MS;
254
+
255
+ /** Per-method JSON-RPC reply timeouts. Overridable for tests and tuning. */
256
+ private requestTimeouts: Record<keyof typeof REQUEST_TIMEOUTS_MS, number> = { ...REQUEST_TIMEOUTS_MS };
257
+
186
258
  /**
187
259
  * @param options.toolEventEmitter - Injectable event emitter for decoupling from EventBus.server
188
260
  * @param options.skipServerEventHandlers - Skip setting up EventBus.server listeners (for client-side usage)
261
+ * @param options.restartDelayMs - Delay before restarting a crashed server (default 2000)
262
+ * @param options.requestTimeoutsMs - Per-method JSON-RPC reply timeout overrides
189
263
  */
190
- constructor(options?: { toolEventEmitter?: IToolEventEmitter; skipServerEventHandlers?: boolean }) {
264
+ constructor(options?: {
265
+ toolEventEmitter?: IToolEventEmitter;
266
+ skipServerEventHandlers?: boolean;
267
+ restartDelayMs?: number;
268
+ requestTimeoutsMs?: Partial<Record<keyof typeof REQUEST_TIMEOUTS_MS, number>>;
269
+ }) {
191
270
  super();
192
271
  this.autoCorrectionService = AutoCorrectionService.getInstance();
193
272
 
194
273
  if (options?.toolEventEmitter) {
195
274
  this.toolEventEmitter = options.toolEventEmitter;
196
275
  }
276
+ if (options?.restartDelayMs !== undefined) {
277
+ this.restartDelayMs = options.restartDelayMs;
278
+ }
279
+ if (options?.requestTimeoutsMs) {
280
+ this.requestTimeouts = { ...this.requestTimeouts, ...options.requestTimeoutsMs };
281
+ }
197
282
 
198
283
  // Set up event listeners for SDK-initiated server registration
199
284
  // Skipped when running client-side (no EventBus.server listeners needed)
@@ -208,9 +293,16 @@ export class ExternalMcpServerManager extends EventEmitter {
208
293
  private setupEventHandlers(): void {
209
294
 
210
295
  // Handle external server registration requests from SDK
211
- EventBus.server.on(Events.Mcp.EXTERNAL_SERVER_REGISTER, async (payload: any) => {
296
+ this.eventSubscriptions.push(EventBus.server.on(Events.Mcp.EXTERNAL_SERVER_REGISTER, async (
297
+ payload: ExternalMcpLifecycleRequestPayload<ExternalMcpRegistrationRequestData>
298
+ ) => {
212
299
  try {
213
300
 
301
+ // EventBus registrations originate outside this manager (normally
302
+ // from a socket). They are caller-controlled and stdio means host
303
+ // process execution, so require the operator's explicit opt-in.
304
+ assertUnsafeStdioMcpEnabled(payload.data?.transport);
305
+
214
306
  const serverConfig = {
215
307
  id: payload.data.id,
216
308
  name: payload.data.name,
@@ -262,15 +354,16 @@ export class ExternalMcpServerManager extends EventEmitter {
262
354
  }
263
355
  });
264
356
  }
265
- });
357
+ }));
266
358
 
267
359
  // Handle external server unregistration requests from SDK
268
- EventBus.server.on(Events.Mcp.EXTERNAL_SERVER_UNREGISTER, async (payload: any) => {
360
+ this.eventSubscriptions.push(EventBus.server.on(Events.Mcp.EXTERNAL_SERVER_UNREGISTER, async (
361
+ payload: ExternalMcpLifecycleRequestPayload<ExternalMcpUnregistrationRequestData>
362
+ ) => {
269
363
  try {
270
364
 
271
365
  const serverId = payload.data.serverId;
272
- await this.stopServer(serverId);
273
- this.servers.delete(serverId);
366
+ await this.unregisterServer(serverId);
274
367
 
275
368
  // Emit success response
276
369
  EventBus.server.emit(Events.Mcp.EXTERNAL_SERVER_UNREGISTERED, {
@@ -288,7 +381,9 @@ export class ExternalMcpServerManager extends EventEmitter {
288
381
 
289
382
 
290
383
  } catch (error) {
291
- logger.error(`Error unregistering external server:`, error);
384
+ // Interpolate the message: passing the raw error as a second console
385
+ // argument makes Bun print a source-code excerpt instead of the message.
386
+ logger.error(`Error unregistering external server: ${error instanceof Error ? error.message : String(error)}`);
292
387
 
293
388
  // Emit error response
294
389
  EventBus.server.emit(Events.Mcp.EXTERNAL_SERVER_UNREGISTERED, {
@@ -304,58 +399,50 @@ export class ExternalMcpServerManager extends EventEmitter {
304
399
  }
305
400
  });
306
401
  }
307
- });
402
+ }));
308
403
 
309
404
  // Handle channel-scoped server registration requests
310
- EventBus.server.on(Events.Mcp.CHANNEL_SERVER_REGISTER, async (payload: any) => {
405
+ this.eventSubscriptions.push(EventBus.server.on(Events.Mcp.CHANNEL_SERVER_REGISTER, async (
406
+ payload: ExternalMcpLifecycleRequestPayload<ExternalMcpRegistrationRequestData>
407
+ ) => {
311
408
  try {
409
+ // Channel membership is not permission to execute a host command.
410
+ // The HTTP/socket entry point must also establish administrator
411
+ // authority; this is the defense-in-depth feature gate.
412
+ assertUnsafeStdioMcpEnabled(payload.data?.transport);
312
413
  logger.info(`[CHANNEL_SERVER_REGISTER] Received registration request: ${JSON.stringify({ agentId: payload.agentId, channelId: payload.channelId, serverId: payload.data?.id })}`);
313
414
 
314
415
 
315
416
  const channelId = payload.data.channelId || payload.channelId;
316
- const serverId = `${channelId}:${payload.data.id}`;
317
417
 
318
- const serverConfig = {
319
- id: serverId,
320
- name: payload.data.name,
321
- version: payload.data.version || '1.0.0',
322
- command: payload.data.command || '',
323
- args: payload.data.args || [],
324
- transport: (payload.data.transport || 'stdio') as 'stdio' | 'http',
325
- url: payload.data.url,
326
- autoStart: payload.data.autoStart !== false,
327
- restartOnCrash: payload.data.restartOnCrash !== false,
328
- maxRestartAttempts: payload.data.maxRestartAttempts || 3,
329
- healthCheckInterval: payload.data.healthCheckInterval || 30000,
330
- startupTimeout: payload.data.startupTimeout || 10000,
331
- environmentVariables: payload.data.environmentVariables || {}
332
- };
333
-
334
- // Track scope BEFORE registration so we can store the registration context
335
- // Note: connectedAgents starts empty - only actual game agents are counted, not the admin who registers
336
- this.serverScopes.set(serverId, {
337
- scope: 'channel',
338
- scopeId: channelId,
339
- connectedAgents: new Set(),
340
- keepAliveMinutes: payload.data.keepAliveMinutes || 5,
341
- // Store registration context for deferred success emission
342
- registrationContext: {
418
+ await this.registerChannelServer(
419
+ channelId,
420
+ {
421
+ id: payload.data.id,
422
+ name: payload.data.name,
423
+ version: payload.data.version || '1.0.0',
424
+ command: payload.data.command || '',
425
+ args: payload.data.args || [],
426
+ autoStart: payload.data.autoStart !== false,
427
+ restartOnCrash: payload.data.restartOnCrash !== false,
428
+ maxRestartAttempts: payload.data.maxRestartAttempts || 3,
429
+ healthCheckInterval: payload.data.healthCheckInterval || 30000,
430
+ startupTimeout: payload.data.startupTimeout || 10000,
431
+ environmentVariables: payload.data.environmentVariables || {},
432
+ keepAliveMinutes: payload.data.keepAliveMinutes
433
+ },
434
+ // Registration context for deferred success emission after tool discovery
435
+ {
343
436
  agentId: payload.agentId,
344
437
  channelId,
345
438
  originalServerId: payload.data.id,
346
439
  serverName: payload.data.name
347
440
  }
348
- });
349
-
350
- // Register the server (this starts the process and tool discovery)
351
- // Success event will be emitted after tool discovery completes
352
- await this.registerServer(serverConfig);
353
-
354
- logger.info(`[CHANNEL_SERVER_REGISTER] Server ${serverId} registered, waiting for tool discovery before emitting success`);
441
+ );
355
442
 
356
443
  } catch (error) {
357
444
 
358
- logger.error(`Error registering channel server:`, error);
445
+ logger.error(`Error registering channel server: ${error instanceof Error ? error.message : String(error)}`);
359
446
 
360
447
  // Emit error response
361
448
  EventBus.server.emit(McpEvents.CHANNEL_SERVER_REGISTRATION_FAILED,
@@ -373,18 +460,17 @@ export class ExternalMcpServerManager extends EventEmitter {
373
460
  )
374
461
  );
375
462
  }
376
- });
463
+ }));
377
464
 
378
465
  // Handle channel server unregistration requests
379
- EventBus.server.on(Events.Mcp.CHANNEL_SERVER_UNREGISTER, async (payload: any) => {
466
+ this.eventSubscriptions.push(EventBus.server.on(Events.Mcp.CHANNEL_SERVER_UNREGISTER, async (
467
+ payload: ExternalMcpLifecycleRequestPayload<ExternalMcpUnregistrationRequestData>
468
+ ) => {
380
469
  try {
381
470
 
382
471
  const channelId = payload.data.channelId || payload.channelId;
383
- const serverId = `${channelId}:${payload.data.serverId}`;
384
472
 
385
- await this.stopServer(serverId);
386
- this.servers.delete(serverId);
387
- this.serverScopes.delete(serverId);
473
+ await this.unregisterChannelServer(channelId, payload.data.serverId);
388
474
 
389
475
  // Emit success response
390
476
  EventBus.server.emit(McpEvents.CHANNEL_SERVER_UNREGISTERED,
@@ -404,7 +490,7 @@ export class ExternalMcpServerManager extends EventEmitter {
404
490
 
405
491
  } catch (error) {
406
492
 
407
- logger.error(`Error unregistering channel server:`, error);
493
+ logger.error(`Error unregistering channel server: ${error instanceof Error ? error.message : String(error)}`);
408
494
 
409
495
  // Emit error response
410
496
  EventBus.server.emit(McpEvents.CHANNEL_SERVER_UNREGISTERED,
@@ -422,7 +508,7 @@ export class ExternalMcpServerManager extends EventEmitter {
422
508
  )
423
509
  );
424
510
  }
425
- });
511
+ }));
426
512
 
427
513
  }
428
514
 
@@ -430,6 +516,13 @@ export class ExternalMcpServerManager extends EventEmitter {
430
516
  * Register a new external server configuration
431
517
  */
432
518
  public async registerServer(config: ExternalServerConfig): Promise<void> {
519
+ // This manager currently speaks line-delimited MCP over child-process
520
+ // stdio only. Never let an HTTP-labelled registration fall through to
521
+ // spawn(config.command, ...), which would bypass the stdio feature gate.
522
+ if (config.transport === 'http') {
523
+ throw new Error('HTTP transport registration is not implemented by ExternalMcpServerManager');
524
+ }
525
+
433
526
  logger.info(`[REGISTER_SERVER] Registering server ${config.id}: command="${config.command}", args=${JSON.stringify(config.args)}, autoStart=${config.autoStart}`);
434
527
 
435
528
  // Validate configuration
@@ -470,11 +563,251 @@ export class ExternalMcpServerManager extends EventEmitter {
470
563
  }
471
564
 
472
565
  /**
473
- * Start an external server process
566
+ * Register a channel-scoped server: track its scope, then register and
567
+ * (per config) start it. Resolves after the MCP handshake and tool
568
+ * discovery when autoStart is set, so a resolved promise means the tools
569
+ * are in the registry.
570
+ *
571
+ * A re-registration over an existing scope preserves the connected-agent
572
+ * set and clears any pending keepAlive timer — previously the timer
573
+ * reference was silently overwritten while the timer kept running, so a
574
+ * stale keepAlive could stop a freshly re-registered server later.
575
+ */
576
+ public async registerChannelServer(
577
+ channelId: string,
578
+ config: Omit<ExternalServerConfig, 'id'> & { id: string; keepAliveMinutes?: number },
579
+ registrationContext?: {
580
+ agentId: string;
581
+ channelId: string;
582
+ originalServerId: string;
583
+ serverName: string;
584
+ }
585
+ ): Promise<void> {
586
+ validator.assertIsNonEmptyString(channelId, 'channelId must be a non-empty string');
587
+ const serverId = `${channelId}:${config.id}`;
588
+ const keepAliveMinutes = config.keepAliveMinutes || 5;
589
+
590
+ if (this.retiredChannelIds.has(channelId)) {
591
+ throw new Error(`Channel ${channelId} is deleted; MCP servers cannot be registered`);
592
+ }
593
+
594
+ const existingScope = this.serverScopes.get(serverId);
595
+ if (existingScope?.keepAliveTimer) {
596
+ clearTimeout(existingScope.keepAliveTimer);
597
+ }
598
+
599
+ this.serverScopes.set(serverId, {
600
+ scope: 'channel',
601
+ scopeId: channelId,
602
+ // Only actual channel agents are counted, not the admin who registers.
603
+ // On re-registration, keep whoever is already connected.
604
+ connectedAgents: existingScope?.connectedAgents ?? new Set(),
605
+ keepAliveMinutes,
606
+ registrationContext
607
+ });
608
+
609
+ logger.info(
610
+ `[CHANNEL_SERVER_REGISTER] Registering channel server ${serverId} ` +
611
+ `(keepAlive ${keepAliveMinutes}min, autoStart ${config.autoStart}, restartOnCrash ${config.restartOnCrash})`
612
+ );
613
+
614
+ const { keepAliveMinutes: _ignored, ...serverConfig } = config;
615
+ try {
616
+ await this.registerServer({ ...serverConfig, id: serverId });
617
+
618
+ // Deletion may have won while process startup/handshake was in
619
+ // flight. Remove the just-created runtime before returning so a
620
+ // late registration cannot resurrect a deleted channel's server.
621
+ if (this.retiredChannelIds.has(channelId)) {
622
+ await this.removeServer(serverId, 'registration raced channel deletion');
623
+ throw new Error(`Channel ${channelId} was deleted during MCP server registration`);
624
+ }
625
+ } catch (error) {
626
+ // Registration failed before the server record existed — do not leave
627
+ // a scope entry behind for agents to "join" (unless one existed before).
628
+ if (!existingScope && !this.servers.has(serverId)) {
629
+ this.serverScopes.delete(serverId);
630
+ }
631
+ throw error;
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Unregister a channel-scoped server: stop the process and remove both the
637
+ * server record and the scope tracking, including any keepAlive timer.
638
+ *
639
+ * Idempotent: unregistering a server that is partially or fully gone cleans
640
+ * up whatever remains and resolves. Production showed the half-removed
641
+ * state — record deleted, scope alive — and an unregister that throws
642
+ * "not found" against it leaves the zombie in place forever.
643
+ */
644
+ public async unregisterChannelServer(channelId: string, serverId: string): Promise<void> {
645
+ validator.assertIsNonEmptyString(channelId, 'channelId must be a non-empty string');
646
+ validator.assertIsNonEmptyString(serverId, 'serverId must be a non-empty string');
647
+ await this.removeServer(`${channelId}:${serverId}`, 'channel server unregistration');
648
+ }
649
+
650
+ /**
651
+ * Permanently retire a channel's MCP runtime in this process.
652
+ *
653
+ * The tombstone is installed before process cleanup, which makes concurrent
654
+ * registrations fail both before and after their asynchronous startup.
655
+ * Every known scoped record is removed, including orphaned records whose
656
+ * scope metadata or server entry is only partially present.
657
+ */
658
+ public async retireChannel(channelId: string): Promise<void> {
659
+ validator.assertIsNonEmptyString(channelId, 'channelId must be a non-empty string');
660
+ this.retiredChannelIds.add(channelId);
661
+
662
+ const serverIds = new Set<string>();
663
+ for (const [serverId, scopeData] of this.serverScopes.entries()) {
664
+ if (scopeData.scope === 'channel' && scopeData.scopeId === channelId) {
665
+ serverIds.add(serverId);
666
+ }
667
+ }
668
+ for (const serverId of this.servers.keys()) {
669
+ if (serverId.startsWith(`${channelId}:`)) {
670
+ serverIds.add(serverId);
671
+ }
672
+ }
673
+
674
+ const results = await Promise.allSettled(
675
+ Array.from(serverIds, serverId => (
676
+ this.removeServer(serverId, 'channel deletion')
677
+ ))
678
+ );
679
+ const failures = results.filter(
680
+ (result): result is PromiseRejectedResult => result.status === 'rejected'
681
+ );
682
+ if (failures.length > 0) {
683
+ throw new Error(
684
+ `Failed to retire ${failures.length} MCP server(s) for channel ${channelId}`
685
+ );
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Unregister a server by its full id (global servers, and the SDK-facing
691
+ * EXTERNAL_SERVER_UNREGISTER path). Also removes scope tracking — this
692
+ * path used to delete only the server record, which was exactly the
693
+ * zombie state observed in production: agents kept "joining" a scope
694
+ * whose server no longer existed.
695
+ */
696
+ public async unregisterServer(serverId: string): Promise<void> {
697
+ validator.assertIsNonEmptyString(serverId, 'serverId must be a non-empty string');
698
+ await this.removeServer(serverId, 'server unregistration');
699
+ }
700
+
701
+ /**
702
+ * Stop a server (if running) and remove every trace of it: server record,
703
+ * scope entry, keepAlive timer. Never throws for a missing record — it
704
+ * removes what exists and says what it did.
705
+ */
706
+ private async removeServer(serverId: string, reason: string): Promise<void> {
707
+ const serverData = this.servers.get(serverId);
708
+ const scopeData = this.serverScopes.get(serverId);
709
+
710
+ if (!serverData && !scopeData) {
711
+ logger.warn(`Nothing to unregister for ${serverId} (${reason}) — no record, no scope`);
712
+ return;
713
+ }
714
+
715
+ if (scopeData?.keepAliveTimer) {
716
+ clearTimeout(scopeData.keepAliveTimer);
717
+ scopeData.keepAliveTimer = undefined;
718
+ }
719
+
720
+ if (serverData) {
721
+ try {
722
+ await this.stopServer(serverId, undefined, undefined, reason);
723
+ } catch (error) {
724
+ logger.error(
725
+ `Error stopping ${serverId} during ${reason}: ` +
726
+ `${error instanceof Error ? error.message : String(error)} — removing its record anyway`
727
+ );
728
+ }
729
+ } else {
730
+ logger.warn(
731
+ `Unregistering ${serverId} (${reason}): server record was already gone, ` +
732
+ `removing the orphaned scope entry`
733
+ );
734
+ }
735
+
736
+ this.servers.delete(serverId);
737
+ this.serverScopes.delete(serverId);
738
+ logger.info(`Unregistered server ${serverId} (${reason})`);
739
+ }
740
+
741
+ /**
742
+ * Remove a server that cannot be kept alive (restart budget exhausted,
743
+ * unrecoverable spawn failure). Loud by design: this is the path that
744
+ * prevents zombies, so it reports at error level.
745
+ */
746
+ private removeServerAfterFailure(serverId: string, reason: string): void {
747
+ const serverData = this.servers.get(serverId);
748
+ if (serverData) {
749
+ if (serverData.healthCheckTimer) {
750
+ clearInterval(serverData.healthCheckTimer);
751
+ serverData.healthCheckTimer = undefined;
752
+ }
753
+ if (serverData.startupTimer) {
754
+ clearTimeout(serverData.startupTimer);
755
+ serverData.startupTimer = undefined;
756
+ }
757
+ this.rejectPendingRequests(serverId, reason);
758
+ this.settleReadyWaiters(serverId, new Error(reason));
759
+ if (serverData.process && !serverData.process.killed) {
760
+ serverData.process.kill('SIGKILL');
761
+ }
762
+ }
763
+
764
+ const scopeData = this.serverScopes.get(serverId);
765
+ if (scopeData?.keepAliveTimer) {
766
+ clearTimeout(scopeData.keepAliveTimer);
767
+ }
768
+
769
+ this.servers.delete(serverId);
770
+ this.serverScopes.delete(serverId);
771
+
772
+ logger.error(
773
+ `Server ${serverId} unregistered: ${reason}. ` +
774
+ `Its tools are removed from the registry; re-register the server to restore them.`
775
+ );
776
+
777
+ this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STOPPED, serverId);
778
+ }
779
+
780
+ /**
781
+ * Settle every caller waiting for a server to become ready.
782
+ */
783
+ private settleReadyWaiters(serverId: string, error?: Error): void {
784
+ const serverData = this.servers.get(serverId);
785
+ if (!serverData?.readyWaiters?.length) {
786
+ return;
787
+ }
788
+ const waiters = serverData.readyWaiters;
789
+ serverData.readyWaiters = [];
790
+ for (const waiter of waiters) {
791
+ if (error) {
792
+ waiter.reject(error);
793
+ } else {
794
+ waiter.resolve();
795
+ }
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Start an external server process.
801
+ *
802
+ * Resolves once the MCP handshake AND tool discovery have completed — a
803
+ * resolved startServer() means the server's tools are in the registry.
804
+ * It used to resolve right after spawn, which let callers (agent join,
805
+ * restart) proceed against a server that had not finished — or would
806
+ * never finish — its handshake.
474
807
  */
475
808
  public async startServer(serverId: string, agentId?: AgentId, channelId?: ChannelId): Promise<void> {
476
809
  logger.info(`[START_SERVER] Starting server ${serverId}`);
477
-
810
+
478
811
  const serverData = this.servers.get(serverId);
479
812
  if (!serverData) {
480
813
  // Server was unregistered (e.g., during cleanup) - log warning and return gracefully
@@ -489,9 +822,20 @@ export class ExternalMcpServerManager extends EventEmitter {
489
822
  return;
490
823
  }
491
824
 
825
+ if (status.status === 'starting') {
826
+ // Another caller is already starting this server — wait for that
827
+ // startup instead of spawning a second process.
828
+ logger.info(`[START_SERVER] Server ${serverId} already starting, awaiting readiness`);
829
+ await new Promise<void>((resolve, reject) => {
830
+ serverData.readyWaiters = serverData.readyWaiters ?? [];
831
+ serverData.readyWaiters.push({ resolve, reject });
832
+ });
833
+ return;
834
+ }
492
835
 
493
836
  // Update status
494
837
  status.status = 'starting';
838
+ serverData.expectedExit = false;
495
839
  this.emitServerEvent(McpEvents.EXTERNAL_SERVER_SPAWN, serverId, agentId, channelId);
496
840
 
497
841
  try {
@@ -543,41 +887,99 @@ export class ExternalMcpServerManager extends EventEmitter {
543
887
  // Start health check monitoring
544
888
  this.startHealthChecking(serverId);
545
889
 
546
-
547
890
  } catch (error) {
548
891
  const errorMessage = error instanceof Error ? error.message : String(error);
549
892
  logger.error(`❌ Failed to start server ${config.name}: ${errorMessage}`);
550
893
  this.handleServerError(serverId, errorMessage, agentId, channelId);
894
+ throw error instanceof Error ? error : new Error(errorMessage);
551
895
  }
896
+
897
+ // Wait for the handshake and tool discovery. Settled by
898
+ // initializeMcpConnection() on success, and by handleServerError()
899
+ // or the exit handler on failure — those already report the cause,
900
+ // so a rejection here propagates without being re-handled.
901
+ await new Promise<void>((resolve, reject) => {
902
+ serverData.readyWaiters = serverData.readyWaiters ?? [];
903
+ serverData.readyWaiters.push({ resolve, reject });
904
+ });
552
905
  }
553
906
 
554
907
  /**
555
- * Handle agent joining a channel - start channel servers and track connection
908
+ * Handle agent joining a channel verify each channel server is actually
909
+ * alive before counting the agent as connected.
910
+ *
911
+ * This used to be a blind reference-count bump: a missing server record was
912
+ * silently skipped and a dead process was never probed, so agents "joined"
913
+ * servers that could not serve a single tool call, and the only downstream
914
+ * signal was a NOT FOUND warning when their allowlist resolved.
556
915
  */
557
916
  public async onAgentJoinChannel(agentId: string, channelId: string): Promise<void> {
558
917
  logger.info(`Agent ${agentId} joining channel ${channelId} - checking for channel servers`);
559
918
 
560
919
  // Find all channel-scoped servers for this channel
561
920
  for (const [serverId, scopeData] of this.serverScopes.entries()) {
562
- if (scopeData.scope === 'channel' && scopeData.scopeId === channelId) {
563
- // Add agent to connected agents
564
- scopeData.connectedAgents.add(agentId);
921
+ if (scopeData.scope !== 'channel' || scopeData.scopeId !== channelId) {
922
+ continue;
923
+ }
565
924
 
566
- // Clear any pending keepAlive timer
925
+ const serverData = this.servers.get(serverId);
926
+ if (!serverData) {
927
+ // The production zombie state: a scope entry whose server record is
928
+ // gone. There is no config left to restart from, so remove the
929
+ // orphan loudly instead of pretending the agent connected.
930
+ logger.error(
931
+ `Agent ${agentId} tried to join channel server ${serverId}, but its server record is gone ` +
932
+ `(scope entry was orphaned). Removing the orphaned scope — the server must be re-registered.`
933
+ );
567
934
  if (scopeData.keepAliveTimer) {
568
935
  clearTimeout(scopeData.keepAliveTimer);
569
- scopeData.keepAliveTimer = undefined;
570
936
  }
937
+ this.serverScopes.delete(serverId);
938
+ continue;
939
+ }
571
940
 
572
- // Start server if not already running
573
- const serverData = this.servers.get(serverId);
574
- if (serverData && serverData.status.status !== 'running') {
575
- logger.info(`Starting channel server ${serverId} for agent ${agentId}`);
941
+ try {
942
+ if (serverData.status.status !== 'running') {
943
+ logger.info(`Starting channel server ${serverId} for agent ${agentId} (status: ${serverData.status.status})`);
576
944
  await this.startServer(serverId);
945
+ } else {
946
+ // Status says running — prove it. A live entry with a dead child
947
+ // (or a wedged MCP connection) must trigger recovery at join
948
+ // time, not a silent ref-count bump.
949
+ const alive = serverData.process && !serverData.process.killed && serverData.process.exitCode === null;
950
+ if (!alive) {
951
+ logger.warn(`Channel server ${serverId} has no live process at agent join — restarting`);
952
+ await this.restartServer(serverId);
953
+ } else {
954
+ try {
955
+ await this.sendRequest(serverId, 'tools/list');
956
+ } catch (probeError) {
957
+ logger.warn(
958
+ `Channel server ${serverId} did not answer the liveness probe at agent join ` +
959
+ `(${probeError instanceof Error ? probeError.message : String(probeError)}) — restarting`
960
+ );
961
+ await this.restartServer(serverId);
962
+ }
963
+ }
577
964
  }
965
+ } catch (error) {
966
+ logger.error(
967
+ `Channel server ${serverId} could not be made available for agent ${agentId}: ` +
968
+ `${error instanceof Error ? error.message : String(error)}`
969
+ );
970
+ continue;
971
+ }
972
+
973
+ // Only a server that is verifiably up counts the agent as connected
974
+ scopeData.connectedAgents.add(agentId);
578
975
 
579
- logger.info(`Agent ${agentId} connected to channel server ${serverId} (${scopeData.connectedAgents.size} agents)`);
976
+ // Clear any pending keepAlive timer
977
+ if (scopeData.keepAliveTimer) {
978
+ clearTimeout(scopeData.keepAliveTimer);
979
+ scopeData.keepAliveTimer = undefined;
580
980
  }
981
+
982
+ logger.info(`Agent ${agentId} connected to channel server ${serverId} (${scopeData.connectedAgents.size} agents)`);
581
983
  }
582
984
  }
583
985
 
@@ -602,11 +1004,17 @@ export class ExternalMcpServerManager extends EventEmitter {
602
1004
  logger.info(`Last agent left channel server ${serverId}, starting ${scopeData.keepAliveMinutes}min keepAlive timer`);
603
1005
 
604
1006
  scopeData.keepAliveTimer = setTimeout(async () => {
1007
+ scopeData.keepAliveTimer = undefined;
1008
+ if (!this.servers.has(serverId)) {
1009
+ logger.warn(`KeepAlive expired for ${serverId}, but its server record is already gone — removing the orphaned scope`);
1010
+ this.serverScopes.delete(serverId);
1011
+ return;
1012
+ }
605
1013
  logger.info(`KeepAlive expired for server ${serverId}, stopping server`);
606
1014
  try {
607
- await this.stopServer(serverId);
1015
+ await this.stopServer(serverId, undefined, undefined, 'keepAlive expired');
608
1016
  } catch (error) {
609
- logger.error(`Error stopping server ${serverId} after keepAlive:`, error);
1017
+ logger.error(`Error stopping server ${serverId} after keepAlive: ${error instanceof Error ? error.message : String(error)}`);
610
1018
  }
611
1019
  }, keepAliveMs);
612
1020
  }
@@ -620,27 +1028,32 @@ export class ExternalMcpServerManager extends EventEmitter {
620
1028
  public getServersByScope(scope: 'global' | 'channel' | 'agent', scopeId?: string): ExternalServerStatus[] {
621
1029
  const servers: ExternalServerStatus[] = [];
622
1030
 
623
- for (const [serverId, scopeData] of this.serverScopes.entries()) {
624
- if (scopeData.scope === scope) {
1031
+ for (const [serverId, serverData] of this.servers.entries()) {
1032
+ // Servers registered through the original global registration path
1033
+ // predate serverScopes and are explicitly global, matching the tool
1034
+ // snapshot contract in getAllExternalTools().
1035
+ const scopeData = this.serverScopes.get(serverId);
1036
+ const actualScope = scopeData?.scope ?? 'global';
1037
+ if (actualScope === scope) {
625
1038
  // For channel/agent scope, match scopeId
626
- if ((scope === 'channel' || scope === 'agent') && scopeData.scopeId !== scopeId) {
1039
+ if ((scope === 'channel' || scope === 'agent') && scopeData?.scopeId !== scopeId) {
627
1040
  continue;
628
1041
  }
629
1042
 
630
- const serverData = this.servers.get(serverId);
631
- if (serverData) {
632
- servers.push(serverData.status);
633
- }
1043
+ servers.push(serverData.status);
634
1044
  }
635
1045
  }
636
1046
 
637
- return servers;
1047
+ return servers.sort((a, b) => a.id.localeCompare(b.id));
638
1048
  }
639
1049
 
640
1050
  /**
641
- * Stop an external server process
1051
+ * Stop an external server process.
1052
+ *
1053
+ * An intentional stop: the exit handler will see `expectedExit` and will
1054
+ * neither log the exit as a crash nor restart the process.
642
1055
  */
643
- public async stopServer(serverId: string, agentId?: AgentId, channelId?: ChannelId): Promise<void> {
1056
+ public async stopServer(serverId: string, agentId?: AgentId, channelId?: ChannelId, reason?: string): Promise<void> {
644
1057
  const serverData = this.servers.get(serverId);
645
1058
  if (!serverData) {
646
1059
  throw new Error(`Server ${serverId} not found`);
@@ -652,6 +1065,11 @@ export class ExternalMcpServerManager extends EventEmitter {
652
1065
  return;
653
1066
  }
654
1067
 
1068
+ const droppedTools = status.tools.length;
1069
+ logger.info(
1070
+ `Stopping server ${serverId} (${reason ?? 'no reason given'})` +
1071
+ (droppedTools > 0 ? ` — removing its ${droppedTools} tool(s) from the registry` : '')
1072
+ );
655
1073
 
656
1074
  // Emit stop event
657
1075
  this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STOP, serverId, agentId, channelId);
@@ -669,16 +1087,21 @@ export class ExternalMcpServerManager extends EventEmitter {
669
1087
  // Fail anything still in flight before we kill the process, so callers get
670
1088
  // a clear error rather than waiting out their own timeouts.
671
1089
  this.rejectPendingRequests(serverId, 'server is stopping');
1090
+ this.settleReadyWaiters(serverId, new Error(`Server ${serverId} was stopped (${reason ?? 'no reason given'})`));
672
1091
 
673
1092
  // Terminate process
674
1093
  if (serverData.process) {
675
- serverData.process.kill('SIGTERM');
676
-
677
- // Force kill after timeout
678
- setTimeout(() => {
679
- if (serverData.process && !serverData.process.killed) {
1094
+ serverData.expectedExit = true;
1095
+ const stoppingProcess = serverData.process;
1096
+ stoppingProcess.kill('SIGTERM');
1097
+
1098
+ // Force kill after timeout; the exit handler clears this when the
1099
+ // process goes down on its own.
1100
+ serverData.forceKillTimer = setTimeout(() => {
1101
+ serverData.forceKillTimer = undefined;
1102
+ if (!stoppingProcess.killed && stoppingProcess.exitCode === null) {
680
1103
  logger.warn(`Force killing server ${config.name}`);
681
- serverData.process.kill('SIGKILL');
1104
+ stoppingProcess.kill('SIGKILL');
682
1105
  }
683
1106
  }, 5000);
684
1107
  }
@@ -723,10 +1146,16 @@ export class ExternalMcpServerManager extends EventEmitter {
723
1146
  const tools: ExternalMcpTool[] = [];
724
1147
 
725
1148
  for (const [serverId, serverData] of this.servers) {
1149
+ const scopeData = this.serverScopes.get(serverId);
1150
+ const scope = scopeData?.scope ?? 'global';
1151
+ const scopeId = scopeData?.scopeId;
1152
+
726
1153
  for (const tool of serverData.status.tools) {
727
1154
  tools.push({
728
1155
  ...tool,
729
- serverId
1156
+ serverId,
1157
+ scope,
1158
+ scopeId
730
1159
  });
731
1160
  }
732
1161
  }
@@ -745,6 +1174,25 @@ export class ExternalMcpServerManager extends EventEmitter {
745
1174
 
746
1175
  // Handle process exit
747
1176
  process.on('exit', (code, signal) => {
1177
+ // This process is down — its SIGKILL escalation timer is moot.
1178
+ if (serverData.forceKillTimer) {
1179
+ clearTimeout(serverData.forceKillTimer);
1180
+ serverData.forceKillTimer = undefined;
1181
+ }
1182
+
1183
+ const currentData = this.servers.get(serverId);
1184
+ if (!currentData || currentData.process !== process) {
1185
+ // Exit of a process instance that has already been replaced (restart)
1186
+ // or whose server record is gone (unregistered). Not this record's
1187
+ // state to change.
1188
+ logger.debug(`Ignoring exit of a superseded process for ${serverId} (code ${code ?? 'null'}, signal ${signal ?? 'none'})`);
1189
+ return;
1190
+ }
1191
+
1192
+ const wasExpected = currentData.expectedExit === true;
1193
+ currentData.expectedExit = false;
1194
+
1195
+ const droppedTools = status.tools.length;
748
1196
 
749
1197
  status.status = 'stopped';
750
1198
  status.pid = undefined;
@@ -752,30 +1200,77 @@ export class ExternalMcpServerManager extends EventEmitter {
752
1200
  // perform it again before it can be marked running.
753
1201
  status.initialized = false;
754
1202
  status.initializing = false;
1203
+ status.tools = [];
755
1204
 
756
1205
  // Anything still waiting on this process will never get a reply.
757
1206
  this.rejectPendingRequests(
758
1207
  serverId,
759
1208
  `server exited (code ${code ?? 'null'}, signal ${signal ?? 'none'})`
760
1209
  );
1210
+ this.settleReadyWaiters(
1211
+ serverId,
1212
+ new Error(`Server ${serverId} exited during startup (code ${code ?? 'null'}, signal ${signal ?? 'none'})`)
1213
+ );
761
1214
 
762
1215
  // Clear timers
763
1216
  if (serverData.healthCheckTimer) {
764
1217
  clearInterval(serverData.healthCheckTimer);
765
1218
  serverData.healthCheckTimer = undefined;
766
1219
  }
1220
+ if (serverData.startupTimer) {
1221
+ clearTimeout(serverData.startupTimer);
1222
+ serverData.startupTimer = undefined;
1223
+ }
767
1224
 
768
- // Handle restart if configured
769
- if (config.restartOnCrash && code !== 0 && status.restartCount < config.maxRestartAttempts) {
770
- status.restartCount++;
771
-
772
- setTimeout(() => {
773
- // Check if server still exists before attempting restart
774
- // (it may have been unregistered during the delay)
775
- if (this.servers.has(serverId)) {
776
- this.startServer(serverId);
777
- }
778
- }, 2000); // Wait 2 seconds before restart
1225
+ if (wasExpected) {
1226
+ logger.info(`Server ${serverId} exited after stop (code ${code ?? 'null'}, signal ${signal ?? 'none'})`);
1227
+ return;
1228
+ }
1229
+
1230
+ // Unexpected death. This used to happen in complete silence — no log
1231
+ // line at any level which is how production servers vanished from
1232
+ // the tool registry with nothing to grep for.
1233
+ logger.error(
1234
+ `Server ${serverId} exited unexpectedly (code ${code ?? 'null'}, signal ${signal ?? 'none'})` +
1235
+ (droppedTools > 0 ? ` — its ${droppedTools} tool(s) are removed from the registry` : '')
1236
+ );
1237
+
1238
+ // Restart on ANY unexpected exit when configured — including a clean
1239
+ // exit code. `code !== 0` used to gate this, leaving a child that
1240
+ // exited 0 dead forever with no restart and no log.
1241
+ if (config.restartOnCrash) {
1242
+ if (status.restartCount < config.maxRestartAttempts) {
1243
+ status.restartCount++;
1244
+ logger.warn(
1245
+ `Restarting server ${serverId} in ${this.restartDelayMs}ms ` +
1246
+ `(attempt ${status.restartCount}/${config.maxRestartAttempts})`
1247
+ );
1248
+ setTimeout(() => {
1249
+ // Check if server still exists before attempting restart
1250
+ // (it may have been unregistered during the delay)
1251
+ if (this.servers.has(serverId)) {
1252
+ this.startServer(serverId).catch(error => {
1253
+ logger.error(
1254
+ `Restart of server ${serverId} failed: ` +
1255
+ `${error instanceof Error ? error.message : String(error)}`
1256
+ );
1257
+ });
1258
+ }
1259
+ }, this.restartDelayMs);
1260
+ } else {
1261
+ // Out of restart budget: remove the server entirely rather than
1262
+ // leaving a zombie record + scope that agents can "join".
1263
+ this.removeServerAfterFailure(
1264
+ serverId,
1265
+ `crashed and exhausted its ${config.maxRestartAttempts} restart attempt(s)`
1266
+ );
1267
+ return;
1268
+ }
1269
+ } else {
1270
+ logger.error(
1271
+ `Server ${serverId} will not be restarted (restartOnCrash is off). ` +
1272
+ `An agent joining its channel will start it again on demand.`
1273
+ );
779
1274
  }
780
1275
 
781
1276
  this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STOPPED, serverId);
@@ -903,7 +1398,7 @@ export class ExternalMcpServerManager extends EventEmitter {
903
1398
 
904
1399
  const stdin = serverData.process.stdin;
905
1400
  const requestId = uuidv4();
906
- const timeoutMs = REQUEST_TIMEOUTS_MS[method];
1401
+ const timeoutMs = this.requestTimeouts[method];
907
1402
 
908
1403
  return new Promise((resolve, reject) => {
909
1404
  const timer = setTimeout(() => {
@@ -1021,11 +1516,33 @@ export class ExternalMcpServerManager extends EventEmitter {
1021
1516
 
1022
1517
  try {
1023
1518
  await this.sendRequest(serverId, 'tools/list');
1519
+ serverData.consecutiveHealthFailures = 0;
1024
1520
  this.emitServerHealthStatus(serverId, 'healthy');
1025
1521
  } catch (error) {
1026
1522
  const message = error instanceof Error ? error.message : String(error);
1027
- logger.warn(`Health check failed for ${serverId}: ${message}`);
1523
+ serverData.consecutiveHealthFailures = (serverData.consecutiveHealthFailures ?? 0) + 1;
1524
+ logger.warn(
1525
+ `Health check failed for ${serverId} ` +
1526
+ `(${serverData.consecutiveHealthFailures} consecutive): ${message}`
1527
+ );
1028
1528
  this.emitServerHealthStatus(serverId, 'unhealthy');
1529
+
1530
+ // A process that is alive but no longer answering MCP is as dead as a
1531
+ // crashed one — the exit handler will never fire for it. Recover here.
1532
+ if (
1533
+ serverData.consecutiveHealthFailures >= HEALTH_FAILURES_BEFORE_RESTART &&
1534
+ serverData.config.restartOnCrash &&
1535
+ status.status === 'running'
1536
+ ) {
1537
+ serverData.consecutiveHealthFailures = 0;
1538
+ logger.error(`Server ${serverId} failed ${HEALTH_FAILURES_BEFORE_RESTART} consecutive health checks — restarting it`);
1539
+ this.restartServer(serverId).catch(restartError => {
1540
+ logger.error(
1541
+ `Health-check restart of ${serverId} failed: ` +
1542
+ `${restartError instanceof Error ? restartError.message : String(restartError)}`
1543
+ );
1544
+ });
1545
+ }
1029
1546
  }
1030
1547
  }
1031
1548
 
@@ -1173,6 +1690,16 @@ export class ExternalMcpServerManager extends EventEmitter {
1173
1690
 
1174
1691
  // Now that the connection is live, find out what the server offers.
1175
1692
  await this.discoverServerTools(serverId);
1693
+
1694
+ // A server that came up healthy earns back its full restart budget.
1695
+ // restartCount used to only ever grow, so a server that crashed a few
1696
+ // times over its lifetime — days apart, each recovered — permanently
1697
+ // exhausted its budget and the next crash left it down for good.
1698
+ serverData.status.restartCount = 0;
1699
+ serverData.consecutiveHealthFailures = 0;
1700
+
1701
+ // Whoever awaited startServer() can proceed: the tools are discovered.
1702
+ this.settleReadyWaiters(serverId);
1176
1703
  }
1177
1704
 
1178
1705
  /**
@@ -1217,6 +1744,9 @@ export class ExternalMcpServerManager extends EventEmitter {
1217
1744
 
1218
1745
  logger.error(`❌ Server ${serverId} error: ${error}`);
1219
1746
 
1747
+ // Anyone awaiting this server's startup gets the failure now.
1748
+ this.settleReadyWaiters(serverId, new Error(`Server ${serverId} failed: ${error}`));
1749
+
1220
1750
  // Emit error event
1221
1751
  this.emitServerErrorEvent(serverId, error, agentId, channelId);
1222
1752
  }
@@ -1342,13 +1872,30 @@ export class ExternalMcpServerManager extends EventEmitter {
1342
1872
  */
1343
1873
  public async shutdown(): Promise<void> {
1344
1874
 
1875
+ // Stop accepting lifecycle requests before child-process teardown. Node's
1876
+ // removeAllListeners() below only affects this class's EventEmitter; it
1877
+ // does not detach RxJS EventBus subscriptions.
1878
+ for (const subscription of this.eventSubscriptions) {
1879
+ subscription.unsubscribe();
1880
+ }
1881
+ this.eventSubscriptions = [];
1882
+
1345
1883
  const shutdownPromises = Array.from(this.servers.keys()).map(serverId =>
1346
1884
  this.stopServer(serverId)
1347
1885
  );
1348
1886
 
1349
1887
  await Promise.allSettled(shutdownPromises);
1350
1888
 
1889
+ // Clear pending keepAlive timers so nothing fires against cleared maps
1890
+ for (const scopeData of this.serverScopes.values()) {
1891
+ if (scopeData.keepAliveTimer) {
1892
+ clearTimeout(scopeData.keepAliveTimer);
1893
+ }
1894
+ }
1895
+
1351
1896
  this.servers.clear();
1897
+ this.serverScopes.clear();
1898
+ this.retiredChannelIds.clear();
1352
1899
  this.removeAllListeners();
1353
1900
 
1354
1901
  }
@@ -1411,26 +1958,24 @@ export class ExternalMcpServerManager extends EventEmitter {
1411
1958
  }
1412
1959
 
1413
1960
  /**
1414
- * Restart a server by ID
1961
+ * Restart a server by ID. Resolves after the restarted server has finished
1962
+ * its handshake and tool discovery (see startServer). Throws when the
1963
+ * restart fails, so callers can react instead of proceeding against a dead
1964
+ * server.
1415
1965
  */
1416
1966
  public async restartServer(serverId: string): Promise<boolean> {
1417
- try {
1967
+ logger.info(`Restarting server ${serverId}`);
1418
1968
 
1419
- // Stop the server first
1420
- await this.stopServer(serverId);
1969
+ // Stop the server first
1970
+ await this.stopServer(serverId, undefined, undefined, 'restart');
1421
1971
 
1422
- // Wait a bit before restarting
1423
- await new Promise(resolve => setTimeout(resolve, 1000));
1972
+ // Give the old process a moment to release stdio
1973
+ await new Promise(resolve => setTimeout(resolve, this.restartDelayMs));
1424
1974
 
1425
- // Start the server again
1426
- await this.startServer(serverId);
1427
-
1428
- return true;
1975
+ // Start the server again — resolves after handshake + tool discovery
1976
+ await this.startServer(serverId);
1429
1977
 
1430
- } catch (error) {
1431
- logger.error(`Failed to restart server ${serverId}: ${error instanceof Error ? error.message : String(error)}`);
1432
- return false;
1433
- }
1978
+ return true;
1434
1979
  }
1435
1980
 
1436
1981
  /**