diff --git a/README.md b/README.md index b6eb964..848a364 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Toolomics

- A suite of MCP-based Tools from the HolobiomicsLab. Used by AI-Agents such as ***Mimosa-AI*** + A suite of MCP-based Tools from the HolobiomicsLab. Used by AI-Agents such as Mimosa-AI

@@ -30,7 +30,19 @@ ## Install & deploy tools -### Deploy all tools automatically +### Deploy all automatically with default setup for Mimosa-AI + +The simplest way is to simply run: + +```bash +./start.sh +``` + +This would create a workspace `workspace/` and start all Toolomics MCPs servers enabled using port in the range `5000-5200`. + +**Some MCPs server running in docker can take several minutes to start on first run.** + +### Deploy all tools automatically with custom port range and workspace ```bash ./start.sh diff --git a/deploy.py b/deploy.py index ee3cd78..d415284 100644 --- a/deploy.py +++ b/deploy.py @@ -588,10 +588,12 @@ def save_config(self, config: Dict[str, dict]) -> None: def assign_ports(self, server_files: List[Path], compose_files: List[Path] = None, starting_port: int = HOST_PORT_MIN, host_port_min: int = HOST_PORT_MIN, - host_port_max: int = HOST_PORT_MAX) -> Dict[str, dict]: + host_port_max: int = HOST_PORT_MAX, + enable_new: bool = False) -> Dict[str, dict]: """ Assign ports to server files and docker-compose files with proper range management. - Preserves enabled status for existing servers, new servers are enabled by default. + Preserves enabled status for existing servers, new servers are disabled by default + unless enable_new=True is passed (e.g. via --enable-all flag). Returns dict mapping path to {"port": int, "enabled": bool} """ config = self.load_config() @@ -618,9 +620,10 @@ def assign_ports(self, server_files: List[Path], compose_files: List[Path] = Non if next_host_port > host_port_max: raise RuntimeError(f"No available ports in host range ({host_port_min}-{host_port_max}) for server {server_str}") - config[server_str] = {'port': next_host_port, 'enabled': False} + config[server_str] = {'port': next_host_port, 'enabled': enable_new} used_ports.add(next_host_port) - logger.info(f"Assigned host port {next_host_port} to {server_str} (disabled - edit config to enable)") + status = "enabled" if enable_new else "disabled - edit config to enable" + logger.info(f"Assigned host port {next_host_port} to {server_str} ({status})") next_host_port += 1 # Assign ports to docker-compose files @@ -636,9 +639,10 @@ def assign_ports(self, server_files: List[Path], compose_files: List[Path] = Non if next_host_port > host_port_max: raise RuntimeError(f"No available ports in host range ({host_port_min}-{host_port_max}) for compose {compose_str}") - config[compose_str] = {'port': next_host_port, 'enabled': False} + config[compose_str] = {'port': next_host_port, 'enabled': enable_new} used_ports.add(next_host_port) - logger.info(f"Assigned host port {next_host_port} to {compose_str} (disabled - edit config to enable)") + status = "enabled" if enable_new else "disabled - edit config to enable" + logger.info(f"Assigned host port {next_host_port} to {compose_str} ({status})") next_host_port += 1 self.save_config(config) @@ -675,7 +679,8 @@ def _signal_handler(self, signum, frame): def deploy(self, skip_docker: bool = False, starting_port: int = HOST_PORT_MIN, host_port_min: int = HOST_PORT_MIN, - host_port_max: int = HOST_PORT_MAX): + host_port_max: int = HOST_PORT_MAX, + enable_all: bool = False): """Deploy all MCP servers and Docker services""" if not self.mcp_dir.exists(): raise FileNotFoundError(f"MCP directory {self.mcp_dir} does not exist") @@ -686,7 +691,7 @@ def deploy(self, skip_docker: bool = False, starting_port: int = HOST_PORT_MIN, # Assign ports to all services logger.info("Assigning ports to all services...") - port_config = self.config_manager.assign_ports(server_files, compose_files, starting_port, host_port_min, host_port_max) + port_config = self.config_manager.assign_ports(server_files, compose_files, starting_port, host_port_min, host_port_max, enable_new=enable_all) # Start Docker services if not skip_docker: @@ -827,6 +832,7 @@ def main(): parser.add_argument("--host_port_min", type=int, default=HOST_PORT_MIN, help="Minimum port for port assignment range.") parser.add_argument("--host_port_max", type=int, default=HOST_PORT_MAX, help="Maximum port for port assignment range.") parser.add_argument("--no-docker", action="store_true", help="Skip Docker services") + parser.add_argument("--enable-all", action="store_true", help="Enable all newly discovered MCP servers immediately (skip manual config editing)") parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging") args = parser.parse_args() @@ -844,7 +850,8 @@ def main(): skip_docker=args.no_docker, starting_port=args.starting_port, host_port_min=args.host_port_min, - host_port_max=args.host_port_max + host_port_max=args.host_port_max, + enable_all=args.enable_all ) except Exception as e: diff --git a/mcp_host/mcp_search/server.py b/mcp_host/mcp_search/server.py deleted file mode 100644 index 929716a..0000000 --- a/mcp_host/mcp_search/server.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env python3 - -""" -MCP Server for Searching known MCP Servers with smithery - -Note: Work in progress, fix 422 unprocessable entity error ? -Author: Martin Legrand - HolobiomicsLab, CNRS -""" - -import sys -import os -import requests -from fastmcp import FastMCP -from typing import Dict, Any, Optional -from urllib.parse import urljoin - -description = """ -Search MCP Server allows to search for existing MCP servers registered in the Smithery registry. -It provides tools to search for servers by name, or keywords, and retrieve detailed information about specific servers. -""" - -mcp = FastMCP( - name="discover MCP", - instructions=description, -) - -class MCPRegistryClient: - """Client for interacting with the Smithery MCP registry.""" - - def __init__(self, api_key: Optional[str] = None): - self.base_url = "https://registry.smithery.ai" - self.headers = {"Content-Type": "application/json"} - if api_key: - self.headers["Authorization"] = f"Bearer {api_key}" - else: - raise ValueError("API key is required to access the MCP registry") - - def _make_request( - self, - method: str, - endpoint: str, - params: Optional[Dict] = None, - data: Optional[Dict] = None, - ) -> Dict[str, Any]: - url = urljoin(self.base_url.rstrip(), endpoint) - try: - response = requests.request( - method=method, url=url, headers=self.headers, params=params, json=data - ) - response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError as e: - raise requests.exceptions.HTTPError(f"API request failed: {str(e)}") - except requests.exceptions.RequestException as e: - raise requests.exceptions.RequestException(f"Network error: {str(e)}") - - def list_servers( - self, query: str = "", page: int = 1, page_size: int = 100 - ) -> Dict[str, Any]: - """List all MCP servers from registry.""" - params = {"page": page, "pageSize": page_size} - if query: - params["q"] = query - return self._make_request("GET", "/servers", params=params) - - def get_server_details(self, qualified_name: str) -> Dict[str, Any]: - """Get detailed information about a specific MCP server.""" - endpoint = f"/servers/{qualified_name}" - - return self._make_request("GET", endpoint) - - -apikey = os.getenv("SMITHERY_API_KEY") -if not apikey: - raise ValueError( - "SMITHERY_API_KEY environment variable is not set. Please set it to access the MCP registry." - ) -registry_client = MCPRegistryClient(api_key=apikey) - - -@mcp.tool() -def search_mcp_servers(query: str, limit: int = 10) -> Dict[str, Any]: - """ - Search for MCP servers by name, description, or keywords. - - Args: - query: Search term to match against server names and descriptions - limit: Maximum number of results to return (default: 10, max: 50) - - Returns: - Dictionary containing matching servers with their details - """ - limit = max(1, min(limit, 50)) - - servers_response = registry_client.list_servers(page_size=1000) - - if "error" in servers_response: - return {"success": False, "error": servers_response["error"], "servers": []} - - servers = servers_response.get("servers", []) - if not servers: - return { - "success": True, - "message": "No servers found in registry", - "servers": [], - } - formatted_servers = [] - for server in servers: - formatted_servers.append( - { - "name": server.get("name", ""), - "display_name": server.get("displayName", ""), - "description": server.get("description", ""), - "connections": server.get("connections", []), - "tools": server.get("tools", []), - } - ) - - return { - "success": True, - "query": query, - "total_returned": len(formatted_servers), - "servers": formatted_servers, - } - - -@mcp.tool() -def get_mcp_server_info(qualified_name: str) -> Dict[str, Any]: - """ - Get detailed information about a specific MCP server. - - Args: - qualified_name: The qualified name of the MCP server (e.g., "smithery-ai/fetch") - - Returns: - Dictionary containing detailed server information including tools and security status - """ - if not qualified_name: - return {"success": False, "error": "qualified_name is required"} - - server_details = registry_client.get_server_details(qualified_name) - - if "error" in server_details: - return { - "success": False, - "error": server_details["error"], - "qualified_name": qualified_name, - } - security = server_details.get("security", {}) - tools = server_details.get("tools", []) or [] - - return { - "success": True, - "server": { - "name": server_details.get("name", ""), - "display_name": server_details.get("displayName", ""), - "description": server_details.get("description", ""), - "icon_url": server_details.get("iconUrl"), - "connections": server_details.get("connections", []), - "security": { - "scan_passed": security.get("scanPassed"), - "scan_details": security.get( - "scanDetails", "Security scan status unknown" - ), - }, - "tools": tools, - "tool_count": len(tools), - "tool_summary": [ - { - "name": tool.get("name", ""), - "description": tool.get("description", ""), - "input_schema": tool.get("inputSchema", {}), - } - for tool in tools - ], - }, - } - - -if __name__ == "__main__": - print("Starting MCP Search server with streamable-http transport...") - # Get port from environment variable (set by ToolHive) or command line argument as fallback - port = None - if "MCP_PORT" in os.environ: - port = int(os.environ["MCP_PORT"]) - print(f"Using port from MCP_PORT environment variable: {port}") - elif "FASTMCP_PORT" in os.environ: - port = int(os.environ["FASTMCP_PORT"]) - print(f"Using port from FASTMCP_PORT environment variable: {port}") - elif len(sys.argv) == 2: - port = int(sys.argv[1]) - print(f"Using port from command line argument: {port}") - else: - print("Usage: python server.py ") - print("Or set MCP_PORT/FASTMCP_PORT environment variable") - sys.exit(1) - - print(f"Starting server on port {port}") - mcp.run(transport="streamable-http", port=port, host="127.0.0.1") diff --git a/mcp_host/shell/server.py b/mcp_host/shell/server.py index 11d71f1..b877940 100644 --- a/mcp_host/shell/server.py +++ b/mcp_host/shell/server.py @@ -3,17 +3,23 @@ """ Shell Tools MCP Server -Provides tools for shell navigation and interaction. +Provides tools for shell navigation and interaction with parallel command execution. Author: Martin Legrand - HolobiomicsLab, CNRS """ import os import sys +import asyncio +import uuid +from dataclasses import dataclass, field +from typing import Dict, Optional from fastmcp import FastMCP import shlex -import subprocess from pathlib import Path +from concurrent.futures import ThreadPoolExecutor +import threading +import time project_root = Path(__file__).resolve().parent.parent.parent sys.path.append(str(project_root)) # Add 'a/' to Python's search path @@ -31,19 +37,156 @@ instructions=description, ) + +@dataclass +class CommandTask: + """Represents a command task in the queue.""" + command_id: str + command: str + timeout: int + future: asyncio.Future + created_at: float = field(default_factory=time.time) + + +class CommandQueueExecutor: + """ + Manages parallel command execution with a queue system. + + This executor allows multiple commands to be processed concurrently, + preventing blocking when multiple agents use the tool simultaneously. + """ + + def __init__(self, max_concurrent: int = 10): + """ + Initialize the command queue executor. + + Args: + max_concurrent: Maximum number of commands that can run in parallel + """ + self.max_concurrent = max_concurrent + self._semaphore: Optional[asyncio.Semaphore] = None + self._queue: asyncio.Queue = None + self._active_tasks: Dict[str, CommandTask] = {} + self._lock = threading.Lock() + self._executor = ThreadPoolExecutor(max_workers=max_concurrent) + self._initialized = False + self._loop: Optional[asyncio.AbstractEventLoop] = None + + def _ensure_initialized(self): + """Ensure the executor is initialized with proper asyncio primitives.""" + if not self._initialized: + try: + self._loop = asyncio.get_running_loop() + except RuntimeError: + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + + self._semaphore = asyncio.Semaphore(self.max_concurrent) + self._queue = asyncio.Queue() + self._initialized = True + + async def submit_command(self, command: str, timeout: int = 300) -> CommandResult: + """ + Submit a command for execution and wait for the result. + + This method allows multiple commands to be processed concurrently + using a semaphore to limit the number of parallel executions. + + Args: + command: The shell command to execute + timeout: Command timeout in seconds + + Returns: + CommandResult with the execution outcome + """ + self._ensure_initialized() + + command_id = str(uuid.uuid4())[:8] + print(f"[Queue] Received command {command_id}: {command[:50]}...") + + # Use semaphore to limit concurrent executions + async with self._semaphore: + print(f"[Queue] Executing command {command_id} (active: {self.max_concurrent - self._semaphore._value}/{self.max_concurrent})") + + with self._lock: + task = CommandTask( + command_id=command_id, + command=command, + timeout=timeout, + future=asyncio.get_event_loop().create_future() + ) + self._active_tasks[command_id] = task + + try: + # Run the blocking subprocess in a thread pool to avoid blocking the event loop + result = await asyncio.get_event_loop().run_in_executor( + self._executor, + self._run_command_sync, + command, + timeout, + command_id + ) + return result + finally: + with self._lock: + self._active_tasks.pop(command_id, None) + print(f"[Queue] Completed command {command_id}") + + def _run_command_sync(self, command: str, timeout: int, command_id: str) -> CommandResult: + """ + Synchronously run a command (called from thread pool). + + Args: + command: The shell command to execute + timeout: Command timeout in seconds + command_id: Unique identifier for this command + + Returns: + CommandResult with the execution outcome + """ + return run_bash_subprocess(command, timeout=timeout, command_id=command_id) + + def get_queue_status(self) -> dict: + """Get the current status of the command queue.""" + with self._lock: + return { + "max_concurrent": self.max_concurrent, + "active_commands": len(self._active_tasks), + "available_slots": self._semaphore._value if self._semaphore else self.max_concurrent, + "active_command_ids": list(self._active_tasks.keys()) + } + + def shutdown(self): + """Shutdown the executor and clean up resources.""" + self._executor.shutdown(wait=True) + + +# Global command queue executor +command_executor = CommandQueueExecutor(max_concurrent=10) + + def run_bash_subprocess( command: str, timeout: int = 300, - max_output_size: int = 32000 + max_output_size: int = 32000, + command_id: str = None ) -> CommandResult: """ Run command with streaming output to prevent buffer deadlock. + + Args: + command: The shell command to execute + timeout: Command timeout in seconds + max_output_size: Maximum output size in bytes + command_id: Optional identifier for logging """ - import os - import time - + import subprocess + import select + import fcntl + cwd = "/app/workspace" - + log_prefix = f"[{command_id}] " if command_id else "" + # Force directory cache refresh for Docker bind mounts # The dentry cache can hold stale directory listings - we need aggressive invalidation try: @@ -56,12 +199,12 @@ def run_bash_subprocess( pass # fsync may fail on read-only directory fd, that's OK finally: os.close(dir_fd) - + # Method 2: Touch the directory to invalidate caches os.utime(cwd, None) except (OSError, PermissionError): pass - + # Method 3: Use scandir which opens a fresh directory stream try: with os.scandir(cwd) as entries: @@ -69,7 +212,7 @@ def run_bash_subprocess( _ = [e.name for e in entries] except Exception: pass - + # Verify the directory exists and is accessible if not os.path.exists(cwd): return CommandResult( @@ -77,20 +220,18 @@ def run_bash_subprocess( stderr=f"Workspace directory does not exist: {cwd}", exit_code=-1, ) - + if not os.access(cwd, os.R_OK | os.W_OK | os.X_OK): return CommandResult( status="error", stderr=f"Workspace directory is not accessible: {cwd}", exit_code=-1, ) - - print(f"Running command: {command} with timeout: {timeout} seconds in {cwd}") + + print(f"{log_prefix}Running command: {command} with timeout: {timeout} seconds in {cwd}") start_time = time.time() - + try: - import select - proc = subprocess.Popen( command, shell=True, @@ -101,34 +242,33 @@ def run_bash_subprocess( # Use bytes mode for proper non-blocking I/O with select() text=False ) - + stdout_chunks = [] stderr_chunks = [] stdout_size = 0 stderr_size = 0 - + # Get file descriptors for direct os.read() calls stdout_fd = proc.stdout.fileno() if proc.stdout else None stderr_fd = proc.stderr.fileno() if proc.stderr else None - + # Set non-blocking mode on file descriptors - import fcntl if stdout_fd is not None: flags = fcntl.fcntl(stdout_fd, fcntl.F_GETFL) fcntl.fcntl(stdout_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) if stderr_fd is not None: flags = fcntl.fcntl(stderr_fd, fcntl.F_GETFL) fcntl.fcntl(stderr_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) - + # Track if streams are still open stdout_open = stdout_fd is not None stderr_open = stderr_fd is not None - + while stdout_open or stderr_open: # Check timeout elapsed = time.time() - start_time if elapsed > timeout: - print(f"Command timeout after {elapsed:.2f}s: {command}") + print(f"{log_prefix}Command timeout after {elapsed:.2f}s: {command}") proc.kill() proc.wait(timeout=5) return CommandResult( @@ -136,23 +276,23 @@ def run_bash_subprocess( stderr=f"Command timed out after {timeout} seconds", exit_code=-1 ) - + # Build list of open streams for select readable = [] if stdout_open: readable.append(proc.stdout) if stderr_open: readable.append(proc.stderr) - + if not readable: break - + try: ready_to_read, _, _ = select.select(readable, [], [], 0.1) except (ValueError, OSError): # Pipes closed break - + # Read from ready streams using os.read() for truly non-blocking I/O for stream in ready_to_read: try: @@ -180,37 +320,37 @@ def run_bash_subprocess( stdout_open = False elif stream == proc.stderr: stderr_open = False - + # Also check if process finished (helps detect EOF sooner) if proc.poll() is not None: # Process done, do one more iteration to drain any remaining data # After that, the next iteration will find empty reads and exit pass - + # Wait for process to complete if not already try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() proc.wait() - + duration = time.time() - start_time exit_code = proc.returncode - + # Decode bytes to text stdout_text = b''.join(stdout_chunks)[:max_output_size].decode('utf-8', errors='replace') stderr_text = b''.join(stderr_chunks)[:max_output_size].decode('utf-8', errors='replace') - - print(f"Command completed in {duration:.2f}s with exit code {exit_code}") - print(f"Stdout length: {len(stdout_text)}, Stderr length: {len(stderr_text)}") - + + print(f"{log_prefix}Command completed in {duration:.2f}s with exit code {exit_code}") + print(f"{log_prefix}Stdout length: {len(stdout_text)}, Stderr length: {len(stderr_text)}") + return CommandResult( status="success" if exit_code == 0 else "error", stdout=stdout_text, stderr=stderr_text, exit_code=exit_code, ) - + except subprocess.TimeoutExpired: return CommandResult( status="error", @@ -219,7 +359,7 @@ def run_bash_subprocess( ) except Exception as e: import traceback - print(f"Exception in run_bash_subprocess: {e}") + print(f"{log_prefix}Exception in run_bash_subprocess: {e}") traceback.print_exc() return CommandResult( status="error", @@ -228,13 +368,15 @@ def run_bash_subprocess( ) - @mcp.tool @return_as_dict -def execute_command(command: str, timeout: int = 300) -> dict: +async def execute_command(command: str, timeout: int = 300) -> dict: """ Execute a shell command and return the output with better error handling and security. + This tool uses a command queue system that allows multiple commands to run in parallel, + preventing blocking when multiple agents use the tool simultaneously. + Args: command (str): The shell command to execute timeout (int): Command timeout in seconds (default: 300 = 5 minutes, max: 7200 = 2 hours) @@ -251,7 +393,7 @@ def execute_command(command: str, timeout: int = 300) -> dict: if timeout > 7200: print(f"Warning: Timeout {timeout}s exceeds maximum 7200s (2 hours), capping to 7200s") timeout = 7200 - + print(f"Executing command: {command} (timeout: {timeout}s)") dangerous_patterns = [ @@ -279,7 +421,26 @@ def execute_command(command: str, timeout: int = 300) -> dict: exit_code=-1, ) - return run_bash_subprocess(command, timeout=timeout) + # Submit command to the queue for parallel execution + return await command_executor.submit_command(command, timeout=timeout) + + +@mcp.tool +def get_command_queue_status() -> dict: + """ + Get the current status of the command execution queue. + + Returns information about active commands and available execution slots. + Useful for monitoring parallel command execution. + + Returns: + dict: Queue status including: + - max_concurrent: Maximum parallel commands allowed + - active_commands: Number of currently running commands + - available_slots: Number of available execution slots + - active_command_ids: List of active command identifiers + """ + return command_executor.get_queue_status() print("Starting Shell MCP server with streamable-http transport...") @@ -301,4 +462,5 @@ def execute_command(command: str, timeout: int = 300) -> dict: sys.exit(1) print(f"Starting server on port {port}") + print(f"Command queue initialized with max {command_executor.max_concurrent} concurrent executions") mcp.run(transport="streamable-http", port=port, host="0.0.0.0") diff --git a/start.sh b/start.sh index fc3e468..d6a630c 100755 --- a/start.sh +++ b/start.sh @@ -3,6 +3,13 @@ # Use PYTHON_PATH environment variable if set, otherwise default to python3 PYTHON=${PYTHON_PATH:-python3} +# Detect no-argument mode: use defaults and skip all interactive prompts +if [ $# -eq 0 ]; then + NO_INPUT=true +else + NO_INPUT=false +fi + # Function to check if Python is available check_python() { if ! command -v "$PYTHON" &> /dev/null; then @@ -19,8 +26,7 @@ check_python() { check_pip() { if ! $PYTHON -m pip --version &> /dev/null; then echo "pip is not installed for $PYTHON." - read -p "Would you like to install pip? (y/n): " install_pip - if [[ "$install_pip" =~ ^[Yy]$ ]]; then + if [ "$NO_INPUT" = true ]; then echo "Installing pip..." $PYTHON -m ensurepip --upgrade if ! $PYTHON -m pip --version &> /dev/null; then @@ -29,8 +35,19 @@ check_pip() { fi echo "pip installed successfully!" else - echo "Error: pip is required to install dependencies." - exit 1 + read -p "Would you like to install pip? (y/n): " install_pip + if [[ "$install_pip" =~ ^[Yy]$ ]]; then + echo "Installing pip..." + $PYTHON -m ensurepip --upgrade + if ! $PYTHON -m pip --version &> /dev/null; then + echo "Error: pip installation failed." + exit 1 + fi + echo "pip installed successfully!" + else + echo "Error: pip is required to install dependencies." + exit 1 + fi fi else echo "pip found: $($PYTHON -m pip --version)" @@ -41,21 +58,31 @@ check_pip() { install_requirements() { if [ -f "requirements.txt" ]; then echo "Found requirements.txt" - read -p "Would you like to install dependencies from requirements.txt? (y/n): " install_deps - if [[ "$install_deps" =~ ^[Yy]$ ]]; then + if [ "$NO_INPUT" = true ]; then echo "Installing dependencies..." $PYTHON -m pip install -r requirements.txt if [ $? -eq 0 ]; then echo "Dependencies installed successfully!" else - echo "Warning: Some dependencies may have failed to install." - read -p "Do you want to continue anyway? (y/n): " continue_anyway - if [[ ! "$continue_anyway" =~ ^[Yy]$ ]]; then - exit 1 - fi + echo "Warning: Some dependencies may have failed to install. Continuing anyway." fi else - echo "Skipping dependency installation." + read -p "Would you like to install dependencies from requirements.txt? (y/n): " install_deps + if [[ "$install_deps" =~ ^[Yy]$ ]]; then + echo "Installing dependencies..." + $PYTHON -m pip install -r requirements.txt + if [ $? -eq 0 ]; then + echo "Dependencies installed successfully!" + else + echo "Warning: Some dependencies may have failed to install." + read -p "Do you want to continue anyway? (y/n): " continue_anyway + if [[ ! "$continue_anyway" =~ ^[Yy]$ ]]; then + exit 1 + fi + fi + else + echo "Skipping dependency installation." + fi fi else echo "No requirements.txt found in current directory." @@ -70,34 +97,41 @@ install_requirements echo "=== Prerequisites Check Complete ===" echo "" -# Validate arguments -if [ $# -lt 2 ] || [ $# -gt 3 ]; then - echo "Error: Expected 2-3 arguments" - echo "Usage: $0 [workspace]" - echo "Example: $0 5000 5200" - echo "Example: $0 5000 5200 /path/to/workspace" - exit 1 -fi +# Set defaults when no arguments provided +if [ "$NO_INPUT" = true ]; then + START_PORT=5000 + END_PORT=5200 + WORKSPACE=workspace +else + # Validate arguments + if [ $# -lt 2 ] || [ $# -gt 3 ]; then + echo "Error: Expected 2-3 arguments" + echo "Usage: $0 [workspace]" + echo "Example: $0 5000 5200" + echo "Example: $0 5000 5200 /path/to/workspace" + exit 1 + fi -# Check if arguments are valid integers -if ! [[ "$1" =~ ^[0-9]+$ ]] || ! [[ "$2" =~ ^[0-9]+$ ]]; then - echo "Error: Arguments must be valid port numbers" - exit 1 -fi + # Check if arguments are valid integers + if ! [[ "$1" =~ ^[0-9]+$ ]] || ! [[ "$2" =~ ^[0-9]+$ ]]; then + echo "Error: Arguments must be valid port numbers" + exit 1 + fi -START_PORT=$1 -END_PORT=$2 -WORKSPACE=${3:-workspace/} + START_PORT=$1 + END_PORT=$2 + WORKSPACE=${3:-workspace/} -# Validate port range -if [ "$START_PORT" -gt "$END_PORT" ]; then - echo "Error: Start port must be less than or equal to end port" - exit 1 -fi + # Validate port range + if [ "$START_PORT" -gt "$END_PORT" ]; then + echo "Error: Start port must be less than or equal to end port" + exit 1 + fi -if [ "$START_PORT" -lt 1 ] || [ "$END_PORT" -gt 65535 ]; then - echo "Error: Ports must be in range 1-65535" - exit 1 + if [ "$START_PORT" -lt 1 ] || [ "$END_PORT" -gt 65535 ]; then + echo "Error: Ports must be in range 1-65535" + exit 1 + fi fi # Check for processes using ports @@ -133,29 +167,35 @@ for ((port=$START_PORT; port<=$END_PORT; port++)); do fi done -# If Python processes found, ask user if they want to kill them +# If Python processes found, ask user if they want to kill them (skipped in no-input mode) if [ "$PYTHON_PROCESSES_FOUND" = true ]; then echo "" - echo "The following Python processes are blocking the required ports:" + echo "The following processes are on the required ports range:" for i in "${!PYTHON_PIDS[@]}"; do echo " - Port ${PYTHON_PORTS[$i]} (PID: ${PYTHON_PIDS[$i]}):" echo " ${PYTHON_COMMANDS[$i]}" done echo "" - read -p "Would you like to kill these Python processes? (y/n): " kill_processes - if [[ "$kill_processes" =~ ^[Yy]$ ]]; then - for pid in "${PYTHON_PIDS[@]}"; do - echo "Killing process $pid..." - kill -9 "$pid" 2>/dev/null - if [ $? -eq 0 ]; then - echo " ✓ Process $pid killed successfully" - else - echo " ✗ Failed to kill process $pid (may require sudo)" - fi - done - echo "" + echo "ℹ️ To restart a Toolomics MCP server (e.g. after modifying it), kill its Python process listed above and re-run this script." + if [ "$NO_INPUT" = false ]; then + read -p "Would you like to kill these Python processes? (y/n): " kill_processes + if [[ "$kill_processes" =~ ^[Yy]$ ]]; then + for pid in "${PYTHON_PIDS[@]}"; do + echo "Killing process $pid..." + kill -9 "$pid" 2>/dev/null + if [ $? -eq 0 ]; then + echo " ✓ Process $pid killed successfully" + else + echo " ✗ Failed to kill process $pid (may require sudo)" + fi + done + echo "" + else + echo "Python processes not killed. Some ports may be unavailable." + echo "" + fi else - echo "Python processes not killed. Some ports may be unavailable." + echo "Skipping port cleanup (no-argument mode)." echo "" fi elif [ "$PROCESSES_FOUND" = true ]; then @@ -191,7 +231,11 @@ echo " Workspace: $WORKSPACE" echo "" echo "Deploying MCP servers..." -$PYTHON deploy.py --config config.json --mcp-dir mcp_host --host_port_min "$START_PORT" --host_port_max "$END_PORT" --workspace $WORKSPACE & +if [ "$NO_INPUT" = true ]; then + $PYTHON deploy.py --config config.json --mcp-dir mcp_host --host_port_min "$START_PORT" --host_port_max "$END_PORT" --workspace $WORKSPACE --enable-all & +else + $PYTHON deploy.py --config config.json --mcp-dir mcp_host --host_port_min "$START_PORT" --host_port_max "$END_PORT" --workspace $WORKSPACE & +fi HOST_PID=$! wait $HOST_PID