1212import sys
1313import time
1414import re
15+ import shutil
16+ import subprocess
1517from typing import Dict , List , Any , Optional , Tuple , Set , Union , Callable
1618from dataclasses import dataclass
1719from pathlib import Path
4143# Plugin identifier
4244SLUG = "mcp"
4345
46+ def find_executable (cmd : str ) -> Optional [str ]:
47+ """
48+ Find the full path to an executable command.
49+
50+ This function will:
51+ 1. Check if the command exists in PATH
52+ 2. Check common install locations
53+ 3. Try npm prefix paths
54+
55+ Args:
56+ cmd: The command to find
57+
58+ Returns:
59+ Full path to the executable if found, None otherwise
60+ """
61+ # First check if it's already a full path
62+ if os .path .isfile (cmd ) and os .access (cmd , os .X_OK ):
63+ return cmd
64+
65+ # Next check if it's in PATH
66+ cmd_path = shutil .which (cmd )
67+ if cmd_path :
68+ logger .info (f"Found { cmd } in PATH at { cmd_path } " )
69+ return cmd_path
70+
71+ # Try common locations for Node.js tools
72+ common_paths = [
73+ "/usr/local/bin" ,
74+ "/usr/bin" ,
75+ "/bin" ,
76+ "/opt/homebrew/bin" , # macOS with Homebrew
77+ "/opt/homebrew/opt/node/bin" , # Specific Homebrew Node.js location
78+ "/usr/local/opt/node/bin" ,
79+ os .path .expanduser ("~/.npm-global/bin" ),
80+ os .path .expanduser ("~/.nvm/current/bin" ),
81+ os .path .expanduser ("~/npm/bin" ),
82+ os .path .expanduser ("~/.npm/bin" ),
83+ ]
84+
85+ for path in common_paths :
86+ full_path = os .path .join (path , cmd )
87+ if os .path .isfile (full_path ) and os .access (full_path , os .X_OK ):
88+ logger .info (f"Found { cmd } at { full_path } " )
89+ return full_path
90+
91+ # Try using npm to find global bin path
92+ try :
93+ npm_bin_path = subprocess .run (
94+ ["npm" , "bin" , "-g" ],
95+ capture_output = True ,
96+ text = True ,
97+ check = True
98+ ).stdout .strip ()
99+
100+ full_path = os .path .join (npm_bin_path , cmd )
101+ if os .path .isfile (full_path ) and os .access (full_path , os .X_OK ):
102+ logger .info (f"Found { cmd } in npm global bin at { full_path } " )
103+ return full_path
104+ except :
105+ pass
106+
107+ # If all else fails, create a wrapper script that sources profile files
108+ wrapper_path = create_command_wrapper (cmd )
109+ if wrapper_path :
110+ logger .info (f"Created wrapper script for { cmd } at { wrapper_path } " )
111+ return wrapper_path
112+
113+ logger .error (f"Could not find executable: { cmd } " )
114+ return None
115+
116+ def create_command_wrapper (cmd : str ) -> Optional [str ]:
117+ """
118+ Create a shell wrapper script for a command that might be in PATH after shell initialization.
119+
120+ Args:
121+ cmd: The command to wrap
122+
123+ Returns:
124+ Path to the wrapper script if successful, None otherwise
125+ """
126+ try :
127+ wrapper_dir = Path .home () / ".optillm" / "wrappers"
128+ wrapper_dir .mkdir (parents = True , exist_ok = True )
129+
130+ wrapper_path = wrapper_dir / f"{ cmd } _wrapper.sh"
131+
132+ with open (wrapper_path , 'w' ) as f :
133+ f .write (f"""#!/bin/bash
134+ # Source profile to get correct PATH
135+ if [ -f ~/.bash_profile ]; then
136+ source ~/.bash_profile
137+ elif [ -f ~/.profile ]; then
138+ source ~/.profile
139+ elif [ -f ~/.zshrc ]; then
140+ source ~/.zshrc
141+ fi
142+
143+ # Run command with all arguments
144+ exec { cmd } "$@"
145+ """ )
146+
147+ # Make the script executable
148+ os .chmod (wrapper_path , 0o755 )
149+
150+ return str (wrapper_path )
151+ except Exception as e :
152+ logger .error (f"Error creating wrapper script: { e } " )
153+ return None
154+
44155@dataclass
45156class ServerConfig :
46157 """Configuration for a single MCP server"""
@@ -141,27 +252,36 @@ async def connect(self) -> bool:
141252 try :
142253 logger .info (f"Connecting to MCP server: { self .server_name } " )
143254
144- # Create server parameters
255+ # Find the full path to the command
256+ full_command = find_executable (self .config .command )
257+ if not full_command :
258+ logger .error (f"Failed to find executable for command: { self .config .command } " )
259+ logger .error ("Please make sure the command is in your PATH or use an absolute path" )
260+ return False
261+
262+ # Create environment with PATH included
263+ merged_env = os .environ .copy ()
264+ if self .config .env :
265+ merged_env .update (self .config .env )
266+
267+ # Create server parameters with the full command path
145268 server_params = StdioServerParameters (
146- command = self . config . command ,
269+ command = full_command ,
147270 args = self .config .args ,
148- env = self . config . env
271+ env = merged_env
149272 )
150273
151274 # Create transport using async with
152- transport = None
153275 try :
154- # Using context manager in a way that's compatible with asyncio
276+ # Using context manager directly with try/except/finally for cleanup
155277 ctx = stdio_client (server_params )
156278 transport = await ctx .__aenter__ ()
157279 self .transport = transport
158280
159281 read_stream , write_stream = transport
160282
161- # Create session
283+ # Create and initialize session
162284 self .session = ClientSession (read_stream , write_stream )
163-
164- # Initialize session
165285 await self .session .initialize ()
166286
167287 # Discover capabilities
@@ -172,12 +292,9 @@ async def connect(self) -> bool:
172292 return True
173293
174294 except Exception as e :
175- # Make sure to clean up resources in case of an error
176- if transport :
177- try :
178- await ctx .__aexit__ (type (e ), e , e .__traceback__ )
179- except :
180- pass
295+ # Clean up resources in case of error
296+ if 'ctx' in locals () and 'transport' in locals ():
297+ await ctx .__aexit__ (type (e ), e , e .__traceback__ )
181298 raise
182299
183300 except Exception as e :
0 commit comments