@@ -193,8 +193,9 @@ class MCPHypervisor {
193193
194194 this . log ( `Pruning MCP server: ${ name } ` ) ;
195195 const mcp = this . mcps [ name ] ;
196+ if ( ! mcp . transport ) return true ;
196197 const childProcess = mcp . transport . _process ;
197- if ( childProcess ) childProcess . kill ( 1 ) ;
198+ if ( childProcess ) childProcess . kill ( "SIGTERM" ) ;
198199 mcp . transport . close ( ) ;
199200
200201 delete this . mcps [ name ] ;
@@ -215,10 +216,11 @@ class MCPHypervisor {
215216 for ( const name of Object . keys ( this . mcps ) ) {
216217 if ( ! this . mcps [ name ] ) continue ;
217218 const mcp = this . mcps [ name ] ;
219+ if ( ! mcp . transport ) continue ;
218220 const childProcess = mcp . transport . _process ;
219221 if ( childProcess )
220222 this . log ( `Killing MCP ${ name } (PID: ${ childProcess . pid } )` , {
221- killed : childProcess . kill ( 1 ) ,
223+ killed : childProcess . kill ( "SIGTERM" ) ,
222224 } ) ;
223225
224226 mcp . transport . close ( ) ;
@@ -228,18 +230,51 @@ class MCPHypervisor {
228230 this . mcpLoadingResults = { } ;
229231 }
230232
233+ /**
234+ * Load shell environment for desktop applications.
235+ * MacOS and Linux don't inherit login shell environment. So this function
236+ * fixes the PATH and accessible commands when running AnythingLLM outside of Docker during development on Mac/Linux and in-container (Linux).
237+ * @returns {Promise<{[key: string]: string}> } - Environment variables from shell
238+ */
239+ async #loadShellEnvironment( ) {
240+ try {
241+ if ( process . platform === "win32" ) return process . env ;
242+ const { default : fixPath } = await import ( "fix-path" ) ;
243+ const { default : stripAnsi } = await import ( "strip-ansi" ) ;
244+ fixPath ( ) ;
245+
246+ // Due to node v20 requirement to have a minimum version of fix-path v5, we need to strip ANSI codes manually
247+ // which was the only patch between v4 and v5. Here we just apply manually.
248+ // https://github.com/sindresorhus/fix-path/issues/6
249+ if ( process . env . PATH ) process . env . PATH = stripAnsi ( process . env . PATH ) ;
250+ return process . env ;
251+ } catch ( error ) {
252+ console . warn (
253+ "Failed to load shell environment, using process.env:" ,
254+ error . message
255+ ) ;
256+ return process . env ;
257+ }
258+ }
259+
231260 /**
232261 * Build the MCP server environment variables - ensures proper PATH and NODE_PATH
233262 * inheritance across all platforms and deployment scenarios.
234263 * @param {Object } server - The server definition
235- * @returns {{env: { [key: string]: string } | {}} } - The environment variables
264+ * @returns {Promise< {env: { [key: string]: string } | {}} }> - The environment variables
236265 */
237- #buildMCPServerENV( server ) {
238- // Start with essential environment variables, inheriting from current process
239- // This ensures GUI applications on macOS/Linux get proper PATH inheritance
266+ async #buildMCPServerENV( server ) {
267+ const shellEnv = await this . #loadShellEnvironment( ) ;
240268 let baseEnv = {
241- PATH : process . env . PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" ,
242- NODE_PATH : process . env . NODE_PATH || "/usr/local/lib/node_modules" ,
269+ PATH :
270+ shellEnv . PATH ||
271+ process . env . PATH ||
272+ "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" ,
273+ NODE_PATH :
274+ shellEnv . NODE_PATH ||
275+ process . env . NODE_PATH ||
276+ "/usr/local/lib/node_modules" ,
277+ ...shellEnv , // Include all shell environment variables
243278 } ;
244279
245280 // Docker-specific environment setup
@@ -273,29 +308,57 @@ class MCPHypervisor {
273308 * @returns {MCPServerTypes | null } - The server type
274309 */
275310 #parseServerType( server ) {
276- if ( server . hasOwnProperty ( "command" ) ) return "stdio" ;
277- if ( server . hasOwnProperty ( "url" ) ) return "http" ;
311+ if (
312+ server . type === "sse" ||
313+ server . type === "streamable" ||
314+ server . type === "http"
315+ )
316+ return "http" ;
317+ if ( Object . prototype . hasOwnProperty . call ( server , "command" ) ) return "stdio" ;
318+ if ( Object . prototype . hasOwnProperty . call ( server , "url" ) ) return "http" ;
278319 return "sse" ;
279320 }
280321
281322 /**
282323 * Validate the server definition by type
283324 * - Will throw an error if the server definition is invalid
325+ * @param {string } name - The name of the MCP server
284326 * @param {Object } server - The server definition
285327 * @param {MCPServerTypes } type - The server type
286328 * @returns {void }
287329 */
288- #validateServerDefinitionByType( server , type ) {
330+ #validateServerDefinitionByType( name , server , type ) {
331+ if (
332+ server . type === "sse" ||
333+ server . type === "streamable" ||
334+ server . type === "http"
335+ ) {
336+ if ( ! server . url ) {
337+ throw new Error (
338+ `MCP server "${ name } ": missing required "url" for ${ server . type } transport`
339+ ) ;
340+ }
341+
342+ try {
343+ new URL ( server . url ) ;
344+ } catch ( error ) {
345+ throw new Error ( `MCP server "${ name } ": invalid URL "${ server . url } "` ) ;
346+ }
347+ return ;
348+ }
349+
289350 if ( type === "stdio" ) {
290- if ( server . hasOwnProperty ( "args" ) && ! Array . isArray ( server . args ) )
351+ if (
352+ Object . prototype . hasOwnProperty . call ( server , "args" ) &&
353+ ! Array . isArray ( server . args )
354+ )
291355 throw new Error ( "MCP server args must be an array" ) ;
292356 }
293357
294358 if ( type === "http" ) {
295359 if ( ! [ "sse" , "streamable" ] . includes ( server ?. type ) )
296360 throw new Error ( "MCP server type must have sse or streamable value." ) ;
297361 }
298-
299362 if ( type === "sse" ) return ;
300363 return ;
301364 }
@@ -304,16 +367,16 @@ class MCPHypervisor {
304367 * Setup the server transport by type and server definition
305368 * @param {Object } server - The server definition
306369 * @param {MCPServerTypes } type - The server type
307- * @returns {StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport } - The server transport
370+ * @returns {Promise< StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport> } - The server transport
308371 */
309- #setupServerTransport( server , type ) {
372+ async #setupServerTransport( server , type ) {
310373 // if not stdio then it is http or sse
311374 if ( type !== "stdio" ) return this . createHttpTransport ( server ) ;
312375
313376 return new StdioClientTransport ( {
314377 command : server . command ,
315378 args : server ?. args ?? [ ] ,
316- ...this . #buildMCPServerENV( server ) ,
379+ ...( await this . #buildMCPServerENV( server ) ) ,
317380 } ) ;
318381 }
319382
@@ -328,6 +391,7 @@ class MCPHypervisor {
328391 // If the server block has a type property then use that to determine the transport type
329392 switch ( server . type ) {
330393 case "streamable" :
394+ case "http" :
331395 return new StreamableHTTPClientTransport ( url , {
332396 requestInit : {
333397 headers : server . headers ,
@@ -354,10 +418,10 @@ class MCPHypervisor {
354418 const serverType = this . #parseServerType( server ) ;
355419 if ( ! serverType ) throw new Error ( "MCP server command or url is required" ) ;
356420
357- this . #validateServerDefinitionByType( server , serverType ) ;
421+ this . #validateServerDefinitionByType( name , server , serverType ) ;
358422 this . log ( `Attempting to start MCP server: ${ name } ` ) ;
359423 const mcp = new Client ( { name : name , version : "1.0.0" } ) ;
360- const transport = this . #setupServerTransport( server , serverType ) ;
424+ const transport = await this . #setupServerTransport( server , serverType ) ;
361425
362426 // Add connection event listeners
363427 transport . onclose = ( ) => this . log ( `${ name } - Transport closed` ) ;
@@ -369,10 +433,22 @@ class MCPHypervisor {
369433 // Connect and await the connection with a timeout
370434 this . mcps [ name ] = mcp ;
371435 const connectionPromise = mcp . connect ( transport ) ;
436+
437+ let timeoutId ;
372438 const timeoutPromise = new Promise ( ( _ , reject ) => {
373- setTimeout ( ( ) => reject ( new Error ( "Connection timeout" ) ) , 30_000 ) ; // 30 second timeout
439+ timeoutId = setTimeout (
440+ ( ) => reject ( new Error ( "Connection timeout" ) ) ,
441+ 30_000
442+ ) ; // 30 second timeout
374443 } ) ;
375- await Promise . race ( [ connectionPromise , timeoutPromise ] ) ;
444+
445+ try {
446+ await Promise . race ( [ connectionPromise , timeoutPromise ] ) ;
447+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
448+ } catch ( error ) {
449+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
450+ throw error ;
451+ }
376452 return true ;
377453 }
378454
0 commit comments