@mxf-dev/core 1.0.1 → 2.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 (298) hide show
  1. package/README.md +18 -0
  2. package/dist/adapters/FrameworkAdapter.js +1 -1
  3. package/dist/adapters/FrameworkAdapter.js.map +1 -1
  4. package/dist/config/ConfigManager.d.ts +9 -8
  5. package/dist/config/ConfigManager.d.ts.map +1 -1
  6. package/dist/config/ConfigManager.js +47 -17
  7. package/dist/config/ConfigManager.js.map +1 -1
  8. package/dist/config/memory-strata.config.d.ts +25 -0
  9. package/dist/config/memory-strata.config.d.ts.map +1 -0
  10. package/dist/config/memory-strata.config.js +107 -0
  11. package/dist/config/memory-strata.config.js.map +1 -0
  12. package/dist/constants/ToolNames.d.ts +1 -29
  13. package/dist/constants/ToolNames.d.ts.map +1 -1
  14. package/dist/constants/ToolNames.js +4 -17
  15. package/dist/constants/ToolNames.js.map +1 -1
  16. package/dist/database/DatabaseAdapterFactory.d.ts +14 -2
  17. package/dist/database/DatabaseAdapterFactory.d.ts.map +1 -1
  18. package/dist/database/DatabaseAdapterFactory.js +21 -6
  19. package/dist/database/DatabaseAdapterFactory.js.map +1 -1
  20. package/dist/database/adapters/mongodb/MongoBaseRepository.d.ts +7 -1
  21. package/dist/database/adapters/mongodb/MongoBaseRepository.d.ts.map +1 -1
  22. package/dist/database/adapters/mongodb/MongoBaseRepository.js +12 -2
  23. package/dist/database/adapters/mongodb/MongoBaseRepository.js.map +1 -1
  24. package/dist/events/ClientEventBus.d.ts +16 -77
  25. package/dist/events/ClientEventBus.d.ts.map +1 -1
  26. package/dist/events/ClientEventBus.js +38 -311
  27. package/dist/events/ClientEventBus.js.map +1 -1
  28. package/dist/events/EventBus.d.ts +1 -1
  29. package/dist/events/EventBus.d.ts.map +1 -1
  30. package/dist/events/EventBus.js +5 -4
  31. package/dist/events/EventBus.js.map +1 -1
  32. package/dist/events/EventBusBase.d.ts +139 -34
  33. package/dist/events/EventBusBase.d.ts.map +1 -1
  34. package/dist/events/EventBusBase.js +366 -88
  35. package/dist/events/EventBusBase.js.map +1 -1
  36. package/dist/events/EventNames.d.ts +10 -1
  37. package/dist/events/EventNames.d.ts.map +1 -1
  38. package/dist/events/EventNames.js +3 -0
  39. package/dist/events/EventNames.js.map +1 -1
  40. package/dist/events/PublicEvents.d.ts +6 -2
  41. package/dist/events/PublicEvents.d.ts.map +1 -1
  42. package/dist/events/PublicEvents.js +7 -4
  43. package/dist/events/PublicEvents.js.map +1 -1
  44. package/dist/events/ServerEventBus.d.ts +23 -112
  45. package/dist/events/ServerEventBus.d.ts.map +1 -1
  46. package/dist/events/ServerEventBus.js +58 -323
  47. package/dist/events/ServerEventBus.js.map +1 -1
  48. package/dist/events/event-definitions/LlmBudgetEvents.d.ts +68 -0
  49. package/dist/events/event-definitions/LlmBudgetEvents.d.ts.map +1 -0
  50. package/dist/events/event-definitions/LlmBudgetEvents.js +35 -0
  51. package/dist/events/event-definitions/LlmBudgetEvents.js.map +1 -0
  52. package/dist/interfaces/IMemoryPersistence.d.ts +41 -0
  53. package/dist/interfaces/IMemoryPersistence.d.ts.map +1 -0
  54. package/dist/interfaces/IMemoryPersistence.js +15 -0
  55. package/dist/interfaces/IMemoryPersistence.js.map +1 -0
  56. package/dist/middleware/MxpMiddleware.d.ts +8 -1
  57. package/dist/middleware/MxpMiddleware.d.ts.map +1 -1
  58. package/dist/middleware/MxpMiddleware.js +42 -20
  59. package/dist/middleware/MxpMiddleware.js.map +1 -1
  60. package/dist/models/channelKey.d.ts +1 -0
  61. package/dist/models/channelKey.d.ts.map +1 -1
  62. package/dist/models/channelKey.js +5 -0
  63. package/dist/models/channelKey.js.map +1 -1
  64. package/dist/models/memoryUtility.d.ts +39 -0
  65. package/dist/models/memoryUtility.d.ts.map +1 -0
  66. package/dist/models/memoryUtility.js +39 -0
  67. package/dist/models/memoryUtility.js.map +1 -0
  68. package/dist/models/user.d.ts.map +1 -1
  69. package/dist/models/user.js +23 -1
  70. package/dist/models/user.js.map +1 -1
  71. package/dist/mxp/BinaryProtocolLayer.d.ts +24 -8
  72. package/dist/mxp/BinaryProtocolLayer.d.ts.map +1 -1
  73. package/dist/mxp/BinaryProtocolLayer.js +90 -103
  74. package/dist/mxp/BinaryProtocolLayer.js.map +1 -1
  75. package/dist/mxp/ContextCompressionEngine.d.ts +57 -10
  76. package/dist/mxp/ContextCompressionEngine.d.ts.map +1 -1
  77. package/dist/mxp/ContextCompressionEngine.js +170 -128
  78. package/dist/mxp/ContextCompressionEngine.js.map +1 -1
  79. package/dist/mxp/MxpTokenOptimizer.d.ts +7 -3
  80. package/dist/mxp/MxpTokenOptimizer.d.ts.map +1 -1
  81. package/dist/mxp/MxpTokenOptimizer.js +8 -5
  82. package/dist/mxp/MxpTokenOptimizer.js.map +1 -1
  83. package/dist/prompts/MxfAgentSystemPrompt.d.ts +1 -1
  84. package/dist/prompts/MxfAgentSystemPrompt.d.ts.map +1 -1
  85. package/dist/prompts/MxfAgentSystemPrompt.js +47 -57
  86. package/dist/prompts/MxfAgentSystemPrompt.js.map +1 -1
  87. package/dist/protocols/mcp/ToolError.d.ts +123 -0
  88. package/dist/protocols/mcp/ToolError.d.ts.map +1 -0
  89. package/dist/protocols/mcp/ToolError.js +108 -0
  90. package/dist/protocols/mcp/ToolError.js.map +1 -0
  91. package/dist/protocols/mcp/defineTool.d.ts +133 -0
  92. package/dist/protocols/mcp/defineTool.d.ts.map +1 -0
  93. package/dist/protocols/mcp/defineTool.js +236 -0
  94. package/dist/protocols/mcp/defineTool.js.map +1 -0
  95. package/dist/protocols/mcp/security/HttpTargetGuard.d.ts +38 -0
  96. package/dist/protocols/mcp/security/HttpTargetGuard.d.ts.map +1 -0
  97. package/dist/protocols/mcp/security/HttpTargetGuard.js +227 -0
  98. package/dist/protocols/mcp/security/HttpTargetGuard.js.map +1 -0
  99. package/dist/protocols/mcp/security/McpSecurityGuard.d.ts +9 -1
  100. package/dist/protocols/mcp/security/McpSecurityGuard.d.ts.map +1 -1
  101. package/dist/protocols/mcp/security/McpSecurityGuard.js +10 -2
  102. package/dist/protocols/mcp/security/McpSecurityGuard.js.map +1 -1
  103. package/dist/protocols/mcp/security/McpToolPolicy.d.ts +81 -0
  104. package/dist/protocols/mcp/security/McpToolPolicy.d.ts.map +1 -0
  105. package/dist/protocols/mcp/security/McpToolPolicy.js +202 -0
  106. package/dist/protocols/mcp/security/McpToolPolicy.js.map +1 -0
  107. package/dist/protocols/mcp/services/ExternalMcpServerManager.d.ts +51 -18
  108. package/dist/protocols/mcp/services/ExternalMcpServerManager.d.ts.map +1 -1
  109. package/dist/protocols/mcp/services/ExternalMcpServerManager.js +256 -283
  110. package/dist/protocols/mcp/services/ExternalMcpServerManager.js.map +1 -1
  111. package/dist/protocols/mcp/services/ExternalServerConfigs.d.ts +39 -6
  112. package/dist/protocols/mcp/services/ExternalServerConfigs.d.ts.map +1 -1
  113. package/dist/protocols/mcp/services/ExternalServerConfigs.js +76 -18
  114. package/dist/protocols/mcp/services/ExternalServerConfigs.js.map +1 -1
  115. package/dist/protocols/mcp/tools/AnalyticsTools.js +1 -1
  116. package/dist/protocols/mcp/tools/AnalyticsTools.js.map +1 -1
  117. package/dist/protocols/mcp/tools/CodeAnalysisTools.d.ts.map +1 -1
  118. package/dist/protocols/mcp/tools/CodeAnalysisTools.js +15 -0
  119. package/dist/protocols/mcp/tools/CodeAnalysisTools.js.map +1 -1
  120. package/dist/protocols/mcp/tools/EffectivenessTools.js +3 -3
  121. package/dist/protocols/mcp/tools/EffectivenessTools.js.map +1 -1
  122. package/dist/protocols/mcp/tools/GitTools.d.ts +9 -369
  123. package/dist/protocols/mcp/tools/GitTools.d.ts.map +1 -1
  124. package/dist/protocols/mcp/tools/GitTools.js +222 -517
  125. package/dist/protocols/mcp/tools/GitTools.js.map +1 -1
  126. package/dist/protocols/mcp/tools/InfrastructureTools.d.ts +56 -24
  127. package/dist/protocols/mcp/tools/InfrastructureTools.d.ts.map +1 -1
  128. package/dist/protocols/mcp/tools/InfrastructureTools.js +117 -26
  129. package/dist/protocols/mcp/tools/InfrastructureTools.js.map +1 -1
  130. package/dist/protocols/mcp/tools/MemorySearchTools.d.ts +0 -3
  131. package/dist/protocols/mcp/tools/MemorySearchTools.d.ts.map +1 -1
  132. package/dist/protocols/mcp/tools/MemorySearchTools.js +58 -1
  133. package/dist/protocols/mcp/tools/MemorySearchTools.js.map +1 -1
  134. package/dist/protocols/mcp/tools/OrparTools.d.ts +8 -0
  135. package/dist/protocols/mcp/tools/OrparTools.d.ts.map +1 -1
  136. package/dist/protocols/mcp/tools/OrparTools.js +66 -0
  137. package/dist/protocols/mcp/tools/OrparTools.js.map +1 -1
  138. package/dist/protocols/mcp/tools/SafetyTools.d.ts +7 -525
  139. package/dist/protocols/mcp/tools/SafetyTools.d.ts.map +1 -1
  140. package/dist/protocols/mcp/tools/SafetyTools.js +349 -514
  141. package/dist/protocols/mcp/tools/SafetyTools.js.map +1 -1
  142. package/dist/protocols/mcp/tools/TaskPlanningTools.d.ts.map +1 -1
  143. package/dist/protocols/mcp/tools/TaskPlanningTools.js +56 -26
  144. package/dist/protocols/mcp/tools/TaskPlanningTools.js.map +1 -1
  145. package/dist/protocols/mcp/tools/TestTools.d.ts +15 -376
  146. package/dist/protocols/mcp/tools/TestTools.d.ts.map +1 -1
  147. package/dist/protocols/mcp/tools/TestTools.js +241 -363
  148. package/dist/protocols/mcp/tools/TestTools.js.map +1 -1
  149. package/dist/protocols/mcp/tools/TypeScriptTools.d.ts +6 -276
  150. package/dist/protocols/mcp/tools/TypeScriptTools.d.ts.map +1 -1
  151. package/dist/protocols/mcp/tools/TypeScriptTools.js +223 -337
  152. package/dist/protocols/mcp/tools/TypeScriptTools.js.map +1 -1
  153. package/dist/protocols/mcp/tools/WebTools.d.ts +15 -728
  154. package/dist/protocols/mcp/tools/WebTools.d.ts.map +1 -1
  155. package/dist/protocols/mcp/tools/WebTools.js +151 -257
  156. package/dist/protocols/mcp/tools/WebTools.js.map +1 -1
  157. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.d.ts +5 -3
  158. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.d.ts.map +1 -1
  159. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.js +31 -6
  160. package/dist/protocols/mcp/tools/shell/ShellExecuteHandler.js.map +1 -1
  161. package/dist/schemas/EventPayloadSchema.d.ts +21 -0
  162. package/dist/schemas/EventPayloadSchema.d.ts.map +1 -1
  163. package/dist/schemas/EventPayloadSchema.js +14 -0
  164. package/dist/schemas/EventPayloadSchema.js.map +1 -1
  165. package/dist/services/BackgroundTaskManager.d.ts.map +1 -1
  166. package/dist/services/BackgroundTaskManager.js +12 -4
  167. package/dist/services/BackgroundTaskManager.js.map +1 -1
  168. package/dist/services/BrowserManager.d.ts +1 -1
  169. package/dist/services/BrowserManager.d.ts.map +1 -1
  170. package/dist/services/BrowserManager.js +26 -3
  171. package/dist/services/BrowserManager.js.map +1 -1
  172. package/dist/services/MemoryCompressor.d.ts +11 -2
  173. package/dist/services/MemoryCompressor.d.ts.map +1 -1
  174. package/dist/services/MemoryCompressor.js +8 -26
  175. package/dist/services/MemoryCompressor.js.map +1 -1
  176. package/dist/services/MemoryService.d.ts +16 -3
  177. package/dist/services/MemoryService.d.ts.map +1 -1
  178. package/dist/services/MemoryService.js +79 -55
  179. package/dist/services/MemoryService.js.map +1 -1
  180. package/dist/services/PredictiveAnalyticsService.d.ts +34 -4
  181. package/dist/services/PredictiveAnalyticsService.d.ts.map +1 -1
  182. package/dist/services/PredictiveAnalyticsService.js +41 -25
  183. package/dist/services/PredictiveAnalyticsService.js.map +1 -1
  184. package/dist/services/QValueManager.d.ts +7 -0
  185. package/dist/services/QValueManager.d.ts.map +1 -1
  186. package/dist/services/QValueManager.js +9 -0
  187. package/dist/services/QValueManager.js.map +1 -1
  188. package/dist/services/RewardSignalProcessor.d.ts +38 -1
  189. package/dist/services/RewardSignalProcessor.d.ts.map +1 -1
  190. package/dist/services/RewardSignalProcessor.js +109 -7
  191. package/dist/services/RewardSignalProcessor.js.map +1 -1
  192. package/dist/services/SharedStateService.d.ts +16 -2
  193. package/dist/services/SharedStateService.d.ts.map +1 -1
  194. package/dist/services/SharedStateService.js +44 -3
  195. package/dist/services/SharedStateService.js.map +1 -1
  196. package/dist/services/TaskEffectivenessService.d.ts +34 -18
  197. package/dist/services/TaskEffectivenessService.d.ts.map +1 -1
  198. package/dist/services/TaskEffectivenessService.js +91 -155
  199. package/dist/services/TaskEffectivenessService.js.map +1 -1
  200. package/dist/services/WebSearchService.d.ts.map +1 -1
  201. package/dist/services/WebSearchService.js.map +1 -1
  202. package/dist/services/dag/TaskDagService.d.ts.map +1 -1
  203. package/dist/services/dag/TaskDagService.js +2 -0
  204. package/dist/services/dag/TaskDagService.js.map +1 -1
  205. package/dist/services/orpar-memory/OrparMemoryCoordinator.d.ts +7 -0
  206. package/dist/services/orpar-memory/OrparMemoryCoordinator.d.ts.map +1 -1
  207. package/dist/services/orpar-memory/OrparMemoryCoordinator.js +71 -10
  208. package/dist/services/orpar-memory/OrparMemoryCoordinator.js.map +1 -1
  209. package/dist/types/MemoryUtilityTypes.d.ts +2 -0
  210. package/dist/types/MemoryUtilityTypes.d.ts.map +1 -1
  211. package/dist/types/MemoryUtilityTypes.js.map +1 -1
  212. package/dist/utils/Logger.d.ts +43 -3
  213. package/dist/utils/Logger.d.ts.map +1 -1
  214. package/dist/utils/Logger.js +78 -36
  215. package/dist/utils/Logger.js.map +1 -1
  216. package/dist/utils/MxpEncryption.d.ts +19 -6
  217. package/dist/utils/MxpEncryption.d.ts.map +1 -1
  218. package/dist/utils/MxpEncryption.js +48 -16
  219. package/dist/utils/MxpEncryption.js.map +1 -1
  220. package/package.json +19 -4
  221. package/src/adapters/FrameworkAdapter.ts +1 -1
  222. package/src/config/ConfigManager.ts +52 -19
  223. package/src/config/memory-strata.config.ts +136 -0
  224. package/src/constants/ToolNames.ts +4 -18
  225. package/src/database/DatabaseAdapterFactory.ts +23 -6
  226. package/src/database/adapters/mongodb/MongoBaseRepository.ts +16 -2
  227. package/src/events/ClientEventBus.ts +68 -403
  228. package/src/events/EventBus.ts +7 -8
  229. package/src/events/EventBusBase.ts +479 -116
  230. package/src/events/EventNames.ts +6 -0
  231. package/src/events/PublicEvents.ts +7 -4
  232. package/src/events/ServerEventBus.ts +78 -473
  233. package/src/events/event-definitions/LlmBudgetEvents.ts +74 -0
  234. package/src/interfaces/IMemoryPersistence.ts +51 -0
  235. package/src/middleware/MxpMiddleware.ts +51 -24
  236. package/src/models/channelKey.ts +18 -4
  237. package/src/models/memoryUtility.ts +64 -0
  238. package/src/models/user.ts +36 -4
  239. package/src/mxp/BinaryProtocolLayer.ts +108 -124
  240. package/src/mxp/ContextCompressionEngine.ts +219 -161
  241. package/src/mxp/MxpTokenOptimizer.ts +10 -10
  242. package/src/prompts/MxfAgentSystemPrompt.ts +45 -55
  243. package/src/protocols/mcp/ToolError.ts +201 -0
  244. package/src/protocols/mcp/defineTool.ts +379 -0
  245. package/src/protocols/mcp/security/HttpTargetGuard.ts +267 -0
  246. package/src/protocols/mcp/security/McpSecurityGuard.ts +10 -3
  247. package/src/protocols/mcp/security/McpToolPolicy.ts +221 -0
  248. package/src/protocols/mcp/services/ExternalMcpServerManager.ts +340 -322
  249. package/src/protocols/mcp/services/ExternalServerConfigs.ts +82 -19
  250. package/src/protocols/mcp/tools/AnalyticsTools.ts +1 -1
  251. package/src/protocols/mcp/tools/CodeAnalysisTools.ts +15 -0
  252. package/src/protocols/mcp/tools/EffectivenessTools.ts +3 -3
  253. package/src/protocols/mcp/tools/GitTools.ts +290 -569
  254. package/src/protocols/mcp/tools/InfrastructureTools.ts +189 -43
  255. package/src/protocols/mcp/tools/MemorySearchTools.ts +77 -1
  256. package/src/protocols/mcp/tools/OrparTools.ts +87 -0
  257. package/src/protocols/mcp/tools/SafetyTools.ts +501 -579
  258. package/src/protocols/mcp/tools/TaskPlanningTools.ts +81 -39
  259. package/src/protocols/mcp/tools/TestTools.ts +339 -419
  260. package/src/protocols/mcp/tools/TypeScriptTools.ts +316 -376
  261. package/src/protocols/mcp/tools/WebTools.ts +209 -307
  262. package/src/protocols/mcp/tools/shell/ShellExecuteHandler.ts +53 -14
  263. package/src/schemas/EventPayloadSchema.ts +37 -0
  264. package/src/services/BackgroundTaskManager.ts +12 -4
  265. package/src/services/BrowserManager.ts +41 -1
  266. package/src/services/MemoryCompressor.ts +20 -32
  267. package/src/services/MemoryService.ts +96 -58
  268. package/src/services/PredictiveAnalyticsService.ts +55 -32
  269. package/src/services/QValueManager.ts +10 -0
  270. package/src/services/RewardSignalProcessor.ts +138 -9
  271. package/src/services/SharedStateService.ts +56 -8
  272. package/src/services/TaskEffectivenessService.ts +114 -187
  273. package/src/services/WebSearchService.ts +2 -1
  274. package/src/services/dag/TaskDagService.ts +2 -0
  275. package/src/services/orpar-memory/OrparMemoryCoordinator.ts +86 -10
  276. package/src/types/MemoryUtilityTypes.ts +2 -0
  277. package/src/utils/Logger.ts +116 -40
  278. package/src/utils/MxpEncryption.ts +69 -34
  279. package/dist/mxp/MxpEventForwardingEnhancer.d.ts +0 -134
  280. package/dist/mxp/MxpEventForwardingEnhancer.d.ts.map +0 -1
  281. package/dist/mxp/MxpEventForwardingEnhancer.js +0 -361
  282. package/dist/mxp/MxpEventForwardingEnhancer.js.map +0 -1
  283. package/dist/protocols/mcp/services/ToolSchemaEnhancements.d.ts +0 -56
  284. package/dist/protocols/mcp/services/ToolSchemaEnhancements.d.ts.map +0 -1
  285. package/dist/protocols/mcp/services/ToolSchemaEnhancements.js +0 -57
  286. package/dist/protocols/mcp/services/ToolSchemaEnhancements.js.map +0 -1
  287. package/dist/protocols/mcp/tools/ControlLoopLifecycle.d.ts +0 -270
  288. package/dist/protocols/mcp/tools/ControlLoopLifecycle.d.ts.map +0 -1
  289. package/dist/protocols/mcp/tools/ControlLoopLifecycle.js +0 -256
  290. package/dist/protocols/mcp/tools/ControlLoopLifecycle.js.map +0 -1
  291. package/dist/protocols/mcp/tools/LspTools.d.ts +0 -495
  292. package/dist/protocols/mcp/tools/LspTools.d.ts.map +0 -1
  293. package/dist/protocols/mcp/tools/LspTools.js +0 -469
  294. package/dist/protocols/mcp/tools/LspTools.js.map +0 -1
  295. package/src/mxp/MxpEventForwardingEnhancer.ts +0 -471
  296. package/src/protocols/mcp/services/ToolSchemaEnhancements.ts +0 -92
  297. package/src/protocols/mcp/tools/ControlLoopLifecycle.ts +0 -302
  298. package/src/protocols/mcp/tools/LspTools.ts +0 -476
