Skip to content

Commit 8f1a3be

Browse files
committed
Update mcp_plugin.py
1 parent 164bd74 commit 8f1a3be

File tree

1 file changed

+131
-14
lines changed

1 file changed

+131
-14
lines changed

optillm/plugins/mcp_plugin.py

Lines changed: 131 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import sys
1313
import time
1414
import re
15+
import shutil
16+
import subprocess
1517
from typing import Dict, List, Any, Optional, Tuple, Set, Union, Callable
1618
from dataclasses import dataclass
1719
from pathlib import Path
@@ -41,6 +43,115 @@
4143
# Plugin identifier
4244
SLUG = "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
45156
class 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

Comments
 (0)