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