@@ -111,6 +111,30 @@ export interface ExternalMcpTool {
111
111
  serverId: string;
112
112
  }
113
113
 
114
+ /**
115
+ * A JSON-RPC request that has been written to a server and is awaiting its reply.
116
+ */
117
+ interface PendingRequest {
118
+ resolve: (result: any) => void;
119
+ reject: (error: Error) => void;
120
+ timer: NodeJS.Timeout;
121
+ method: string;
122
+ }
123
+
124
+ /** How long to wait for a reply to a JSON-RPC request, per method. */
125
+ const REQUEST_TIMEOUTS_MS = {
126
+ initialize: 30000,
127
+ 'tools/list': 15000,
128
+ 'tools/call': 30000,
129
+ ping: 5000
130
+ } as const;
131
+
132
+ /** MCP protocol version this client negotiates in the initialize handshake. */
133
+ const MCP_PROTOCOL_VERSION = '2024-11-05';
134
+
135
+ /** Version this client reports to servers. */
136
+ const MCP_CLIENT_VERSION = '1.0.0';
137
+
114
138
  /**
115
139
  * Manages external MCP server processes and their lifecycle
116
140
  */
@@ -121,6 +145,19 @@ export class ExternalMcpServerManager extends EventEmitter {
121
145
  status: ExternalServerStatus;
122
146
  healthCheckTimer?: NodeJS.Timeout;
123
147
  startupTimer?: NodeJS.Timeout;
148
+ /**
149
+ * In-flight JSON-RPC requests, keyed by request id.
150
+ *
151
+ * There is ONE stdout listener per server (installed at spawn) that resolves
152
+ * entries in this map. Previously each call attached its own 'data' listener
153
+ * and re-parsed the whole stream: with N concurrent calls every line was
154
+ * parsed N times, and a listener that timed out was removed only if the
155
+ * timeout path ran — repeated timeouts leaked listeners until Node warned
156
+ * about a possible EventEmitter leak.
157
+ */
158
+ pending: Map<string, PendingRequest>;
159
+ /** Partial line left over from the last stdout chunk. */
160
+ stdoutBuffer: string;
124
161
  }> = new Map();
125
162
 
126
163
  // Scope tracking for channel/agent-scoped servers
@@ -419,7 +456,9 @@ export class ExternalMcpServerManager extends EventEmitter {
419
456
  // Store server configuration and status
420
457
  this.servers.set(config.id, {
421
458
  config,
422
- status
459
+ status,
460
+ pending: new Map(),
461
+ stdoutBuffer: ''
423
462
  });
424
463
 
425
464
 
@@ -627,14 +666,18 @@ export class ExternalMcpServerManager extends EventEmitter {
627
666
  serverData.startupTimer = undefined;
628
667
  }
629
668
 
669
+ // Fail anything still in flight before we kill the process, so callers get
670
+ // a clear error rather than waiting out their own timeouts.
671
+ this.rejectPendingRequests(serverId, 'server is stopping');
672
+
630
673
  // Terminate process
631
674
  if (serverData.process) {
632
675
  serverData.process.kill('SIGTERM');
633
-
676
+
634
677
  // Force kill after timeout
635
678
  setTimeout(() => {
636
679
  if (serverData.process && !serverData.process.killed) {
637
- logger.warn(`🔪 Force killing server ${config.name}`);
680
+ logger.warn(`Force killing server ${config.name}`);
638
681
  serverData.process.kill('SIGKILL');
639
682
  }
640
683
  }, 5000);
@@ -644,6 +687,8 @@ export class ExternalMcpServerManager extends EventEmitter {
644
687
  status.status = 'stopped';
645
688
  status.pid = undefined;
646
689
  status.tools = [];
690
+ status.initialized = false;
691
+ status.initializing = false;
647
692
 
648
693
  // Emit stopped event
649
694
  this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STOPPED, serverId, agentId, channelId);
@@ -700,9 +745,19 @@ export class ExternalMcpServerManager extends EventEmitter {
700
745
 
701
746
  // Handle process exit
702
747
  process.on('exit', (code, signal) => {
703
-
748
+
704
749
  status.status = 'stopped';
705
750
  status.pid = undefined;
751
+ // The handshake does not survive the process. A restarted server has to
752
+ // perform it again before it can be marked running.
753
+ status.initialized = false;
754
+ status.initializing = false;
755
+
756
+ // Anything still waiting on this process will never get a reply.
757
+ this.rejectPendingRequests(
758
+ serverId,
759
+ `server exited (code ${code ?? 'null'}, signal ${signal ?? 'none'})`
760
+ );
706
761
 
707
762
  // Clear timers
708
763
  if (serverData.healthCheckTimer) {
@@ -732,11 +787,11 @@ export class ExternalMcpServerManager extends EventEmitter {
732
787
  this.handleServerError(serverId, error.message);
733
788
  });
734
789
 
735
- // Handle stdout for tool discovery and health checks
790
+ // ONE stdout listener per server. Every JSON-RPC reply arrives here and is
791
+ // routed to its waiting caller by request id.
736
792
  if (process.stdout) {
737
- process.stdout.on('data', (data) => {
738
- const output = data.toString();
739
- this.handleServerOutput(serverId, output);
793
+ process.stdout.on('data', (data: Buffer) => {
794
+ this.handleServerOutput(serverId, data.toString());
740
795
  });
741
796
  }
742
797
 
@@ -754,30 +809,167 @@ export class ExternalMcpServerManager extends EventEmitter {
754
809
  });
755
810
  }
756
811
 
757
- // Mark as started when process is spawned successfully
812
+ // The OS started the process. That is NOT the same as the server being ready
813
+ // to serve MCP: it says nothing about whether the JSON-RPC handshake will
814
+ // succeed. Status stays 'starting' until initializeMcpConnection() completes,
815
+ // so a server that spawns and then fails its handshake never reports 'running'.
758
816
  process.on('spawn', () => {
759
- logger.info(`[SPAWN] Server ${serverId} process spawned successfully`);
760
- status.status = 'running';
761
-
762
- // Clear startup timer
763
- if (serverData.startupTimer) {
764
- clearTimeout(serverData.startupTimer);
765
- serverData.startupTimer = undefined;
817
+ logger.info(`[SPAWN] Server ${serverId} process spawned; starting MCP handshake`);
818
+
819
+ // stdin writes are buffered by the OS, so the handshake can be written
820
+ // immediately the child reads it when it is ready. The old code slept
821
+ // two seconds here and hoped that was long enough.
822
+ this.initializeMcpConnection(serverId).catch((err) => {
823
+ logger.error(`[MCP] Failed to initialize connection to ${serverId}: ${err.message}`);
824
+ this.handleServerError(serverId, `MCP initialization failed: ${err.message}`);
825
+ });
826
+ });
827
+ }
828
+
829
+ /**
830
+ * Route a chunk of a server's stdout to whoever is waiting for it.
831
+ *
832
+ * MCP over stdio is line-delimited JSON. Chunks do not respect line boundaries,
833
+ * so a partial line is carried over to the next chunk.
834
+ */
835
+ private handleServerOutput(serverId: string, chunk: string): void {
836
+ const serverData = this.servers.get(serverId);
837
+ if (!serverData) {
838
+ return;
839
+ }
840
+
841
+ serverData.stdoutBuffer += chunk;
842
+
843
+ const lines = serverData.stdoutBuffer.split('\n');
844
+ // The last element is either an incomplete line or '' — keep it for next time.
845
+ serverData.stdoutBuffer = lines.pop() ?? '';
846
+
847
+ for (const line of lines) {
848
+ const trimmed = line.trim();
849
+ if (!trimmed) {
850
+ continue;
766
851
  }
767
852
 
768
- this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STARTED, serverId);
769
-
770
- // Initialize MCP connection first, then discover tools
771
- logger.info(`[SPAWN] Will initialize MCP connection for ${serverId} in 2 seconds...`);
772
- setTimeout(() => {
773
- this.initializeMcpConnection(serverId).catch((err) => {
774
- logger.error(`[MCP] Failed to initialize connection to ${serverId}: ${err.message}`);
775
- this.handleServerError(serverId, `MCP initialization failed: ${err.message}`);
776
- });
777
- }, 2000); // Wait 2 seconds for server to fully start
853
+ let message: any;
854
+ try {
855
+ message = JSON.parse(trimmed);
856
+ } catch {
857
+ // Servers spawned through npx sometimes print non-JSON banners to
858
+ // stdout. Not our reply; not an error.
859
+ logger.debug(`[MCP-STDOUT] ${serverId}: ${trimmed.slice(0, 200)}`);
860
+ continue;
861
+ }
862
+
863
+ // A message with no id is a notification from the server, not a reply.
864
+ if (message.id === undefined || message.id === null) {
865
+ continue;
866
+ }
867
+
868
+ const requestId = String(message.id);
869
+ const pending = serverData.pending.get(requestId);
870
+ if (!pending) {
871
+ continue;
872
+ }
873
+
874
+ clearTimeout(pending.timer);
875
+ serverData.pending.delete(requestId);
876
+
877
+ if (message.error) {
878
+ pending.reject(new Error(
879
+ `MCP error from ${serverId} (${pending.method}): ` +
880
+ `${message.error.message ?? JSON.stringify(message.error)}`
881
+ ));
882
+ } else {
883
+ pending.resolve(message.result);
884
+ }
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Send a JSON-RPC request and wait for its reply.
890
+ *
891
+ * The reply is matched by id in handleServerOutput(). On timeout the pending
892
+ * entry is removed, so nothing accumulates.
893
+ */
894
+ private sendRequest(
895
+ serverId: string,
896
+ method: keyof typeof REQUEST_TIMEOUTS_MS,
897
+ params: Record<string, any> = {}
898
+ ): Promise<any> {
899
+ const serverData = this.servers.get(serverId);
900
+ if (!serverData?.process?.stdin) {
901
+ return Promise.reject(new Error(`Server ${serverId} is not running or has no stdin`));
902
+ }
903
+
904
+ const stdin = serverData.process.stdin;
905
+ const requestId = uuidv4();
906
+ const timeoutMs = REQUEST_TIMEOUTS_MS[method];
907
+
908
+ return new Promise((resolve, reject) => {
909
+ const timer = setTimeout(() => {
910
+ serverData.pending.delete(requestId);
911
+ reject(new Error(
912
+ `Timed out after ${timeoutMs}ms waiting for ${method} from ${serverId}`
913
+ ));
914
+ }, timeoutMs);
915
+
916
+ serverData.pending.set(requestId, { resolve, reject, timer, method });
917
+
918
+ const request = {
919
+ jsonrpc: '2.0',
920
+ id: requestId,
921
+ method,
922
+ params
923
+ };
924
+
925
+ stdin.write(JSON.stringify(request) + '\n', (writeError) => {
926
+ if (writeError) {
927
+ clearTimeout(timer);
928
+ serverData.pending.delete(requestId);
929
+ reject(new Error(
930
+ `Failed to write ${method} to ${serverId}: ${writeError.message}`
931
+ ));
932
+ }
933
+ });
778
934
  });
779
935
  }
780
936
 
937
+ /**
938
+ * Send a JSON-RPC notification. Notifications have no id and get no reply.
939
+ */
940
+ private sendNotification(
941
+ serverId: string,
942
+ method: string,
943
+ params: Record<string, any> = {}
944
+ ): void {
945
+ const serverData = this.servers.get(serverId);
946
+ if (!serverData?.process?.stdin) {
947
+ throw new Error(`Server ${serverId} is not running or has no stdin`);
948
+ }
949
+
950
+ const notification = { jsonrpc: '2.0', method, params };
951
+ serverData.process.stdin.write(JSON.stringify(notification) + '\n');
952
+ }
953
+
954
+ /**
955
+ * Fail every in-flight request for a server. Called when the process dies, so
956
+ * callers get an error instead of hanging until their individual timeouts.
957
+ */
958
+ private rejectPendingRequests(serverId: string, reason: string): void {
959
+ const serverData = this.servers.get(serverId);
960
+ if (!serverData) {
961
+ return;
962
+ }
963
+
964
+ for (const [requestId, pending] of serverData.pending) {
965
+ clearTimeout(pending.timer);
966
+ pending.reject(new Error(`${pending.method} on ${serverId} aborted: ${reason}`));
967
+ serverData.pending.delete(requestId);
968
+ }
969
+
970
+ serverData.stdoutBuffer = '';
971
+ }
972
+
781
973
  /**
782
974
  * Start health check monitoring for a server
783
975
  */
@@ -793,42 +985,48 @@ export class ExternalMcpServerManager extends EventEmitter {
793
985
  }
794
986
 
795
987
  /**
796
- * Perform health check for a server
988
+ * Check that a server is actually answering.
989
+ *
990
+ * This used to be `process && !process.killed` — which only says the OS has not
991
+ * reaped the process. A server that had wedged, deadlocked, or stopped reading
992
+ * stdin stayed "healthy" forever.
993
+ *
994
+ * A real probe means a round trip through the JSON-RPC layer. `tools/list` is
995
+ * the probe: every server we can talk to implements it (we call it at
996
+ * discovery), it takes no arguments, and a reply proves the server is reading
997
+ * stdin, parsing JSON-RPC, and writing stdout.
797
998
  */
798
999
  private async performHealthCheck(serverId: string): Promise<void> {
799
1000
  const serverData = this.servers.get(serverId);
800
1001
  if (!serverData) return;
801
1002
 
802
- const { config, status } = serverData;
1003
+ const { status } = serverData;
803
1004
 
804
- // Emit health check event
805
1005
  this.emitServerEvent(McpEvents.EXTERNAL_SERVER_HEALTH_CHECK, serverId);
806
1006
 
807
- // Simple health check - process is running
808
- const isHealthy = serverData.process && !serverData.process.killed;
809
-
810
1007
  status.lastHealthCheck = Date.now();
811
-
812
- if (status.status === 'running') {
813
- status.uptime = Date.now() - (status.lastHealthCheck - config.healthCheckInterval);
1008
+
1009
+ // A process that is gone is unhealthy without needing a round trip.
1010
+ if (!serverData.process || serverData.process.killed) {
1011
+ this.emitServerHealthStatus(serverId, 'unhealthy');
1012
+ return;
814
1013
  }
815
1014
 
816
- // Emit health status
817
- this.emitServerHealthStatus(serverId, isHealthy ? 'healthy' : 'unhealthy');
818
- }
1015
+ // Only probe a server that finished its handshake — one still starting has
1016
+ // not agreed to speak MCP yet.
1017
+ if (status.status !== 'running') {
1018
+ this.emitServerHealthStatus(serverId, 'unhealthy');
1019
+ return;
1020
+ }
819
1021
 
820
- /**
821
- * Handle server output for tool discovery
822
- *
823
- * MCP protocol message parsing is handled in:
824
- * - initializeMcpConnection(): Handles MCP initialize handshake
825
- * - discoverRealToolsFromServer(): Handles tools/list JSON-RPC calls
826
- * - executeToolOnServer(): Handles tools/call JSON-RPC calls
827
- */
828
- private handleServerOutput(serverId: string, output: string): void {
829
- // Server stdout is processed by the MCP JSON-RPC handlers in initializeMcpConnection,
830
- // discoverRealToolsFromServer, and executeToolOnServer methods.
831
- // This handler is used for any additional output logging if needed.
1022
+ try {
1023
+ await this.sendRequest(serverId, 'tools/list');
1024
+ this.emitServerHealthStatus(serverId, 'healthy');
1025
+ } catch (error) {
1026
+ const message = error instanceof Error ? error.message : String(error);
1027
+ logger.warn(`Health check failed for ${serverId}: ${message}`);
1028
+ this.emitServerHealthStatus(serverId, 'unhealthy');
1029
+ }
832
1030
  }
833
1031
 
834
1032
  /**
@@ -907,7 +1105,18 @@ export class ExternalMcpServerManager extends EventEmitter {
907
1105
  }
908
1106
 
909
1107
  /**
910
- * Initialize MCP connection with server
1108
+ * Perform the MCP handshake, then discover the server's tools.
1109
+ *
1110
+ * The handshake is two messages, not one:
1111
+ * 1. `initialize` — request/response, negotiates protocol version.
1112
+ * 2. `notifications/initialized` — a notification telling the server the
1113
+ * client is ready.
1114
+ *
1115
+ * The second was never sent. It is required by the MCP spec, and a strict
1116
+ * server is entitled to reject `tools/list` from a client that never sent it.
1117
+ *
1118
+ * The server is only marked 'running' once this completes. Spawning a process
1119
+ * says nothing about whether it speaks MCP.
911
1120
  */
912
1121
  private async initializeMcpConnection(serverId: string): Promise<void> {
913
1122
  const serverData = this.servers.get(serverId);
@@ -915,9 +1124,8 @@ export class ExternalMcpServerManager extends EventEmitter {
915
1124
  throw new Error(`Server ${serverId} not found or not running`);
916
1125
  }
917
1126
 
918
- // Prevent duplicate initialization
919
1127
  if (serverData.status.initialized) {
920
- logger.warn(`⚠️ MCP connection already initialized for server ${serverData.config.name}`);
1128
+ logger.warn(`MCP connection already initialized for server ${serverData.config.name}`);
921
1129
  return;
922
1130
  }
923
1131
 
@@ -927,203 +1135,74 @@ export class ExternalMcpServerManager extends EventEmitter {
927
1135
  throw new Error('Process streams not available');
928
1136
  }
929
1137
 
930
- // Mark as initializing to prevent concurrent initialization
931
1138
  serverData.status.initializing = true;
932
1139
 
933
-
934
- // Send MCP initialize request with required parameters
935
- const initializeRequest = {
936
- jsonrpc: "2.0",
937
- id: "initialize",
938
- method: "initialize",
939
- params: {
940
- protocolVersion: "2024-11-05",
941
- capabilities: {
942
- experimental: {},
943
- sampling: {}
944
- },
945
- clientInfo: {
946
- name: "MXF Framework",
947
- version: "0.32.0"
948
- }
1140
+ // 1. initialize
1141
+ const result = await this.sendRequest(serverId, 'initialize', {
1142
+ protocolVersion: MCP_PROTOCOL_VERSION,
1143
+ capabilities: {
1144
+ experimental: {},
1145
+ sampling: {}
1146
+ },
1147
+ clientInfo: {
1148
+ name: 'MXF Framework',
1149
+ version: MCP_CLIENT_VERSION
949
1150
  }
950
- };
951
-
952
- const requestJson = JSON.stringify(initializeRequest) + '\n';
953
- process.stdin.write(requestJson);
954
-
955
- // Wait for MCP initialize response
956
- const responseBuffer = await this.waitForMcpResponse(process.stdout, "initialize");
957
- const response = JSON.parse(responseBuffer);
1151
+ });
958
1152
 
959
- if (response.error) {
960
- logger.error(`❌ MCP initialize error: ${response.error.message}`);
961
- throw new Error(`MCP initialize error: ${response.error.message}`);
962
- }
1153
+ logger.info(
1154
+ `[MCP] ${config.name} initialized ` +
1155
+ `(server: ${result?.serverInfo?.name ?? 'unknown'} ${result?.serverInfo?.version ?? ''})`
1156
+ );
963
1157
 
1158
+ // 2. notifications/initialized — required by the spec before any other request
1159
+ this.sendNotification(serverId, 'notifications/initialized');
964
1160
 
965
- // Discover tools after MCP connection is initialized
966
- await this.discoverServerTools(serverId);
967
-
968
- // Mark as successfully initialized
1161
+ // The handshake succeeded, so the server is genuinely serving MCP now.
1162
+ // Discovery below issues tools/list, which requires this status.
1163
+ serverData.status.status = 'running';
969
1164
  serverData.status.initialized = true;
970
1165
  serverData.status.initializing = false;
971
- }
972
-
973
- /**
974
- * Wait for MCP response with specific ID
975
- */
976
- private async waitForMcpResponse(stdout: NodeJS.ReadableStream, requestId: string): Promise<string> {
977
- return new Promise((resolve, reject) => {
978
- let responseBuffer = '';
979
- let timeoutId: NodeJS.Timeout;
980
- let resolved = false;
981
1166
 
982
- const responseHandler = (data: Buffer): void => {
983
- if (resolved) return; // Prevent duplicate responses
984
-
985
- responseBuffer += data.toString();
986
-
987
- // Try to parse JSON response (MCP responses are line-delimited JSON)
988
- const lines = responseBuffer.split('\n');
989
- for (let i = 0; i < lines.length - 1; i++) {
990
- const line = lines[i].trim();
991
- if (line) {
992
- try {
993
- const response = JSON.parse(line.trim());
994
-
995
- // Check if this is our response
996
- if (response.id === requestId) {
997
- resolved = true;
998
- clearTimeout(timeoutId);
999
- stdout.removeListener('data', responseHandler);
1000
-
1001
- // Return the clean JSON line instead of the entire buffer
1002
- resolve(line.trim());
1003
- return;
1004
- }
1005
- } catch (parseError) {
1006
- // Ignore JSON parse errors for incomplete lines
1007
- continue;
1008
- }
1009
- }
1010
- }
1011
-
1012
- // Keep the last incomplete line in buffer
1013
- responseBuffer = lines[lines.length - 1];
1014
- };
1167
+ if (serverData.startupTimer) {
1168
+ clearTimeout(serverData.startupTimer);
1169
+ serverData.startupTimer = undefined;
1170
+ }
1015
1171
 
1016
- // Set up timeout (30 seconds for MCP response - some servers like n8n need more time)
1017
- timeoutId = setTimeout(() => {
1018
- if (!resolved) {
1019
- resolved = true;
1020
- stdout.removeListener('data', responseHandler);
1021
- reject(new Error(`Timeout waiting for MCP response after 30 seconds`));
1022
- }
1023
- }, 30000);
1172
+ this.emitServerEvent(McpEvents.EXTERNAL_SERVER_STARTED, serverId);
1024
1173
 
1025
- // Listen for response
1026
- stdout.on('data', responseHandler);
1027
- });
1174
+ // Now that the connection is live, find out what the server offers.
1175
+ await this.discoverServerTools(serverId);
1028
1176
  }
1029
1177
 
1030
1178
  /**
1031
- * Discover tools from external MCP server using JSON-RPC tools/list method
1179
+ * Ask a server what tools it has.
1032
1180
  */
1033
- private async discoverRealToolsFromServer(serverId: string): Promise<Array<{ name: string; description: string; inputSchema: Record<string, any> }>> {
1181
+ private async discoverRealToolsFromServer(
1182
+ serverId: string
1183
+ ): Promise<Array<{ name: string; description: string; inputSchema: Record<string, any> }>> {
1034
1184
  const serverData = this.servers.get(serverId);
1035
1185
  if (!serverData || !serverData.process) {
1036
1186
  throw new Error(`Server ${serverId} not found or not running`);
1037
1187
  }
1038
1188
 
1039
- const { process, config } = serverData;
1040
-
1041
- return new Promise((resolve, reject) => {
1042
- if (!process.stdin || !process.stdout) {
1043
- reject(new Error('Process streams not available'));
1044
- return;
1045
- }
1046
-
1047
- const requestId = `tools-list-${uuidv4()}`;
1048
-
1049
- // Create MCP tool call request according to the protocol
1050
- const mcpRequest = {
1051
- jsonrpc: '2.0',
1052
- id: requestId,
1053
- method: 'tools/list',
1054
- params: {}
1055
- };
1056
-
1057
- let responseData = '';
1058
- let timeoutId: NodeJS.Timeout;
1059
- let resolved = false;
1189
+ const { config } = serverData;
1060
1190
 
1061
- const handleData = (chunk: Buffer): void => {
1062
- if (resolved) return; // Prevent duplicate responses
1063
-
1064
- responseData += chunk.toString();
1065
-
1066
- // Look for complete JSON-RPC response
1067
- const lines = responseData.split('\n');
1068
- for (let i = 0; i < lines.length - 1; i++) {
1069
- const line = lines[i].trim();
1070
- if (line) {
1071
- try {
1072
- const response = JSON.parse(line.trim());
1073
-
1074
- // Check if this is our response
1075
- if (response.id === requestId) {
1076
- resolved = true;
1077
- clearTimeout(timeoutId);
1078
- process.stdout!.off('data', handleData);
1079
-
1080
- if (response.error) {
1081
- reject(new Error(`MCP Error: ${response.error.message}`));
1082
- return;
1083
- }
1084
-
1085
- if (response.result && response.result.tools) {
1086
- const tools = response.result.tools.map((tool: any) => ({
1087
- name: tool.name,
1088
- description: tool.description || `Tool from ${config.name}`,
1089
- inputSchema: tool.inputSchema || { type: 'object', properties: {}, required: [] }
1090
- }));
1091
-
1092
- resolve(tools);
1093
- } else {
1094
- logger.warn(`⚠️ No tools found in response from ${config.name}`);
1095
- resolve([]);
1096
- }
1097
- return;
1098
- }
1099
- } catch (parseError) {
1100
- // Continue looking for valid JSON in other lines
1101
- }
1102
- }
1103
- }
1104
-
1105
- // Keep the last incomplete line in buffer
1106
- responseData = lines[lines.length - 1];
1107
- };
1191
+ const result = await this.sendRequest(serverId, 'tools/list');
1108
1192
 
1109
- // Set up timeout (15 seconds for tool execution)
1110
- timeoutId = setTimeout(() => {
1111
- if (!resolved) {
1112
- resolved = true;
1113
- process.stdout!.off('data', handleData);
1114
- reject(new Error(`Timeout waiting for tools/list response from ${config.name} after 15 seconds`));
1115
- }
1116
- }, 15000);
1117
-
1118
- // Listen for response
1119
- process.stdout.on('data', handleData);
1193
+ if (!result?.tools) {
1194
+ logger.warn(`No tools in the tools/list response from ${config.name}`);
1195
+ return [];
1196
+ }
1120
1197
 
1121
- // Send tools/list request
1122
- const requestLine = JSON.stringify(mcpRequest) + '\n';
1123
- process.stdin.write(requestLine);
1124
- });
1198
+ return result.tools.map((tool: any) => ({
1199
+ name: tool.name,
1200
+ description: tool.description || `Tool from ${config.name}`,
1201
+ inputSchema: tool.inputSchema || { type: 'object', properties: {}, required: [] }
1202
+ }));
1125
1203
  }
1126
1204
 
1205
+
1127
1206
  /**
1128
1207
  * Handle server errors
1129
1208
  */
@@ -1250,6 +1329,7 @@ export class ExternalMcpServerManager extends EventEmitter {
1250
1329
  'SYSTEM' as AgentId,
1251
1330
  'SYSTEM' as ChannelId,
1252
1331
  {
1332
+ serverId,
1253
1333
  name: config.name,
1254
1334
  version: config.version,
1255
1335
  tools
@@ -1357,12 +1437,29 @@ export class ExternalMcpServerManager extends EventEmitter {
1357
1437
  * Execute a tool on an external MCP server via JSON-RPC with auto-correction support
1358
1438
  */
1359
1439
  public async executeToolOnServer(
1360
- serverId: string,
1361
- toolName: string,
1440
+ serverId: string,
1441
+ toolName: string,
1362
1442
  input: any,
1363
- agentId: string = 'system',
1364
- channelId: string = 'default'
1443
+ agentId: string,
1444
+ channelId: string
1365
1445
  ): Promise<any> {
1446
+ // Identity is required. These parameters used to default to 'system' and
1447
+ // 'default', which quietly defeated the registry's own rule that every tool
1448
+ // execution carry a real agentId and channelId — and made auto-correction
1449
+ // learn against a fake agent.
1450
+ if (typeof agentId !== 'string' || agentId.length === 0) {
1451
+ throw new Error(
1452
+ `executeToolOnServer requires an agentId (tool "${toolName}" on server "${serverId}"). ` +
1453
+ `External tool calls must be attributable to an agent.`
1454
+ );
1455
+ }
1456
+ if (typeof channelId !== 'string' || channelId.length === 0) {
1457
+ throw new Error(
1458
+ `executeToolOnServer requires a channelId (tool "${toolName}" on server "${serverId}"). ` +
1459
+ `External tool calls must be attributable to a channel.`
1460
+ );
1461
+ }
1462
+
1366
1463
  // PROACTIVE CORRECTION: Apply known corrections before attempting execution
1367
1464
  // NOTE: n8n-mcp server has built-in n8n_autofix_workflow and validation tools,
1368
1465
  // so proactive correction is not needed for n8n_* tools. This is kept for
@@ -1425,11 +1522,14 @@ export class ExternalMcpServerManager extends EventEmitter {
1425
1522
  }
1426
1523
 
1427
1524
  /**
1428
- * Internal method to execute tool once (without retry logic)
1525
+ * Execute a tool once, without the retry/auto-correction wrapper.
1526
+ *
1527
+ * The reply is matched by request id through the server's single stdout
1528
+ * listener — see sendRequest() and handleServerOutput().
1429
1529
  */
1430
1530
  private async executeToolOnServerInternal(
1431
- serverId: string,
1432
- toolName: string,
1531
+ serverId: string,
1532
+ toolName: string,
1433
1533
  input: any
1434
1534
  ): Promise<any> {
1435
1535
  const serverData = this.servers.get(serverId);
@@ -1441,100 +1541,18 @@ export class ExternalMcpServerManager extends EventEmitter {
1441
1541
  throw new Error(`Server ${serverId} is not running`);
1442
1542
  }
1443
1543
 
1444
- // Verify tool exists on server
1544
+ // Verify the server actually offers this tool before calling it.
1445
1545
  const tool = serverData.status.tools.find(t => t.name === toolName);
1446
1546
  if (!tool) {
1447
- throw new Error(`Tool ${toolName} not found on server ${serverId}`);
1547
+ throw new Error(
1548
+ `Tool ${toolName} not found on server ${serverId}. ` +
1549
+ `Available: ${serverData.status.tools.map(t => t.name).join(', ') || 'none'}`
1550
+ );
1448
1551
  }
1449
1552
 
1450
- return new Promise((resolve, reject) => {
1451
- try {
1452
- const requestId = uuidv4();
1453
-
1454
- // Create MCP tool call request according to the protocol
1455
- const mcpRequest = {
1456
- jsonrpc: "2.0",
1457
- id: requestId,
1458
- method: "tools/call",
1459
- params: {
1460
- name: toolName,
1461
- arguments: input
1462
- }
1463
- };
1464
-
1465
- let responseBuffer = '';
1466
- let timeoutId: NodeJS.Timeout;
1467
- let resolved = false;
1468
-
1469
- const responseHandler = (data: Buffer): void => {
1470
- if (resolved) return; // Prevent duplicate responses
1471
-
1472
- responseBuffer += data.toString();
1473
-
1474
- // Try to parse JSON response (MCP responses are line-delimited JSON)
1475
- const lines = responseBuffer.split('\n');
1476
- for (let i = 0; i < lines.length - 1; i++) {
1477
- const line = lines[i].trim();
1478
- if (line) {
1479
- try {
1480
- const response = JSON.parse(line.trim());
1481
-
1482
- // Check if this is our response
1483
- if (response.id === requestId) {
1484
- resolved = true;
1485
- clearTimeout(timeoutId);
1486
- serverData.process!.stdout!.removeListener('data', responseHandler);
1487
-
1488
- if (response.error) {
1489
- const errorMessage = `MCP error: ${response.error.message || response.error}`;
1490
- logger.error(`❌ MCP tool call error: ${errorMessage}`);
1491
- reject(new Error(errorMessage));
1492
- } else {
1493
- resolve(response.result);
1494
- }
1495
- return;
1496
- }
1497
- } catch (parseError) {
1498
- // Ignore JSON parse errors for incomplete lines
1499
- continue;
1500
- }
1501
- }
1502
- }
1503
-
1504
- // Keep the last incomplete line in buffer
1505
- responseBuffer = lines[lines.length - 1];
1506
- };
1507
-
1508
- // Set up timeout (30 seconds for tool execution)
1509
- timeoutId = setTimeout(() => {
1510
- if (!resolved) {
1511
- resolved = true;
1512
- serverData.process!.stdout!.removeListener('data', responseHandler);
1513
- reject(new Error(`Timeout waiting for tool execution response from ${serverId} after 30 seconds`));
1514
- }
1515
- }, 30000);
1516
-
1517
- // Listen for response
1518
- if (!serverData.process || !serverData.process.stdout) {
1519
- reject(new Error(`Server ${serverId} process stdout not available`));
1520
- return;
1521
- }
1522
- serverData.process.stdout.on('data', responseHandler);
1523
-
1524
- // Send the request
1525
- if (!serverData.process.stdin) {
1526
- reject(new Error(`Server ${serverId} process stdin not available`));
1527
- return;
1528
- }
1529
- const requestJson = JSON.stringify(mcpRequest) + '\n';
1530
- serverData.process.stdin.write(requestJson);
1531
-
1532
-
1533
- } catch (error) {
1534
- const errorMessage = error instanceof Error ? error.message : String(error);
1535
- logger.error(`❌ Failed to execute tool ${toolName} on server ${serverId}: ${errorMessage}`);
1536
- reject(error);
1537
- }
1553
+ return this.sendRequest(serverId, 'tools/call', {
1554
+ name: toolName,
1555
+ arguments: input
1538
1556
  });
1539
1557
  }
1540
1558
  }