diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..423488fd7d0f8b606ec807078f367139784c480a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install arvados-cwl-runner via conda (e.g., from bioconda) +RUN conda install -c bioconda arvados-cwl-runner -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/arvados-cwl-runner_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/arvados-cwl-runner_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/arvados-cwl-runner_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/arvados-cwl-runner_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/arvados-cwl-runner_server.py new file mode 100644 index 0000000000000000000000000000000000000000..bcf352a006238910f8c5b6a06278a09edc300123 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/arvados-cwl-runner_server.py @@ -0,0 +1,270 @@ +import subprocess +import os +from pathlib import Path +from typing import Optional, List, Union + +@mcp.tool() +def arvados_cwl_runner( + workflow: str, + job_order: Optional[str] = None, + project_uuid: Optional[str] = None, + name: Optional[str] = None, + output_name: Optional[str] = None, + local: bool = False, + api: str = "containers", + eval_timeout: float = 20.0, + log_level: str = "INFO", + enable_reuse: bool = True, + submit: bool = True, + wait: bool = True, + priority: int = 1, + cluster_id: Optional[str] = None, + storage_classes: str = "default", + tmpdir_prefix: Optional[str] = None, + debug: bool = False, +): + """ + Run a CWL workflow on Arvados or locally. + + Args: + workflow: Path to the CWL workflow file (.cwl). + job_order: Path to the input parameters file (YAML or JSON). + project_uuid: Arvados project UUID where the workflow should run. + name: Name for the pipeline instance or container request. + output_name: Name for the output collection. + local: Run the workflow locally instead of on the Arvados cluster. + api: Arvados API to use (containers or jobs). Default is containers. + eval_timeout: Time to wait for CWL expression evaluation (seconds). + log_level: Logging level (DEBUG, INFO, WARNING, ERROR). + enable_reuse: Enable job/container reuse. + submit: Submit the workflow to Arvados (True) or run it in the foreground (False). + wait: Wait for the workflow to complete before exiting. + priority: Workflow priority (1-1000). + cluster_id: Specific Arvados cluster ID to submit to. + storage_classes: Comma-separated list of storage classes for outputs. + tmpdir_prefix: Path prefix for temporary directories. + debug: Enable debug logging and keep temporary files. + """ + + # Input validation + workflow_path = Path(workflow) + if not workflow_path.exists(): + return {"error": f"Workflow file not found: {workflow}"} + + cmd = ["arvados-cwl-runner"] + + # Boolean flags + if local: + cmd.append("--local") + if not enable_reuse: + cmd.append("--disable-reuse") + if not submit: + cmd.append("--no-submit") + if not wait: + cmd.append("--no-wait") + if debug: + cmd.append("--debug") + + # String/Value parameters + cmd.extend(["--api", api]) + cmd.extend(["--eval-timeout", str(eval_timeout)]) + cmd.extend(["--log-level", log_level]) + cmd.extend(["--priority", str(priority)]) + cmd.extend(["--collection-storage-classes", storage_classes]) + + if project_uuid: + cmd.extend(["--project-uuid", project_uuid]) + if name: + cmd.extend(["--name", name]) + if output_name: + cmd.extend(["--output-name", output_name]) + if cluster_id: + cmd.extend(["--cluster-id", cluster_id]) + if tmpdir_prefix: + cmd.extend(["--tmpdir-prefix", tmpdir_prefix]) + + # Positional arguments + cmd.append(str(workflow_path)) + + if job_order: + job_order_path = Path(job_order) + if not job_order_path.exists(): + return {"error": f"Job order file not found: {job_order}"} + cmd.append(str(job_order_path)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "failed" + } + +@mcp.tool() +def arvados_cwl_validate( + workflow: str, +): + """ + Validate a CWL workflow file for syntax and Arvados compatibility. + + Args: + workflow: Path to the CWL workflow file. + """ + workflow_path = Path(workflow) + if not workflow_path.exists(): + return {"error": f"Workflow file not found: {workflow}"} + + cmd = ["arvados-cwl-runner", "--validate", str(workflow_path)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "valid" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Validation failed", + "status": "invalid" + } + +@mcp.tool() +def arvados_cwl_create_workflow( + workflow: str, + project_uuid: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, +): + """ + Register a CWL workflow in Arvados as a reusable Workflow object. + + Args: + workflow: Path to the CWL workflow file. + project_uuid: Arvados project UUID where the workflow should be stored. + name: Name for the workflow object in Arvados. + description: Description for the workflow. + """ + workflow_path = Path(workflow) + if not workflow_path.exists(): + return {"error": f"Workflow file not found: {workflow}"} + + cmd = ["arvados-cwl-runner", "--create-workflow"] + + if project_uuid: + cmd.extend(["--project-uuid", project_uuid]) + if name: + cmd.extend(["--name", name]) + if description: + cmd.extend(["--description", description]) + + cmd.append(str(workflow_path)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "failed" + } + +@mcp.tool() +def arvados_cwl_update_workflow( + workflow_uuid: str, + workflow: str, + name: Optional[str] = None, + description: Optional[str] = None, +): + """ + Update an existing Arvados Workflow object with a new CWL definition. + + Args: + workflow_uuid: The UUID of the Arvados Workflow object to update. + workflow: Path to the new CWL workflow file. + name: New name for the workflow object. + description: New description for the workflow. + """ + workflow_path = Path(workflow) + if not workflow_path.exists(): + return {"error": f"Workflow file not found: {workflow}"} + + cmd = ["arvados-cwl-runner", "--update-workflow", workflow_uuid] + + if name: + cmd.extend(["--name", name]) + if description: + cmd.extend(["--description", description]) + + cmd.append(str(workflow_path)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "failed" + } + +@mcp.tool() +def arvados_cwl_create_template( + workflow: str, +): + """ + Create an Arvados Pipeline Template from a CWL workflow (legacy API). + + Args: + workflow: Path to the CWL workflow file. + """ + workflow_path = Path(workflow) + if not workflow_path.exists(): + return {"error": f"Workflow file not found: {workflow}"} + + cmd = ["arvados-cwl-runner", "--create-template", str(workflow_path)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "failed" + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/arvados-cwl-runner_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/arvados-cwl-runner_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ac33e21f727f95a44766c86ff4590df5d624bf34 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/arvados-cwl-runner_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/arvados-cwl-runner_server.py') +SERVER_NAME = 'biosci_arvados_cwl_runner' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..000600a1167daca6b20962f4ae9a2b0d9216b88b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-arvados-cwl-runner: + build: . + image: mcp-arvados-cwl-runner:latest + container_name: mcp-arvados-cwl-runner + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=arvados-cwl-runner + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9e7fbef47cb463330a5767e74fa2ef3533f39628 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - arvados-cwl-runner + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_arvados-cwl-runner/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d25db4d616a48de43407b52e112dc59dc8f0ce5f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install auspice via conda (e.g., from bioconda) +RUN conda install -c bioconda auspice -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/auspice_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/auspice_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/auspice_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/auspice_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/auspice_server.py new file mode 100644 index 0000000000000000000000000000000000000000..76cb7ab07feea71e1588531cb8198ccaaa0dadd7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/auspice_server.py @@ -0,0 +1,398 @@ +import subprocess +from pathlib import Path +from typing import Dict, List, Optional, Any + +# This is a placeholder for the MCP decorator. +# In a real MCP environment, this would be provided by the MCP framework. +def tool(): + def decorator(f): + return f + return decorator + +mcp = type("mcp", (), {"tool": tool})() + + +@mcp.tool() +def view( + dataset_dir: Optional[List[Path]] = None, + host: str = "localhost", + port: int = 4000, + verbose: bool = False, + allow_remote_access: bool = False, + handlers: Optional[Path] = None, + config: Optional[Path] = None, + extend: Optional[Path] = None, +) -> Dict[str, str]: + """ + Starts the Auspice server to view and explore phylogenomic data. + Note: This command starts a long-running server process and may not be suitable + for automated workflows that expect a command to terminate. + + Args: + dataset_dir: Directory of datasets to serve. Can be specified multiple times. + host: Host IP address to listen on. + port: Port to listen on. + verbose: Print more information to the console. + allow_remote_access: Allow remote connections to the server. + handlers: Path to a Javascript file with custom API handlers. + config: Path to a custom config JSON file. + extend: Path to a directory containing custom client code. + + Returns: + A dictionary containing the command executed, stdout, and stderr. + """ + cmd = ["auspice", "view"] + + if dataset_dir: + for d in dataset_dir: + if not d.is_dir(): + raise ValueError(f"Dataset directory not found: {d}") + cmd.extend(["--datasetDir", str(d)]) + + cmd.extend(["--host", host]) + cmd.extend(["--port", str(port)]) + + if verbose: + cmd.append("--verbose") + if allow_remote_access: + cmd.append("--allow-remote-access") + + if handlers: + if not handlers.is_file(): + raise FileNotFoundError(f"Handlers file not found: {handlers}") + cmd.extend(["--handlers", str(handlers)]) + + if config: + if not config.is_file(): + raise FileNotFoundError(f"Config file not found: {config}") + cmd.extend(["--config", str(config)]) + + if extend: + if not extend.is_dir(): + raise ValueError(f"Extend directory not found: {extend}") + cmd.extend(["--extend", str(extend)]) + + try: + # This will block until the server is manually stopped. + result = subprocess.run( + cmd, capture_output=True, text=True, check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Error: 'auspice' command not found. Please ensure it is installed and in your PATH.", + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + } + + +@mcp.tool() +def build( + verbose: bool = False, + extend: Optional[Path] = None, + deploy_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Creates a production bundle of the Auspice client-side app. + The output is typically created in a './dist' directory. + + Args: + verbose: Print more information to the console. + extend: Path to a directory containing custom client code. + deploy_path: Path to deploy the app to (e.g., for GitHub pages). + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + cmd = ["auspice", "build"] + + if verbose: + cmd.append("--verbose") + + if extend: + if not extend.is_dir(): + raise ValueError(f"Extend directory not found: {extend}") + cmd.extend(["--extend", str(extend)]) + + if deploy_path: + cmd.extend(["--deploy-path", deploy_path]) + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True + ) + output_dir = Path("./dist") + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": { + "build_directory": str(output_dir) if output_dir.exists() else "Not created" + } + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Error: 'auspice' command not found. Please ensure it is installed and in your PATH.", + "output_files": {} + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": {} + } + + +@mcp.tool() +def export_v1( + dataset_dir: Path, + output_dir: Path, + verbose: bool = False, +) -> Dict[str, Any]: + """ + Exports auspice v1 JSONs to create a static site. + This is a deprecated command and will be removed in a future version. + + Args: + dataset_dir: Directory of datasets to export. + output_dir: Directory to export the auspice client and datasets to. + verbose: Print more information to the console. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output directory. + """ + cmd = ["auspice", "export", "v1"] + + if not dataset_dir.is_dir(): + raise ValueError(f"Dataset directory not found: {dataset_dir}") + cmd.extend(["--dataset-dir", str(dataset_dir)]) + + output_dir.mkdir(parents=True, exist_ok=True) + cmd.extend(["--output-dir", str(output_dir)]) + + if verbose: + cmd.append("--verbose") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": { + "export_directory": str(output_dir) + } + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Error: 'auspice' command not found. Please ensure it is installed and in your PATH.", + "output_files": {} + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": {} + } + + +@mcp.tool() +def export_v2( + dataset_dir: Path, + output_dir: Path, + verbose: bool = False, + config: Optional[Path] = None, + extend: Optional[Path] = None, + deploy_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Exports auspice v2 JSONs to create a static site. + + Args: + dataset_dir: Directory of datasets to export. + output_dir: Directory to export the auspice client and datasets to. + verbose: Print more information to the console. + config: Path to a custom config JSON file. + extend: Path to a directory containing custom client code. + deploy_path: Path to deploy the app to (e.g., for GitHub pages). + + Returns: + A dictionary containing the command executed, stdout, stderr, and output directory. + """ + cmd = ["auspice", "export", "v2"] + + if not dataset_dir.is_dir(): + raise ValueError(f"Dataset directory not found: {dataset_dir}") + cmd.extend(["--dataset-dir", str(dataset_dir)]) + + output_dir.mkdir(parents=True, exist_ok=True) + cmd.extend(["--output-dir", str(output_dir)]) + + if verbose: + cmd.append("--verbose") + + if config: + if not config.is_file(): + raise FileNotFoundError(f"Config file not found: {config}") + cmd.extend(["--config", str(config)]) + + if extend: + if not extend.is_dir(): + raise ValueError(f"Extend directory not found: {extend}") + cmd.extend(["--extend", str(extend)]) + + if deploy_path: + cmd.extend(["--deploy-path", deploy_path]) + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": { + "export_directory": str(output_dir) + } + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Error: 'auspice' command not found. Please ensure it is installed and in your PATH.", + "output_files": {} + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": {} + } + + +@mcp.tool() +def develop( + verbose: bool = False, + extend: Optional[Path] = None, + config: Optional[Path] = None, + dataset_dir: Optional[List[Path]] = None, + port: int = 4000, + host: str = "localhost", +) -> Dict[str, str]: + """ + Starts the Auspice development server with hot-reloading. + Note: This command starts a long-running server process and may not be suitable + for automated workflows that expect a command to terminate. + + Args: + verbose: Print more information to the console. + extend: Path to a directory containing custom client code. + config: Path to a custom config JSON file. + dataset_dir: Directory of datasets to serve. Can be specified multiple times. + port: Port to listen on. + host: Host IP address to listen on. + + Returns: + A dictionary containing the command executed, stdout, and stderr. + """ + cmd = ["auspice", "develop"] + + if verbose: + cmd.append("--verbose") + + if extend: + if not extend.is_dir(): + raise ValueError(f"Extend directory not found: {extend}") + cmd.extend(["--extend", str(extend)]) + + if config: + if not config.is_file(): + raise FileNotFoundError(f"Config file not found: {config}") + cmd.extend(["--config", str(config)]) + + if dataset_dir: + for d in dataset_dir: + if not d.is_dir(): + raise ValueError(f"Dataset directory not found: {d}") + cmd.extend(["--datasetDir", str(d)]) + + cmd.extend(["--port", str(port)]) + cmd.extend(["--host", host]) + + try: + # This will block until the server is manually stopped. + result = subprocess.run( + cmd, capture_output=True, text=True, check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Error: 'auspice' command not found. Please ensure it is installed and in your PATH.", + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + } + + +@mcp.tool() +def version() -> Dict[str, str]: + """ + Prints the version number of auspice and exits. + + Returns: + A dictionary containing the command executed, stdout, stderr, and parsed version. + """ + cmd = ["auspice", "version"] + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "version": result.stdout.strip(), + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Error: 'auspice' command not found. Please ensure it is installed and in your PATH.", + "version": "", + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "version": "", + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/auspice_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/auspice_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b809ae5fdf2881a1fde324cd692d2a9caff62a43 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/auspice_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/auspice_server.py') +SERVER_NAME = 'biosci_auspice' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a3df520dc961f08ec57d610d823931b1d415765 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-auspice: + build: . + image: mcp-auspice:latest + container_name: mcp-auspice + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=auspice + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..492ce6777fe7c7088cc1c347fa0b1d14cf597302 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - auspice + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_auspice/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ea6fbd530245c85ba9bce7c9fb9fbfde679a175d --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install barrnap via conda (e.g., from bioconda) +RUN conda install -c bioconda barrnap -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/barrnap_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/barrnap_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/barrnap_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/barrnap_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/barrnap_server.py new file mode 100644 index 0000000000000000000000000000000000000000..04656c2d7249255f86bca4589227d673d0b64175 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/barrnap_server.py @@ -0,0 +1,336 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, List + +# Helper function to execute barrnap commands and handle output. +# This function is internal and not exposed as an MCP tool. +def _run_barrnap_command( + command_args: List[str], + output_gff_path: Optional[Path] = None, + debug: bool = False, + quiet: bool = False, +) -> dict: + """ + Internal helper to execute barrnap commands. + Captures stdout, stderr, and handles CalledProcessError. + If output_gff_path is provided, stdout is written to that file. + """ + cmd = ["barrnap"] + command_args + + if debug: + cmd.append("--debug") + if quiet: + cmd.append("--quiet") + + stdout_capture = "" + stderr_capture = "" + output_files_generated = [] + + try: + # barrnap writes GFF to stdout by default. + # If output_gff_path is provided, we capture stdout and write it to the file. + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + stdout_capture = process.stdout + stderr_capture = process.stderr + + if output_gff_path: + output_gff_path.write_text(stdout_capture) + output_files_generated.append(str(output_gff_path)) + stdout_capture = f"GFF output written to {output_gff_path}" + + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "returncode": e.returncode, + "output_files": [], + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "barrnap command not found. Please ensure barrnap is installed and in your PATH.", + "error": "barrnap not found", + "returncode": 127, + "output_files": [], + } + + return { + "command_executed": " ".join(cmd), + "stdout": stdout_capture, + "stderr": stderr_capture, + "output_files": output_files_generated, + } + +@mcp.tool() +def barrnap_annotate( + fasta_file: Path, + output_gff_file: Path, + kingdom: Optional[str] = "bac", + enable_all_rna: bool = False, + disable_rrna: bool = False, + enable_trna: bool = False, + enable_ncrna: bool = False, + enable_mrna: bool = False, + threads: int = 1, + fast: bool = False, + evalue: Optional[float] = None, + incseq: bool = False, + incseqreg: bool = False, + outseq_file: Optional[Path] = None, + add_ids: bool = False, + db_directory: Optional[Path] = None, + debug: bool = False, + quiet: bool = False, +) -> dict: + """ + Annotates RNA features (rRNA, tRNA, tmRNA, ncRNA, mRNA) in microbial genomes + (bacteria, archaea, fungi) from an input FASTA file. + Outputs results in GFF3 format to the specified output file. + + Args: + fasta_file: Path to the input FASTA file containing genomic sequences. + output_gff_file: Path to the output GFF3 file where annotations will be written. + kingdom: The database to use for annotation. Choices: "bac" (Bacteria), + "arc" (Archaea), "fun" (Fungi). Defaults to "bac". + enable_all_rna: If True, enables scanning for all RNA types (rRNA, tRNA, tmRNA, ncRNA, mRNA). + This is equivalent to `--all`. + disable_rrna: If True, disables rRNA scanning. This is equivalent to `--no-rrna`. + enable_trna: If True, enables tRNA scanning. This is equivalent to `--trna`. + enable_ncrna: If True, enables ncRNA scanning. This is equivalent to `--ncrna`. + enable_mrna: If True, enables mRNA scanning (including CDS, RBS, sig_pep, terminator). + This is equivalent to `--mrna`. + threads: Number of CPUs to use for the search. Must be at least 1. Defaults to 1. + fast: If True, uses simpler HMMs instead of CMs, which is faster but less accurate. + This is equivalent to `--fast`. + evalue: E-value cutoff for hits to keep. Must be greater than 0 if provided. + This is equivalent to `--evalue`. + incseq: If True, includes the full input sequences in the output GFF. + This is equivalent to `--incseq`. + incseqreg: If True, includes `##sequence-region` headers in the GFF. + This is equivalent to `--incseqreg`. + outseq_file: Path to a FASTA file where hit sequences will be written. + This is equivalent to `--outseq`. + add_ids: If True, adds unique ID= tags to each GFF3 feature. + This is equivalent to `--addids`. + db_directory: Path to a different database folder to use. + This is equivalent to `--dbdir`. + debug: If True, writes all temporary files to '.' and prints debug information. + This is equivalent to `--debug`. + quiet: If True, suppresses all messages to stderr. This is equivalent to `--quiet`. + + Returns: + A dictionary containing: + - "command_executed": The full command string executed. + - "stdout": Standard output from the tool. + - "stderr": Standard error from the tool. + - "output_files": A list of paths to generated output files (GFF3 and optional FASTA). + """ + # Input validation + if not fasta_file.is_file(): + raise ValueError(f"Input FASTA file not found: {fasta_file}") + if not output_gff_file.parent.is_dir(): + raise ValueError(f"Output GFF directory does not exist: {output_gff_file.parent}") + + valid_kingdoms = {"bac", "arc", "fun"} + if kingdom is not None and kingdom not in valid_kingdoms: + raise ValueError(f"Invalid kingdom: '{kingdom}'. Must be one of {', '.join(valid_kingdoms)}.") + + if threads < 1: + raise ValueError(f"Number of threads must be at least 1, got {threads}.") + + if evalue is not None and evalue <= 0: + raise ValueError(f"E-value cutoff must be greater than 0, got {evalue}.") + + if outseq_file and not outseq_file.parent.is_dir(): + raise ValueError(f"Output FASTA directory for hit sequences does not exist: {outseq_file.parent}") + + if db_directory and not db_directory.is_dir(): + raise ValueError(f"Database directory not found: {db_directory}") + + command_args = [str(fasta_file)] + + # Database management + if db_directory: + command_args.extend(["--dbdir", str(db_directory)]) + + # Search options + if kingdom: + command_args.extend(["--kingdom", kingdom]) + if enable_all_rna: + command_args.append("--all") + if disable_rrna: + command_args.append("--no-rrna") + if enable_trna: + command_args.append("--trna") + if enable_ncrna: + command_args.append("--ncrna") + if enable_mrna: + command_args.append("--mrna") + + # Speed options + if threads > 1: # barrnap default is 1 thread, so only add if > 1 + command_args.extend(["--threads", str(threads)]) + if fast: + command_args.append("--fast") + + # Filtering options + if evalue is not None: + command_args.extend(["--evalue", str(evalue)]) + + # Output options + if incseq: + command_args.append("--incseq") + if incseqreg: + command_args.append("--incseqreg") + if outseq_file: + command_args.extend(["--outseq", str(outseq_file)]) + if add_ids: + command_args.append("--addids") + + result = _run_barrnap_command( + command_args=command_args, + output_gff_path=output_gff_file, + debug=debug, + quiet=quiet, + ) + + # Add outseq_file to output_files if it was generated by barrnap + if outseq_file and outseq_file.exists() and str(outseq_file) not in result["output_files"]: + result["output_files"].append(str(outseq_file)) + + return result + +@mcp.tool() +def barrnap_list_databases( + db_directory: Optional[Path] = None, + debug: bool = False, + quiet: bool = False, +) -> dict: + """ + Lists the installed barrnap databases and their contents. + + Args: + db_directory: Path to a different database folder to use. + This is equivalent to `--dbdir`. + debug: If True, writes all temporary files to '.' and prints debug information. + This is equivalent to `--debug`. + quiet: If True, suppresses all messages to stderr. This is equivalent to `--quiet`. + + Returns: + A dictionary containing: + - "command_executed": The full command string executed. + - "stdout": Standard output from the tool, listing databases. + - "stderr": Standard error from the tool. + - "output_files": An empty list, as no files are generated. + """ + command_args = ["--listdb"] + + if db_directory: + if not db_directory.is_dir(): + raise ValueError(f"Database directory not found: {db_directory}") + command_args.extend(["--dbdir", str(db_directory)]) + + return _run_barrnap_command( + command_args=command_args, + debug=debug, + quiet=quiet, + ) + +@mcp.tool() +def barrnap_update_databases( + db_directory: Optional[Path] = None, + debug: bool = False, + quiet: bool = False, +) -> dict: + """ + Updates barrnap databases from the internet. + + Args: + db_directory: Path to a different database folder to use. + This is equivalent to `--dbdir`. + debug: If True, writes all temporary files to '.' and prints debug information. + This is equivalent to `--debug`. + quiet: If True, suppresses all messages to stderr. This is equivalent to `--quiet`. + + Returns: + A dictionary containing: + - "command_executed": The full command string executed. + - "stdout": Standard output from the tool, typically update messages. + - "stderr": Standard error from the tool. + - "output_files": An empty list, as no files are generated. + """ + command_args = ["--updatedb"] + + if db_directory: + if not db_directory.is_dir(): + raise ValueError(f"Database directory not found: {db_directory}") + command_args.extend(["--dbdir", str(db_directory)]) + + return _run_barrnap_command( + command_args=command_args, + debug=debug, + quiet=quiet, + ) + +@mcp.tool() +def barrnap_get_version( + debug: bool = False, + quiet: bool = False, +) -> dict: + """ + Prints the barrnap version in the format 'barrnap X.Y'. + + Args: + debug: If True, writes all temporary files to '.' and prints debug information. + This is equivalent to `--debug`. + quiet: If True, suppresses all messages to stderr. This is equivalent to `--quiet`. + + Returns: + A dictionary containing: + - "command_executed": The full command string executed. + - "stdout": Standard output from the tool, containing the version string. + - "stderr": Standard error from the tool. + - "output_files": An empty list, as no files are generated. + """ + command_args = ["--version"] + return _run_barrnap_command( + command_args=command_args, + debug=debug, + quiet=quiet, + ) + +@mcp.tool() +def barrnap_get_citation( + debug: bool = False, + quiet: bool = False, +) -> dict: + """ + Prints the barrnap citation information. + + Args: + debug: If True, writes all temporary files to '.' and prints debug information. + This is equivalent to `--debug`. + quiet: If True, suppresses all messages to stderr. This is equivalent to `--quiet`. + + Returns: + A dictionary containing: + - "command_executed": The full command string executed. + - "stdout": Standard output from the tool, containing the citation. + - "stderr": Standard error from the tool. + - "output_files": An empty list, as no files are generated. + """ + command_args = ["--citation"] + return _run_barrnap_command( + command_args=command_args, + debug=debug, + quiet=quiet, + ) \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/barrnap_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/barrnap_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a5e94f41063e1d955c341e93b430484db7bef809 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/barrnap_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/barrnap_server.py') +SERVER_NAME = 'biosci_barrnap' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..02c3e28e1c54228dcf0d8b743b2f15fcde7a0c1c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-barrnap: + build: . + image: mcp-barrnap:latest + container_name: mcp-barrnap + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=barrnap + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..eda56f8ba60811468f2c9d2f53a2ff421f7837b6 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - barrnap + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_barrnap/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a472346995a69a9ad4e37300bc0e533fc606e73a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bin2cell via conda (e.g., from bioconda) +RUN conda install -c bioconda bin2cell -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bin2cell_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bin2cell_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bin2cell_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/bin2cell_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/bin2cell_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9c3468bfc6f56fc8083e0537920e6317cfd23b02 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/bin2cell_server.py @@ -0,0 +1,278 @@ +import subprocess +from pathlib import Path +from typing import Optional, List + +@mcp.tool() +def bin2cell_prepare_bins( + input_dir: str, + output_h5ad: str, + bin_size: int = 2, + destripe: bool = True, +): + """ + Reads Visium HD data from a SpaceRanger output directory and optionally performs destriping + to correct for technical effects in 2um bin data. + + Args: + input_dir: Path to the SpaceRanger output directory (containing 'outs' folder). + output_h5ad: Path where the processed bin-level AnnData object will be saved. + bin_size: Resolution of bins to load (default is 2um). + destripe: Whether to apply the destriping correction for variable bin dimensions. + """ + # Input validation + input_path = Path(input_dir) + if not input_path.exists(): + return {"error": f"Input directory {input_dir} does not exist."} + + output_path = Path(output_h5ad) + if not output_path.parent.exists(): + output_path.parent.mkdir(parents=True, exist_ok=True) + + if bin_size <= 0: + return {"error": "bin_size must be a positive integer."} + + # Construct Python command + # We use a python script string to execute the library functions + destripe_cmd = "b2c.pp.destripe(adata)" if destripe else "pass" + python_script = f""" +import bin2cell as b2c +import scanpy as sc +import os + +try: + # Load Visium HD data + adata = b2c.pp.read_visium_hd_folder('{input_dir}', bin_size={bin_size}) + + # Perform destriping if requested + if {destripe}: + b2c.pp.destripe(adata) + + # Save the result + adata.write('{output_h5ad}') + print("Successfully prepared bin data.") +except Exception as e: + print(f"Error: {{str(e)}}") + exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"bin2cell.pp.read_visium_hd_folder and destripe={destripe}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_h5ad] + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to prepare bin data", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": e.cmd + } + +@mcp.tool() +def bin2cell_run_stardist( + image_path: str, + output_mask_path: str, + model_name: str = "2D_versatile_he", + prob_thresh: float = 0.5, + nms_thresh: float = 0.3, +): + """ + Performs cell segmentation on a morphology image using StarDist. + + Args: + image_path: Path to the high-resolution morphology image (e.g., tissue_hires_image.png). + output_mask_path: Path where the resulting segmentation mask (.tif) will be saved. + model_name: StarDist model to use (default: '2D_versatile_he'). + prob_thresh: Probability threshold for StarDist detection. + nms_thresh: Non-maximum suppression threshold for StarDist. + """ + # Input validation + img_path = Path(image_path) + if not img_path.exists(): + return {"error": f"Image file {image_path} does not exist."} + + out_mask = Path(output_mask_path) + if not out_mask.parent.exists(): + out_mask.parent.mkdir(parents=True, exist_ok=True) + + python_script = f""" +import bin2cell as b2c +import cv2 +import numpy as np + +try: + # Run StarDist segmentation via bin2cell wrapper + # Note: bin2cell.tl.stardist handles the model loading and prediction + b2c.tl.stardist( + '{image_path}', + '{output_mask_path}', + model='{model_name}', + prob_thresh={prob_thresh}, + nms_thresh={nms_thresh} + ) + print("Successfully generated segmentation mask.") +except Exception as e: + print(f"Error: {{str(e)}}") + exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"bin2cell.tl.stardist on {image_path}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_mask_path] + } + except subprocess.CalledProcessError as e: + return { + "error": "StarDist segmentation failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": e.cmd + } + +@mcp.tool() +def bin2cell_extract_cells( + bin_h5ad_path: str, + mask_path: str, + output_cell_h5ad_path: str, + qc_metrics: bool = True, +): + """ + Groups subcellular bins into cells based on a segmentation mask and generates a cell-level AnnData object. + + Args: + bin_h5ad_path: Path to the bin-level AnnData object (output from bin2cell_prepare_bins). + mask_path: Path to the segmentation mask file (output from bin2cell_run_stardist). + output_cell_h5ad_path: Path where the final cell-level AnnData object will be saved. + qc_metrics: Whether to calculate standard scanpy QC metrics for the new cell object. + """ + # Input validation + bin_path = Path(bin_h5ad_path) + if not bin_path.exists(): + return {"error": f"Bin h5ad file {bin_h5ad_path} does not exist."} + + m_path = Path(mask_path) + if not m_path.exists(): + return {"error": f"Mask file {mask_path} does not exist."} + + out_cell_path = Path(output_cell_h5ad_path) + if not out_cell_path.parent.exists(): + out_cell_path.parent.mkdir(parents=True, exist_ok=True) + + python_script = f""" +import bin2cell as b2c +import scanpy as sc + +try: + # Load the bin-level data + adata_bins = sc.read_h5ad('{bin_h5ad_path}') + + # Extract cells based on the mask + adata_cells = b2c.tl.extract_cells(adata_bins, '{mask_path}') + + # Calculate QC metrics if requested + if {qc_metrics}: + sc.pp.calculate_qc_metrics(adata_cells, inplace=True) + + # Save the cell-level object + adata_cells.write('{output_cell_h5ad_path}') + print("Successfully extracted cells from bins.") +except Exception as e: + print(f"Error: {{str(e)}}") + exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"bin2cell.tl.extract_cells using mask {mask_path}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_cell_h5ad_path] + } + except subprocess.CalledProcessError as e: + return { + "error": "Cell extraction failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": e.cmd + } + +@mcp.tool() +def bin2cell_visualize_segmentation( + bin_h5ad_path: str, + output_image_path: str, + basis: str = "spatial", +): + """ + Generates a visualization of the bin-to-cell assignments. + + Args: + bin_h5ad_path: Path to the bin-level AnnData object containing cell assignments. + output_image_path: Path to save the visualization plot (e.g., .png or .pdf). + basis: The coordinate system to use for plotting (default: 'spatial'). + """ + bin_path = Path(bin_h5ad_path) + if not bin_path.exists(): + return {"error": f"Bin h5ad file {bin_h5ad_path} does not exist."} + + python_script = f""" +import scanpy as sc +import matplotlib.pyplot as plt +import bin2cell as b2c + +try: + adata = sc.read_h5ad('{bin_h5ad_path}') + if 'cell_id' not in adata.obs.columns: + print("Error: cell_id not found in adata.obs. Run extract_cells first.") + exit(1) + + # Plotting logic + sc.pl.embedding(adata, basis='{basis}', color='cell_id', show=False) + plt.savefig('{output_image_path}') + print("Successfully saved visualization.") +except Exception as e: + print(f"Error: {{str(e)}}") + exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"Visualization of cell_id on {basis}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_image_path] + } + except subprocess.CalledProcessError as e: + return { + "error": "Visualization failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": e.cmd + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/bin2cell_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/bin2cell_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0ae86fbd415d126785f1a48e243c53fa78ea77b3 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/bin2cell_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/bin2cell_server.py') +SERVER_NAME = 'biosci_bin2cell' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..098ceb799750eea4200969287a28a53212a12fc4 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bin2cell: + build: . + image: mcp-bin2cell:latest + container_name: mcp-bin2cell + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bin2cell + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..eb8ff7f84e83b73a3a12f8d25a0f5767d0133558 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bin2cell + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bin2cell/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-biocbaseutils/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-biocbaseutils/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4181ef30167d5cae30f272f9b0ddf98485a23cce --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-biocbaseutils/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-biocbaseutils + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-biocbaseutils/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-biocbaseutils/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-biocbaseutils/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..02ebac92236f7770576c155e661a1d5f225a564f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-curatedatlasqueryr via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-curatedatlasqueryr -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-curatedatlasqueryr_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-curatedatlasqueryr_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-curatedatlasqueryr_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/app/bioconductor-curatedatlasqueryr_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/app/bioconductor-curatedatlasqueryr_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..59071fbb2bb10a4f5ffe6cc5976bf509c144ff2b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/app/bioconductor-curatedatlasqueryr_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/app/bioconductor-curatedatlasqueryr_server.py') +SERVER_NAME = 'biosci_bioconductor_curatedatlasqueryr' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..61ced17959568e3d32fc631fcbb885443248c4d9 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-curatedatlasqueryr: + build: . + image: mcp-bioconductor-curatedatlasqueryr:latest + container_name: mcp-bioconductor-curatedatlasqueryr + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-curatedatlasqueryr + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-curatedatlasqueryr/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-delayedmatrixstats/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-delayedmatrixstats/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-delayedmatrixstats/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ebseq/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ebseq/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ebseq/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8efe558b37798abdb85fea94e1317a5793f49e90 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-experimentsubset via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-experimentsubset -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-experimentsubset_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-experimentsubset_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-experimentsubset_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/bioconductor-experimentsubset_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/bioconductor-experimentsubset_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0145224014ed8533a40c338feb2dfcc0046d6065 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/bioconductor-experimentsubset_server.py @@ -0,0 +1,178 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, Dict, List + +# This is a placeholder for the MCP decorator. +# In a real MCP environment, this would be provided by the MCP framework. +def mcp_tool_placeholder(*args, **kwargs): + def decorator(func): + return func + return decorator + +mcp = type('mcp', (), {'tool': mcp_tool_placeholder}) + + +@mcp.tool() +def manage_experiment_subset( + input_rds: Path, + output_rds: Path, + subset_name: str = "mcp_subset", + row_subset: Optional[str] = None, + col_subset: Optional[str] = None, + row_subset_file: Optional[Path] = None, + col_subset_file: Optional[Path] = None, +) -> Dict[str, any]: + """ + Provides a command-line interface to the R/Bioconductor 'ExperimentSubset' package. + + This tool subsets a Bioconductor experiment object (e.g., SummarizedExperiment, + SingleCellExperiment) stored in an RDS file based on provided row (e.g., genes) + or column (e.g., cells) identifiers. + + Args: + input_rds: Path to the input RDS file containing a Bioconductor experiment object. + output_rds: Path to save the output subsetted RDS file. + subset_name: A name to assign to the created subset. + row_subset: A comma-separated string of row names to include in the subset. + col_subset: A comma-separated string of column names to include in the subset. + row_subset_file: Path to a file containing row names to include (one per line). + col_subset_file: Path to a file containing column names to include (one per line). + + Returns: + A dictionary containing the execution command, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + if not input_rds.exists(): + raise FileNotFoundError(f"Input file not found: {input_rds}") + + if row_subset and row_subset_file: + raise ValueError("Cannot specify both 'row_subset' and 'row_subset_file'.") + + if col_subset and col_subset_file: + raise ValueError("Cannot specify both 'col_subset' and 'col_subset_file'.") + + if not any([row_subset, col_subset, row_subset_file, col_subset_file]): + raise ValueError("At least one subsetting criterion must be provided " + "('row_subset', 'col_subset', 'row_subset_file', or 'col_subset_file').") + + if not output_rds.parent.exists(): + output_rds.parent.mkdir(parents=True, exist_ok=True) + + # --- R Script Generation --- + r_script_content = f""" + # Load required libraries + if (!require("optparse", quietly = TRUE)) install.packages("optparse", repos = "http://cran.us.r-project.org") + if (!require("ExperimentSubset", quietly = TRUE)) {{ + if (!require("BiocManager", quietly = TRUE)) install.packages("BiocManager", repos = "http://cran.us.r-project.org") + BiocManager::install("ExperimentSubset", update=FALSE) + }} + library(optparse) + library(ExperimentSubset) + + # Define and parse command-line options + option_list <- list( + make_option(c("-i", "--input"), type="character", help="Input RDS file path"), + make_option(c("-o", "--output"), type="character", help="Output RDS file path"), + make_option(c("-n", "--name"), type="character", default="mcp_subset", help="Name for the subset"), + make_option(c("-r", "--rows"), type="character", default=NULL, help="Comma-separated row names"), + make_option(c("-c", "--cols"), type="character", default=NULL, help="Comma-separated column names"), + make_option(c("--row_file"), type="character", default=NULL, help="File with row names"), + make_option(c("--col_file"), type="character", default=NULL, help="File with column names") + ) + + opt_parser <- OptionParser(option_list=option_list) + opt <- parse_args(opt_parser) + + if (is.null(opt$input) || is.null(opt$output)) {{ + print_help(opt_parser) + stop("Input and output files must be supplied.", call.=FALSE) + }} + + # Load the experiment object + cat("Loading input RDS file:", opt$input, "\\n") + exp_obj <- readRDS(opt$input) + + # Create ExperimentSubset object + es <- ExperimentSubset(exp_obj) + + # Determine row and column subsets + row_indices <- NULL + if (!is.null(opt$rows)) {{ + row_indices <- trimws(strsplit(opt$rows, ",")[[1]]) + }} else if (!is.null(opt$row_file)) {{ + row_indices <- readLines(opt$row_file) + }} + + col_indices <- NULL + if (!is.null(opt$cols)) {{ + col_indices <- trimws(strsplit(opt$cols, ",")[[1]]) + }} else if (!is.null(opt$col_file)) {{ + col_indices <- readLines(opt$col_file) + }} + + # Create the subset using the subsetData function + cat("Creating subset '", opt$name, "'...\\n", sep="") + es <- subsetData(es, subsetName = opt$name, rows = row_indices, cols = col_indices) + + # Retrieve the actual subsetted object from the container + subset_obj <- getSubset(es, subsetName = opt$name) + + # Save the subsetted object + cat("Saving subsetted object to:", opt$output, "\\n") + saveRDS(subset_obj, file = opt$output) + + cat("Successfully created subset.\\n") + """ + + # --- Subprocess Execution --- + cmd: List[str] = [] + try: + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".R") as r_script_file: + r_script_file.write(r_script_content) + r_script_path = r_script_file.name + + cmd = [ + "Rscript", r_script_path, + "--input", str(input_rds), + "--output", str(output_rds), + "--name", subset_name + ] + + if row_subset: + cmd.extend(["--rows", row_subset]) + if col_subset: + cmd.extend(["--cols", col_subset]) + if row_subset_file: + cmd.extend(["--row_file", str(row_subset_file)]) + if col_subset_file: + cmd.extend(["--col_file", str(col_subset_file)]) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_rds)] + } + + except FileNotFoundError: + raise RuntimeError("Rscript not found. Please ensure R is installed and in your system's PATH.") + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R script execution failed.", + "return_code": e.returncode + } + finally: + # Clean up the temporary R script + if 'r_script_path' in locals() and Path(r_script_path).exists(): + Path(r_script_path).unlink() diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/bioconductor-experimentsubset_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/bioconductor-experimentsubset_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..987e1e52691de75b7adc44897e785a14133cefb9 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/bioconductor-experimentsubset_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/bioconductor-experimentsubset_server.py') +SERVER_NAME = 'biosci_bioconductor_experimentsubset' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..14db9f06094e56cfbc89047242ced0ca6d097862 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-experimentsubset: + build: . + image: mcp-bioconductor-experimentsubset:latest + container_name: mcp-bioconductor-experimentsubset + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-experimentsubset + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0522c599fba65423c66bcf93f421921e6df214cf --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-experimentsubset + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-experimentsubset/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-genefilter/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-genefilter/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-genefilter/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-genefilter/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-genefilter/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b8e8117174138cbb23e50012e6aebca949f11ffa --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-genefilter/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-genefilter: + build: . + image: mcp-bioconductor-genefilter:latest + container_name: mcp-bioconductor-genefilter + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-genefilter + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8e4385ba0e7fd397a459e7260eff11f045abdb1a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-ggsc via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-ggsc -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-ggsc_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-ggsc_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-ggsc_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/app/bioconductor-ggsc_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/app/bioconductor-ggsc_server.py new file mode 100644 index 0000000000000000000000000000000000000000..cbffb0b68329713b1e53f28844ac45be84bfec1a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/app/bioconductor-ggsc_server.py @@ -0,0 +1,360 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List + +# This script assumes the presence of a companion R script (e.g., 'run_ggsc.R') +# that acts as a command-line interface for the 'ggsc' R package. +# The R environment must have 'ggsc', 'SingleCellExperiment', 'ggplot2', +# and an argument parser like 'optparse' installed. +R_SCRIPT_EXECUTABLE = "run_ggsc.R" + +# Per instructions, the @mcp.tool decorator is used. +# A dummy decorator is defined here for syntax validity in a standalone context. +# In a true MCP environment, this would be provided by the MCP framework. +def tool(func): + """Dummy decorator to match the required syntax.""" + return func + +class MCP: + """Dummy class to hold the tool decorator.""" + tool = staticmethod(tool) + +mcp = MCP() + + +@mcp.tool() +def plot_reduced_dim( + input_rds: Path, + output_plot: Path, + dim_red: str, + color_by: str, + facet_by: Optional[str] = None, + plot_title: Optional[str] = None, + img_width: int = 7, + img_height: int = 7 +) -> dict: + """ + Generates a reduced dimension plot (e.g., UMAP, t-SNE) from a SingleCellExperiment object. + + This tool wraps the `plot_reduced_dim_sce` function from the R/Bioconductor package `ggsc`. + The input must be a .rds file containing a SingleCellExperiment object. + + Args: + input_rds: Path to the input RDS file containing a SingleCellExperiment object. + output_plot: Path to save the output plot image (e.g., plot.png). + dim_red: Name of the dimension reduction to use (e.g., "UMAP", "TSNE"). + color_by: Variable in colData to color the points by (e.g., "label", "cluster"). + facet_by: Optional variable in colData to facet the plot by. + plot_title: Optional title for the plot. + img_width: Width of the output image in inches. + img_height: Height of the output image in inches. + + Returns: + A dictionary containing the execution details and output file path. + """ + # --- Input Validation --- + if not input_rds.is_file(): + raise FileNotFoundError(f"Input RDS file not found: {input_rds}") + if img_width <= 0 or img_height <= 0: + raise ValueError("Image width and height must be positive integers.") + + # Ensure output directory exists + output_plot.parent.mkdir(parents=True, exist_ok=True) + + # --- Command Construction --- + cmd = [ + "Rscript", + R_SCRIPT_EXECUTABLE, + "reduced_dim", + "--input_rds", str(input_rds), + "--output_plot", str(output_plot), + "--dim_red", dim_red, + "--color_by", color_by, + "--img_width", str(img_width), + "--img_height", str(img_height) + ] + + if facet_by: + cmd.extend(["--facet_by", facet_by]) + if plot_title: + cmd.extend(["--plot_title", plot_title]) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + except FileNotFoundError: + raise RuntimeError("Rscript or the wrapper script not found. Please ensure R and the tool's R script are in the system's PATH.") + except subprocess.CalledProcessError as e: + error_message = f"ggsc R script failed for reduced_dim plot.\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}" + logging.error(error_message) + raise RuntimeError(error_message) from e + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_plot)] + } + + +@mcp.tool() +def plot_gene_expression( + input_rds: Path, + output_plot: Path, + gene: str, + dim_red: str, + assay: str = "logcounts", + plot_title: Optional[str] = None, + img_width: int = 7, + img_height: int = 7 +) -> dict: + """ + Plots the expression of a single gene on a reduced dimension plot. + + This tool wraps the `plot_gene_sce` function from the R/Bioconductor package `ggsc`. + The input must be a .rds file containing a SingleCellExperiment object. + + Args: + input_rds: Path to the input RDS file containing a SingleCellExperiment object. + output_plot: Path to save the output plot image. + gene: The name of the gene to plot. + dim_red: Name of the dimension reduction to use (e.g., "UMAP"). + assay: The assay to use for expression values (default: "logcounts"). + plot_title: Optional title for the plot. + img_width: Width of the output image in inches. + img_height: Height of the output image in inches. + + Returns: + A dictionary containing the execution details and output file path. + """ + # --- Input Validation --- + if not input_rds.is_file(): + raise FileNotFoundError(f"Input RDS file not found: {input_rds}") + if not gene: + raise ValueError("A gene name must be provided.") + if img_width <= 0 or img_height <= 0: + raise ValueError("Image width and height must be positive integers.") + + output_plot.parent.mkdir(parents=True, exist_ok=True) + + # --- Command Construction --- + cmd = [ + "Rscript", + R_SCRIPT_EXECUTABLE, + "gene_plot", + "--input_rds", str(input_rds), + "--output_plot", str(output_plot), + "--gene", gene, + "--dim_red", dim_red, + "--assay", assay, + "--img_width", str(img_width), + "--img_height", str(img_height) + ] + + if plot_title: + cmd.extend(["--plot_title", plot_title]) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + except FileNotFoundError: + raise RuntimeError("Rscript or the wrapper script not found. Please ensure R and the tool's R script are in the system's PATH.") + except subprocess.CalledProcessError as e: + error_message = f"ggsc R script failed for gene expression plot.\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}" + logging.error(error_message) + raise RuntimeError(error_message) from e + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_plot)] + } + + +@mcp.tool() +def plot_expression_heatmap( + input_rds: Path, + output_plot: Path, + genes: List[str], + annotation_col: str, + assay: str = "logcounts", + plot_title: Optional[str] = None, + img_width: int = 7, + img_height: int = 10 +) -> dict: + """ + Generates a heatmap of gene expression for a set of genes. + + This tool wraps the `plot_heatmap_sce` function from the R/Bioconductor package `ggsc`. + The input must be a .rds file containing a SingleCellExperiment object. + + Args: + input_rds: Path to the input RDS file containing a SingleCellExperiment object. + output_plot: Path to save the output plot image. + genes: A list of gene names to include in the heatmap. + annotation_col: Column in colData to use for cell annotation. + assay: The assay to use for expression values (default: "logcounts"). + plot_title: Optional title for the plot. + img_width: Width of the output image in inches. + img_height: Height of the output image in inches. + + Returns: + A dictionary containing the execution details and output file path. + """ + # --- Input Validation --- + if not input_rds.is_file(): + raise FileNotFoundError(f"Input RDS file not found: {input_rds}") + if not genes: + raise ValueError("At least one gene must be provided in the list.") + if img_width <= 0 or img_height <= 0: + raise ValueError("Image width and height must be positive integers.") + + output_plot.parent.mkdir(parents=True, exist_ok=True) + + # --- Command Construction --- + genes_str = ",".join(genes) + cmd = [ + "Rscript", + R_SCRIPT_EXECUTABLE, + "heatmap", + "--input_rds", str(input_rds), + "--output_plot", str(output_plot), + "--genes", genes_str, + "--annotation_col", annotation_col, + "--assay", assay, + "--img_width", str(img_width), + "--img_height", str(img_height) + ] + + if plot_title: + cmd.extend(["--plot_title", plot_title]) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + except FileNotFoundError: + raise RuntimeError("Rscript or the wrapper script not found. Please ensure R and the tool's R script are in the system's PATH.") + except subprocess.CalledProcessError as e: + error_message = f"ggsc R script failed for expression heatmap.\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}" + logging.error(error_message) + raise RuntimeError(error_message) from e + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_plot)] + } + + +@mcp.tool() +def plot_expression_violin( + input_rds: Path, + output_plot: Path, + gene: str, + group_by: str, + assay: str = "logcounts", + plot_title: Optional[str] = None, + img_width: int = 7, + img_height: int = 5 +) -> dict: + """ + Generates a violin plot of gene expression across different groups. + + This tool wraps the `plot_violin_sce` function from the R/Bioconductor package `ggsc`. + The input must be a .rds file containing a SingleCellExperiment object. + + Args: + input_rds: Path to the input RDS file containing a SingleCellExperiment object. + output_plot: Path to save the output plot image. + gene: The name of the gene to plot. + group_by: Variable in colData to group the violins by (e.g., "cluster"). + assay: The assay to use for expression values (default: "logcounts"). + plot_title: Optional title for the plot. + img_width: Width of the output image in inches. + img_height: Height of the output image in inches. + + Returns: + A dictionary containing the execution details and output file path. + """ + # --- Input Validation --- + if not input_rds.is_file(): + raise FileNotFoundError(f"Input RDS file not found: {input_rds}") + if not gene: + raise ValueError("A gene name must be provided.") + if img_width <= 0 or img_height <= 0: + raise ValueError("Image width and height must be positive integers.") + + output_plot.parent.mkdir(parents=True, exist_ok=True) + + # --- Command Construction --- + cmd = [ + "Rscript", + R_SCRIPT_EXECUTABLE, + "violin", + "--input_rds", str(input_rds), + "--output_plot", str(output_plot), + "--gene", gene, + "--group_by", group_by, + "--assay", assay, + "--img_width", str(img_width), + "--img_height", str(img_height) + ] + + if plot_title: + cmd.extend(["--plot_title", plot_title]) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + except FileNotFoundError: + raise RuntimeError("Rscript or the wrapper script not found. Please ensure R and the tool's R script are in the system's PATH.") + except subprocess.CalledProcessError as e: + error_message = f"ggsc R script failed for expression violin plot.\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}" + logging.error(error_message) + raise RuntimeError(error_message) from e + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_plot)] + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/app/bioconductor-ggsc_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/app/bioconductor-ggsc_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..cdf9661d130ac447c96d1ccd3f01c7698a2d7bae --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/app/bioconductor-ggsc_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/app/bioconductor-ggsc_server.py') +SERVER_NAME = 'biosci_bioconductor_ggsc' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..66d94a739f2052f5fb7c429bf4b4e1aec1181186 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-ggsc: + build: . + image: mcp-bioconductor-ggsc:latest + container_name: mcp-bioconductor-ggsc + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-ggsc + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..179d5ec77128b05773abab6ecc408a5b49d85d51 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-ggsc + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggsc/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b693edfda5ebe7e256e6e03048b2b6fb76e37ff7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-mspurity via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-mspurity -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-mspurity_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-mspurity_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-mspurity_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/app/bioconductor-mspurity_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/app/bioconductor-mspurity_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6081d9589a0cf28b4d8e45b6fb2a27ff314bfdb6 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/app/bioconductor-mspurity_server.py @@ -0,0 +1,312 @@ +import subprocess +import os +import tempfile +from pathlib import Path +from typing import Optional, List + +@mcp.tool() +def mspurity_purity_a( + ms_files: List[str], + output_rds: str, + cores: int = 1, + mz_purity_limit: float = 0.0, + iw_low: float = -0.5, + iw_high: float = 0.5, + nearest: bool = True, + v: bool = True, + offsets: bool = False +): + """ + Assess precursor purity for LC-MS/MS data. Calculates the percentage of the + intensity in the isolation window that is attributed to the precursor ion. + + Args: + ms_files: List of paths to LC-MS/MS files (mzML, mzXML). + output_rds: Path to save the resulting purityA object as an RDS file. + cores: Number of CPU cores for parallel processing. + mz_purity_limit: Purity threshold for filtering. + iw_low: Lower offset for isolation window (e.g., -0.5). + iw_high: Upper offset for isolation window (e.g., 0.5). + nearest: Whether to use the nearest scan for purity calculation. + v: Verbose output. + offsets: Whether to use isolation window offsets from the file metadata. + """ + for f in ms_files: + if not Path(f).exists(): + raise FileNotFoundError(f"Input file not found: {f}") + + out_path = Path(output_rds) + out_path.parent.mkdir(parents=True, exist_ok=True) + + ms_files_r = ", ".join([f'"{f}"' for f in ms_files]) + r_script = f""" + library(msPurity) + pa <- purityA(fileList = c({ms_files_r}), + cores = {cores}, + mz_purity_limit = {mz_purity_limit}, + iw_low = {iw_low}, + iw_high = {iw_high}, + nearest = {"TRUE" if nearest else "FALSE"}, + v = {"TRUE" if v else "FALSE"}, + offsets = {"TRUE" if offsets else "FALSE"}) + saveRDS(pa, file = "{output_rds}") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script) + tmp_path = tmp.name + + try: + cmd = ["Rscript", tmp_path] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(out_path)] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": " ".join(e.cmd) + } + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) + +@mcp.tool() +def mspurity_purity_x( + ms_files: List[str], + output_rds: str, + cores: int = 1, + mz_purity_limit: float = 0.0, + v: bool = True +): + """ + Assess precursor purity for Direct Infusion (DI-MS/MS) data. + + Args: + ms_files: List of paths to DI-MS/MS files. + output_rds: Path to save the resulting purityX object as an RDS file. + cores: Number of CPU cores. + mz_purity_limit: Purity threshold. + v: Verbose output. + """ + for f in ms_files: + if not Path(f).exists(): + raise FileNotFoundError(f"Input file not found: {f}") + + ms_files_r = ", ".join([f'"{f}"' for f in ms_files]) + r_script = f""" + library(msPurity) + px <- purityX(fileList = c({ms_files_r}), + cores = {cores}, + mz_purity_limit = {mz_purity_limit}, + v = {"TRUE" if v else "FALSE"}) + saveRDS(px, file = "{output_rds}") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script) + tmp_path = tmp.name + + try: + cmd = ["Rscript", tmp_path] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) + +@mcp.tool() +def mspurity_average_intensities( + pa_rds: str, + output_rds: str, + avg_mz_err: float = 0.005, + avg_rt_err: float = 10.0, + v: bool = True +): + """ + Average the intensities of MS/MS spectra across scans within a purityA object. + + Args: + pa_rds: Path to the input purityA RDS file. + output_rds: Path to save the updated purityA object. + avg_mz_err: m/z error for averaging. + avg_rt_err: Retention time error for averaging (seconds). + v: Verbose output. + """ + if not Path(pa_rds).exists(): + raise FileNotFoundError(f"Input RDS file not found: {pa_rds}") + + r_script = f""" + library(msPurity) + pa <- readRDS("{pa_rds}") + pa <- averageIntensities(pa, avg_mz_err = {avg_mz_err}, avg_rt_err = {avg_rt_err}, v = {"TRUE" if v else "FALSE"}) + saveRDS(pa, file = "{output_rds}") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script) + tmp_path = tmp.name + + try: + cmd = ["Rscript", tmp_path] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) + +@mcp.tool() +def mspurity_create_database( + pa_rds: str, + db_path: str, + xset_rds: Optional[str] = None, + v: bool = True +): + """ + Create a SQLite database from a purityA object, optionally linking with XCMS results. + + Args: + pa_rds: Path to the input purityA RDS file. + db_path: Path to the output SQLite database file. + xset_rds: Optional path to an XCMS xset or XcmsExperiment RDS file. + v: Verbose output. + """ + if not Path(pa_rds).exists(): + raise FileNotFoundError(f"Input RDS file not found: {pa_rds}") + + xset_code = f'xset <- readRDS("{xset_rds}")' if xset_rds else "xset <- NULL" + + r_script = f""" + library(msPurity) + pa <- readRDS("{pa_rds}") + {xset_code} + createDatabase(pa, xset = xset, dbName = "{db_path}", v = {"TRUE" if v else "FALSE"}) + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script) + tmp_path = tmp.name + + try: + cmd = ["Rscript", tmp_path] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "output_files": [db_path] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) + +@mcp.tool() +def mspurity_spectral_matching( + pa_rds: str, + output_rds: str, + lib_path: str, + ms_err: float = 0.01, + v: bool = True +): + """ + Perform spectral matching against a library for the MS/MS spectra in a purityA object. + + Args: + pa_rds: Path to the input purityA RDS file. + output_rds: Path to save the purityA object with matching results. + lib_path: Path to the spectral library (RDS or SQLite). + ms_err: m/z error for matching. + v: Verbose output. + """ + if not Path(pa_rds).exists(): + raise FileNotFoundError(f"Input RDS file not found: {pa_rds}") + if not Path(lib_path).exists(): + raise FileNotFoundError(f"Library file not found: {lib_path}") + + r_script = f""" + library(msPurity) + pa <- readRDS("{pa_rds}") + pa <- spectralMatching(pa, lib_path = "{lib_path}", ms_err = {ms_err}, v = {"TRUE" if v else "FALSE"}) + saveRDS(pa, file = "{output_rds}") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script) + tmp_path = tmp.name + + try: + cmd = ["Rscript", tmp_path] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) + +@mcp.tool() +def mspurity_filter_frag( + pa_rds: str, + output_rds: str, + min_int: float = 0.0, + v: bool = True +): + """ + Filter fragmentation spectra based on intensity or other criteria. + + Args: + pa_rds: Path to the input purityA RDS file. + output_rds: Path to save the filtered purityA object. + min_int: Minimum intensity threshold for peaks. + v: Verbose output. + """ + if not Path(pa_rds).exists(): + raise FileNotFoundError(f"Input RDS file not found: {pa_rds}") + + r_script = f""" + library(msPurity) + pa <- readRDS("{pa_rds}") + pa <- filterFrag(pa, min_int = {min_int}, v = {"TRUE" if v else "FALSE"}) + saveRDS(pa, file = "{output_rds}") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script) + tmp_path = tmp.name + + try: + cmd = ["Rscript", tmp_path] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c5b434c3bcdfeca344c322cc39bdf54a145647cf --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-mspurity: + build: . + image: mcp-bioconductor-mspurity:latest + container_name: mcp-bioconductor-mspurity + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-mspurity + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0bac186c238387cb25135baf8c703b4d5e9bef8a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-mspurity + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mspurity/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..092f3d6b4424cc3647a52adc9af228e1cb8277d7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-rhdf5lib via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-rhdf5lib -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-rhdf5lib_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-rhdf5lib_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-rhdf5lib_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/bioconductor-rhdf5lib_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/bioconductor-rhdf5lib_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b8d2220d9af2cea444bb287a1944c86b649b3272 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/bioconductor-rhdf5lib_server.py @@ -0,0 +1,25 @@ +import subprocess +from pathlib import Path +from typing import Optional, List + +# NOTE: The 'mcp' import is not included as per the instructions. +# import mcp + +# @mcp.tool() +def rhdf5lib_info(): + """ + Provides information about the bioconductor-rhdf5lib package. + + This package is a library and does not provide any command-line executables. + It offers the HDF5 C and C++ libraries for other R packages to link against. + Therefore, this function is for informational purposes only and does not + execute any subprocess. + """ + # This is a library package with no executable. + # The return dictionary reflects that no command can be executed. + return { + "command_executed": "N/A (bioconductor-rhdf5lib is a library, not a command-line tool)", + "stdout": "This package provides C and C++ HDF5 libraries for use within the R environment. It has no direct command-line interface.", + "stderr": "", + "output_files": [] + } diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/bioconductor-rhdf5lib_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/bioconductor-rhdf5lib_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f84c93d914726025b6b20f451499a31fd7006322 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/bioconductor-rhdf5lib_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/bioconductor-rhdf5lib_server.py') +SERVER_NAME = 'biosci_bioconductor_rhdf5lib' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..76b1d4811a976df2b79fe83db8d4a84fd0ba1942 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-rhdf5lib: + build: . + image: mcp-bioconductor-rhdf5lib:latest + container_name: mcp-bioconductor-rhdf5lib + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-rhdf5lib + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..017af4d64e893f7c7b7ba39e41e213740bfc4479 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-rhdf5lib + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-rhdf5lib/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0199e6470eb8a2609d41ef0bd1ef8f0982c7c79b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-scclassifr via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-scclassifr -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-scclassifr_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-scclassifr_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-scclassifr_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/app/bioconductor-scclassifr_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/app/bioconductor-scclassifr_server.py new file mode 100644 index 0000000000000000000000000000000000000000..565e41f4cf0287753665eed1f72053b3f1d7c5b0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/app/bioconductor-scclassifr_server.py @@ -0,0 +1,431 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Literal, Optional, List, Dict, Any + +# This is a placeholder for the actual MCP decorator as per the instructions. +# In a real MCP environment, you would use `from mcp import tool`. +class mcp: + @staticmethod + def tool(): + def decorator(func): + return func + return decorator + +# Define the available algorithms as a Literal type for type safety and validation +Algorithm = Literal["logistic", "svm", "randomforest", "dlda", "scmap", "scsorter", "seurat"] + +# --- R Script Templates --- +# These scripts are executed by the Python functions to interact with the scClassifR library. + +R_TRAIN_SCRIPT = """ +# R script to train a scClassifR model +suppressPackageStartupMessages(library(optparse)) +suppressPackageStartupMessages(library(scClassifR)) + +option_list <- list( + make_option(c("-e", "--expr_mat"), type="character", default=NULL, + help="Path to the expression matrix CSV file (cells as columns, genes as rows)", metavar="character"), + make_option(c("-c", "--cell_types"), type="character", default=NULL, + help="Path to the cell types CSV file (two columns: cell_id, cell_type)", metavar="character"), + make_option(c("-a", "--algorithm"), type="character", default="logistic", + help="Classification algorithm to use", metavar="character"), + make_option(c("-o", "--output_model"), type="character", default="model.rds", + help="Path to save the output model RDS file", metavar="character") +) + +opt_parser <- OptionParser(option_list=option_list) +opt <- parse_args(opt_parser) + +if (is.null(opt$expr_mat) || is.null(opt$cell_types)){ + print_help(opt_parser) + stop("Expression matrix and cell types files must be supplied.", call.=FALSE) +} + +# Read data +message("Reading expression matrix...") +expr_mat <- as.matrix(read.csv(opt$expr_mat, row.names=1, check.names=FALSE)) +message("Reading cell types...") +cell_types_df <- read.csv(opt$cell_types) + +# Align cell types with expression matrix columns +if (ncol(cell_types_df) < 2) { + stop("Cell types file must have at least two columns (e.g., cell_id, cell_type).", call.=FALSE) +} +colnames(cell_types_df)[1:2] <- c("cell_id", "cell_type") +cell_types_vec <- cell_types_df$cell_type +names(cell_types_vec) <- cell_types_df$cell_id +cell_types_aligned <- cell_types_vec[colnames(expr_mat)] + +if(any(is.na(cell_types_aligned))) { + warning("Some cells in the expression matrix do not have a corresponding cell type. These will be ignored.") + valid_cells <- !is.na(cell_types_aligned) + expr_mat <- expr_mat[, valid_cells] + cell_types_aligned <- cell_types_aligned[valid_cells] +} + +# Train model +message("Training model with algorithm: ", opt$algorithm) +fit <- run_scClassifR(expr_mat = expr_mat, + cell_type = cell_types_aligned, + algorithm = opt$algorithm) + +# Save model +message("Saving model to: ", opt$output_model) +saveRDS(fit, file = opt$output_model) + +message("Training complete.") +""" + +R_PREDICT_SCRIPT = """ +# R script to predict using a trained scClassifR model +suppressPackageStartupMessages(library(optparse)) +suppressPackageStartupMessages(library(scClassifR)) + +option_list <- list( + make_option(c("-t", "--test_data"), type="character", default=NULL, + help="Path to the test data matrix CSV file (cells as columns, genes as rows)", metavar="character"), + make_option(c("-m", "--model"), type="character", default=NULL, + help="Path to the trained model RDS file", metavar="character"), + make_option(c("-o", "--output_predictions"), type="character", default="predictions.csv", + help="Path to save the output predictions CSV file", metavar="character") +) + +opt_parser <- OptionParser(option_list=option_list) +opt <- parse_args(opt_parser) + +if (is.null(opt$test_data) || is.null(opt$model)){ + print_help(opt_parser) + stop("Test data and model file must be supplied.", call.=FALSE) +} + +# Read data and model +message("Reading test data from: ", opt$test_data) +test_data <- as.matrix(read.csv(opt$test_data, row.names=1, check.names=FALSE)) + +message("Loading model from: ", opt$model) +fit <- readRDS(opt$model) + +# Predict +message("Predicting cell types...") +predictions <- predict_scClassifR(test_data = test_data, + fit = fit) + +# Standardize and save predictions +pred_df <- data.frame(cell_id = colnames(test_data), + predicted_cell_type = predictions$pred_labs) + +message("Saving predictions to: ", opt$output_predictions) +write.csv(pred_df, file = opt$output_predictions, row.names = FALSE) + +message("Prediction complete.") +""" + +R_CLASSIFY_SCRIPT = """ +# R script to train and predict with scClassifR in one step +suppressPackageStartupMessages(library(optparse)) +suppressPackageStartupMessages(library(scClassifR)) + +option_list <- list( + make_option(c("--train_data"), type="character", default=NULL, + help="Path to the training expression matrix CSV file", metavar="character"), + make_option(c("--train_cell_types"), type="character", default=NULL, + help="Path to the training cell types CSV file", metavar="character"), + make_option(c("--test_data"), type="character", default=NULL, + help="Path to the test expression matrix CSV file", metavar="character"), + make_option(c("-a", "--algorithm"), type="character", default="logistic", + help="Classification algorithm to use", metavar="character"), + make_option(c("-o", "--output_predictions"), type="character", default="predictions.csv", + help="Path to save the output predictions CSV file", metavar="character"), + make_option(c("-m", "--output_model"), type="character", default=NULL, + help="Optional path to save the trained model RDS file", metavar="character") +) + +opt_parser <- OptionParser(option_list=option_list) +opt <- parse_args(opt_parser) + +if (is.null(opt$train_data) || is.null(opt$train_cell_types) || is.null(opt$test_data)){ + print_help(opt_parser) + stop("Training data, training cell types, and test data must be supplied.", call.=FALSE) +} + +# Read training data +message("Reading training data...") +train_data <- as.matrix(read.csv(opt$train_data, row.names=1, check.names=FALSE)) +train_cell_types_df <- read.csv(opt$train_cell_types) + +# Align training cell types +if (ncol(train_cell_types_df) < 2) { + stop("Training cell types file must have at least two columns (e.g., cell_id, cell_type).", call.=FALSE) +} +colnames(train_cell_types_df)[1:2] <- c("cell_id", "cell_type") +train_cell_types_vec <- train_cell_types_df$cell_type +names(train_cell_types_vec) <- train_cell_types_df$cell_id +train_cell_types_aligned <- train_cell_types_vec[colnames(train_data)] + +if(any(is.na(train_cell_types_aligned))) { + warning("Some cells in the training expression matrix do not have a corresponding cell type. These will be ignored.") + valid_cells <- !is.na(train_cell_types_aligned) + train_data <- train_data[, valid_cells] + train_cell_types_aligned <- train_cell_types_aligned[valid_cells] +} + +# Read test data +message("Reading test data...") +test_data <- as.matrix(read.csv(opt$test_data, row.names=1, check.names=FALSE)) + +# Classify +message("Running classification with algorithm: ", opt$algorithm) +results <- classify_scClassifR(train_data = train_data, + train_cell_type = train_cell_types_aligned, + test_data = test_data, + algorithm = opt$algorithm) + +# Save predictions +pred_df <- data.frame(cell_id = colnames(test_data), + predicted_cell_type = results$pred_labs) + +message("Saving predictions to: ", opt$output_predictions) +write.csv(pred_df, file = opt$output_predictions, row.names = FALSE) + +# Optionally save model +if (!is.null(opt$output_model)) { + message("Saving model to: ", opt$output_model) + saveRDS(results$fit, file = opt$output_model) +} + +message("Classification complete.") +""" + +@mcp.tool() +def scclassifr_train( + expression_matrix: Path, + cell_types: Path, + output_model_path: Path, + algorithm: Algorithm = "logistic", +) -> Dict[str, Any]: + """ + Trains a scClassifR model on a given expression matrix and cell type labels. + + This tool requires R to be installed in the environment, along with the + Bioconductor package 'scClassifR' and the CRAN package 'optparse'. + + Args: + expression_matrix: Path to the expression matrix CSV file. The first column + should be gene names/IDs (and set as row names), and + subsequent columns should be cell IDs. + cell_types: Path to the cell types CSV file. It must contain at least + two columns: the first for cell IDs (matching the expression + matrix) and the second for cell type labels. + output_model_path: Path to save the trained model as an RDS file. + algorithm: The classification algorithm to use. + + Returns: + A dictionary containing the command executed, stdout, stderr, and + the path to the output model file. + """ + # Input validation + if not expression_matrix.is_file(): + raise FileNotFoundError(f"Expression matrix file not found: {expression_matrix}") + if not cell_types.is_file(): + raise FileNotFoundError(f"Cell types file not found: {cell_types}") + + command_executed = "" + r_script_path_str = "" + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file: + r_script_file.write(R_TRAIN_SCRIPT) + r_script_path_str = r_script_file.name + + cmd = [ + "Rscript", + r_script_path_str, + "--expr_mat", str(expression_matrix.resolve()), + "--cell_types", str(cell_types.resolve()), + "--algorithm", algorithm, + "--output_model", str(output_model_path.resolve()), + ] + command_executed = " ".join(cmd) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_model_path.resolve())] + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"R script execution failed with exit code {e.returncode}", + "output_files": [] + } + finally: + if r_script_path_str and Path(r_script_path_str).exists(): + Path(r_script_path_str).unlink() + +@mcp.tool() +def scclassifr_predict( + test_data: Path, + model: Path, + output_predictions_path: Path, +) -> Dict[str, Any]: + """ + Predicts cell types for a test dataset using a pre-trained scClassifR model. + + This tool requires R to be installed in the environment, along with the + Bioconductor package 'scClassifR' and the CRAN package 'optparse'. + + Args: + test_data: Path to the test data expression matrix CSV file. Format should + match the training data (genes as rows, cells as columns). + model: Path to the pre-trained model RDS file generated by scclassifr_train. + output_predictions_path: Path to save the prediction results as a CSV file. + + Returns: + A dictionary containing the command executed, stdout, stderr, and + the path to the output predictions file. + """ + # Input validation + if not test_data.is_file(): + raise FileNotFoundError(f"Test data file not found: {test_data}") + if not model.is_file(): + raise FileNotFoundError(f"Model file not found: {model}") + + command_executed = "" + r_script_path_str = "" + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file: + r_script_file.write(R_PREDICT_SCRIPT) + r_script_path_str = r_script_file.name + + cmd = [ + "Rscript", + r_script_path_str, + "--test_data", str(test_data.resolve()), + "--model", str(model.resolve()), + "--output_predictions", str(output_predictions_path.resolve()), + ] + command_executed = " ".join(cmd) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_predictions_path.resolve())] + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"R script execution failed with exit code {e.returncode}", + "output_files": [] + } + finally: + if r_script_path_str and Path(r_script_path_str).exists(): + Path(r_script_path_str).unlink() + +@mcp.tool() +def scclassifr_classify( + train_data: Path, + train_cell_types: Path, + test_data: Path, + output_predictions_path: Path, + algorithm: Algorithm = "logistic", + output_model_path: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Trains a scClassifR model and predicts on a test set in a single step. + + This tool requires R to be installed in the environment, along with the + Bioconductor package 'scClassifR' and the CRAN package 'optparse'. + + Args: + train_data: Path to the training expression matrix CSV file. + train_cell_types: Path to the training cell types CSV file. + test_data: Path to the test expression matrix CSV file. + output_predictions_path: Path to save the prediction results as a CSV file. + algorithm: The classification algorithm to use. + output_model_path: Optional path to save the trained model as an RDS file. + + Returns: + A dictionary containing the command executed, stdout, stderr, and + paths to the output files (predictions and optionally the model). + """ + # Input validation + if not train_data.is_file(): + raise FileNotFoundError(f"Training data file not found: {train_data}") + if not train_cell_types.is_file(): + raise FileNotFoundError(f"Training cell types file not found: {train_cell_types}") + if not test_data.is_file(): + raise FileNotFoundError(f"Test data file not found: {test_data}") + + command_executed = "" + r_script_path_str = "" + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file: + r_script_file.write(R_CLASSIFY_SCRIPT) + r_script_path_str = r_script_file.name + + cmd = [ + "Rscript", + r_script_path_str, + "--train_data", str(train_data.resolve()), + "--train_cell_types", str(train_cell_types.resolve()), + "--test_data", str(test_data.resolve()), + "--algorithm", algorithm, + "--output_predictions", str(output_predictions_path.resolve()), + ] + + if output_model_path: + cmd.extend(["--output_model", str(output_model_path.resolve())]) + + command_executed = " ".join(cmd) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + + output_files = [str(output_predictions_path.resolve())] + if output_model_path: + output_files.append(str(output_model_path.resolve())) + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"R script execution failed with exit code {e.returncode}", + "output_files": [] + } + finally: + if r_script_path_str and Path(r_script_path_str).exists(): + Path(r_script_path_str).unlink() \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/app/bioconductor-scclassifr_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/app/bioconductor-scclassifr_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..25572150f7704ef52f628431094317584d8bc5fe --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/app/bioconductor-scclassifr_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/app/bioconductor-scclassifr_server.py') +SERVER_NAME = 'biosci_bioconductor_scclassifr' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..f82f48002d24d6289d3b99f9fba2179a4f651aae --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-scclassifr: + build: . + image: mcp-bioconductor-scclassifr:latest + container_name: mcp-bioconductor-scclassifr + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-scclassifr + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3fb43e58b7333525a1ea7c834d4fb1019f2b9276 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-scclassifr + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scclassifr/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6ee53d55bd3a3c228d647c5517fa0f11641009fd --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-scuttle via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-scuttle -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-scuttle_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-scuttle_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-scuttle_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/bioconductor-scuttle_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/bioconductor-scuttle_server.py new file mode 100644 index 0000000000000000000000000000000000000000..21d810a8c5b16b334b531d6f864405712d83f988 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/bioconductor-scuttle_server.py @@ -0,0 +1,205 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional + +# Assume @mcp.tool is imported +# from mcp import tool as mcp_tool + +# This is a placeholder for the actual decorator as per instructions. +def mcp_tool_placeholder(*args, **kwargs): + def decorator(func): + return func + return decorator + +mcp = type("MCP", (), {"tool": mcp_tool_placeholder}) + + +@mcp.tool() +def normalize_counts( + counts_matrix: Path, + output_matrix: Path, + size_factors_file: Optional[Path] = None, + center_size_factors: bool = True, + log_transform: bool = True, + pseudo_count: float = 1.0, +): + """ + Performs scaling normalization on a count matrix using scuttle::normalizeCounts. + + This function can compute normalized expression values, optionally on the log-scale. + It serves as a versatile wrapper for one of scuttle's primary normalization functions. + + Args: + counts_matrix: Path to the input count matrix file in CSV format. + The first column should contain gene names, and the header should contain cell names. + output_matrix: Path to save the output normalized matrix in CSV format. + size_factors_file: Optional path to a single-column file containing size factors for each cell. + If not provided, library sizes are used as size factors. + center_size_factors: A boolean indicating whether to center the size factors to have a mean of 1. + log_transform: A boolean indicating whether to apply a log2-transformation to the normalized counts. + pseudo_count: A positive pseudo-count to be added to counts before log-transformation. + This is only used if log_transform is True. + """ + # Input validation + if not counts_matrix.is_file(): + raise FileNotFoundError(f"Input counts matrix not found at {counts_matrix}") + if log_transform and pseudo_count <= 0: + raise ValueError("pseudo_count must be a positive number when log_transform is True.") + + size_factors_r_arg = "NULL" + if size_factors_file: + if not size_factors_file.is_file(): + raise FileNotFoundError(f"Size factors file not found at {size_factors_file}") + size_factors_r_arg = f'"{size_factors_file.resolve()}"' + + # Convert Python bool to R's TRUE/FALSE strings + center_sf_r = "TRUE" if center_size_factors else "FALSE" + log_transform_r = "TRUE" if log_transform else "FALSE" + + r_script_content = f""" + library(scuttle) + library(SingleCellExperiment) + + # Read the counts matrix + counts <- as.matrix(read.csv("{counts_matrix.resolve()}", row.names=1, check.names=FALSE)) + sce <- SingleCellExperiment(assays = list(counts = counts)) + + # Handle size factors if provided + size_factors_path <- {size_factors_r_arg} + size_factors_arg <- NULL + if (!is.null(size_factors_path)) {{ + sf <- read.table(size_factors_path, header=FALSE) + size_factors_arg <- as.numeric(sf[,1]) + }} + + # Perform normalization + normalized_matrix <- normalizeCounts( + sce, + size.factors=size_factors_arg, + center.size.factors={center_sf_r}, + log={log_transform_r}, + pseudo.count={pseudo_count} + ) + + # Write the output + write.csv(normalized_matrix, file="{output_matrix.resolve()}", row.names=TRUE, quote=FALSE) + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as tmp_script: + script_path = Path(tmp_script.name) + tmp_script.write(r_script_content) + + cmd = ["Rscript", str(script_path)] + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_matrix)] + } + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"R script execution failed with exit code {e.returncode}.\n" + f"Command: {command_executed}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) from e + finally: + # Ensure temporary script is always cleaned up + if script_path.exists(): + script_path.unlink() + + +@mcp.tool() +def aggregate_across_cells( + counts_matrix: Path, + ids_file: Path, + output_aggregated_matrix: Path, + use_assay_type: str = "counts", +): + """ + Aggregates counts across cells for specified groups using scuttle::aggregateAcrossCells. + + This is useful for creating pseudo-bulk expression profiles from single-cell data. + + Args: + counts_matrix: Path to the input count matrix file in CSV format. + The first column should contain gene names, and the header should contain cell names. + ids_file: Path to a single-column file containing group IDs (e.g., cluster or sample IDs) for each cell. + The order of IDs must match the order of columns (cells) in the counts_matrix. + output_aggregated_matrix: Path to save the output aggregated matrix in CSV format. + use_assay_type: The name of the assay in the SingleCellExperiment object to use for aggregation. + Defaults to "counts". + """ + # Input validation + if not counts_matrix.is_file(): + raise FileNotFoundError(f"Input counts matrix not found at {counts_matrix}") + if not ids_file.is_file(): + raise FileNotFoundError(f"IDs file not found at {ids_file}") + + r_script_content = f""" + library(scuttle) + library(SingleCellExperiment) + + # Read the counts matrix + counts <- as.matrix(read.csv("{counts_matrix.resolve()}", row.names=1, check.names=FALSE)) + sce <- SingleCellExperiment(assays = list(counts = counts)) + + # Read the IDs + ids <- read.table("{ids_file.resolve()}", header=FALSE, stringsAsFactors=TRUE) + ids_vector <- ids[,1] + + # Check if dimensions match + if (length(ids_vector) != ncol(sce)) {{ + stop(paste0("Number of IDs (", length(ids_vector), ") does not match the number of cells (", ncol(sce), ") in the count matrix.")) + }} + + # Perform aggregation + aggregated_sce <- aggregateAcrossCells(sce, ids=ids_vector, use.assay.type="{use_assay_type}") + + # Write the output + # The output assay from aggregateAcrossCells is named 'counts' by default. + aggregated_matrix <- assay(aggregated_sce, "counts") + write.csv(aggregated_matrix, file="{output_aggregated_matrix.resolve()}", row.names=TRUE, quote=FALSE) + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as tmp_script: + script_path = Path(tmp_script.name) + tmp_script.write(r_script_content) + + cmd = ["Rscript", str(script_path)] + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_aggregated_matrix)] + } + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"R script execution failed with exit code {e.returncode}.\n" + f"Command: {command_executed}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) from e + finally: + # Ensure temporary script is always cleaned up + if script_path.exists(): + script_path.unlink() \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/bioconductor-scuttle_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/bioconductor-scuttle_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f397a69f25d1df31f5e028f3dc2dca159e2850a6 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/bioconductor-scuttle_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/bioconductor-scuttle_server.py') +SERVER_NAME = 'biosci_bioconductor_scuttle' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..7c909d0bead237aa9ab2f732c37ddf5c18ebc617 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-scuttle: + build: . + image: mcp-bioconductor-scuttle:latest + container_name: mcp-bioconductor-scuttle + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-scuttle + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5e6f64cd74a25ad0f22345f422a742ec8fde9ad4 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-scuttle + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scuttle/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..49ed5db0945a4bc42fc7769b3635b498da4a576b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-shortread via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-shortread -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-shortread_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-shortread_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-shortread_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/bioconductor-shortread_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/bioconductor-shortread_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d289a3702eecb63da2e5365fa7d46e30fccfc6a7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/bioconductor-shortread_server.py @@ -0,0 +1,439 @@ +import subprocess +import tempfile +import uuid +from pathlib import Path +from typing import List, Optional, Dict, Any + +# Mock decorator for standalone execution +class mcp: + def tool(func): + return func + +@mcp.tool +def generate_qa_report( + fastq_files: List[Path], + output_dir: Path, + report_title: str = "ShortRead_QA_Report" +) -> Dict[str, Any]: + """ + Generates a comprehensive quality assessment (QA) report for one or more FASTQ files. + + This function uses the qa() and report() methods from the ShortRead R package + to create an HTML report summarizing read counts, base quality, nucleotide + frequencies, and other metrics. + + Args: + fastq_files: A list of paths to the input FASTQ files. + output_dir: The directory where the HTML report will be saved. + report_title: The title to be used for the generated HTML report. + + Returns: + A dictionary containing the execution command, stdout, stderr, and a list + of output files, including the main report HTML file. + """ + if not fastq_files: + raise ValueError("At least one FASTQ file must be provided.") + for f in fastq_files: + if not f.exists(): + raise FileNotFoundError(f"Input file not found: {f}") + + output_dir.mkdir(parents=True, exist_ok=True) + + # Pass file paths as a single comma-separated string to the R script + fastq_paths_str = ",".join(map(str, fastq_files)) + + r_script = f""" + # Ensure all required libraries are loaded + if (!requireNamespace("ShortRead", quietly = TRUE)) {{ + stop("ShortRead package is not installed.") + }} + library(ShortRead) + + # Retrieve command line arguments + args <- commandArgs(trailingOnly=TRUE) + fastq_paths_str <- args[1] + output_dir_path <- args[2] + report_name <- args[3] + + # Split the comma-separated string back into a vector of paths + fastq_files <- strsplit(fastq_paths_str, ",")[[1]] + + # Generate QA summary object + qa_summary <- qa(fastq_files, type="fastq") + + # Generate the HTML report + report(qa_summary, dest=output_dir_path, name=report_name) + + # Print the path to the main report file for easy identification + cat(paste0("Report generated at: ", file.path(output_dir_path, "index.html"), "\\n")) + """ + + cmd = [] + result_stdout = "" + result_stderr = "" + output_files = [] + + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as fp: + fp.write(r_script) + script_path = Path(fp.name) + + cmd = [ + "Rscript", + str(script_path), + fastq_paths_str, + str(output_dir), + report_title, + ] + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + result_stdout = process.stdout + result_stderr = process.stderr + + report_file = output_dir / "index.html" + if report_file.exists(): + output_files.append(str(report_file)) + # Add other potential report files if necessary + for item in output_dir.glob('**/*'): + if item.is_file() and str(item) not in output_files: + output_files.append(str(item)) + + + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"R script execution failed with exit code {e.returncode}.\n" + f"Command: {' '.join(cmd)}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + finally: + if 'script_path' in locals() and script_path.exists(): + script_path.unlink() + + return { + "command_executed": " ".join(cmd), + "stdout": result_stdout, + "stderr": result_stderr, + "output_files": {"qa_report_directory": str(output_dir), "report_files": output_files} + } + +@mcp.tool +def filter_fastq( + input_fastq: Path, + output_fastq: Path, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + max_n_bases: Optional[int] = None, + filter_low_complexity: bool = False +) -> Dict[str, Any]: + """ + Filters reads in a FASTQ file based on length, N content, and complexity. + + Args: + input_fastq: Path to the input FASTQ file. + output_fastq: Path to write the filtered FASTQ file. + min_length: Minimum read length to keep. + max_length: Maximum read length to keep. + max_n_bases: Maximum number of 'N' bases allowed in a read. + filter_low_complexity: If True, filters out low-complexity reads (e.g., 'AAAAAAAA...'). + + Returns: + A dictionary containing the execution command, stdout, stderr, and the path to the output file. + """ + if not input_fastq.exists(): + raise FileNotFoundError(f"Input file not found: {input_fastq}") + if output_fastq.parent: + output_fastq.parent.mkdir(parents=True, exist_ok=True) + + # Use 'NA' for optional numeric args not provided, which R can interpret + min_len_arg = min_length if min_length is not None else 'NA' + max_len_arg = max_length if max_length is not None else 'NA' + max_n_arg = max_n_bases if max_n_bases is not None else 'NA' + + r_script = f""" + library(ShortRead) + + args <- commandArgs(trailingOnly=TRUE) + input_path <- args[1] + output_path <- args[2] + min_len <- as.integer(args[3]) + max_len <- as.integer(args[4]) + max_n <- as.integer(args[5]) + filter_complex <- as.logical(args[6]) + + reads <- readFastq(input_path) + + # Build a list of filters to apply + filters <- c() + + if (!is.na(min_len) || !is.na(max_len)) {{ + min_val <- if (is.na(min_len)) 0 else min_len + max_val <- if (is.na(max_len)) Inf else max_len + filters <- c(filters, widthFilter(min=min_val, max=max_val)) + }} + + if (!is.na(max_n)) {{ + filters <- c(filters, nFilter(threshold=max_n)) + }} + + if (filter_complex) {{ + # DUST filter for low complexity + filters <- c(filters, dustytoLowComplexityFilter()) + }} + + # Apply filters if any were specified + if (length(filters) > 0) {{ + # Combine all filters into a single filter function + combined_filter <- do.call(compose, filters) + filtered_reads <- reads[combined_filter(reads)] + }} else {{ + filtered_reads <- reads + }} + + writeFastq(filtered_reads, output_path, compress=FALSE) + cat(paste0("Filtered reads written to: ", output_path, "\\n")) + """ + + cmd = [] + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as fp: + fp.write(r_script) + script_path = Path(fp.name) + + cmd = [ + "Rscript", + str(script_path), + str(input_fastq), + str(output_fastq), + str(min_len_arg), + str(max_len_arg), + str(max_n_arg), + "TRUE" if filter_low_complexity else "FALSE" + ] + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"filtered_fastq": str(output_fastq)} + } + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"R script execution failed with exit code {e.returncode}.\n" + f"Command: {' '.join(cmd)}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + finally: + if 'script_path' in locals() and script_path.exists(): + script_path.unlink() + +@mcp.tool +def trim_fastq( + input_fastq: Path, + output_fastq: Path, + trim_left: Optional[int] = None, + trim_right: Optional[int] = None, + trim_quality_threshold: Optional[int] = None +) -> Dict[str, Any]: + """ + Trims reads from a FASTQ file by position or quality. + + Args: + input_fastq: Path to the input FASTQ file. + output_fastq: Path to write the trimmed FASTQ file. + trim_left: Number of bases to trim from the 5' (left) end of each read. + trim_right: Number of bases to trim from the 3' (right) end of each read. + trim_quality_threshold: Trim trailing bases from the 3' end that are below this Phred quality score. + + Returns: + A dictionary containing the execution command, stdout, stderr, and the path to the output file. + """ + if not input_fastq.exists(): + raise FileNotFoundError(f"Input file not found: {input_fastq}") + if output_fastq.parent: + output_fastq.parent.mkdir(parents=True, exist_ok=True) + + trim_left_arg = trim_left if trim_left is not None else 'NA' + trim_right_arg = trim_right if trim_right is not None else 'NA' + trim_qual_arg = trim_quality_threshold if trim_quality_threshold is not None else 'NA' + + r_script = f""" + library(ShortRead) + + args <- commandArgs(trailingOnly=TRUE) + input_path <- args[1] + output_path <- args[2] + left_trim <- as.integer(args[3]) + right_trim <- as.integer(args[4]) + qual_trim <- as.integer(args[5]) + + reads <- readFastq(input_path) + + # 1. Quality trimming (from the right end) + if (!is.na(qual_trim)) {{ + # Convert Phred score to corresponding ASCII character + quality_char <- rawToChar(as.raw(qual_trim + 33)) + # trimTails removes bases from the right end until a base with quality >= quality_char is found + reads <- trimTails(reads, k=1, a=quality_char, halfwidth=0) + }} + + # 2. Positional trimming + if (!is.na(left_trim) || !is.na(right_trim)) {{ + current_widths <- width(reads) + start_pos <- if (!is.na(left_trim)) left_trim + 1 else 1 + + end_pos <- current_widths + if (!is.na(right_trim)) {{ + end_pos <- end_pos - right_trim + }} + + # Ensure start is not after end for any read + end_pos[end_pos < start_pos] <- start_pos - 1 + + reads <- narrow(reads, start=start_pos, end=end_pos) + }} + + writeFastq(reads, output_path, compress=FALSE) + cat(paste0("Trimmed reads written to: ", output_path, "\\n")) + """ + + cmd = [] + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as fp: + fp.write(r_script) + script_path = Path(fp.name) + + cmd = [ + "Rscript", + str(script_path), + str(input_fastq), + str(output_fastq), + str(trim_left_arg), + str(trim_right_arg), + str(trim_qual_arg) + ] + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"trimmed_fastq": str(output_fastq)} + } + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"R script execution failed with exit code {e.returncode}.\n" + f"Command: {' '.join(cmd)}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + finally: + if 'script_path' in locals() and script_path.exists(): + script_path.unlink() + +@mcp.tool +def sample_fastq( + input_fastq: Path, + output_fastq: Path, + n_reads: int, + ordered: bool = False +) -> Dict[str, Any]: + """ + Subsamples a specified number of reads from a FASTQ file. + + Args: + input_fastq: Path to the input FASTQ file. + output_fastq: Path to write the sampled FASTQ file. + n_reads: The number of reads to sample. + ordered: If True, takes the first n_reads; if False, performs random sampling. + + Returns: + A dictionary containing the execution command, stdout, stderr, and the path to the output file. + """ + if not input_fastq.exists(): + raise FileNotFoundError(f"Input file not found: {input_fastq}") + if n_reads <= 0: + raise ValueError("n_reads must be a positive integer.") + if output_fastq.parent: + output_fastq.parent.mkdir(parents=True, exist_ok=True) + + r_script = f""" + library(ShortRead) + + args <- commandArgs(trailingOnly=TRUE) + input_path <- args[1] + output_path <- args[2] + num_reads <- as.integer(args[3]) + is_ordered <- as.logical(args[4]) + + # Create a sampler object + sampler <- FastqSampler(input_path, n=num_reads, ordered=is_ordered) + + # Extract the sample + set.seed(123) # for reproducibility in random sampling + sampled_reads <- yield(sampler) + + # Close the sampler connection + close(sampler) + + writeFastq(sampled_reads, output_path, compress=FALSE) + cat(paste0(num_reads, " reads sampled to: ", output_path, "\\n")) + """ + + cmd = [] + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as fp: + fp.write(r_script) + script_path = Path(fp.name) + + cmd = [ + "Rscript", + str(script_path), + str(input_fastq), + str(output_fastq), + str(n_reads), + "TRUE" if ordered else "FALSE" + ] + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"sampled_fastq": str(output_fastq)} + } + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"R script execution failed with exit code {e.returncode}.\n" + f"Command: {' '.join(cmd)}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + finally: + if 'script_path' in locals() and script_path.exists(): + script_path.unlink() \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/bioconductor-shortread_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/bioconductor-shortread_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..94c809152874040d89b0010a161670fc2df345cc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/bioconductor-shortread_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/bioconductor-shortread_server.py') +SERVER_NAME = 'biosci_bioconductor_shortread' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..e5fbe972d4c91c6e95055b2deae82e8adf07186e --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-shortread: + build: . + image: mcp-bioconductor-shortread:latest + container_name: mcp-bioconductor-shortread + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-shortread + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..42305183482f14a6b1c00be1d192ded37a6392c2 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-shortread + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-shortread/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-signifinder/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-signifinder/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-signifinder/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..98cc94cca9af084a6cb227c60ff769c5af2d6ea8 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-spatialexperimentio via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-spatialexperimentio -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-spatialexperimentio_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-spatialexperimentio_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-spatialexperimentio_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/bioconductor-spatialexperimentio_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/bioconductor-spatialexperimentio_server.py new file mode 100644 index 0000000000000000000000000000000000000000..446babb44f912ee6de43543c0d22505e4c2ef85a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/bioconductor-spatialexperimentio_server.py @@ -0,0 +1,346 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Dict, List, Literal, Optional + +# This is a placeholder for the MCP decorator. +# In a real MCP environment, this would be provided by the MCP framework. +def tool(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + +mcp = type("mcp", (), {"tool": tool}) + + +@mcp.tool +def read_xenium( + xenium_dir: Path, + output_rds_path: Path, + type: Literal["spe", "sce"] = "spe", + z_option: str = "3D", + mols_as_assay: bool = False, + add_molecules: bool = True, +) -> Dict: + """ + Reads 10X Genomics Xenium data and creates a SpatialExperiment or SingleCellExperiment object. + + Args: + xenium_dir: Path to the Xenium output directory. + output_rds_path: Path to save the output RDS file. + type: Type of object to return, either 'spe' (SpatialExperiment) or 'sce' (SingleCellExperiment). + z_option: How to handle z-planes. Can be '3D' (default), 'split', or an integer specifying a z-plane. + mols_as_assay: Whether to store molecules as an assay. + add_molecules: Whether to add molecules to rowData. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the path to the output RDS file. + """ + # Input validation + if not xenium_dir.is_dir(): + raise ValueError(f"Input directory not found: {xenium_dir}") + if type not in ["spe", "sce"]: + raise ValueError("`type` must be either 'spe' or 'sce'.") + + # Validate z_option: must be '3D', 'split', or a valid integer string + is_valid_z_option = z_option in ["3D", "split"] or z_option.isdigit() + if not is_valid_z_option: + raise ValueError("`z_option` must be '3D', 'split', or an integer string.") + + # Format z_option for R script: strings need quotes, integers do not + z_option_r = f"'{z_option}'" if z_option in ["3D", "split"] else z_option + + # Format booleans for R + mols_as_assay_r = str(mols_as_assay).upper() + add_molecules_r = str(add_molecules).upper() + + r_script_content = f""" + library(SpatialExperimentIO) + + message("Reading Xenium data from: {xenium_dir}") + spe_object <- readXenium( + xenium_dir = "{xenium_dir}", + type = "{type}", + z_option = {z_option_r}, + mols_as_assay = {mols_as_assay_r}, + add_molecules = {add_molecules_r} + ) + + message("Saving object to: {output_rds_path}") + saveRDS(spe_object, file = "{output_rds_path}") + message("Done.") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as tmp_r_script: + tmp_r_script.write(r_script_content) + script_path = tmp_r_script.name + + cmd = ["Rscript", script_path] + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + # Clean up the temporary script file + Path(script_path).unlink() + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_rds_path)], + } + except subprocess.CalledProcessError as e: + # Clean up the temporary script file even on error + Path(script_path).unlink() + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R script execution failed.", + "return_code": e.returncode, + } + + +@mcp.tool +def read_cosmx( + cosmx_dir: Path, + output_rds_path: Path, + type: Literal["spe", "sce"] = "spe", + z_option: str = "3D", + mols_as_assay: bool = False, + add_molecules: bool = True, +) -> Dict: + """ + Reads Nanostring CosMx data and creates a SpatialExperiment or SingleCellExperiment object. + + Args: + cosmx_dir: Path to the CosMx output directory. + output_rds_path: Path to save the output RDS file. + type: Type of object to return, either 'spe' (SpatialExperiment) or 'sce' (SingleCellExperiment). + z_option: How to handle z-planes. Can be '3D' (default), 'split', or an integer specifying a z-plane. + mols_as_assay: Whether to store molecules as an assay. + add_molecules: Whether to add molecules to rowData. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the path to the output RDS file. + """ + # Input validation + if not cosmx_dir.is_dir(): + raise ValueError(f"Input directory not found: {cosmx_dir}") + if type not in ["spe", "sce"]: + raise ValueError("`type` must be either 'spe' or 'sce'.") + + is_valid_z_option = z_option in ["3D", "split"] or z_option.isdigit() + if not is_valid_z_option: + raise ValueError("`z_option` must be '3D', 'split', or an integer string.") + + z_option_r = f"'{z_option}'" if z_option in ["3D", "split"] else z_option + mols_as_assay_r = str(mols_as_assay).upper() + add_molecules_r = str(add_molecules).upper() + + r_script_content = f""" + library(SpatialExperimentIO) + + message("Reading CosMx data from: {cosmx_dir}") + spe_object <- readCosMx( + cosmx_dir = "{cosmx_dir}", + type = "{type}", + z_option = {z_option_r}, + mols_as_assay = {mols_as_assay_r}, + add_molecules = {add_molecules_r} + ) + + message("Saving object to: {output_rds_path}") + saveRDS(spe_object, file = "{output_rds_path}") + message("Done.") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as tmp_r_script: + tmp_r_script.write(r_script_content) + script_path = tmp_r_script.name + + cmd = ["Rscript", script_path] + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + Path(script_path).unlink() + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_rds_path)], + } + except subprocess.CalledProcessError as e: + Path(script_path).unlink() + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R script execution failed.", + "return_code": e.returncode, + } + + +@mcp.tool +def read_merscope( + merscope_dir: Path, + output_rds_path: Path, + type: Literal["spe", "sce"] = "spe", + z_option: str = "3D", + mols_as_assay: bool = False, + add_molecules: bool = True, +) -> Dict: + """ + Reads Vizgen MERSCOPE data and creates a SpatialExperiment or SingleCellExperiment object. + + Args: + merscope_dir: Path to the MERSCOPE output directory. + output_rds_path: Path to save the output RDS file. + type: Type of object to return, either 'spe' (SpatialExperiment) or 'sce' (SingleCellExperiment). + z_option: How to handle z-planes. Can be '3D' (default), 'split', or an integer specifying a z-plane. + mols_as_assay: Whether to store molecules as an assay. + add_molecules: Whether to add molecules to rowData. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the path to the output RDS file. + """ + # Input validation + if not merscope_dir.is_dir(): + raise ValueError(f"Input directory not found: {merscope_dir}") + if type not in ["spe", "sce"]: + raise ValueError("`type` must be either 'spe' or 'sce'.") + + is_valid_z_option = z_option in ["3D", "split"] or z_option.isdigit() + if not is_valid_z_option: + raise ValueError("`z_option` must be '3D', 'split', or an integer string.") + + z_option_r = f"'{z_option}'" if z_option in ["3D", "split"] else z_option + mols_as_assay_r = str(mols_as_assay).upper() + add_molecules_r = str(add_molecules).upper() + + r_script_content = f""" + library(SpatialExperimentIO) + + message("Reading MERSCOPE data from: {merscope_dir}") + spe_object <- readMerscope( + merscope_dir = "{merscope_dir}", + type = "{type}", + z_option = {z_option_r}, + mols_as_assay = {mols_as_assay_r}, + add_molecules = {add_molecules_r} + ) + + message("Saving object to: {output_rds_path}") + saveRDS(spe_object, file = "{output_rds_path}") + message("Done.") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as tmp_r_script: + tmp_r_script.write(r_script_content) + script_path = tmp_r_script.name + + cmd = ["Rscript", script_path] + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + Path(script_path).unlink() + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_rds_path)], + } + except subprocess.CalledProcessError as e: + Path(script_path).unlink() + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R script execution failed.", + "return_code": e.returncode, + } + + +@mcp.tool +def read_starmap_plus( + starmap_dir: Path, + output_rds_path: Path, + type: Literal["spe", "sce"] = "spe", +) -> Dict: + """ + Reads Broad Institute STARmapPLUS data and creates a SpatialExperiment or SingleCellExperiment object. + + Args: + starmap_dir: Path to the STARmapPLUS output directory. + output_rds_path: Path to save the output RDS file. + type: Type of object to return, either 'spe' (SpatialExperiment) or 'sce' (SingleCellExperiment). + + Returns: + A dictionary containing the command executed, stdout, stderr, and the path to the output RDS file. + """ + # Input validation + if not starmap_dir.is_dir(): + raise ValueError(f"Input directory not found: {starmap_dir}") + if type not in ["spe", "sce"]: + raise ValueError("`type` must be either 'spe' or 'sce'.") + + r_script_content = f""" + library(SpatialExperimentIO) + + message("Reading STARmapPLUS data from: {starmap_dir}") + spe_object <- readStarmapPLUS( + starmap_dir = "{starmap_dir}", + type = "{type}" + ) + + message("Saving object to: {output_rds_path}") + saveRDS(spe_object, file = "{output_rds_path}") + message("Done.") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as tmp_r_script: + tmp_r_script.write(r_script_content) + script_path = tmp_r_script.name + + cmd = ["Rscript", script_path] + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + Path(script_path).unlink() + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_rds_path)], + } + except subprocess.CalledProcessError as e: + Path(script_path).unlink() + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R script execution failed.", + "return_code": e.returncode, + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/bioconductor-spatialexperimentio_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/bioconductor-spatialexperimentio_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..82bb4a1bf3e409ec581728f01de28809a6ae6450 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/bioconductor-spatialexperimentio_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/bioconductor-spatialexperimentio_server.py') +SERVER_NAME = 'biosci_bioconductor_spatialexperimentio' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..746a09393525cfddf784759fbf929e2e2b937ce5 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-spatialexperimentio: + build: . + image: mcp-bioconductor-spatialexperimentio:latest + container_name: mcp-bioconductor-spatialexperimentio + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-spatialexperimentio + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..022e4233e0946773a6bb568d4611dd4fcbd6a10e --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-spatialexperimentio + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-spatialexperimentio/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..cfc7b71104b7e0cc293eaaa48fbbae7a56c0d742 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install bioconductor-tidyomics via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-tidyomics -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/bioconductor-tidyomics_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/bioconductor-tidyomics_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/bioconductor-tidyomics_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/bioconductor-tidyomics_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/bioconductor-tidyomics_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e5cb8cd750ac31dbfdc9a64da3ba06731aef7c9b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/bioconductor-tidyomics_server.py @@ -0,0 +1,194 @@ +import subprocess +import tempfile +from pathlib import Path +import logging +from typing import List, Optional + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Mock MCP decorator for standalone execution +class mcp: + @staticmethod + def tool(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + # In a real MCP environment, this would register the function. + # For this script, we just return the original function. + return wrapper + +@mcp.tool +def tidyomics_list_packages() -> dict: + """ + Lists the core packages included in the tidyomics ecosystem. + + This function executes an R script that calls `tidyomics::tidyomics_packages()` + to retrieve and print the names of the core tidyomics packages. + + Returns: + dict: A dictionary containing the command executed, stdout, stderr, + and a list of the tidyomics packages. + """ + r_script_content = """ + tryCatch({ + suppressPackageStartupMessages(library(tidyomics)) + packages <- tidyomics::tidyomics_packages() + cat(packages, sep = "\\n") + }, error = function(e) { + message("Error: ", e$message) + quit(status = 1) + }) + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file: + r_script_file.write(r_script_content) + r_script_path = Path(r_script_file.name) + + command = ["Rscript", str(r_script_path)] + command_executed = " ".join(command) + logger.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + + stdout_lines = result.stdout.strip().split('\n') + packages = [line for line in stdout_lines if line] + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "packages": packages + } + except subprocess.CalledProcessError as e: + logger.error(f"Error executing tidyomics_list_packages: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R script execution failed." + } + finally: + r_script_path.unlink() + +@mcp.tool +def tidyomics_attach_packages(quietly: bool = False) -> dict: + """ + Loads the tidyomics meta-package and captures the attachment messages. + + This function simulates the primary use of the package, which is to load + the entire tidyomics ecosystem into an R session. The startup messages, + which show attached packages and version info, are captured from stderr. + + Args: + quietly (bool): If True, suppresses startup messages. Defaults to False. + + Returns: + dict: A dictionary containing the command executed, stdout, and stderr + (which contains the attachment log). + """ + # R's boolean literals are uppercase + quietly_r = "TRUE" if quietly else "FALSE" + + r_script_content = f""" + tryCatch({{ + library(tidyomics, quietly = {quietly_r}) + }}, error = function(e) {{ + message("Error: ", e$message) + quit(status = 1) + }}) + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file: + r_script_file.write(r_script_content) + r_script_path = Path(r_script_file.name) + + command = ["Rscript", str(r_script_path)] + command_executed = " ".join(command) + logger.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + } + except subprocess.CalledProcessError as e: + logger.error(f"Error executing tidyomics_attach_packages: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R script execution failed." + } + finally: + r_script_path.unlink() + +@mcp.tool +def tidyomics_check_conflicts() -> dict: + """ + Reports on namespace conflicts between tidyomics packages and other loaded packages. + + This function executes `tidyomics::tidyomics_conflicts()` to identify functions + that are masked by others, which is a common issue when loading multiple + packages with overlapping function names. + + Returns: + dict: A dictionary containing the command executed, stdout (which contains + the conflict report), and stderr. + """ + r_script_content = """ + tryCatch({ + suppressPackageStartupMessages(library(tidyomics)) + conflicts <- tidyomics::tidyomics_conflicts() + print(conflicts) + }, error = function(e) { + message("Error: ", e$message) + quit(status = 1) + }) + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file: + r_script_file.write(r_script_content) + r_script_path = Path(r_script_file.name) + + command = ["Rscript", str(r_script_path)] + command_executed = " ".join(command) + logger.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + } + except subprocess.CalledProcessError as e: + logger.error(f"Error executing tidyomics_check_conflicts: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R script execution failed." + } + finally: + r_script_path.unlink() \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/bioconductor-tidyomics_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/bioconductor-tidyomics_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2b75712935b800fd7829f325c9ed5674d79a26a1 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/bioconductor-tidyomics_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/bioconductor-tidyomics_server.py') +SERVER_NAME = 'biosci_bioconductor_tidyomics' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..9b52cded7a055fcee257fccf09f8018d2e24a424 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-tidyomics: + build: . + image: mcp-bioconductor-tidyomics:latest + container_name: mcp-bioconductor-tidyomics + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-tidyomics + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..204ac4df1f6f51224574fd73d04681c9d3c2e6df --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-tidyomics + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-tidyomics/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..fe99026d68d574e29eaf2ce234ecd03abfad3f1e --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install dropkick via conda (e.g., from bioconda) +RUN conda install -c bioconda dropkick -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/dropkick_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/dropkick_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/dropkick_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/dropkick_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/dropkick_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ba47211738ac267003c71fbdfa180b5c96e9eac3 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/dropkick_server.py @@ -0,0 +1,193 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# MCP decorator is not imported as per instructions, +# but it is assumed to be available in the execution environment. +# from mcp import tool as mcp_tool +# +# def mcp_tool(): +# def decorator(f): +# return f +# return decorator +# +# mcp = type("mcp", (), {"tool": mcp_tool}) + + +@mcp.tool() +def dropkick_run( + counts_file: Path, + output_dir: Optional[Path] = None, + method: str = "subtraction", + thresh: Optional[float] = None, + genes: int = 100, + n_jobs: int = 1, +): + """ + Runs the main dropkick filtering pipeline on single-cell RNA sequencing data. + + This tool filters cells from raw count matrices, identifies high-quality cells, + and saves the results, including cell probability scores and labels, + in a new .h5ad file. + + Args: + counts_file: Path to the input counts file (.h5ad, .csv, or .tsv). + output_dir: Output directory. Defaults to the input file's directory. + method: Ambient background correction method. One of ['subtraction', 'multiplication']. + thresh: Override automatic empty droplet threshold. Must be a float between 0 and 1. + genes: Number of ambient genes to use. + n_jobs: Number of CPU cores to use. + + Returns: + A dictionary containing the executed command, stdout, stderr, and the path to the output .h5ad file. + """ + # --- Input Validation --- + if not counts_file.is_file(): + raise FileNotFoundError(f"Input file not found: {counts_file}") + + if method not in ["subtraction", "multiplication"]: + raise ValueError(f"Invalid method '{method}'. Must be 'subtraction' or 'multiplication'.") + + if thresh is not None and not (0.0 <= thresh <= 1.0): + raise ValueError(f"Threshold 'thresh' must be between 0.0 and 1.0, but got {thresh}.") + + if genes <= 0: + raise ValueError(f"Number of genes must be a positive integer, but got {genes}.") + + if n_jobs <= 0: + raise ValueError(f"Number of jobs 'n_jobs' must be a positive integer, but got {n_jobs}.") + + # --- Command Construction --- + cmd = ["dropkick", "run", str(counts_file)] + + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + cmd.extend(["--out", str(output_dir)]) + out_path = output_dir + else: + out_path = counts_file.parent + + if method != "subtraction": + cmd.extend(["--method", method]) + + if thresh is not None: + cmd.extend(["--thresh", str(thresh)]) + + if genes != 100: + cmd.extend(["--genes", str(genes)]) + + if n_jobs != 1: + cmd.extend(["--n_jobs", str(n_jobs)]) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + except FileNotFoundError: + raise RuntimeError("dropkick executable not found. Please ensure it is installed and in your PATH.") + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"dropkick run failed with exit code {e.returncode}", + "output_files": [] + } + + # --- Output Handling --- + # dropkick creates an output file named _dropkick.h5ad + input_basename = counts_file.stem + output_h5ad = out_path / f"{input_basename}_dropkick.h5ad" + + output_files = [] + if output_h5ad.exists(): + output_files.append(str(output_h5ad)) + else: + # Log a warning if the expected output is not found + result.stderr += f"\nWARNING: Expected output file not found at {output_h5ad}" + + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + + +@mcp.tool() +def dropkick_qc( + counts_file: Path, + output_dir: Optional[Path] = None, +): + """ + Runs a quick quality control check on scRNA-seq data. + + This tool generates a plot of the total UMI distribution and ambient genes, + which is saved as a PNG image file. + + Args: + counts_file: Path to the input counts file (.h5ad, .csv, or .tsv). + output_dir: Output directory. Defaults to the input file's directory. + + Returns: + A dictionary containing the executed command, stdout, stderr, and the path to the output QC plot. + """ + # --- Input Validation --- + if not counts_file.is_file(): + raise FileNotFoundError(f"Input file not found: {counts_file}") + + # --- Command Construction --- + cmd = ["dropkick", "qc", str(counts_file)] + + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + cmd.extend(["--out", str(output_dir)]) + out_path = output_dir + else: + out_path = counts_file.parent + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + except FileNotFoundError: + raise RuntimeError("dropkick executable not found. Please ensure it is installed and in your PATH.") + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"dropkick qc failed with exit code {e.returncode}", + "output_files": [] + } + + # --- Output Handling --- + # dropkick qc creates an output file named _qc.png + input_basename = counts_file.stem + output_png = out_path / f"{input_basename}_qc.png" + + output_files = [] + if output_png.exists(): + output_files.append(str(output_png)) + else: + # Log a warning if the expected output is not found + result.stderr += f"\nWARNING: Expected output file not found at {output_png}" + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/dropkick_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/dropkick_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e804008db3278e3e697c94bdbda2e0d8d33b6650 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/dropkick_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/dropkick_server.py') +SERVER_NAME = 'biosci_dropkick' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..fedcde33ee80da29a12ca9733d05204565e3cfe8 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-dropkick: + build: . + image: mcp-dropkick:latest + container_name: mcp-dropkick + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=dropkick + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dad3f0acf4878ef9f80138f938642ec863c13cb8 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - dropkick + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_dropkick/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0fba7c013d69f5087a707d9ce0a234c4f581f523 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install fgbio via conda (e.g., from bioconda) +RUN conda install -c bioconda fgbio -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/fgbio_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/fgbio_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/fgbio_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/fgbio_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/fgbio_server.py new file mode 100644 index 0000000000000000000000000000000000000000..89779612e7de30bb6593e91b0a52f6d59833999e --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/fgbio_server.py @@ -0,0 +1,650 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# MCP-related imports are not needed for the final code as per instructions. +# A placeholder class is used here for syntax validation. +class MCPPlaceholder: + def tool(self): + def decorator(func): + return func + return decorator + +mcp = MCPPlaceholder() + +def _run_fgbio_command(command: List[str], output_files: List[Path]) -> dict: + """ + Executes an fgbio command using subprocess and returns a structured output. + + Args: + command: The command to execute as a list of strings. + output_files: A list of expected output file paths. + + Returns: + A dictionary containing execution details and results. + """ + command_str = " ".join(command) + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": command_str, + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(p) for p in output_files] + } + except FileNotFoundError: + raise RuntimeError("fgbio command not found. Please ensure it is in your system's PATH.") + except subprocess.CalledProcessError as e: + # Return a structured error response if the tool fails + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "output_files": [] + } + +@mcp.tool() +def annotate_bam_with_umis( + input_bam: Path, + output_bam: Path, + fastq: List[Path], + read_structure: List[str], + umi_tag: str = "RX", + molecular_index_tag: str = "MI", + allow_missing: bool = False, +) -> dict: + """ + Annotates a BAM file with UMIs from a separate FASTQ file. + + Args: + input_bam: The input BAM file to annotate. + output_bam: The output BAM file with UMI tags. + fastq: One or more FASTQ files containing the UMIs. + read_structure: The read structure of the FASTQ files. + umi_tag: The tag to use for the raw UMI sequence. + molecular_index_tag: The tag to use for the corrected/final UMI. + allow_missing: If true, allow reads to be missing from the FASTQ. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + for fq in fastq: + if not fq.is_file(): + raise FileNotFoundError(f"Input FASTQ file not found: {fq}") + + command = ["fgbio", "AnnotateBamWithUmis", "--input", str(input_bam), "--output", str(output_bam)] + for fq in fastq: + command.extend(["--fastq", str(fq)]) + for rs in read_structure: + command.extend(["--read-structure", rs]) + command.extend(["--umi-tag", umi_tag, "--molecular-index-tag", molecular_index_tag]) + if allow_missing: + command.append("--allow-missing") + + return _run_fgbio_command(command, [output_bam]) + +@mcp.tool() +def call_molecular_consensus_reads( + input_bam: Path, + output_bam: Path, + ref: Path, + min_reads: int = 1, + min_input_base_q: int = 20, + min_consensus_base_q: int = 40, + max_base_error_rate: float = 0.1, + min_base_agreement: float = 0.8, + no_op_if_no_consensus: bool = False, + threads: int = 1, + umi_tag: str = "MI", + read_name_prefix: Optional[str] = None, + unclipped_consensus: bool = False, +) -> dict: + """ + Calls consensus sequences from reads with the same UMI. + + Args: + input_bam: Input BAM file with UMI-grouped reads. + output_bam: Output BAM file with consensus reads. + ref: Reference FASTA file. + min_reads: The minimum number of reads supporting a consensus. + min_input_base_q: Minimum base quality for a base to be included from an input read. + min_consensus_base_q: Minimum base quality for a base in the consensus read. + max_base_error_rate: The maximum rate of disagreement for a base to be called. + min_base_agreement: The minimum fraction of reads that must agree for a base to be called. + no_op_if_no_consensus: If true, write the original read if no consensus can be formed. + threads: Number of threads to use. + umi_tag: The tag containing the UMI. + read_name_prefix: A prefix for consensus read names. + unclipped_consensus: If true, generate the consensus on unclipped reads. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if not ref.is_file(): + raise FileNotFoundError(f"Reference FASTA file not found: {ref}") + if min_reads < 1: + raise ValueError("--min-reads must be >= 1.") + if threads < 1: + raise ValueError("--threads must be >= 1.") + + command = [ + "fgbio", "CallMolecularConsensusReads", + "--input", str(input_bam), + "--output", str(output_bam), + "--ref", str(ref), + "--min-reads", str(min_reads), + "--min-input-base-q", str(min_input_base_q), + "--min-consensus-base-q", str(min_consensus_base_q), + "--max-base-error-rate", str(max_base_error_rate), + "--min-base-agreement", str(min_base_agreement), + "--threads", str(threads), + "--umi-tag", umi_tag, + ] + if no_op_if_no_consensus: + command.append("--no-op-if-no-consensus") + if unclipped_consensus: + command.append("--unclipped-consensus") + if read_name_prefix: + command.extend(["--read-name-prefix", read_name_prefix]) + + return _run_fgbio_command(command, [output_bam]) + +@mcp.tool() +def clip_bam( + input_bam: Path, + output_bam: Path, + ref: Path, + clipping_mode: str = "Soft", + soft_clip_overhanging_reads: bool = True, + auto_clip_attributes: bool = True, + read_one_five_prime_clip: int = 0, + read_one_three_prime_clip: int = 0, + read_two_five_prime_clip: int = 0, + read_two_three_prime_clip: int = 0, +) -> dict: + """ + Clips reads in a BAM file based on distance from the end of the reference contig. + + Args: + input_bam: The input BAM file. + output_bam: The output clipped BAM file. + ref: The reference FASTA file. + clipping_mode: The type of clipping to perform (Soft, Hard, NoClip). + soft_clip_overhanging_reads: If true, soft-clip reads that overhang the end of the reference. + auto_clip_attributes: If true, automatically clip attributes like 'OQ'. + read_one_five_prime_clip: Number of bases to clip from the 5' end of read 1. + read_one_three_prime_clip: Number of bases to clip from the 3' end of read 1. + read_two_five_prime_clip: Number of bases to clip from the 5' end of read 2. + read_two_three_prime_clip: Number of bases to clip from the 3' end of read 2. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if not ref.is_file(): + raise FileNotFoundError(f"Reference FASTA file not found: {ref}") + if clipping_mode not in ["Soft", "Hard", "NoClip"]: + raise ValueError(f"Invalid clipping mode: {clipping_mode}") + + command = [ + "fgbio", "ClipBam", + "--input", str(input_bam), + "--output", str(output_bam), + "--ref", str(ref), + "--clipping-mode", clipping_mode, + "--read-one-five-prime-clip", str(read_one_five_prime_clip), + "--read-one-three-prime-clip", str(read_one_three_prime_clip), + "--read-two-five-prime-clip", str(read_two_five_prime_clip), + "--read-two-three-prime-clip", str(read_two_three_prime_clip), + ] + if soft_clip_overhanging_reads: + command.append("--soft-clip-overhanging-reads") + if auto_clip_attributes: + command.append("--auto-clip-attributes") + + return _run_fgbio_command(command, [output_bam]) + +@mcp.tool() +def collect_duplex_umi_metrics( + input_bam: Path, + output_prefix: Path, + umi_tag: str = "MI", + family_size_tag: str = "ZA", + min_map_q: int = 1, + threads: int = 1, +) -> dict: + """ + Collects metrics about duplex UMI families. + + Args: + input_bam: Input BAM file with UMI information. + output_prefix: The prefix for output metrics files. + umi_tag: The tag containing the UMI. + family_size_tag: The tag containing the family size. + min_map_q: Minimum mapping quality to include a read. + threads: Number of threads to use. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if threads < 1: + raise ValueError("--threads must be >= 1.") + + command = [ + "fgbio", "CollectDuplexUmiMetrics", + "--input", str(input_bam), + "--output", str(output_prefix), + "--umi-tag", umi_tag, + "--family-size-tag", family_size_tag, + "--min-map-q", str(min_map_q), + "--threads", str(threads), + ] + + output_files = [ + output_prefix.with_suffix(".duplex_umi_metrics.txt"), + output_prefix.with_suffix(".duplex_umi_family_size.histogram.pdf"), + output_prefix.with_suffix(".duplex_umi_family_size.histogram.tsv"), + ] + return _run_fgbio_command(command, output_files) + +@mcp.tool() +def collect_insert_size_metrics( + input_bam: Path, + output_metrics: Path, + ref: Optional[Path] = None, + histogram: Optional[Path] = None, + min_map_q: int = 20, + deviations: float = 10.0, + min_histogram_width: int = 0, + max_histogram_width: int = 0, + include_duplicates: bool = False, + paired_only: bool = True, +) -> dict: + """ + Collects insert size metrics from a BAM file. + + Args: + input_bam: The input BAM file. + output_metrics: The output metrics file. + ref: The reference FASTA file. + histogram: Optional path to write the insert size histogram PDF. + min_map_q: Minimum mapping quality to include a read. + deviations: The number of standard deviations from the mean to include. + min_histogram_width: Minimum width for the histogram. + max_histogram_width: Maximum width for the histogram. + include_duplicates: If true, include duplicate reads in the analysis. + paired_only: If true, only include properly paired reads. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if ref and not ref.is_file(): + raise FileNotFoundError(f"Reference FASTA file not found: {ref}") + + command = [ + "fgbio", "CollectInsertSizeMetrics", + "--input", str(input_bam), + "--output", str(output_metrics), + "--min-map-q", str(min_map_q), + "--deviations", str(deviations), + "--min-histogram-width", str(min_histogram_width), + "--max-histogram-width", str(max_histogram_width), + ] + if ref: + command.extend(["--ref", str(ref)]) + if histogram: + command.extend(["--histogram", str(histogram)]) + if include_duplicates: + command.append("--include-duplicates") + if paired_only: + command.append("--paired-only") + + output_files = [output_metrics] + if histogram: + output_files.append(histogram) + return _run_fgbio_command(command, output_files) + +@mcp.tool() +def compare_bam( + input_1: Path, + input_2: Path, + ref: Optional[Path] = None, + intervals: Optional[Path] = None, + verbose: bool = False, + max_diffs: int = 1000, +) -> dict: + """ + Compares two BAM files for differences. + + Args: + input_1: The first BAM file. + input_2: The second BAM file. + ref: The reference FASTA file. + intervals: An interval list to restrict comparison. + verbose: If true, print all differences. + max_diffs: The maximum number of differences to report. + """ + if not input_1.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_1}") + if not input_2.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_2}") + if ref and not ref.is_file(): + raise FileNotFoundError(f"Reference FASTA file not found: {ref}") + if intervals and not intervals.is_file(): + raise FileNotFoundError(f"Intervals file not found: {intervals}") + + command = [ + "fgbio", "CompareBam", + "--input-1", str(input_1), + "--input-2", str(input_2), + "--max-diffs", str(max_diffs), + ] + if ref: + command.extend(["--ref", str(ref)]) + if intervals: + command.extend(["--intervals", str(intervals)]) + if verbose: + command.append("--verbose") + + return _run_fgbio_command(command, []) + +@mcp.tool() +def demux_fastqs( + inputs: List[Path], + output_dir: Path, + barcodes: Path, + metrics: Path, + read_structures: List[str], + max_mismatches: int = 1, + min_mismatch_delta: int = 2, + max_no_calls: int = 1, + min_base_quality: int = 10, + threads: int = 1, +) -> dict: + """ + Demultiplexes FASTQ files based on a barcode file. + + Args: + inputs: Input FASTQ files (interleaved or paired). + output_dir: The directory to write demultiplexed FASTQs to. + barcodes: A file containing barcodes and sample names. + metrics: A file to write demultiplexing metrics to. + read_structures: The read structure of the input FASTQs. + max_mismatches: Maximum mismatches allowed in a barcode. + min_mismatch_delta: Minimum difference between the best and second-best barcode match. + max_no_calls: Maximum number of no-calls (N) in a barcode. + min_base_quality: Minimum base quality for a barcode base. + threads: Number of threads to use. + """ + for fq in inputs: + if not fq.is_file(): + raise FileNotFoundError(f"Input FASTQ file not found: {fq}") + if not barcodes.is_file(): + raise FileNotFoundError(f"Barcodes file not found: {barcodes}") + if threads < 1: + raise ValueError("--threads must be >= 1.") + + command = [ + "fgbio", "DemuxFastqs", + "--output", str(output_dir), + "--barcodes", str(barcodes), + "--metrics", str(metrics), + "--max-mismatches", str(max_mismatches), + "--min-mismatch-delta", str(min_mismatch_delta), + "--max-no-calls", str(max_no_calls), + "--min-base-quality", str(min_base_quality), + "--threads", str(threads), + ] + for fq in inputs: + command.extend(["--inputs", str(fq)]) + for rs in read_structures: + command.extend(["--read-structures", rs]) + + # Output files are dynamically generated in the output_dir, so we can't list them all. + # We'll return the metrics file and the output directory. + return _run_fgbio_command(command, [metrics, output_dir]) + +@mcp.tool() +def fastq_to_bam( + input_fqs: List[Path], + output_bam: Path, + read_structures: List[str], + sample: str, + library: str, + platform_unit: str, + platform: str = "Illumina", + sort_order: str = "coordinate", + reference: Optional[Path] = None, + tmp_dir: Optional[Path] = None, +) -> dict: + """ + Converts FASTQ files to an unmapped or mapped BAM file. + + Args: + input_fqs: One or more FASTQ files to convert. + output_bam: The output BAM file. + read_structures: The read structure of the FASTQ files. + sample: The sample name. + library: The library name. + platform_unit: The platform unit (e.g., flowcell barcode). + platform: The sequencing platform. + sort_order: The sort order for the output BAM. + reference: Optional reference FASTA for coordinate sorting. + tmp_dir: Optional temporary directory. + """ + for fq in input_fqs: + if not fq.is_file(): + raise FileNotFoundError(f"Input FASTQ file not found: {fq}") + if sort_order not in ["coordinate", "queryname", "unsorted"]: + raise ValueError(f"Invalid sort order: {sort_order}") + if sort_order == "coordinate" and not reference: + raise ValueError("A reference must be provided for coordinate sorting.") + if reference and not reference.is_file(): + raise FileNotFoundError(f"Reference FASTA file not found: {reference}") + + command = [ + "fgbio", "FastqToBam", + "--output", str(output_bam), + "--sample", sample, + "--library", library, + "--platform-unit", platform_unit, + "--platform", platform, + "--sort-order", sort_order, + ] + for fq in input_fqs: + command.extend(["--input", str(fq)]) + for rs in read_structures: + command.extend(["--read-structures", rs]) + if reference: + command.extend(["--reference", str(reference)]) + if tmp_dir: + command.extend(["--tmp-dir", str(tmp_dir)]) + + return _run_fgbio_command(command, [output_bam]) + +@mcp.tool() +def filter_consensus_reads( + input_bam: Path, + output_bam: Path, + ref: Path, + min_reads: int = 1, + min_base_q: int = 20, + min_mean_base_q: float = 0.0, + max_base_error_rate: float = 0.1, + max_no_call_fraction: float = 0.1, + reverse_per_base: bool = False, + reverse_per_read: bool = False, +) -> dict: + """ + Filters consensus reads from a BAM file based on quality and composition. + + Args: + input_bam: Input BAM file of consensus reads. + output_bam: Output filtered BAM file. + ref: Reference FASTA file. + min_reads: Minimum number of raw reads that contributed to the consensus. + min_base_q: Minimum base quality for any base in the consensus read. + min_mean_base_q: Minimum mean base quality for the consensus read. + max_base_error_rate: Maximum error rate for any base in the consensus. + max_no_call_fraction: Maximum fraction of Ns in the consensus read. + reverse_per_base: If true, reverse the per-base filters. + reverse_per_read: If true, reverse the per-read filters. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if not ref.is_file(): + raise FileNotFoundError(f"Reference FASTA file not found: {ref}") + + command = [ + "fgbio", "FilterConsensusReads", + "--input", str(input_bam), + "--output", str(output_bam), + "--ref", str(ref), + "--min-reads", str(min_reads), + "--min-base-q", str(min_base_q), + "--min-mean-base-q", str(min_mean_base_q), + "--max-base-error-rate", str(max_base_error_rate), + "--max-no-call-fraction", str(max_no_call_fraction), + ] + if reverse_per_base: + command.append("--reverse-per-base") + if reverse_per_read: + command.append("--reverse-per-read") + + return _run_fgbio_command(command, [output_bam]) + +@mcp.tool() +def group_reads_by_umi( + input_bam: Path, + output_bam: Path, + strategy: str = "Adjacency", + edits: int = 1, + min_map_q: int = 1, + raw_tag: str = "RX", + umi_tag: str = "MI", + family_size_tag: str = "ZA", + min_reads: int = 1, + threads: int = 1, + allow_missing_umis: bool = False, +) -> dict: + """ + Groups reads by UMI, correcting for sequencing errors. + + Args: + input_bam: Input BAM file with UMI tags. + output_bam: Output BAM file with corrected UMI tags and family size tags. + strategy: The UMI grouping strategy (e.g., Adjacency, Identity). + edits: The maximum edit distance for UMIs to be grouped together. + min_map_q: Minimum mapping quality to include a read. + raw_tag: The tag containing the raw UMI sequence. + umi_tag: The tag to store the corrected UMI. + family_size_tag: The tag to store the molecular family size. + min_reads: Minimum number of reads for a UMI to be included. + threads: Number of threads to use. + allow_missing_umis: If true, allow reads that are missing the raw UMI tag. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if strategy not in ["Adjacency", "Identity", "Paired"]: + raise ValueError(f"Invalid strategy: {strategy}") + if threads < 1: + raise ValueError("--threads must be >= 1.") + + command = [ + "fgbio", "GroupReadsByUmi", + "--input", str(input_bam), + "--output", str(output_bam), + "--strategy", strategy, + "--edits", str(edits), + "--min-map-q", str(min_map_q), + "--raw-tag", raw_tag, + "--umi-tag", umi_tag, + "--family-size-tag", family_size_tag, + "--min-reads", str(min_reads), + "--threads", str(threads), + ] + if allow_missing_umis: + command.append("--allow-missing-umis") + + return _run_fgbio_command(command, [output_bam]) + +@mcp.tool() +def set_mate_information( + input_bam: Path, + output_bam: Path, + ref: Optional[Path] = None, + sort_order: str = "queryname", +) -> dict: + """ + Fixes mate pair information in a BAM file. + + Args: + input_bam: The input BAM file. + output_bam: The output BAM file with corrected mate information. + ref: The reference FASTA file, required if sorting to coordinate. + sort_order: The sort order to apply before fixing mates. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if sort_order not in ["queryname", "coordinate"]: + raise ValueError(f"Invalid sort order: {sort_order}") + if sort_order == "coordinate" and not ref: + raise ValueError("A reference must be provided for coordinate sorting.") + if ref and not ref.is_file(): + raise FileNotFoundError(f"Reference FASTA file not found: {ref}") + + command = [ + "fgbio", "SetMateInformation", + "--input", str(input_bam), + "--output", str(output_bam), + "--sort-order", sort_order, + ] + if ref: + command.extend(["--ref", str(ref)]) + + return _run_fgbio_command(command, [output_bam]) + +@mcp.tool() +def split_bam( + input_bam: Path, + output_dir: Path, + tag: Optional[str] = None, + by_read_group: bool = False, + by_library: bool = False, + ref: Optional[Path] = None, +) -> dict: + """ + Splits a BAM file into multiple BAMs based on a tag, read group, or library. + + Args: + input_bam: The input BAM file to split. + output_dir: The directory to write the output BAM files to. + tag: The BAM tag to split on. + by_read_group: If true, split by read group. + by_library: If true, split by library. + ref: The reference FASTA file. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if sum([tag is not None, by_read_group, by_library]) != 1: + raise ValueError("Exactly one of --tag, --by-read-group, or --by-library must be specified.") + if ref and not ref.is_file(): + raise FileNotFoundError(f"Reference FASTA file not found: {ref}") + + command = [ + "fgbio", "SplitBam", + "--input", str(input_bam), + "--output", str(output_dir), + ] + if tag: + command.extend(["--tag", tag]) + if by_read_group: + command.append("--by-read-group") + if by_library: + command.append("--by-library") + if ref: + command.extend(["--ref", str(ref)]) + + # Output files are dynamic, so we return the directory. + return _run_fgbio_command(command, [output_dir]) \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/fgbio_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/fgbio_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..acbfc264e9c624d28110429723e1d6b1d6d9cbc5 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/fgbio_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/fgbio_server.py') +SERVER_NAME = 'biosci_fgbio' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..7487134571869fa24811a5d1a88a41383b58b844 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-fgbio: + build: . + image: mcp-fgbio:latest + container_name: mcp-fgbio + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=fgbio + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e88743b4cc21e314b6e4f5bd22a99f0aa605b896 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - fgbio + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_fgbio/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..218ba073ed52e41b06724a0b5cbe6e2989298445 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install filechunkio via conda (e.g., from bioconda) +RUN conda install -c bioconda filechunkio -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/filechunkio_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/filechunkio_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/filechunkio_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/filechunkio_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/filechunkio_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9e6065b07b208b210dd5b3f08e66a7e952172adf --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/filechunkio_server.py @@ -0,0 +1,161 @@ +import math +import os +import subprocess +import tempfile +from pathlib import Path +from typing import List + +# @mcp.tool is a placeholder for the actual decorator. +# The user specified "NO NEED to import mcp". + +@mcp.tool() +def create_chunks( + input_file: Path, + output_dir: Path, + chunk_size_mb: int = 10, + prefix: str = "chunk_" +): + """ + Splits a large file into smaller, fixed-size chunks. + + This tool provides a command-line-like interface for the 'filechunkio' library, + which itself does not have a CLI. It is useful for preparing large files for + multipart uploads or parallel processing. The execution requires a Python + environment with the 'filechunkio' library installed. + + Args: + input_file: Path to the large file to be split. + output_dir: Directory to save the output chunk files. It will be created if it doesn't exist. + chunk_size_mb: The size of each chunk in megabytes (MB). Must be a positive integer. + prefix: A string to prefix each output chunk file name. + """ + # --- Input Validation --- + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found or is not a regular file: {input_file}") + + if chunk_size_mb <= 0: + raise ValueError("chunk_size_mb must be a positive integer.") + + # --- File Path Handling --- + # The output directory is created within the executed script to ensure it's handled + # by the execution environment, but we can also create it here for early validation. + output_dir.mkdir(parents=True, exist_ok=True) + + # --- Dynamic Script Generation --- + # This approach adheres to the MCP pattern of using subprocess.run by creating a + # temporary script that utilizes the filechunkio library. + script_content = f""" +import math +import os +import sys +from pathlib import Path + +try: + from filechunkio import FileChunkIO +except ImportError: + sys.stderr.write("Error: The 'filechunkio' library is not installed in the Python environment.\\n") + sys.exit(1) + +def main(): + input_file = Path('{input_file.resolve()}') + output_dir = Path('{output_dir.resolve()}') + chunk_size_mb = {chunk_size_mb} + prefix = '{prefix}' + + output_dir.mkdir(parents=True, exist_ok=True) + + chunk_size_bytes = chunk_size_mb * 1024 * 1024 + file_size = os.path.getsize(input_file) + + if file_size == 0: + print("Input file is empty. No chunks will be created.") + print("---OUTPUT FILES---") + return + + num_chunks = int(math.ceil(file_size / float(chunk_size_bytes))) + + output_files = [] + print(f"Splitting file '{{input_file.name}}' ({{file_size}} bytes) into {{num_chunks}} chunks of ~{{chunk_size_mb}} MB.") + + for i in range(num_chunks): + offset = chunk_size_bytes * i + bytes_to_read = min(chunk_size_bytes, file_size - offset) + + output_chunk_path = output_dir / f"{{prefix}}{{i:04d}}.part" + + try: + with FileChunkIO(str(input_file), 'r', offset=offset, bytes=bytes_to_read) as chunk: + with open(output_chunk_path, 'wb') as chunk_out: + # Read in smaller blocks to handle memory efficiently for large chunks + buffer_size = 1024 * 1024 # 1MB buffer + while True: + data = chunk.read(buffer_size) + if not data: + break + chunk_out.write(data) + + output_files.append(str(output_chunk_path)) + print(f"Successfully created chunk: {{output_chunk_path}}") + except Exception as e: + sys.stderr.write(f"Failed to create chunk {{i}}: {{e}}\\n") + sys.exit(1) + + # Print a separator and then the list of output files for easy parsing + print("---OUTPUT FILES---") + for f in output_files: + print(f) + +if __name__ == "__main__": + main() +""" + + script_path = None + command = [] + try: + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".py") as tmp_script: + tmp_script.write(script_content) + script_path = Path(tmp_script.name) + + # --- Subprocess Execution --- + command = ["python", str(script_path)] + + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + + stdout = process.stdout + stderr = process.stderr + + # --- Structured Result Return --- + # Parse stdout to find the list of output files + output_files_list = [] + if "---OUTPUT FILES---" in stdout: + _, file_list_str = stdout.split("---OUTPUT FILES---", 1) + for line in file_list_str.strip().splitlines(): + if line.strip(): + p = Path(line.strip()) + if p.exists(): + output_files_list.append(str(p)) + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": output_files_list + } + + except subprocess.CalledProcessError as e: + # Capture output even on failure and return a structured error + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + finally: + # Clean up the temporary script + if script_path and script_path.exists(): + script_path.unlink() \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/filechunkio_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/filechunkio_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..58baf926ba40d4dd3758390da33d29de6543af8b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/filechunkio_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/filechunkio_server.py') +SERVER_NAME = 'biosci_filechunkio' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..329e95f2fbfa7ff6fddaf3de13cbf6c0a94d967a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-filechunkio: + build: . + image: mcp-filechunkio:latest + container_name: mcp-filechunkio + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=filechunkio + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a0fcde106db38d79d05eeacc6031896f25a0e022 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - filechunkio + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_filechunkio/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_foldseek/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_foldseek/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_foldseek/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_genomad/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_genomad/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_genomad/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6e00cb1838cf5af18b49110381266a402d523abd --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install htseq via conda (e.g., from bioconda) +RUN conda install -c bioconda htseq -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/htseq_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/htseq_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/htseq_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/htseq_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/htseq_server.py new file mode 100644 index 0000000000000000000000000000000000000000..780129842686fc298384085062bebb4d6238cbd7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/htseq_server.py @@ -0,0 +1,182 @@ +import subprocess +from pathlib import Path +from typing import Optional, List + +# Assume mcp is imported and the decorator is available +# import mcp + +@mcp.tool() +def htseq_count( + alignment_file: Path, + gff_file: Path, + output_file: Optional[Path] = None, + format: str = "sam", + order: str = "name", + stranded: str = "yes", + a: int = 10, + type: str = "exon", + idattr: str = "gene_id", + mode: str = "union", + nonunique: str = "none", + samout: Optional[Path] = None, + quiet: bool = False, + max_reads_in_buffer: int = 30000000, + nprocesses: int = 1, + add_chromosome_info: bool = False, + feature_query: str = "name", + secondary_alignments: str = "score", + supplementary_alignments: str = "score", +) -> dict: + """ + Runs htseq-count to count how many sequencing reads map to each feature. + + This tool takes an alignment file (SAM/BAM) and a feature file (GFF/GTF) + and calculates for each feature the number of reads mapping to it. + + Args: + alignment_file: The alignment file in SAM or BAM format. + gff_file: The feature file in GFF or GTF format. + output_file: Optional path to write the counts output. If not provided, + counts are returned in the 'stdout' field. + format: Type of alignment_file data. Must be 'sam' or 'bam'. + order: For paired-end data, sorting order of the alignment file. + Must be 'pos' or 'name'. + stranded: Whether the data is from a strand-specific assay. + Must be 'yes', 'no', or 'reverse'. + a: Skip all reads with alignment quality lower than this value. + type: Feature type (3rd column in GFF file) to be used. + idattr: GFF attribute to be used as feature ID (e.g., 'gene_id'). + mode: Mode to handle reads overlapping multiple features. Must be + 'union', 'intersection-strict', or 'intersection-nonempty'. + nonunique: Mode to handle reads that align to multiple features. + Must be 'none' or 'all'. + samout: Optional path to write out all SAM alignment records, annotated + with the feature they have been assigned to. + quiet: Suppress progress report and warnings. + max_reads_in_buffer: Maximum number of reads to keep in memory. + Must be a positive, even number. + nprocesses: Number of processes to use. + add_chromosome_info: Store chromosome and position of the feature. + feature_query: Method to query features. Must be 'name' or 'read'. + secondary_alignments: How to deal with secondary alignments. + Must be 'score' or 'ignore'. + supplementary_alignments: How to deal with supplementary alignments. + Must be 'score' or 'ignore'. + + Returns: + A dictionary containing the command executed, stdout, stderr, + and a list of output files. + """ + # --- Input Validation --- + if not alignment_file.is_file(): + raise FileNotFoundError(f"Alignment file not found: {alignment_file}") + if not gff_file.is_file(): + raise FileNotFoundError(f"GFF/GTF file not found: {gff_file}") + + # Validate choice parameters + valid_formats = ["sam", "bam"] + if format not in valid_formats: + raise ValueError(f"Invalid format '{format}'. Must be one of {valid_formats}") + + valid_orders = ["pos", "name"] + if order not in valid_orders: + raise ValueError(f"Invalid order '{order}'. Must be one of {valid_orders}") + + valid_stranded = ["yes", "no", "reverse"] + if stranded not in valid_stranded: + raise ValueError(f"Invalid strandedness '{stranded}'. Must be one of {valid_stranded}") + + valid_modes = ["union", "intersection-strict", "intersection-nonempty"] + if mode not in valid_modes: + raise ValueError(f"Invalid mode '{mode}'. Must be one of {valid_modes}") + + valid_nonunique = ["none", "all"] + if nonunique not in valid_nonunique: + raise ValueError(f"Invalid nonunique mode '{nonunique}'. Must be one of {valid_nonunique}") + + valid_feature_query = ["name", "read"] + if feature_query not in valid_feature_query: + raise ValueError(f"Invalid feature_query '{feature_query}'. Must be one of {valid_feature_query}") + + valid_secondary = ["score", "ignore"] + if secondary_alignments not in valid_secondary: + raise ValueError(f"Invalid secondary_alignments mode '{secondary_alignments}'. Must be one of {valid_secondary}") + + valid_supplementary = ["score", "ignore"] + if supplementary_alignments not in valid_supplementary: + raise ValueError(f"Invalid supplementary_alignments mode '{supplementary_alignments}'. Must be one of {valid_supplementary}") + + # Validate integer parameters + if a < 0: + raise ValueError("Minimum alignment quality 'a' cannot be negative.") + if max_reads_in_buffer <= 0 or max_reads_in_buffer % 2 != 0: + raise ValueError("'max_reads_in_buffer' must be a positive, even number.") + if nprocesses < 1: + raise ValueError("'nprocesses' must be at least 1.") + + # --- Command Construction --- + cmd = ["htseq-count"] + cmd.extend(["--format", format]) + cmd.extend(["--order", order]) + cmd.extend(["--stranded", stranded]) + cmd.extend(["-a", str(a)]) + cmd.extend(["--type", type]) + cmd.extend(["--idattr", idattr]) + cmd.extend(["--mode", mode]) + cmd.extend(["--nonunique", nonunique]) + cmd.extend(["--max-reads-in-buffer", str(max_reads_in_buffer)]) + cmd.extend(["--nprocesses", str(nprocesses)]) + cmd.extend(["--feature-query", feature_query]) + cmd.extend(["--secondary-alignments", secondary_alignments]) + cmd.extend(["--supplementary-alignments", supplementary_alignments]) + + if samout: + cmd.extend(["--samout", str(samout)]) + if quiet: + cmd.append("--quiet") + if add_chromosome_info: + cmd.append("--add-chromosome-info") + + # Positional arguments must be last + cmd.extend([str(alignment_file), str(gff_file)]) + command_executed = " ".join(cmd) + + # --- Subprocess Execution --- + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + + output_files: List[str] = [] + if output_file: + output_file.write_text(result.stdout) + output_files.append(str(output_file)) + if samout: + output_files.append(str(samout)) + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'htseq-count' command not found. Please ensure HTSeq is installed and in your PATH.", + "error": "Command not found", + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"htseq-count failed with exit code {e.returncode}", + "output_files": [] + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/htseq_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/htseq_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f19fa64dbc17fc317871060c18fbafe677f36cce --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/htseq_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/htseq_server.py') +SERVER_NAME = 'biosci_htseq' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..9866d73ab39b96b4a7d11b9f2fba35598e22e1c2 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-htseq: + build: . + image: mcp-htseq:latest + container_name: mcp-htseq + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=htseq + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..93913d98e91a3b073b9de5b75cdad7f2180ef59c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - htseq + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_htseq/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c0060c346e4817572532973e52ef1b76763cc483 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install java-jdk via conda (e.g., from bioconda) +RUN conda install -c bioconda java-jdk -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/java-jdk_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/java-jdk_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/java-jdk_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/java-jdk_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/java-jdk_server.py new file mode 100644 index 0000000000000000000000000000000000000000..5c8acb0b2e626b7d58e048ce952437a667b1cc04 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/java-jdk_server.py @@ -0,0 +1,260 @@ +import subprocess +import shlex +from pathlib import Path +from typing import Optional, List, Dict, Literal + +# MCP-compliant tool definitions for the java-jdk tool + +@mcp.tool() +def java_execute_jar( + jar_file: Path, + args: Optional[List[str]] = None, + use_32bit_datamodel: bool = False, + use_64bit_datamodel: bool = False, + select_server_vm: bool = False, + class_path: Optional[List[Path]] = None, + system_properties: Optional[Dict[str, str]] = None, + verbose_output: Optional[Literal["class", "gc", "jni"]] = None, + show_version_and_continue: bool = False, + require_version: Optional[str] = None, + jre_restrict_search: Optional[bool] = None, + enable_assertions: Optional[str] = None, + disable_assertions: Optional[str] = None, + enable_system_assertions: bool = False, + disable_system_assertions: bool = False, + agent_library: Optional[str] = None, + agent_path: Optional[str] = None, + java_agent: Optional[str] = None, + splash_screen_image: Optional[Path] = None, +): + """ + Executes a Java application packaged as a JAR file. + + This corresponds to the `java -jar jarfile [args...]` command. + + Args: + jar_file: Path to the executable JAR file. + args: A list of arguments to pass to the Java application's main method. + use_32bit_datamodel: Use a 32-bit data model if available (-d32). + use_64bit_datamodel: Use a 64-bit data model if available (-d64). + select_server_vm: Select the "server" VM (-server). + class_path: A list of directories, JAR archives, and ZIP archives to search for class files (-cp, -classpath). + system_properties: A dictionary of system properties to set (-D=). + verbose_output: Enable verbose output for 'class', 'gc', or 'jni'. + show_version_and_continue: Print product version and continue execution (-showversion). + require_version: Require a specific Java version to run (-version:). Deprecated. + jre_restrict_search: If True, include user private JREs in version search (-jre-restrict-search). If False, exclude them (-no-jre-restrict-search). Deprecated. + enable_assertions: Enable assertions. Provide an empty string for `-ea` or a string like `:` for granularity. + disable_assertions: Disable assertions. Provide an empty string for `-da` or a string like `:` for granularity. + enable_system_assertions: Enable system assertions (-esa). + disable_system_assertions: Disable system assertions (-dsa). + agent_library: Load a native agent library by name, e.g., 'hprof' or 'jdwp=help' (-agentlib:[=]). + agent_path: Load a native agent library by full pathname (-agentpath:[=]). + java_agent: Load a Java programming language agent (-javaagent:[=]). + splash_screen_image: Show a splash screen with the specified image (-splash:). + """ + # Input validation + if not jar_file.is_file(): + raise FileNotFoundError(f"JAR file not found: {jar_file}") + if use_32bit_datamodel and use_64bit_datamodel: + raise ValueError("Cannot specify both -d32 and -d64 options.") + if splash_screen_image and not splash_screen_image.is_file(): + raise FileNotFoundError(f"Splash screen image not found: {splash_screen_image}") + if class_path: + for p in class_path: + if not p.exists(): + raise FileNotFoundError(f"Classpath entry not found: {p}") + + cmd = ["java"] + + # Options + if use_32bit_datamodel: cmd.append("-d32") + if use_64bit_datamodel: cmd.append("-d64") + if select_server_vm: cmd.append("-server") + if class_path: cmd.extend(["-cp", ":".join(map(str, class_path))]) + if system_properties: + for key, value in system_properties.items(): + cmd.append(f"-D{key}={value}") + if verbose_output: cmd.append(f"-verbose:{verbose_output}") + if show_version_and_continue: cmd.append("-showversion") + if require_version: cmd.append(f"-version:{require_version}") + if jre_restrict_search is not None: + cmd.append("-jre-restrict-search" if jre_restrict_search else "-no-jre-restrict-search") + if enable_assertions is not None: cmd.append(f"-ea{enable_assertions}") + if disable_assertions is not None: cmd.append(f"-da{disable_assertions}") + if enable_system_assertions: cmd.append("-esa") + if disable_system_assertions: cmd.append("-dsa") + if agent_library: cmd.append(f"-agentlib:{agent_library}") + if agent_path: cmd.append(f"-agentpath:{agent_path}") + if java_agent: cmd.append(f"-javaagent:{java_agent}") + if splash_screen_image: cmd.append(f"-splash:{str(splash_screen_image)}") + + # Main execution command + cmd.extend(["-jar", str(jar_file)]) + + # Application arguments + if args: cmd.extend(args) + + command_executed = shlex.join(cmd) + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Java execution failed", + "return_code": e.returncode + } + +@mcp.tool() +def java_execute_class( + class_name: str, + class_path: List[Path], + args: Optional[List[str]] = None, + use_32bit_datamodel: bool = False, + use_64bit_datamodel: bool = False, + select_server_vm: bool = False, + system_properties: Optional[Dict[str, str]] = None, + verbose_output: Optional[Literal["class", "gc", "jni"]] = None, + show_version_and_continue: bool = False, + require_version: Optional[str] = None, + jre_restrict_search: Optional[bool] = None, + enable_assertions: Optional[str] = None, + disable_assertions: Optional[str] = None, + enable_system_assertions: bool = False, + disable_system_assertions: bool = False, + agent_library: Optional[str] = None, + agent_path: Optional[str] = None, + java_agent: Optional[str] = None, + splash_screen_image: Optional[Path] = None, +): + """ + Executes a Java class file. + + This corresponds to the `java [options] class [args...]` command. + + Args: + class_name: The fully qualified name of the class to execute. + class_path: A list of directories, JAR archives, and ZIP archives to search for class files (-cp, -classpath). This is required. + args: A list of arguments to pass to the Java application's main method. + use_32bit_datamodel: Use a 32-bit data model if available (-d32). + use_64bit_datamodel: Use a 64-bit data model if available (-d64). + select_server_vm: Select the "server" VM (-server). + system_properties: A dictionary of system properties to set (-D=). + verbose_output: Enable verbose output for 'class', 'gc', or 'jni'. + show_version_and_continue: Print product version and continue execution (-showversion). + require_version: Require a specific Java version to run (-version:). Deprecated. + jre_restrict_search: If True, include user private JREs in version search (-jre-restrict-search). If False, exclude them (-no-jre-restrict-search). Deprecated. + enable_assertions: Enable assertions. Provide an empty string for `-ea` or a string like `:` for granularity. + disable_assertions: Disable assertions. Provide an empty string for `-da` or a string like `:` for granularity. + enable_system_assertions: Enable system assertions (-esa). + disable_system_assertions: Disable system assertions (-dsa). + agent_library: Load a native agent library by name, e.g., 'hprof' or 'jdwp=help' (-agentlib:[=]). + agent_path: Load a native agent library by full pathname (-agentpath:[=]). + java_agent: Load a Java programming language agent (-javaagent:[=]). + splash_screen_image: Show a splash screen with the specified image (-splash:). + """ + # Input validation + if not class_path: + raise ValueError("A non-empty class_path must be provided to execute a class.") + for p in class_path: + if not p.exists(): + raise FileNotFoundError(f"Classpath entry not found: {p}") + if use_32bit_datamodel and use_64bit_datamodel: + raise ValueError("Cannot specify both -d32 and -d64 options.") + if splash_screen_image and not splash_screen_image.is_file(): + raise FileNotFoundError(f"Splash screen image not found: {splash_screen_image}") + + cmd = ["java"] + + # Options + if use_32bit_datamodel: cmd.append("-d32") + if use_64bit_datamodel: cmd.append("-d64") + if select_server_vm: cmd.append("-server") + if system_properties: + for key, value in system_properties.items(): + cmd.append(f"-D{key}={value}") + if verbose_output: cmd.append(f"-verbose:{verbose_output}") + if show_version_and_continue: cmd.append("-showversion") + if require_version: cmd.append(f"-version:{require_version}") + if jre_restrict_search is not None: + cmd.append("-jre-restrict-search" if jre_restrict_search else "-no-jre-restrict-search") + if enable_assertions is not None: cmd.append(f"-ea{enable_assertions}") + if disable_assertions is not None: cmd.append(f"-da{disable_assertions}") + if enable_system_assertions: cmd.append("-esa") + if disable_system_assertions: cmd.append("-dsa") + if agent_library: cmd.append(f"-agentlib:{agent_library}") + if agent_path: cmd.append(f"-agentpath:{agent_path}") + if java_agent: cmd.append(f"-javaagent:{java_agent}") + if splash_screen_image: cmd.append(f"-splash:{str(splash_screen_image)}") + + # Classpath is required for this mode + cmd.extend(["-classpath", ":".join(map(str, class_path))]) + + # Main execution command + cmd.append(class_name) + + # Application arguments + if args: cmd.extend(args) + + command_executed = shlex.join(cmd) + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Java execution failed", + "return_code": e.returncode + } + +@mcp.tool() +def java_show_info( + version: bool = False, + non_standard_options_help: bool = False +): + """ + Shows Java version information or help for non-standard options. + + Args: + version: If True, print the product version and exit (`java -version`). + non_standard_options_help: If True, print help on non-standard options (`java -X`). + """ + if not (version ^ non_standard_options_help): + raise ValueError("Exactly one of 'version' or 'non_standard_options_help' must be True.") + + cmd = ["java"] + if version: + cmd.append("-version") + else: # non_standard_options_help is True + cmd.append("-X") + + command_executed = shlex.join(cmd) + try: + # Note: `java -version` often prints to stderr, and `java -X` can have a non-zero exit code + # on some versions while still printing help. We don't use check=True here. + result = subprocess.run(cmd, capture_output=True, text=True) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "return_code": result.returncode + } + except FileNotFoundError: + raise RuntimeError("Java executable not found. Please ensure it is in your PATH.") \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/java-jdk_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/java-jdk_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0ee17b23b1b83c5cd5b181687e066d54271b1d62 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/java-jdk_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/java-jdk_server.py') +SERVER_NAME = 'biosci_java_jdk' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..cc8a826c3123b37e12dfbdadac00a5fddbeee745 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-java-jdk: + build: . + image: mcp-java-jdk:latest + container_name: mcp-java-jdk + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=java-jdk + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4b0dd85c23da221fca5aed00095b2724c58934ad --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - java-jdk + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_java-jdk/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/app/jellyfish_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/app/jellyfish_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f1d5c5c778cbadfcf9d83bca5a63f5edf992c1f9 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/app/jellyfish_server.py @@ -0,0 +1,509 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Dict, Any + +# Helper function to run jellyfish commands +def _run_jellyfish_command( + command: List[str], + input_files: Optional[List[Path]] = None, + output_files: Optional[List[Path]] = None, + check_input_exists: bool = True, + check_output_parent_exists: bool = True, +) -> Dict[str, Any]: + """ + Executes a jellyfish command and handles output/errors. + + Args: + command: A list of strings representing the command and its arguments. + input_files: A list of input file paths to check for existence. + output_files: A list of output file paths whose parent directories should be checked. + check_input_exists: If True, checks if input_files exist. + check_output_parent_exists: If True, checks if parent directories for output_files exist. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + + Raises: + ValueError: If input files do not exist or output directories are invalid. + subprocess.CalledProcessError: If the command returns a non-zero exit code. + """ + full_command = ["jellyfish"] + command + + # Input file validation + if check_input_exists and input_files: + for f in input_files: + if not f.is_file(): + raise ValueError(f"Input file not found: {f}") + + # Output file directory validation + if check_output_parent_exists and output_files: + for f in output_files: + if f.parent and not f.parent.is_dir(): + f.parent.mkdir(parents=True, exist_ok=True) + + try: + process = subprocess.run( + [str(arg) for arg in full_command], + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": " ".join([str(arg) for arg in full_command]), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(f) for f in output_files] if output_files else [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join([str(arg) for arg in full_command]), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Command failed with exit code {e.returncode}", + "output_files": [], + } + except FileNotFoundError: + return { + "command_executed": " ".join([str(arg) for arg in full_command]), + "stdout": "", + "stderr": "jellyfish command not found. Is it installed and in PATH?", + "error": "jellyfish executable not found", + "output_files": [], + } + + +@mcp.tool() +def jellyfish_count( + kmer_length: int, + hash_size_bytes: int, + output_database: Path, + input_files: List[Path], + threads: int = 1, + canonical: bool = False, + low_qual_threshold: int = 0, + min_qual_char: str = "!", + num_reprobes: int = 100, + num_buckets: int = 0, + max_counter_value: int = 1000000000, + verbose: bool = False, + mer_len_dna: Optional[int] = None, # This parameter is ambiguous, often -m is kmer_length. + # Keeping it as optional for now if it has a different meaning. +) -> Dict[str, Any]: + """ + Counts k-mers in DNA sequences from FASTA/FASTQ files. + + Args: + kmer_length: The length of k-mers to count (e.g., 21). + hash_size_bytes: The size of the hash table in bytes (e.g., 100M). + output_database: Path to the output Jellyfish database file (.jf). + input_files: List of input FASTA/FASTQ files. + threads: Number of threads to use for counting. + canonical: Count canonical k-mers (lexicographically smallest of k-mer and its reverse complement). + low_qual_threshold: Bases with quality below this threshold are ignored. + min_qual_char: Minimum quality character (ASCII-33 offset). + num_reprobes: Number of reprobes for hash table. + num_buckets: Number of buckets for hash table. + max_counter_value: Maximum value of a counter. + verbose: Enable verbose output. + mer_len_dna: An ambiguous parameter, often -m is kmer_length. If it has a different meaning, specify. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not (1 <= kmer_length <= 32): # Common k-mer length range for Jellyfish 2.x + raise ValueError("kmer_length must be between 1 and 32.") + if hash_size_bytes <= 0: + raise ValueError("hash_size_bytes must be a positive integer.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + if low_qual_threshold < 0: + raise ValueError("low_qual_threshold cannot be negative.") + if not input_files: + raise ValueError("At least one input file is required.") + + command = [ + "count", + "-m", str(kmer_length), + "-s", str(hash_size_bytes), + "-o", str(output_database), + "-t", str(threads), + ] + if canonical: + command.append("-C") + if low_qual_threshold > 0: + command.extend(["-q", str(low_qual_threshold)]) + if min_qual_char != "!": + command.extend(["-Q", min_qual_char]) + if num_reprobes != 100: + command.extend(["-r", str(num_reprobes)]) + if num_buckets > 0: + command.extend(["-b", str(num_buckets)]) + if max_counter_value != 1000000000: + command.extend(["-M", str(max_counter_value)]) + if verbose: + command.append("-v") + if mer_len_dna is not None: + # This is a guess, as -m is usually kmer_length. + # If it's a different option, it needs clarification. + command.extend(["--mer-len-dna", str(mer_len_dna)]) + + command.extend([str(f) for f in input_files]) + + return _run_jellyfish_command( + command, input_files=input_files, output_files=[output_database] + ) + + +@mcp.tool() +def jellyfish_bc( + input_database: Path, + output_database: Path, + threads: int = 1, +) -> Dict[str, Any]: + """ + Builds a canonical k-mer database from an existing Jellyfish database. + This command takes a non-canonical k-mer database and converts it to a canonical one. + + Args: + input_database: Path to the input Jellyfish database file (.jf). + output_database: Path to the output canonical Jellyfish database file (.jf). + threads: Number of threads to use. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if threads <= 0: + raise ValueError("threads must be a positive integer.") + + command = [ + "bc", + "-o", str(output_database), + "-t", str(threads), + str(input_database), + ] + + return _run_jellyfish_command( + command, input_files=[input_database], output_files=[output_database] + ) + + +@mcp.tool() +def jellyfish_info( + input_database: Path, + verbose: bool = False, +) -> Dict[str, Any]: + """ + Displays information about a Jellyfish k-mer database. + + Args: + input_database: Path to the input Jellyfish database file (.jf). + verbose: Enable verbose output. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + command = ["info", str(input_database)] + if verbose: + command.append("-v") + + return _run_jellyfish_command(command, input_files=[input_database], output_files=[]) + + +@mcp.tool() +def jellyfish_stats( + input_database: Path, + output_file: Optional[Path] = None, + verbose: bool = False, +) -> Dict[str, Any]: + """ + Computes and displays statistics on a Jellyfish k-mer database. + + Args: + input_database: Path to the input Jellyfish database file (.jf). + output_file: Optional path to write statistics to. If None, prints to stdout. + verbose: Enable verbose output. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + command = ["stats", str(input_database)] + if output_file: + command.extend([">", str(output_file)]) # Redirection is shell-specific, better to capture stdout + if verbose: + command.append("-v") + + # If output_file is provided, we'll capture stdout and write it to the file. + # This avoids shell redirection issues with subprocess. + result = _run_jellyfish_command(command, input_files=[input_database], output_files=[]) + + if output_file and "stdout" in result and result["stdout"]: + try: + output_file.parent.mkdir(parents=True, exist_ok=True) + with open(output_file, "w") as f: + f.write(result["stdout"]) + result["output_files"].append(str(output_file)) + result["stdout"] = f"Statistics written to {output_file}" + except IOError as e: + result["stderr"] += f"\nError writing stats to file {output_file}: {e}" + result["error"] = "File write error" + + return result + + +@mcp.tool() +def jellyfish_histo( + input_database: Path, + output_file: Optional[Path] = None, + max_count_limit: Optional[int] = None, + verbose: bool = False, +) -> Dict[str, Any]: + """ + Generates a histogram of k-mer counts from a Jellyfish database. + + Args: + input_database: Path to the input Jellyfish database file (.jf). + output_file: Optional path to write the histogram to. If None, prints to stdout. + max_count_limit: Limit the histogram to k-mers with counts up to this value. + verbose: Enable verbose output. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + command = ["histo", str(input_database)] + if max_count_limit is not None: + if max_count_limit <= 0: + raise ValueError("max_count_limit must be a positive integer.") + command.extend(["-l", str(max_count_limit)]) + if verbose: + command.append("-v") + + result = _run_jellyfish_command(command, input_files=[input_database], output_files=[]) + + if output_file and "stdout" in result and result["stdout"]: + try: + output_file.parent.mkdir(parents=True, exist_ok=True) + with open(output_file, "w") as f: + f.write(result["stdout"]) + result["output_files"].append(str(output_file)) + result["stdout"] = f"Histogram written to {output_file}" + except IOError as e: + result["stderr"] += f"\nError writing histogram to file {output_file}: {e}" + result["error"] = "File write error" + + return result + + +@mcp.tool() +def jellyfish_dump( + input_database: Path, + output_file: Optional[Path] = None, + lower_count: int = 1, + upper_count: Optional[int] = None, + with_counts: bool = False, + text_output: bool = False, + fasta_output: bool = False, # Jellyfish dump can output FASTA, but default is kmer count text + verbose: bool = False, +) -> Dict[str, Any]: + """ + Dumps k-mers and their counts from a Jellyfish database to a human-readable format. + + Args: + input_database: Path to the input Jellyfish database file (.jf). + output_file: Optional path to write the dumped k-mers to. If None, prints to stdout. + lower_count: Only dump k-mers with counts greater than or equal to this value. + upper_count: Only dump k-mers with counts less than or equal to this value. + with_counts: Output k-mers with their counts (default is just k-mers). + text_output: Output in plain text format (k-mer count). + fasta_output: Output in FASTA format (k-mer as sequence, count as header). + verbose: Enable verbose output. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if lower_count < 1: + raise ValueError("lower_count must be a positive integer.") + if upper_count is not None and upper_count < lower_count: + raise ValueError("upper_count cannot be less than lower_count.") + + command = ["dump", str(input_database)] + if lower_count != 1: + command.extend(["-L", str(lower_count)]) + if upper_count is not None: + command.extend(["-U", str(upper_count)]) + if with_counts: + command.append("-c") + if text_output: + command.append("-t") + if fasta_output: + command.append("-F") # Assuming -F for FASTA output, common in k-mer tools + if verbose: + command.append("-v") + + result = _run_jellyfish_command(command, input_files=[input_database], output_files=[]) + + if output_file and "stdout" in result and result["stdout"]: + try: + output_file.parent.mkdir(parents=True, exist_ok=True) + with open(output_file, "w") as f: + f.write(result["stdout"]) + result["output_files"].append(str(output_file)) + result["stdout"] = f"Dumped k-mers written to {output_file}" + except IOError as e: + result["stderr"] += f"\nError writing dumped k-mers to file {output_file}: {e}" + result["error"] = "File write error" + + return result + + +@mcp.tool() +def jellyfish_merge( + output_database: Path, + input_databases: List[Path], + threads: int = 1, + verbose: bool = False, +) -> Dict[str, Any]: + """ + Merges multiple Jellyfish k-mer databases into a single database. + + Args: + output_database: Path to the output merged Jellyfish database file (.jf). + input_databases: List of input Jellyfish database files (.jf) to merge. + threads: Number of threads to use for merging. + verbose: Enable verbose output. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_databases or len(input_databases) < 2: + raise ValueError("At least two input databases are required for merging.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + + command = [ + "merge", + "-o", str(output_database), + "-t", str(threads), + ] + if verbose: + command.append("-v") + command.extend([str(f) for f in input_databases]) + + return _run_jellyfish_command( + command, input_files=input_databases, output_files=[output_database] + ) + + +@mcp.tool() +def jellyfish_query( + input_database: Path, + kmer_sequences: List[str], + canonical: bool = False, + verbose: bool = False, +) -> Dict[str, Any]: + """ + Queries a Jellyfish k-mer database for the counts of specific k-mers. + + Args: + input_database: Path to the input Jellyfish database file (.jf). + kmer_sequences: List of k-mer sequences to query. + canonical: Query for canonical k-mers (lexicographically smallest of k-mer and its reverse complement). + verbose: Enable verbose output. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not kmer_sequences: + raise ValueError("At least one k-mer sequence must be provided for querying.") + + command = ["query", str(input_database)] + if canonical: + command.append("-C") + if verbose: + command.append("-v") + command.extend(kmer_sequences) + + return _run_jellyfish_command(command, input_files=[input_database], output_files=[]) + + +@mcp.tool() +def jellyfish_cite() -> Dict[str, Any]: + """ + Displays the citation information for Jellyfish. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + command = ["cite"] + return _run_jellyfish_command(command, output_files=[]) + + +@mcp.tool() +def jellyfish_mem( + kmer_length: int, + hash_size_bytes: int, + num_threads: int = 1, +) -> Dict[str, Any]: + """ + Estimates memory usage for a Jellyfish k-mer counting operation. + + Args: + kmer_length: The length of k-mers. + hash_size_bytes: The size of the hash table in bytes. + num_threads: Number of threads to consider for memory estimation. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not (1 <= kmer_length <= 32): + raise ValueError("kmer_length must be between 1 and 32.") + if hash_size_bytes <= 0: + raise ValueError("hash_size_bytes must be a positive integer.") + if num_threads <= 0: + raise ValueError("num_threads must be a positive integer.") + + command = [ + "mem", + "-m", str(kmer_length), + "-s", str(hash_size_bytes), + "-t", str(num_threads), + ] + + return _run_jellyfish_command(command, output_files=[]) + + +@mcp.tool() +def jellyfish_jf( + action: str, + input_file: Path, + output_file: Optional[Path] = None, + extra_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Performs generic operations on Jellyfish database files (.jf). + Note: The 'jf' subcommand is not well-documented in the provided help. + This function provides a generic interface, assuming 'jf' is a utility for .jf files. + Specific actions and their parameters are inferred or left to 'extra_args'. + + Args: + action: The specific action to perform (e.g., "check", "repair", "convert"). This is an inferred parameter. + input_file: Path to the input Jellyfish database file (.jf). + output_file: Optional path for an output file, if the action produces one. + extra_args: A list of additional arguments specific to the chosen action. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not action: + raise ValueError("An action must be specified for 'jellyfish jf'.") + + command = ["jf", action, str(input_file)] + if output_file: + command.append(str(output_file)) + if extra_args: + command.extend(extra_args) + + output_files_list = [output_file] if output_file else [] + return _run_jellyfish_command( + command, input_files=[input_file], output_files=output_files_list + ) \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/app/jellyfish_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/app/jellyfish_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3f631026276c8442e195213459fba9af95be3961 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/app/jellyfish_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/app/jellyfish_server.py') +SERVER_NAME = 'biosci_jellyfish' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..a31683e36448a10c513086382f472eba77a939dd --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-jellyfish: + build: . + image: mcp-jellyfish:latest + container_name: mcp-jellyfish + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=jellyfish + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ed4410e9f384b8f7343b8b358f775cee61e2a506 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - jellyfish + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_jellyfish/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..38e86dc750b6aa44aa0c484700d6571e3038d75b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install libcifpp via conda (e.g., from bioconda) +RUN conda install -c bioconda libcifpp -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/libcifpp_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/libcifpp_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/libcifpp_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/libcifpp_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/libcifpp_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c2d84bb01a737952dd8d7fe54ea0a3f49d511e73 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/libcifpp_server.py @@ -0,0 +1,277 @@ +import subprocess +from pathlib import Path +from typing import Optional, List + +@mcp.tool() +def cif_validate( + input_file: str, + dictionary: Optional[str] = None, + validate_links: bool = False, + check_mandatory: bool = False, + verbose: bool = False +): + """ + Validates a CIF, mmCIF, or PDB file against a CIF dictionary using cif-validate. + + This tool ensures that the file is syntactically valid and its content + conforms to the specified CIF dictionary (e.g., mmcif_pdbx.dic). + + Args: + input_file: Path to the CIF/mmCIF or PDB file to validate. Supports .gz compression. + dictionary: Path to a specific CIF dictionary file. If not provided, the library's internal default dictionary is used. + validate_links: If True, validates parent/child relationships (links) between categories. + check_mandatory: If True, checks if all mandatory items are present. + verbose: If True, provides detailed validation output. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file not found: {input_file}"} + + cmd = ["cif-validate"] + + if dictionary: + dict_path = Path(dictionary) + if not dict_path.exists(): + return {"error": f"Dictionary file not found: {dictionary}"} + cmd.extend(["--dict", str(dict_path)]) + + if validate_links: + cmd.append("--validate-links") + + if check_mandatory: + cmd.append("--check-mandatory") + + if verbose: + cmd.append("--verbose") + + cmd.append(str(input_path)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "Validation successful" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Validation failed or found issues", + "exit_code": e.returncode + } + +@mcp.tool() +def cif_diff( + file1: str, + file2: str, + quiet: bool = False, + ignore_order: bool = False +): + """ + Compares two CIF files and reports the differences using cif-diff. + + Args: + file1: Path to the first CIF file. + file2: Path to the second CIF file. + quiet: If True, suppresses output and only returns status via exit code. + ignore_order: If True, ignores the order of categories and rows. + """ + p1 = Path(file1) + p2 = Path(file2) + + if not p1.exists(): + return {"error": f"File 1 not found: {file1}"} + if not p2.exists(): + return {"error": f"File 2 not found: {file2}"} + + cmd = ["cif-diff"] + if quiet: + cmd.append("--quiet") + if ignore_order: + cmd.append("--ignore-order") + + cmd.extend([str(p1), str(p2)]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "Files are identical" + } + except subprocess.CalledProcessError as e: + # cif-diff returns 1 if files differ, which is a valid result state + if e.returncode == 1: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "status": "Differences found" + } + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Execution failed with exit code {e.returncode}" + } + +@mcp.tool() +def cif_merge( + input_files: List[str], + output_file: str, + dictionary: Optional[str] = None, + validate: bool = False +): + """ + Merges multiple CIF or PDB files into a single output CIF file using cif-merge. + + Args: + input_files: List of paths to CIF/PDB files to merge. + output_file: Path where the merged CIF file will be saved. + dictionary: Optional path to a CIF dictionary for validation during merging. + validate: If True, performs validation on the merged result. + """ + if not input_files: + return {"error": "No input files provided."} + + cmd = ["cif-merge", "-o", output_file] + + if dictionary: + dict_path = Path(dictionary) + if not dict_path.exists(): + return {"error": f"Dictionary file not found: {dictionary}"} + cmd.extend(["--dict", str(dict_path)]) + + if validate: + cmd.append("--validate") + + for f in input_files: + p = Path(f) + if not p.exists(): + return {"error": f"Input file not found: {f}"} + cmd.append(str(p)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_file] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Merge operation failed" + } + +@mcp.tool() +def cif_grep( + query: str, + input_files: List[str], + count: bool = False, + list_files: bool = False +): + """ + Searches for specific data or patterns in one or more CIF files using cif-grep. + + Args: + query: The search pattern or tag name to look for. + input_files: List of paths to CIF/mmCIF files to search. + count: If True, only returns the number of matches found. + list_files: If True, only lists the names of files with matches. + """ + if not input_files: + return {"error": "No input files provided."} + + cmd = ["cif-grep"] + if count: + cmd.append("-c") + if list_files: + cmd.append("-l") + + cmd.append(query) + + for f in input_files: + p = Path(f) + if not p.exists(): + return {"error": f"Input file not found: {f}"} + cmd.append(str(p)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + # grep returns 1 if no matches are found + if e.returncode == 1: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "status": "No matches found" + } + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Search failed with exit code {e.returncode}" + } + +@mcp.tool() +def mmcif_pdbx( + dictionary_file: str, + output_file: Optional[str] = None, + check: bool = False, + convert_to_binary: bool = False +): + """ + Utility tool for working with mmCIF dictionaries (mmcif_pdbx). + + Args: + dictionary_file: Path to the CIF dictionary file (e.g., .dic). + output_file: Optional path for the output file (e.g., compiled dictionary). + check: If True, checks the dictionary for consistency. + convert_to_binary: If True, converts the dictionary to a binary format for faster loading. + """ + p = Path(dictionary_file) + if not p.exists(): + return {"error": f"Dictionary file not found: {dictionary_file}"} + + cmd = ["mmcif_pdbx"] + + if check: + cmd.append("--check") + + if convert_to_binary: + cmd.append("--binary") + + if output_file: + cmd.extend(["-o", output_file]) + + cmd.append(str(p)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_file] if output_file else [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Dictionary operation failed" + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/libcifpp_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/libcifpp_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..13f14749dcbee49b503058777576ff148056445e --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/libcifpp_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/libcifpp_server.py') +SERVER_NAME = 'biosci_libcifpp' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..fc3376dabe08e7959a5fabf5df07a7565e1a3d47 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-libcifpp: + build: . + image: mcp-libcifpp:latest + container_name: mcp-libcifpp + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=libcifpp + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..38ad866988512d7e8b646acdfb551f933077736b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - libcifpp + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_libcifpp/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2f54d18f08bf933f847ef9ebbfee1d5e2d6030cb --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install locarna via conda (e.g., from bioconda) +RUN conda install -c bioconda locarna -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/locarna_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/locarna_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/locarna_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/locarna_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/locarna_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c9a91e92ef7f90ea6e746c40f4f994cfd657cb99 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/locarna_server.py @@ -0,0 +1,343 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +@mcp.tool() +def locarna( + fasta_1: str, + fasta_2: str, + tau: int = 50, + struct_weight: int = 180, + indel: int = 400, + indel_opening: int = 400, + min_prob: float = 0.0005, + max_diff: int = -1, + max_diff_am: int = -1, + free_endgaps: bool = False, + sequ_weight: int = 100, + match: int = 50, + mismatch: int = 0, + consensus_structure: Optional[str] = None, +) -> Dict[str, Any]: + """ + Perform pairwise local or global alignment of RNA sequences using LocARNA. + + Args: + fasta_1: Path to the first FASTA file. + fasta_2: Path to the second FASTA file. + tau: Sequence contribution (default 50). + struct_weight: Structure contribution (default 180). + indel: Indel penalty (default 400). + indel_opening: Indel opening penalty (default 400). + min_prob: Minimal base pair probability (default 0.0005). + max_diff: Maximal difference for alignment (default -1, no limit). + max_diff_am: Maximal difference for alignment in AM (default -1). + free_endgaps: Whether to allow free endgaps (local alignment). + sequ_weight: Weight for sequence similarity. + match: Match score. + mismatch: Mismatch penalty. + consensus_structure: Optional fixed consensus structure. + """ + p1 = Path(fasta_1) + p2 = Path(fasta_2) + if not p1.exists() or not p2.exists(): + return {"error": "One or both input FASTA files do not exist."} + + cmd = [ + "locarna", + str(p1), + str(p2), + f"--tau={tau}", + f"--struct-weight={struct_weight}", + f"--indel={indel}", + f"--indel-opening={indel_opening}", + f"--min-prob={min_prob}", + f"--max-diff={max_diff}", + f"--max-diff-am={max_diff_am}", + f"--sequ-weight={sequ_weight}", + f"--match={match}", + f"--mismatch={mismatch}" + ] + + if free_endgaps: + cmd.append("--free-endgaps") + if consensus_structure: + cmd.extend(["--consensus-structure", consensus_structure]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def mlocarna( + input_fasta: str, + output_dir: str = "mlocarna_results", + threads: int = 1, + iterative: bool = False, + refinement: bool = False, + probabilistic: bool = False, + consistency: bool = False, + consistency_weight: int = 100, + tg_struct: Optional[str] = None, + min_prob: float = 0.001, +) -> Dict[str, Any]: + """ + Perform multiple alignment of RNA sequences using the mlocarna wrapper. + + Args: + input_fasta: Path to the input FASTA file containing multiple sequences. + output_dir: Directory to store results. + threads: Number of threads to use. + iterative: Use iterative refinement. + refinement: Perform alignment refinement. + probabilistic: Use probabilistic consistency transformation. + consistency: Use consistency transformation. + consistency_weight: Weight for consistency. + tg_struct: Target structure for structure-guided alignment. + min_prob: Minimal base pair probability. + """ + p_in = Path(input_fasta) + if not p_in.exists(): + return {"error": f"Input file {input_fasta} not found."} + + cmd = [ + "mlocarna", + str(p_in), + f"--threads={threads}", + f"--min-prob={min_prob}", + f"--output-dir={output_dir}" + ] + + if iterative: + cmd.append("--iterative") + if refinement: + cmd.append("--refinement") + if probabilistic: + cmd.append("--probabilistic") + if consistency: + cmd.append("--consistency") + cmd.append(f"--consistency-weight={consistency_weight}") + if tg_struct: + cmd.extend(["--tg-struct", tg_struct]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": output_dir, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def locarna_p( + fasta_1: str, + fasta_2: str, + tau: int = 50, + struct_weight: int = 180, + min_prob: float = 0.0005, + temperature: float = 37.0, +) -> Dict[str, Any]: + """ + Calculate the partition function and base pair probabilities for RNA alignment. + + Args: + fasta_1: Path to the first FASTA file. + fasta_2: Path to the second FASTA file. + tau: Sequence contribution. + struct_weight: Structure contribution. + min_prob: Minimal base pair probability. + temperature: Temperature in Celsius. + """ + p1 = Path(fasta_1) + p2 = Path(fasta_2) + if not p1.exists() or not p2.exists(): + return {"error": "Input files not found."} + + cmd = [ + "locarna_p", + str(p1), + str(p2), + f"--tau={tau}", + f"--struct-weight={struct_weight}", + f"--min-prob={min_prob}", + f"--temperature={temperature}" + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def sparse_locarna( + fasta_1: str, + fasta_2: str, + exp_prob: float = 0.0, + min_prob: float = 0.0005, + struct_weight: int = 180, + indel: int = 400, +) -> Dict[str, Any]: + """ + Perform memory-efficient RNA alignment using Sparse LocARNA. + + Args: + fasta_1: Path to the first FASTA file. + fasta_2: Path to the second FASTA file. + exp_prob: Expected probability for sparsification. + min_prob: Minimal base pair probability. + struct_weight: Structure contribution. + indel: Indel penalty. + """ + p1 = Path(fasta_1) + p2 = Path(fasta_2) + if not p1.exists() or not p2.exists(): + return {"error": "Input files not found."} + + cmd = [ + "sparse_locarna", + str(p1), + str(p2), + f"--exp-prob={exp_prob}", + f"--min-prob={min_prob}", + f"--struct-weight={struct_weight}", + f"--indel={indel}" + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def locarna_eval( + alignment_file: str, + reference_file: str, + verbose: bool = False +) -> Dict[str, Any]: + """ + Evaluate a LocARNA alignment against a reference alignment. + + Args: + alignment_file: Path to the alignment to evaluate (Clustal or Stockholm format). + reference_file: Path to the reference alignment. + verbose: Enable verbose output. + """ + p_aln = Path(alignment_file) + p_ref = Path(reference_file) + + if not p_aln.exists() or not p_ref.exists(): + return {"error": "Alignment or reference file not found."} + + cmd = ["locarna-eval", str(p_aln), str(p_ref)] + if verbose: + cmd.append("--verbose") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def locarna_predict_and_plot( + input_fasta: str, + output_ps: str = "alignment.ps", + width: int = 80, + columns: int = 3 +) -> Dict[str, Any]: + """ + Predict RNA consensus structure and generate a secondary structure plot. + + Args: + input_fasta: Path to the input FASTA/alignment file. + output_ps: Filename for the output PostScript plot. + width: Width of the plot. + columns: Number of columns in the plot. + """ + p_in = Path(input_fasta) + if not p_in.exists(): + return {"error": f"Input file {input_fasta} not found."} + + cmd = [ + "locarna-predict-and-plot", + str(p_in), + "--out", output_ps, + "--width", str(width), + "--columns", str(columns) + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_ps], + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/locarna_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/locarna_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0d6b521f1c7dc872b5fc8350c488467e10a59863 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/locarna_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/locarna_server.py') +SERVER_NAME = 'biosci_locarna' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..005c7386259f8d1e35e27864130c4cbd69f4296a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-locarna: + build: . + image: mcp-locarna:latest + container_name: mcp-locarna + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=locarna + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2408eea7d962edc9ea0fa0095488f08e62a86f0c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - locarna + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_locarna/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4cf7afbb0214ab52f8cd6b6edf2d42fd9151ec22 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install mageck via conda (e.g., from bioconda) +RUN conda install -c bioconda mageck -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/mageck_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/mageck_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/mageck_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/mageck_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/mageck_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2d09a74aa26a69206cdb188c620f1c067224a73d --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/mageck_server.py @@ -0,0 +1,646 @@ +import subprocess +import logging +from pathlib import Path +from typing import List, Optional, Literal + +# Set up a logger for better feedback +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# This is a mock decorator. In a real MCP environment, this would be provided by the MCP framework. +class mcp: + @staticmethod + def tool(): + def decorator(f): + return f + return decorator + +@mcp.tool() +def mageck_count( + library_file: Path, + fastq_files: List[Path], + sample_labels: List[str], + output_prefix: str, + sgrna_len: int = 20, + trim_5: int = 0, + count_n: bool = False, + unmapped_to_file: bool = False, + pdf_report: bool = False, + norm_method: Literal['median', 'total', 'none'] = 'median', + day0_label: Optional[str] = None, + gmt_file: Optional[Path] = None, + additional_id: bool = False, + threads: int = 1 +) -> dict: + """ + Run the MAGeCK 'count' command to generate a read count table from FASTQ files. + + Args: + library_file: The library file used in the screen. + fastq_files: A list of FASTQ files for read counting. + sample_labels: The labels for samples, corresponding to the FASTQ files. + output_prefix: The prefix for the output file(s). + sgrna_len: The length of the sgRNA sequence. + trim_5: The number of base pairs to be trimmed from the 5' end. + count_n: If True, count sgRNAs containing 'N' bases. + unmapped_to_file: If True, save unmapped reads to a file. + pdf_report: If True, generate a PDF report of the quality control. + norm_method: Method for normalization ('median', 'total', 'none'). + day0_label: The sample label for day 0. Must be in the sample_labels list. + gmt_file: The GMT file for pathway analysis on QC results. + additional_id: If True, add an additional column for gene IDs in the count table. + threads: The number of threads to use. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # Input validation + if not library_file.is_file(): + raise FileNotFoundError(f"Library file not found: {library_file}") + if not fastq_files: + raise ValueError("At least one FASTQ file must be provided.") + for fq in fastq_files: + if not fq.is_file(): + raise FileNotFoundError(f"FASTQ file not found: {fq}") + if len(fastq_files) != len(sample_labels): + raise ValueError("The number of FASTQ files must match the number of sample labels.") + if day0_label and day0_label not in sample_labels: + raise ValueError(f"day0_label '{day0_label}' not found in sample_labels.") + if gmt_file and not gmt_file.is_file(): + raise FileNotFoundError(f"GMT file not found: {gmt_file}") + if threads < 1: + raise ValueError("Number of threads must be at least 1.") + + # Command construction + cmd = [ + "mageck", "count", + "-l", str(library_file), + "-r" + ] + cmd.extend([str(f) for f in fastq_files]) + cmd.extend(["--sample-label", ",".join(sample_labels)]) + cmd.extend(["-n", output_prefix]) + cmd.extend(["--sgrna-len", str(sgrna_len)]) + cmd.extend(["--trim-5", str(trim_5)]) + cmd.extend(["--norm-method", norm_method]) + cmd.extend(["--threads", str(threads)]) + + if count_n: + cmd.append("--count-n") + if unmapped_to_file: + cmd.append("--unmapped-to-file") + if pdf_report: + cmd.append("--pdf-report") + if day0_label: + cmd.extend(["--day0-label", day0_label]) + if gmt_file: + cmd.extend(["--gmt-file", str(gmt_file)]) + if additional_id: + cmd.append("--additional-id") + + # Execute command + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True + ) + + # Define expected output files + output_files = [ + f"{output_prefix}.count.txt", + f"{output_prefix}.log", + f"{output_prefix}.countsummary.txt" + ] + if pdf_report: + output_files.append(f"{output_prefix}.QC.pdf") + if unmapped_to_file: + output_files.extend([f"{label}.unmapped.fastq" for label in sample_labels]) + + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(Path(f)) for f in output_files if Path(f).exists()] + } + except subprocess.CalledProcessError as e: + logger.error(f"MAGeCK count failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Process failed with exit code {e.returncode}" + } + +@mcp.tool() +def mageck_test( + count_table: Path, + treatment_samples: List[str], + control_samples: List[str], + output_prefix: str, + sgrna_len: int = 20, + adjust_method: Literal['fdr', 'holm', 'pounds'] = 'fdr', + sort_by: Literal['pos', 'neg'] = 'pos', + variance_estimation_samples: Optional[List[str]] = None, + remove_zero: Literal['none', 'control', 'all', 'both'] = 'none', + remove_zero_threshold: int = 0, + norm_method: Literal['median', 'total', 'control', 'none'] = 'median', + control_sgrna: Optional[Path] = None, + paired: bool = False, + permutation_round: int = 10000, + pdf_plot: bool = False, + gene_lfc_method: Literal['alphamean', 'mean'] = 'alphamean', + gene_test_fdr_threshold: float = 0.25, + cnv_norm: Optional[Path] = None, + cell_line: Optional[str] = None, + control_gene: Optional[Path] = None +) -> dict: + """ + Run the MAGeCK 'test' command to perform statistical tests on a count table. + + Args: + count_table: The read count table from 'mageck count'. + treatment_samples: A list of treatment sample labels. + control_samples: A list of control sample labels. + output_prefix: The prefix for the output file(s). + sgrna_len: The length of the sgRNA sequence. + adjust_method: Method for multiple testing correction. + sort_by: Sort gene summary by positive ('pos') or negative ('neg') selection. + variance_estimation_samples: Sample labels for variance estimation. + remove_zero: Method to remove zero counts. + remove_zero_threshold: Threshold for removing zero counts. + norm_method: Method for normalization. + control_sgrna: A file containing a list of control sgRNAs. + paired: If True, perform a paired test. + permutation_round: The number of permutations. + pdf_plot: If True, generate PDF plots. + gene_lfc_method: Method to calculate gene Log Fold Change (LFC). + gene_test_fdr_threshold: The FDR threshold for gene testing. + cnv_norm: The Copy Number Variation (CNV) normalization file. + cell_line: The cell line for CNV normalization. + control_gene: The control gene file for normalization. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # Input validation + if not count_table.is_file(): + raise FileNotFoundError(f"Count table not found: {count_table}") + if not treatment_samples: + raise ValueError("At least one treatment sample must be provided.") + if not control_samples: + raise ValueError("At least one control sample must be provided.") + if paired and len(treatment_samples) != len(control_samples): + raise ValueError("For paired tests, the number of treatment and control samples must be equal.") + if control_sgrna and not control_sgrna.is_file(): + raise FileNotFoundError(f"Control sgRNA file not found: {control_sgrna}") + if cnv_norm and not cnv_norm.is_file(): + raise FileNotFoundError(f"CNV normalization file not found: {cnv_norm}") + if control_gene and not control_gene.is_file(): + raise FileNotFoundError(f"Control gene file not found: {control_gene}") + + # Command construction + cmd = [ + "mageck", "test", + "-k", str(count_table), + "-t", ",".join(treatment_samples), + "-c", ",".join(control_samples), + "-n", output_prefix, + "--sgrna-len", str(sgrna_len), + "--adjust-method", adjust_method, + "--sort-by", sort_by, + "--remove-zero", remove_zero, + "--remove-zero-threshold", str(remove_zero_threshold), + "--norm-method", norm_method, + "--permutation-round", str(permutation_round), + "--gene-lfc-method", gene_lfc_method, + "--gene-test-fdr-threshold", str(gene_test_fdr_threshold) + ] + + if variance_estimation_samples: + cmd.extend(["--variance-estimation-samples", ",".join(variance_estimation_samples)]) + if control_sgrna: + cmd.extend(["--control-sgrna", str(control_sgrna)]) + if paired: + cmd.append("--paired") + if pdf_plot: + cmd.append("--pdf-plot") + if cnv_norm: + cmd.extend(["--cnv-norm", str(cnv_norm)]) + if cell_line: + cmd.extend(["--cell-line", cell_line]) + if control_gene: + cmd.extend(["--control-gene", str(control_gene)]) + + # Execute command + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True + ) + + output_files = [ + f"{output_prefix}.gene_summary.txt", + f"{output_prefix}.sgrna_summary.txt", + f"{output_prefix}.log" + ] + if pdf_plot: + output_files.append(f"{output_prefix}.plot.pdf") + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(Path(f)) for f in output_files if Path(f).exists()] + } + except subprocess.CalledProcessError as e: + logger.error(f"MAGeCK test failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Process failed with exit code {e.returncode}" + } + +@mcp.tool() +def mageck_mle( + count_table: Path, + design_matrix: Path, + output_prefix: str, + sgrna_len: int = 20, + norm_method: Literal['median', 'total', 'control', 'none'] = 'median', + control_sgrna: Optional[Path] = None, + threads: int = 1, + permutation_round: int = 10000, + pdf_plot: bool = False, + gene_lfc_method: Literal['alphamean', 'mean'] = 'alphamean', + gene_test_fdr_threshold: float = 0.25, + cnv_norm: Optional[Path] = None, + cell_line: Optional[str] = None, + control_gene: Optional[Path] = None, + beta_labels: Optional[List[str]] = None +) -> dict: + """ + Run the MAGeCK 'mle' command for Maximum-Likelihood Estimation based analysis. + + Args: + count_table: The read count table from 'mageck count'. + design_matrix: The design matrix file. + output_prefix: The prefix for the output file(s). + sgrna_len: The length of the sgRNA sequence. + norm_method: Method for normalization. + control_sgrna: A file containing a list of control sgRNAs. + threads: The number of threads to use. + permutation_round: The number of permutations. + pdf_plot: If True, generate PDF plots. + gene_lfc_method: Method to calculate gene Log Fold Change (LFC). + gene_test_fdr_threshold: The FDR threshold for gene testing. + cnv_norm: The Copy Number Variation (CNV) normalization file. + cell_line: The cell line for CNV normalization. + control_gene: The control gene file for normalization. + beta_labels: Labels for beta scores, separated by comma. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # Input validation + if not count_table.is_file(): + raise FileNotFoundError(f"Count table not found: {count_table}") + if not design_matrix.is_file(): + raise FileNotFoundError(f"Design matrix file not found: {design_matrix}") + if control_sgrna and not control_sgrna.is_file(): + raise FileNotFoundError(f"Control sgRNA file not found: {control_sgrna}") + if cnv_norm and not cnv_norm.is_file(): + raise FileNotFoundError(f"CNV normalization file not found: {cnv_norm}") + if control_gene and not control_gene.is_file(): + raise FileNotFoundError(f"Control gene file not found: {control_gene}") + if threads < 1: + raise ValueError("Number of threads must be at least 1.") + + # Command construction + cmd = [ + "mageck", "mle", + "-k", str(count_table), + "-d", str(design_matrix), + "-n", output_prefix, + "--sgrna-len", str(sgrna_len), + "--norm-method", norm_method, + "--threads", str(threads), + "--permutation-round", str(permutation_round), + "--gene-lfc-method", gene_lfc_method, + "--gene-test-fdr-threshold", str(gene_test_fdr_threshold) + ] + + if control_sgrna: + cmd.extend(["--control-sgrna", str(control_sgrna)]) + if pdf_plot: + cmd.append("--pdf-plot") + if cnv_norm: + cmd.extend(["--cnv-norm", str(cnv_norm)]) + if cell_line: + cmd.extend(["--cell-line", cell_line]) + if control_gene: + cmd.extend(["--control-gene", str(control_gene)]) + if beta_labels: + cmd.extend(["--beta-labels", ",".join(beta_labels)]) + + # Execute command + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True + ) + + output_files = [ + f"{output_prefix}.gene_summary.txt", + f"{output_prefix}.sgrna_summary.txt", + f"{output_prefix}.log" + ] + if pdf_plot: + output_files.append(f"{output_prefix}.plot.pdf") + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(Path(f)) for f in output_files if Path(f).exists()] + } + except subprocess.CalledProcessError as e: + logger.error(f"MAGeCK mle failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Process failed with exit code {e.returncode}" + } + +@mcp.tool() +def mageck_pathway( + gene_ranking: Path, + output_prefix: str, + gmt_file: Optional[Path] = None, + permutation: int = 1000, + fdr_method: Literal['fdr', 'holm'] = 'fdr', + pathway_size_min: int = 10, + pathway_size_max: int = 500 +) -> dict: + """ + Run the MAGeCK 'pathway' command for pathway enrichment analysis. + + Args: + gene_ranking: The gene summary file from 'mageck test' or 'mageck mle'. + output_prefix: The prefix for the output file(s). + gmt_file: The GMT file for pathway analysis. + permutation: The number of permutations. + fdr_method: The method for multiple testing correction. + pathway_size_min: The minimum size of a pathway. + pathway_size_max: The maximum size of a pathway. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # Input validation + if not gene_ranking.is_file(): + raise FileNotFoundError(f"Gene ranking file not found: {gene_ranking}") + if gmt_file and not gmt_file.is_file(): + raise FileNotFoundError(f"GMT file not found: {gmt_file}") + if pathway_size_min < 1: + raise ValueError("pathway_size_min must be at least 1.") + if pathway_size_max < pathway_size_min: + raise ValueError("pathway_size_max must be greater than or equal to pathway_size_min.") + + # Command construction + cmd = [ + "mageck", "pathway", + "--gene-ranking", str(gene_ranking), + "-n", output_prefix, + "--permutation", str(permutation), + "--fdr-method", fdr_method, + "--pathway-size-min", str(pathway_size_min), + "--pathway-size-max", str(pathway_size_max) + ] + + if gmt_file: + cmd.extend(["--gmt-file", str(gmt_file)]) + + # Execute command + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True + ) + + output_files = [f"{output_prefix}.pathway_summary.txt"] + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(Path(f)) for f in output_files if Path(f).exists()] + } + except subprocess.CalledProcessError as e: + logger.error(f"MAGeCK pathway failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Process failed with exit code {e.returncode}" + } + +@mcp.tool() +def mageck_flute( + gene_summary: Path, + output_prefix: str, + treatment_id: Optional[List[str]] = None, + control_id: Optional[List[str]] = None, + top: int = 5, + remove_prefix: bool = False, + x_axis: Optional[str] = None, + y_axis: Optional[str] = None, + label_column: Optional[str] = None, + width: float = 8.0, + height: float = 6.0, + plot_type: Literal['volcano', 'rank', 'square'] = 'volcano', + scale_by_size: bool = False, + day0: bool = False +) -> dict: + """ + Run the MAGeCK 'flute' command for visualizing MAGeCK results. + + Args: + gene_summary: The gene summary file from 'mageck test'. + output_prefix: The prefix for the output file(s). + treatment_id: The treatment sample label(s). + control_id: The control sample label(s). + top: The number of top-ranked genes to label in the plot. + remove_prefix: If True, remove prefix in the gene names. + x_axis: The column name for the x-axis. + y_axis: The column name for the y-axis. + label_column: The column name for labels. + width: The width of the plot. + height: The height of the plot. + plot_type: The type of the plot. + scale_by_size: If True, scale the plot by size. + day0: If True, indicates the screen is a day 0 screen. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # Input validation + if not gene_summary.is_file(): + raise FileNotFoundError(f"Gene summary file not found: {gene_summary}") + + # Command construction + cmd = [ + "mageck", "flute", + str(gene_summary), + "-n", output_prefix, + "--top", str(top), + "--width", str(width), + "--height", str(height), + "--plot-type", plot_type + ] + + if treatment_id: + cmd.extend(["--treatment-id", ",".join(treatment_id)]) + if control_id: + cmd.extend(["--control-id", ",".join(control_id)]) + if remove_prefix: + cmd.append("--remove-prefix") + if x_axis: + cmd.extend(["--x-axis", x_axis]) + if y_axis: + cmd.extend(["--y-axis", y_axis]) + if label_column: + cmd.extend(["--label", label_column]) + if scale_by_size: + cmd.append("--scale-by-size") + if day0: + cmd.append("--day0") + + # Execute command + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True + ) + + # Flute can generate multiple plots. This is a best-effort guess. + output_files = [ + f"{output_prefix}_volcano.pdf", + f"{output_prefix}_rank.pdf", + f"{output_prefix}_square.pdf", + f"{output_prefix}_summary.txt" + ] + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(Path(f)) for f in output_files if Path(f).exists()] + } + except subprocess.CalledProcessError as e: + logger.error(f"MAGeCK flute failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Process failed with exit code {e.returncode}" + } + +@mcp.tool() +def mageck_vispr( + count_table: Path, + sample_labels: List[str], + design_matrix: Path, + output_prefix: str, + sgrna_len: int = 20, + norm_method: Literal['median', 'total', 'none'] = 'median', + project_name: Optional[str] = None +) -> dict: + """ + Run the MAGeCK 'vispr' command to generate files for VISPR visualization. + + Args: + count_table: The read count table from 'mageck count'. + sample_labels: The labels for samples, separated by comma. + design_matrix: The design matrix file. + output_prefix: The prefix for the output file(s) and directory. + sgrna_len: The length of the sgRNA sequence. + norm_method: Method for normalization. + project_name: The name of the project. + + Returns: + A dictionary containing the executed command, stdout, stderr, and the output directory. + """ + # Input validation + if not count_table.is_file(): + raise FileNotFoundError(f"Count table not found: {count_table}") + if not design_matrix.is_file(): + raise FileNotFoundError(f"Design matrix file not found: {design_matrix}") + if not sample_labels: + raise ValueError("At least one sample label must be provided.") + + # Command construction + cmd = [ + "mageck", "vispr", + "-k", str(count_table), + "-l", ",".join(sample_labels), + "-d", str(design_matrix), + "-n", output_prefix, + "--sgrna-len", str(sgrna_len), + "--norm-method", norm_method + ] + + if project_name: + cmd.extend(["--project-name", project_name]) + + # Execute command + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True + ) + + # VISPR creates a directory with the name of the output prefix + output_dir = Path(output_prefix) + output_files = [str(output_dir)] if output_dir.is_dir() else [] + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + except subprocess.CalledProcessError as e: + logger.error(f"MAGeCK vispr failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Process failed with exit code {e.returncode}" + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/mageck_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/mageck_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9b9f7e5a22b01340d4e970668c5c632e850beee0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/mageck_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/mageck_server.py') +SERVER_NAME = 'biosci_mageck' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d39a5c9a98465156717134e3088326949a552872 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-mageck: + build: . + image: mcp-mageck:latest + container_name: mcp-mageck + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=mageck + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..25dc251f7006cda5958506e42743c56b191db135 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - mageck + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mageck/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8630abc23df676d0240d181bc5ea4b63d53009f7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install mappy via conda (e.g., from bioconda) +RUN conda install -c bioconda mappy -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/mappy_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/mappy_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/mappy_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/app/mappy_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/app/mappy_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a5218b7a5d7478b6dbdfa8f9211b00cc31f936ac --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/app/mappy_server.py @@ -0,0 +1,334 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Literal, Dict, Any +import os + +# IMPORTANT NOTE: +# The provided documentation for "mappy" indicates it is a "Minimap2 Python binding" +# and explicitly states that `python -m mappy --help` fails with "No code object available for mappy". +# This means 'mappy' itself does not have a command-line interface that can be converted +# into an MCP tool using `subprocess.run`. +# +# However, the "URL Docs Extract" provided is entirely about the command-line usage of `minimap2`, +# which 'mappy' is a binding for. +# +# Therefore, this MCP tool definition assumes the intent was to create MCP tools for the +# `minimap2` command-line executable, based on the detailed usage examples and options +# described in the provided documentation. The functions are named `minimap2_...` accordingly. + +# Define common presets as an Enum or Literal for clarity and validation +Minimap2Preset = Literal[ + "map-pb", "map-ont", "map-iclr", "splice", "splice:hq", "splice:sr", + "ava-pb", "ava-ont", "sr", "asm5" +] + +# Define output formats +Minimap2OutputFormat = Literal["paf", "sam"] + +# Define strand options for splice +Minimap2Strand = Literal["f", "r", "n"] + +@mcp.tool() +def minimap2_index( + reference_file: Path, + output_index_file: Path, + kmer_size: Optional[int] = None, + window_size: Optional[int] = None, + homopolymer_compressed: bool = False, + batch_size: Optional[str] = None, # e.g., "4G" + threads: int = 3, +) -> Dict[str, Any]: + """ + Creates a minimap2 index (.mmi) from a reference FASTA/FASTQ file. + + This function wraps the `minimap2 -d` command to build a reference index. + Once built, this index can be used in subsequent alignment calls to speed up mapping. + + Args: + reference_file: Path to the input reference FASTA or FASTQ file. + (e.g., `ref.fa`) + output_index_file: Path to save the generated minimap2 index (.mmi) file. + (e.g., `ref.mmi`) + kmer_size: k-mer size. Default varies by preset (e.g., 15 for map-ont, 19 for map-pb). + If not specified, minimap2's internal default for indexing will be used. + Must be a positive integer if provided. + window_size: Minimizer window size. Default varies by preset (e.g., 10 for map-ont). + If not specified, minimap2's internal default for indexing will be used. + Must be a positive integer if provided. + homopolymer_compressed: If True, use homopolymer-compressed k-mers. Default is False. + Typically used for PacBio CLR reads (map-pb preset) to improve + performance and sensitivity. + batch_size: Batch size for indexing, e.g., "4G". This controls memory usage during indexing. + threads: Number of threads to use for indexing. Default is 3. Must be a positive integer. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + On success, `output_files` will contain the path to the generated `.mmi` file. + """ + # Input validation + if not reference_file.is_file(): + raise FileNotFoundError(f"Reference file not found: {reference_file}") + if output_index_file.suffix != ".mmi": + raise ValueError(f"Output index file must have .mmi extension: {output_index_file}") + output_index_file.parent.mkdir(parents=True, exist_ok=True) + + if kmer_size is not None and kmer_size <= 0: + raise ValueError("kmer_size must be a positive integer.") + if window_size is not None and window_size <= 0: + raise ValueError("window_size must be a positive integer.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + + command = ["minimap2", "-d", str(output_index_file)] + + if kmer_size is not None: + command.extend(["-k", str(kmer_size)]) + if window_size is not None: + command.extend(["-w", str(window_size)]) + if homopolymer_compressed: + command.append("-H") + if batch_size is not None: + command.extend(["-I", batch_size]) + + command.extend(["-t", str(threads)]) + + command.append(str(reference_file)) + + try: + process = subprocess.run(command, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(command), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(output_index_file)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "output_files": [], + } + +@mcp.tool() +def minimap2_align( + reference_input: Path, + query_files: List[Path], + output_file: Path, + output_format: Minimap2OutputFormat = "paf", + preset: Optional[Minimap2Preset] = None, + output_cigar_paf: bool = False, + num_secondary_alignments: int = 5, + secondary_ratio: float = 0.8, + long_cigar_tag: bool = False, + cs_tag: Optional[Literal[True, "long"]] = None, # True for --cs, "long" for --cs=long + force_strand: Optional[Minimap2Strand] = None, + splice_flank_no: bool = False, + junction_bed_file: Optional[Path] = None, + junction_bonus: int = 9, + max_intron_length: int = 200000, + write_junctions_file: Optional[Path] = None, + pass1_junctions_file: Optional[Path] = None, + threads: int = 3, +) -> Dict[str, Any]: + """ + Performs sequence alignment using minimap2. + + This function wraps the `minimap2` command for various alignment tasks. + It aligns query sequences (FASTA/FASTQ) against a reference genome (FASTA/FASTQ) + or a pre-built minimap2 index (.mmi). + + Args: + reference_input: Path to the reference FASTA/FASTQ file or a pre-built .mmi index. + (e.g., `ref.fa` or `ref.mmi`) + query_files: A list of paths to one or more query FASTA/FASTQ files. + For paired-end reads, provide two files (e.g., `[read1.fq, read2.fq]`). + Minimap2 will internally interleave them if they are adjacent and have same names. + output_file: Path to save the alignment output. The format is determined by `output_format`. + (e.g., `alignment.paf` or `alignment.sam`) + output_format: The desired output format: "paf" (default) or "sam". + preset: Pre-defined option set for specific data types. + Examples: "map-ont" (Oxford Nanopore genomic reads, default if no preset is given), + "splice" (spliced long reads), "sr" (short genomic reads), + "asm5" (assembly-to-assembly alignment). + If not specified, minimap2's default behavior is equivalent to "map-ont". + output_cigar_paf: If True, output CIGAR string in the 'cg' tag for PAF format. + Only applicable if `output_format` is "paf". Default is False. + num_secondary_alignments: Output up to this many secondary alignments. Default is 5. + Set to 0 to disable secondary alignments. Must be non-negative. + secondary_ratio: Minimum secondary-to-primary score ratio to retain secondary alignments. + Only secondary alignments with a score higher than this ratio of their + corresponding primary alignment's score will be kept. Default is 0.8. + Must be between 0.0 and 1.0. + long_cigar_tag: If True, move long CIGAR strings (those with >65535 operations) to the 'CG' tag + and leave a fully clipped CIGAR at the SAM CIGAR column. + Recommended for ultra-long reads if the output SAM/CRAM will be converted to BAM. + Default is False. + cs_tag: If True, output the 'cs' tag encoding bases at mismatches and INDELs. + If "long", output the long form of the 'cs' tag, which includes identical sequences. + Default is None (no cs tag). + force_strand: For spliced alignment, force minimap2 to consider only 'f' (forward), + 'r' (reverse), or 'n' (unknown, default) transcript strand. + Only applicable with 'splice', 'splice:hq', or 'splice:sr' presets. + splice_flank_no: If True, for spliced alignment, minimap2 will only model GT..AG splicing signals, + ignoring the additional base. Recommended for SIRV control data. + Only applicable with 'splice' presets. Default is False. + junction_bed_file: Path to a BED12 or 5-column BED file with annotated splice junctions. + Minimap2 will add a bonus score if an aligned junction matches an annotation. + Only applicable with 'splice' presets. + junction_bonus: Bonus score added if an aligned junction matches an annotated junction. Default is 9. + Only applicable with 'splice' presets when `junction_bed_file` is provided. + Must be a positive integer. + max_intron_length: Maximum intron length allowed. Default is 200000 (200kb). + Only applicable with 'splice' presets. Must be a positive integer. + write_junctions_file: Path to a BED file to write detected junctions. + Used for 2-pass RNA-seq alignment (first pass). + Only applicable with 'splice:sr' preset. + pass1_junctions_file: Path to a BED file containing junctions from a first pass. + Used for 2-pass RNA-seq alignment (second pass). + Only applicable with 'splice:sr' preset. + threads: Number of threads to use for alignment. Default is 3. Must be a positive integer. + + Returns: + A dictionary containing the command executed, stdout (empty as output is redirected), + stderr, and a list of output files. + On success, `output_files` will contain the path to the main alignment file + and optionally the `write_junctions_file`. + """ + # Input validation + if not reference_input.is_file(): + raise FileNotFoundError(f"Reference input file/index not found: {reference_input}") + if not query_files: + raise ValueError("At least one query file must be provided.") + for q_file in query_files: + if not q_file.is_file(): + raise FileNotFoundError(f"Query file not found: {q_file}") + + output_file.parent.mkdir(parents=True, exist_ok=True) + + if output_format == "paf" and output_file.suffix not in [".paf", ".txt", ""]: + # PAF files typically don't have a specific suffix, but .paf is common. + # An empty suffix is also possible if the user just provides a name. + pass + elif output_format == "sam" and output_file.suffix not in [".sam", ".bam", ""]: + # If SAM output, typically .sam or .bam (if converted later). + # This tool outputs SAM text, so .sam is appropriate. + pass + + if output_format == "sam" and output_cigar_paf: + raise ValueError("output_cigar_paf is only applicable when output_format is 'paf'.") + + if not (0 <= num_secondary_alignments): + raise ValueError("num_secondary_alignments must be a non-negative integer.") + if not (0.0 <= secondary_ratio <= 1.0): + raise ValueError("secondary_ratio must be between 0.0 and 1.0.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + + is_splice_preset = preset in ["splice", "splice:hq", "splice:sr"] + if force_strand is not None and not is_splice_preset: + raise ValueError("force_strand is only applicable with 'splice' presets.") + if splice_flank_no and not is_splice_preset: + raise ValueError("splice_flank_no is only applicable with 'splice' presets.") + if junction_bed_file is not None and not is_splice_preset: + raise ValueError("junction_bed_file is only applicable with 'splice' presets.") + if junction_bonus <= 0: + raise ValueError("junction_bonus must be a positive integer.") + if max_intron_length <= 0: + raise ValueError("max_intron_length must be a positive integer.") + + if write_junctions_file is not None and preset != "splice:sr": + raise ValueError("write_junctions_file is only applicable with 'splice:sr' preset.") + if pass1_junctions_file is not None and preset != "splice:sr": + raise ValueError("pass1_junctions_file is only applicable with 'splice:sr' preset.") + + if write_junctions_file: + write_junctions_file.parent.mkdir(parents=True, exist_ok=True) + if write_junctions_file.suffix != ".bed": + raise ValueError(f"write_junctions_file must have .bed extension: {write_junctions_file}") + if pass1_junctions_file and not pass1_junctions_file.is_file(): + raise FileNotFoundError(f"Pass1 junctions file not found: {pass1_junctions_file}") + if pass1_junctions_file and pass1_junctions_file.suffix != ".bed": + raise ValueError(f"pass1_junctions_file must have .bed extension: {pass1_junctions_file}") + if junction_bed_file and not junction_bed_file.is_file(): + raise FileNotFoundError(f"Junction BED file not found: {junction_bed_file}") + if junction_bed_file and junction_bed_file.suffix != ".bed": + raise ValueError(f"junction_bed_file must have .bed extension: {junction_bed_file}") + + + command = ["minimap2"] + + if preset: + command.extend(["-x", preset]) + + if output_format == "sam": + command.append("-a") + elif output_format == "paf" and output_cigar_paf: + command.append("-c") + + if num_secondary_alignments != 5: # Default is 5 + command.extend(["-N", str(num_secondary_alignments)]) + if secondary_ratio != 0.8: # Default is 0.8 + command.extend(["-p", str(secondary_ratio)]) + if long_cigar_tag: + command.append("-L") + if cs_tag: + if cs_tag == "long": + command.append("--cs=long") + else: # cs_tag is True + command.append("--cs") + + if is_splice_preset: + if force_strand: + command.extend(["-u", force_strand]) + if splice_flank_no: + command.append("--splice-flank=no") + if junction_bed_file: + command.extend(["--junc-bed", str(junction_bed_file)]) + if junction_bonus != 9: # Default is 9 + command.extend(["--junc-bonus", str(junction_bonus)]) + if max_intron_length != 200000: # Default is 200k + command.extend(["-G", str(max_intron_length)]) + if preset == "splice:sr": + if write_junctions_file: + command.extend(["--write-junc", str(write_junctions_file)]) + if pass1_junctions_file: + command.extend(["--pass1", str(pass1_junctions_file)]) + + command.extend(["-t", str(threads)]) + + command.append(str(reference_input)) + command.extend([str(q) for q in query_files]) + + # Execute minimap2 and redirect stdout to output_file + try: + with open(output_file, "w") as outfile: + process = subprocess.run(command, stdout=outfile, stderr=subprocess.PIPE, text=True, check=True) + + output_files = [str(output_file)] + if write_junctions_file: + output_files.append(str(write_junctions_file)) + + return { + "command_executed": " ".join(command) + f" > {output_file}", + "stdout": "", # stdout is redirected to file + "stderr": process.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + # If an error occurs, the output_file might be partially written or empty. + # We should still report it if it exists and has content, but indicate an error. + output_files_on_error = [] + if output_file.exists() and output_file.stat().st_size > 0: + output_files_on_error.append(str(output_file)) + if write_junctions_file and write_junctions_file.exists() and write_junctions_file.stat().st_size > 0: + output_files_on_error.append(str(write_junctions_file)) + + return { + "command_executed": " ".join(command) + f" > {output_file}", + "stdout": e.stdout, # In case of error, stdout might contain partial output + "stderr": e.stderr, + "error": str(e), + "output_files": output_files_on_error, + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..2838122e383361154fc122d87668eb26622fcf41 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-mappy: + build: . + image: mcp-mappy:latest + container_name: mcp-mappy + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=mappy + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mappy/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9ea3dcd4e7f0095f435acdd947c521c320e017f9 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install mash via conda (e.g., from bioconda) +RUN conda install -c bioconda mash -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/mash_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/mash_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/mash_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/mash_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/mash_server.py new file mode 100644 index 0000000000000000000000000000000000000000..527ed42e3cf594fb74ee2baecaf03aa2706e66dc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/mash_server.py @@ -0,0 +1,655 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# MCP decorator is not imported as per instructions. +# It is assumed to be available in the execution environment. + +# @mcp.tool() +def mash_sketch( + files: List[Path], + output_file: Optional[Path] = None, + kmer_size: int = 21, + sketch_size: int = 1000, + threads: int = 1, + file_list: Optional[Path] = None, + individual_sequences: bool = False, + reads: bool = False, + min_copies: int = 1, + min_coverage: int = 1, + genome_size: Optional[int] = None, + warning_threshold: float = 0.01, + buffer_size: Optional[str] = None, + verbose: bool = False, + seed: int = 42, + non_canonical: bool = False, + amino_acid: bool = False, + amino_acid_01: bool = False, + comment: Optional[str] = None, + concatenated: bool = False, +) -> dict: + """ + Create sketches (reduced representations for fast operations) from sequence files. + + Args: + files: One or more FASTA/FASTQ file(s) to sketch. + output_file: Output file for the sketch. If not provided, sketches are written to stdout. + kmer_size: k-mer size (must be <= 32). + sketch_size: Number of hashes per sketch. + threads: Number of parallel threads to use. + file_list: A file containing a list of input files, one per line. + individual_sequences: Sketch individual sequences (e.g., contigs) instead of whole files. + reads: Input files are sequence reads. This is required for coverage filtering. + min_copies: Minimum copies of each k-mer to include (k-mer filtering). + min_coverage: Minimum coverage for k-mers from reads to be considered (requires 'reads' to be True). + genome_size: Target genome size for p-value calculation. If not set, it's estimated. + warning_threshold: Probability of a random match for p-value calculation. + buffer_size: Input buffer size. Suffixes K, M, G are allowed (e.g., '100M'). + verbose: Print verbose messages to stderr. + seed: Seed for the hash function. + non_canonical: Use non-canonical k-mers. + amino_acid: Use amino acid alphabet (k <= 16). + amino_acid_01: Use amino acid alphabet with 0-1 encoding (k <= 16). + comment: Set a comment for the sketch. + concatenated: Create a sketch from a single, concatenated sequence. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # Input validation + if not files and not file_list: + raise ValueError("Either 'files' or 'file_list' must be provided.") + if files and file_list: + raise ValueError("'files' and 'file_list' are mutually exclusive.") + if kmer_size > 32 or kmer_size < 1: + raise ValueError("'kmer_size' must be between 1 and 32.") + if sketch_size <= 0: + raise ValueError("'sketch_size' must be a positive integer.") + if threads <= 0: + raise ValueError("'threads' must be a positive integer.") + if min_copies <= 0: + raise ValueError("'min_copies' must be a positive integer.") + if min_coverage > 1 and not reads: + raise ValueError("'min_coverage' > 1 requires 'reads' to be True.") + if amino_acid and amino_acid_01: + raise ValueError("'amino_acid' and 'amino_acid_01' are mutually exclusive.") + if (amino_acid or amino_acid_01) and kmer_size > 16: + raise ValueError("For amino acid sketches, 'kmer_size' must be <= 16.") + + cmd = ["mash", "sketch"] + + cmd.extend(["-k", str(kmer_size)]) + cmd.extend(["-s", str(sketch_size)]) + cmd.extend(["-p", str(threads)]) + cmd.extend(["-S", str(seed)]) + + if output_file: + cmd.extend(["-o", str(output_file)]) + if file_list: + cmd.extend(["-l", str(file_list)]) + if individual_sequences: + cmd.append("-i") + if reads: + cmd.append("-r") + if min_copies > 1: + cmd.extend(["-m", str(min_copies)]) + if min_coverage > 1: + cmd.extend(["-c", str(min_coverage)]) + if genome_size is not None: + cmd.extend(["-g", str(genome_size)]) + if warning_threshold != 0.01: + cmd.extend(["-w", str(warning_threshold)]) + if buffer_size: + cmd.extend(["-b", buffer_size]) + if verbose: + cmd.append("-v") + if non_canonical: + cmd.append("-n") + if amino_acid: + cmd.append("-a") + if amino_acid_01: + cmd.append("-A") + if comment: + cmd.extend(["-I", comment]) + if concatenated: + cmd.append("-C") + + if files: + for file_path in files: + if not file_path.exists(): + raise FileNotFoundError(f"Input file not found: {file_path}") + cmd.append(str(file_path)) + + try: + result = subprocess.run( + cmd, check=True, capture_output=True, text=True + ) + output_files = [str(output_file)] if output_file else [] + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Mash sketch failed", + "return_code": e.returncode, + } + +# @mcp.tool() +def mash_dist( + reference_sketch: Path, + query_files: List[Path], + query_list: Optional[Path] = None, + threads: int = 1, + tabular: bool = False, + max_distance: float = 1.0, + max_p_value: float = 1.0, + sketch_size: Optional[int] = None, + kmer_size: Optional[int] = None, + seed: int = 42, + top_pair_only: bool = False, + amino_acid: bool = False, + amino_acid_01: bool = False, + individual_sequences: bool = False, + write_comments: Optional[Path] = None, + read_comments: Optional[Path] = None, +) -> dict: + """ + Estimate the distance of query sequences to references. + + Args: + reference_sketch: The reference sketch file (.msh). + query_files: One or more query files (FASTA/FASTQ or .msh). + query_list: A file containing a list of query files, one per line. + threads: Number of parallel threads to use. + tabular: Output in a tab-separated format. + max_distance: Maximum distance to report. + max_p_value: Maximum p-value to report. + sketch_size: Override sketch size from the sketch file. + kmer_size: Override k-mer size from the sketch file. + seed: Seed for the hash function. + top_pair_only: For pairwise distances, only write the top pair by distance. + amino_acid: Use amino acid alphabet (k <= 16). + amino_acid_01: Use amino acid alphabet with 0-1 encoding (k <= 16). + individual_sequences: Sketch individual sequences if query files are not sketches. + write_comments: Write comments to the specified file. + read_comments: Read comments from the specified file. + + Returns: + A dictionary containing the executed command, stdout, and stderr. + """ + # Input validation + if not reference_sketch.exists(): + raise FileNotFoundError(f"Reference sketch not found: {reference_sketch}") + if not query_files and not query_list: + raise ValueError("Either 'query_files' or 'query_list' must be provided.") + if query_files and query_list: + raise ValueError("'query_files' and 'query_list' are mutually exclusive.") + if threads <= 0: + raise ValueError("'threads' must be a positive integer.") + if amino_acid and amino_acid_01: + raise ValueError("'amino_acid' and 'amino_acid_01' are mutually exclusive.") + + cmd = ["mash", "dist"] + cmd.extend(["-p", str(threads)]) + cmd.extend(["-S", str(seed)]) + + if tabular: + cmd.append("-t") + if max_distance < 1.0: + cmd.extend(["-d", str(max_distance)]) + if max_p_value < 1.0: + cmd.extend(["-v", str(max_p_value)]) + if sketch_size is not None: + if sketch_size <= 0: + raise ValueError("'sketch_size' must be a positive integer.") + cmd.extend(["-s", str(sketch_size)]) + if kmer_size is not None: + if kmer_size <= 0 or kmer_size > 32: + raise ValueError("'kmer_size' must be between 1 and 32.") + cmd.extend(["-k", str(kmer_size)]) + if top_pair_only: + cmd.append("-w") + if amino_acid: + cmd.append("-a") + if amino_acid_01: + cmd.append("-A") + if individual_sequences: + cmd.append("-i") + if write_comments: + cmd.extend(["-c", str(write_comments)]) + if read_comments: + cmd.extend(["-C", str(read_comments)]) + if query_list: + cmd.extend(["-l", str(query_list)]) + + cmd.append(str(reference_sketch)) + + if query_files: + for file_path in query_files: + if not file_path.exists(): + raise FileNotFoundError(f"Query file not found: {file_path}") + cmd.append(str(file_path)) + + try: + result = subprocess.run( + cmd, check=True, capture_output=True, text=True + ) + output_files = [str(write_comments)] if write_comments else [] + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Mash dist failed", + "return_code": e.returncode, + } + +# @mcp.tool() +def mash_screen( + reference_sketch: Path, + query_files: List[Path], + threads: int = 1, + p_value_threshold: float = 0.01, + winner_takes_all: bool = False, + identity_threshold: float = 0.95, + bloom_filter_size: Optional[str] = None, + no_bloom_filter: bool = False, + amino_acid: bool = False, + amino_acid_01: bool = False, + seed: int = 42, +) -> dict: + """ + Determine whether query sequences are within a larger mixture of sequences. + + Args: + reference_sketch: The reference sketch file (.msh). + query_files: One or more query FASTA/FASTQ files. + threads: Number of parallel threads to use. + p_value_threshold: p-value threshold for winning. + winner_takes_all: Only report the best match for each query. + identity_threshold: Identity threshold for winning (0 to 1). + bloom_filter_size: Bloom filter size. Suffixes K, M, G are allowed. + no_bloom_filter: Do not use a Bloom filter (slower). + amino_acid: Use amino acid alphabet (k <= 16). + amino_acid_01: Use amino acid alphabet with 0-1 encoding (k <= 16). + seed: Seed for the hash function. + + Returns: + A dictionary containing the executed command, stdout, and stderr. + """ + # Input validation + if not reference_sketch.exists(): + raise FileNotFoundError(f"Reference sketch not found: {reference_sketch}") + if not query_files: + raise ValueError("'query_files' must be provided and not empty.") + if threads <= 0: + raise ValueError("'threads' must be a positive integer.") + if not (0.0 <= identity_threshold <= 1.0): + raise ValueError("'identity_threshold' must be between 0 and 1.") + if bloom_filter_size and no_bloom_filter: + raise ValueError("'bloom_filter_size' and 'no_bloom_filter' are mutually exclusive.") + if amino_acid and amino_acid_01: + raise ValueError("'amino_acid' and 'amino_acid_01' are mutually exclusive.") + + cmd = ["mash", "screen"] + cmd.extend(["-p", str(threads)]) + cmd.extend(["-S", str(seed)]) + + if p_value_threshold != 0.01: + cmd.extend(["-v", str(p_value_threshold)]) + if winner_takes_all: + cmd.append("-w") + if identity_threshold != 0.95: + cmd.extend(["-i", str(identity_threshold)]) + if bloom_filter_size: + cmd.extend(["-b", bloom_filter_size]) + if no_bloom_filter: + cmd.append("-n") + if amino_acid: + cmd.append("-a") + if amino_acid_01: + cmd.append("-A") + + cmd.append(str(reference_sketch)) + + for file_path in query_files: + if not file_path.exists(): + raise FileNotFoundError(f"Query file not found: {file_path}") + cmd.append(str(file_path)) + + try: + result = subprocess.run( + cmd, check=True, capture_output=True, text=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Mash screen failed", + "return_code": e.returncode, + } + +# @mcp.tool() +def mash_triangle( + sketches: Path, + output_file: Optional[Path] = None, + threads: int = 1, + list_names_only: bool = False, + omit_query_vs_query: bool = False, + max_distance: float = 1.0, + max_p_value: float = 1.0, + write_comments: Optional[Path] = None, + read_comments: Optional[Path] = None, +) -> dict: + """ + Estimate a lower-triangular distance matrix from a single sketch file. + + Args: + sketches: A single sketch file (.msh) containing multiple sketches. + output_file: Output file for the distance matrix. Defaults to stdout. + threads: Number of parallel threads to use. + list_names_only: List sequence names only. + omit_query_vs_query: Omit query-vs-query distances. + max_distance: Maximum distance to report. + max_p_value: Maximum p-value to report. + write_comments: Write comments to the specified file. + read_comments: Read comments from the specified file. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # Input validation + if not sketches.exists(): + raise FileNotFoundError(f"Sketch file not found: {sketches}") + if threads <= 0: + raise ValueError("'threads' must be a positive integer.") + + cmd = ["mash", "triangle"] + cmd.extend(["-p", str(threads)]) + + if output_file: + cmd.extend(["-o", str(output_file)]) + if list_names_only: + cmd.append("-l") + if omit_query_vs_query: + cmd.append("-E") + if max_distance < 1.0: + cmd.extend(["-d", str(max_distance)]) + if max_p_value < 1.0: + cmd.extend(["-v", str(max_p_value)]) + if write_comments: + cmd.extend(["-c", str(write_comments)]) + if read_comments: + cmd.extend(["-C", str(read_comments)]) + + cmd.append(str(sketches)) + + try: + result = subprocess.run( + cmd, check=True, capture_output=True, text=True + ) + output_files = [] + if output_file: + output_files.append(str(output_file)) + if write_comments: + output_files.append(str(write_comments)) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Mash triangle failed", + "return_code": e.returncode, + } + +# @mcp.tool() +def mash_paste( + output_name: str, + sketch_files: List[Path], + sketch_list_file: Optional[Path] = None, +) -> dict: + """ + Create a single sketch file from multiple sketch files. + + Args: + output_name: The name/prefix for the output sketch file. '.msh' will be appended. + sketch_files: One or more sketch files to combine. + sketch_list_file: A file containing a list of sketch files, one per line. + + Returns: + A dictionary containing the executed command, stdout, stderr, and the path to the output sketch file. + """ + # Input validation + if not sketch_files and not sketch_list_file: + raise ValueError("Either 'sketch_files' or 'sketch_list_file' must be provided.") + if sketch_files and sketch_list_file: + raise ValueError("'sketch_files' and 'sketch_list_file' are mutually exclusive.") + + cmd = ["mash", "paste", output_name] + + if sketch_list_file: + if not sketch_list_file.exists(): + raise FileNotFoundError(f"Sketch list file not found: {sketch_list_file}") + cmd.extend(["-l", str(sketch_list_file)]) + + if sketch_files: + for file_path in sketch_files: + if not file_path.exists(): + raise FileNotFoundError(f"Sketch file not found: {file_path}") + cmd.append(str(file_path)) + + try: + result = subprocess.run( + cmd, check=True, capture_output=True, text=True + ) + output_file = f"{output_name}.msh" + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_file], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Mash paste failed", + "return_code": e.returncode, + } + +# @mcp.tool() +def mash_info( + sketch_files: List[Path], + tabular: bool = False, + show_comments: bool = False, + show_data: bool = False, +) -> dict: + """ + Display information about sketch files. + + Args: + sketch_files: One or more sketch files to inspect. + tabular: Output in a tab-separated format. + show_comments: Show comments within the sketch files. + show_data: Show sketch data (hashes). + + Returns: + A dictionary containing the executed command, stdout, and stderr. + """ + # Input validation + if not sketch_files: + raise ValueError("'sketch_files' must be provided and not empty.") + + cmd = ["mash", "info"] + + if tabular: + cmd.append("-t") + if show_comments: + cmd.append("-c") + if show_data: + cmd.append("-d") + + for file_path in sketch_files: + if not file_path.exists(): + raise FileNotFoundError(f"Sketch file not found: {file_path}") + cmd.append(str(file_path)) + + try: + result = subprocess.run( + cmd, check=True, capture_output=True, text=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Mash info failed", + "return_code": e.returncode, + } + +# @mcp.tool() +def mash_bounds() -> dict: + """ + Print a table of Mash error bounds. This command takes no arguments. + + Returns: + A dictionary containing the executed command, stdout, and stderr. + """ + cmd = ["mash", "bounds"] + try: + result = subprocess.run( + cmd, check=True, capture_output=True, text=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Mash bounds failed", + "return_code": e.returncode, + } + +# @mcp.tool() +def mash_taxscreen( + taxonomy_map: Path, + screen_output: Path, + output_file: Optional[Path] = None, + threads: int = 1, + taxonomy_tree: Optional[Path] = None, + taxonomy_names: Optional[Path] = None, + min_identity: float = 0.95, + min_hashes: int = 10, + rank: str = "species", + report_unclassified: bool = False, + report_zeroes: bool = False, +) -> dict: + """ + Create a Kraken-style taxonomic report based on mash screen output. + + Args: + taxonomy_map: A file mapping sequence IDs to tax IDs. + screen_output: The output file from 'mash screen'. + output_file: Output file for the report. Defaults to stdout. + threads: Number of parallel threads to use. + taxonomy_tree: Taxonomy tree file (e.g., nodes.dmp). + taxonomy_names: Taxonomy names file (e.g., names.dmp). + min_identity: Minimum identity to consider a hit. + min_hashes: Minimum number of hashes to consider a hit. + rank: Rank to report (e.g., species, genus). + report_unclassified: Report unclassified reads. + report_zeroes: Report taxa with zero hits. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # Input validation + if not taxonomy_map.exists(): + raise FileNotFoundError(f"Taxonomy map file not found: {taxonomy_map}") + if not screen_output.exists(): + raise FileNotFoundError(f"Mash screen output file not found: {screen_output}") + if threads <= 0: + raise ValueError("'threads' must be a positive integer.") + if not (0.0 <= min_identity <= 1.0): + raise ValueError("'min_identity' must be between 0 and 1.") + if min_hashes <= 0: + raise ValueError("'min_hashes' must be a positive integer.") + + cmd = ["mash", "taxscreen"] + + if output_file: + cmd.extend(["-o", str(output_file)]) + if threads > 1: + cmd.extend(["-p", str(threads)]) + if taxonomy_tree: + cmd.extend(["-t", str(taxonomy_tree)]) + if taxonomy_names: + cmd.extend(["-n", str(taxonomy_names)]) + if min_identity != 0.95: + cmd.extend(["-m", str(min_identity)]) + if min_hashes != 10: + cmd.extend(["-l", str(min_hashes)]) + if rank != "species": + cmd.extend(["-r", rank]) + if report_unclassified: + cmd.append("--report-unclassified") + if report_zeroes: + cmd.append("--report-zeroes") + + cmd.append(str(taxonomy_map)) + cmd.append(str(screen_output)) + + try: + result = subprocess.run( + cmd, check=True, capture_output=True, text=True + ) + output_files = [str(output_file)] if output_file else [] + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Mash taxscreen failed", + "return_code": e.returncode, + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/mash_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/mash_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..64de5517f93a5f91cf492d2f3581a896cbc25e17 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/mash_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/mash_server.py') +SERVER_NAME = 'biosci_mash' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..59aa6986d816e37f0633fc7e61f2b5d48e83ad7f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-mash: + build: . + image: mcp-mash:latest + container_name: mcp-mash + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=mash + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bad3fd0f70c2665f4210fece911e36ba15aa78c2 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - mash + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mash/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e4129a2c36e0fe2ebc2fcdfdc4409cad110b5ef7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install multiqc-xenium-extra via conda (e.g., from bioconda) +RUN conda install -c bioconda multiqc-xenium-extra -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY multiqc-xenium-extra_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/multiqc-xenium-extra_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/multiqc-xenium-extra_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/app/multiqc-xenium-extra_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/app/multiqc-xenium-extra_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ab734dd549a4f6025377bb6fa7a171a2f7b9d635 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/app/multiqc-xenium-extra_server.py @@ -0,0 +1,197 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List +import tempfile + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# MCP decorator is assumed to be available in the execution environment. +# +# from mcp import mcp + +@mcp.tool() +def run_multiqc_xenium( + analysis_dir: Path, + outdir: Optional[Path] = None, + force: bool = False, + title: Optional[str] = None, + filename: Optional[str] = None, + template: Optional[str] = None, + config_file: Optional[Path] = None, + dirs: bool = False, + dirs_depth: Optional[int] = None, + ignore: Optional[List[str]] = None, + file_list: Optional[Path] = None, + module: Optional[List[str]] = None, + exclude: Optional[List[str]] = None, + data_format: Optional[str] = None, + zip_data_dir: bool = False, + no_data_dir: bool = False, + export: bool = False, + pdf: bool = False, + flat: bool = False, + interactive: bool = True, + verbose: bool = False, + quiet: bool = False, +) -> dict: + """ + Runs MultiQC with the multiqc-xenium-extra plugin on a given analysis directory. + + This tool generates a comprehensive report from 10x Genomics Xenium output files + by leveraging the MultiQC framework and the specialized Xenium plugin. It wraps + the standard 'multiqc' command-line tool. + + Args: + analysis_dir: Path to the directory containing analysis results to be scanned. + outdir: Directory to write the MultiQC report to. If not provided, a temporary directory will be used. + force: If True, overwrite any existing reports. + title: Title for the MultiQC report. + filename: Name for the output report file. Defaults to 'multiqc_report.html'. + template: Name of a report template to use (e.g., 'default', 'simple'). + config_file: Path to a custom MultiQC configuration file in YAML or JSON format. + dirs: If True, search subdirectories of analysis_dir for analysis files. + dirs_depth: Limit the subdirectory search depth. Only used if `dirs` is True. + ignore: List of file/directory name patterns to ignore. Can be specified multiple times. + file_list: Path to a file containing a list of files to be searched, one per line. + module: List of specific MultiQC modules to run. Can be specified multiple times. + exclude: List of MultiQC modules to exclude from the report. Can be specified multiple times. + data_format: Format for the exported data ('tsv', 'yaml', 'json'). + zip_data_dir: If True, compress the multiqc_data directory into a zip file. + no_data_dir: If True, do not create the multiqc_data directory. + export: If True, export plots as static images. + pdf: If True, create a PDF version of the report. Requires external dependencies like LaTeX. + flat: If True, use a flat directory structure for exported plots. + interactive: If True (default), use interactive plots in the report. Set to False for static plots. + verbose: If True, increase the verbosity of the MultiQC output. + quiet: If True, suppress all console output from MultiQC. + + Returns: + A dictionary containing the execution command, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + if not analysis_dir.is_dir(): + raise ValueError(f"Analysis directory not found: {analysis_dir}") + + if config_file and not config_file.is_file(): + raise ValueError(f"Config file not found: {config_file}") + + if file_list and not file_list.is_file(): + raise ValueError(f"File list not found: {file_list}") + + if data_format and data_format not in ["tsv", "yaml", "json"]: + raise ValueError(f"Invalid data_format: '{data_format}'. Must be one of 'tsv', 'yaml', 'json'.") + + if verbose and quiet: + raise ValueError("Cannot set both 'verbose' and 'quiet' to True.") + + # --- Command Construction --- + cmd = ["multiqc"] + + # Boolean flags + if force: cmd.append("--force") + if dirs: cmd.append("--dirs") + if zip_data_dir: cmd.append("--zip-data-dir") + if no_data_dir: cmd.append("--no-data-dir") + if export: cmd.append("--export") + if pdf: cmd.append("--pdf") + if flat: cmd.append("--flat") + if not interactive: cmd.append("--static") + if verbose: cmd.append("--verbose") + if quiet: cmd.append("--quiet") + + # Handle output directory + temp_dir_manager = None + if outdir: + outdir.mkdir(parents=True, exist_ok=True) + cmd.extend(["--outdir", str(outdir)]) + effective_outdir = outdir + else: + temp_dir_manager = tempfile.TemporaryDirectory() + effective_outdir = Path(temp_dir_manager.name) + cmd.extend(["--outdir", str(effective_outdir)]) + + # Arguments with values + if title: cmd.extend(["--title", title]) + if filename: cmd.extend(["--filename", filename]) + if template: cmd.extend(["--template", template]) + if config_file: cmd.extend(["--config", str(config_file)]) + if dirs_depth is not None: cmd.extend(["--dirs-depth", str(dirs_depth)]) + if data_format: cmd.extend(["--data-format", data_format]) + if file_list: cmd.extend(["--file-list", str(file_list)]) + + # List-based arguments + if ignore: + for pattern in ignore: cmd.extend(["--ignore", pattern]) + if module: + for mod in module: cmd.extend(["--module", mod]) + if exclude: + for ex in exclude: cmd.extend(["--exclude", ex]) + + # Positional argument + cmd.append(str(analysis_dir)) + + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + + # --- Subprocess Execution --- + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + # --- Output Handling --- + output_files = [] + report_name = filename if filename else "multiqc_report.html" + if not report_name.lower().endswith(('.html', '.htm')): + report_name += '.html' + + report_path = effective_outdir / report_name + if report_path.is_file(): + output_files.append(str(report_path)) + + data_dir_path = effective_outdir / "multiqc_data" + if data_dir_path.is_dir(): + output_files.append(str(data_dir_path)) + + zip_path = effective_outdir / "multiqc_data.zip" + if zip_data_dir and zip_path.is_file(): + output_files.append(str(zip_path)) + + pdf_path = report_path.with_suffix('.pdf') + if pdf and pdf_path.is_file(): + output_files.append(str(pdf_path)) + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + + except FileNotFoundError: + error_msg = "Error: 'multiqc' command not found. Please ensure MultiQC is installed and in your system's PATH." + logger.error(error_msg) + return { + "command_executed": command_executed, + "stdout": "", + "stderr": error_msg, + "output_files": [], + } + except subprocess.CalledProcessError as e: + logger.error(f"MultiQC execution failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + } + finally: + # Clean up the temporary directory if it was created + if temp_dir_manager: + temp_dir_manager.cleanup() \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/app/multiqc-xenium-extra_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/app/multiqc-xenium-extra_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..59c4766ea400658362714b6078f51bab5da8e12c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/app/multiqc-xenium-extra_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/app/multiqc-xenium-extra_server.py') +SERVER_NAME = 'biosci_multiqc_xenium_extra' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b9e9c28a997ae1603cddda615e90ceceb650afb3 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-multiqc-xenium-extra: + build: . + image: mcp-multiqc-xenium-extra:latest + container_name: mcp-multiqc-xenium-extra + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=multiqc-xenium-extra + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5c7b2df0639ec4c3777a2fc7261d9fe8cceefa69 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - multiqc-xenium-extra + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_multiqc-xenium-extra/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mztosqlite/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mztosqlite/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_mztosqlite/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nanoplot/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nanoplot/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nanoplot/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nextclade/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nextclade/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c385ef4a2a15c189839bb56afce3a993a98d05c1 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nextclade/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-nextclade: + build: . + image: mcp-nextclade:latest + container_name: mcp-nextclade + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=nextclade + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nextclade/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nextclade/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dbec6b04c45d8c8dca0370cb783db2c0540ffc12 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nextclade/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - nextclade + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nextclade/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nextclade/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_nextclade/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..49241ee0954fe79a3b6562f5069f9380d2a431dc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install paste-bio via conda (e.g., from bioconda) +RUN conda install -c bioconda paste-bio -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY paste-bio_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/paste-bio_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/paste-bio_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/app/paste-bio_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/app/paste-bio_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d13de9f4ea2dbb514feaf50747c8bb0a1bf06897 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/app/paste-bio_server.py @@ -0,0 +1,170 @@ +import subprocess +from pathlib import Path +from typing import List, Optional + +@mcp.tool() +def paste_pairwise_align( + files: List[str], + output_dir: str = ".", + alpha: float = 0.1, + cost: str = "kl", + output_coordinates: bool = False, + weights: Optional[List[str]] = None, + start_alignments: Optional[List[str]] = None, +): + """ + Align spots across pairwise slices using PASTE. + + PASTE (Probabilistic Alignment of Spatial Transcriptomics Experiments) leverages both + gene expression similarity and spatial distances between spots to align spatial transcriptomics data. + Pairwise alignment returns a mapping of spots between each consecutive pair of slices. + + Args: + files: List of paths to data files (.csv). Must provide two files per slice: + gene expression followed by spatial coordinates (e.g., [s1_exp, s1_coord, s2_exp, s2_coord]). + output_dir: Directory to store output files. + alpha: Alpha parameter for PASTE (trade-off between expression and spatial distance). + cost: Expression dissimilarity cost ('kl' or 'Euclidean'). + output_coordinates: If True, output new coordinates. + weights: Optional list of paths to weights files of spots in each slice (.csv). + start_alignments: Optional list of paths to initial alignments for OT (.csv). + """ + # Input validation + if len(files) % 2 != 0: + return {"error": "Files list must contain pairs of (expression.csv, coordinates.csv) for each slice."} + + for f in files: + if not Path(f).exists(): + return {"error": f"File not found: {f}"} + + if cost not in ["kl", "Euclidean"]: + return {"error": "Cost must be either 'kl' or 'Euclidean'."} + + # Construct command + cmd = ["python", "paste-cmd-line.py", "-m", "pairwise", "-f"] + cmd.extend(files) + + cmd.extend(["-d", output_dir]) + cmd.extend(["-a", str(alpha)]) + cmd.extend(["-c", cost]) + + if output_coordinates: + cmd.append("-x") + + if weights: + cmd.append("-w") + cmd.extend(weights) + + if start_alignments: + cmd.append("-s") + cmd.extend(start_alignments) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": list(Path(output_dir).glob("*.csv")) + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def paste_center_align( + files: List[str], + output_dir: str = ".", + alpha: float = 0.1, + cost: str = "kl", + n_components: int = 15, + lmbda: Optional[List[float]] = None, + initial_slice_index: int = 1, + threshold: float = 0.001, + output_coordinates: bool = False, + weights: Optional[List[str]] = None, + start_alignments: Optional[List[str]] = None, +): + """ + Integrate multiple spatial transcriptomics slices into one center slice using PASTE. + + Center alignment outputs the low dimensional representation (NMF decomposition) of the + center slice gene expression and mappings between the center slice and each input slice. + + Args: + files: List of paths to data files (.csv). Must provide two files per slice: + gene expression followed by spatial coordinates (e.g., [s1_exp, s1_coord, s2_exp, s2_coord]). + output_dir: Directory to store output files. + alpha: Alpha parameter for PASTE. + cost: Expression dissimilarity cost ('kl' or 'Euclidean'). + n_components: Number of components for the NMF step in center_align. + lmbda: Lambda parameter (floats). A probability vector of length n (number of slices). + initial_slice_index: Specify which slice (1-indexed) is the initial coordinate reference. + threshold: Convergence threshold for center_align. + output_coordinates: If True, output new coordinates. + weights: Optional list of paths to weights files of spots in each slice (.csv). + start_alignments: Optional list of paths to initial alignments for OT (.csv). + """ + # Input validation + if len(files) % 2 != 0: + return {"error": "Files list must contain pairs of (expression.csv, coordinates.csv) for each slice."} + + num_slices = len(files) // 2 + + for f in files: + if not Path(f).exists(): + return {"error": f"File not found: {f}"} + + if cost not in ["kl", "Euclidean"]: + return {"error": "Cost must be either 'kl' or 'Euclidean'."} + + if initial_slice_index < 1 or initial_slice_index > num_slices: + return {"error": f"initial_slice_index must be between 1 and {num_slices}."} + + # Construct command + cmd = ["python", "paste-cmd-line.py", "-m", "center", "-f"] + cmd.extend(files) + + cmd.extend(["-d", output_dir]) + cmd.extend(["-a", str(alpha)]) + cmd.extend(["-c", cost]) + cmd.extend(["-p", str(n_components)]) + cmd.extend(["-i", str(initial_slice_index)]) + cmd.extend(["-t", str(threshold)]) + + if lmbda: + if len(lmbda) != num_slices: + return {"error": f"lmbda must have length {num_slices}."} + cmd.append("-l") + cmd.extend([str(l) for l in lmbda]) + + if output_coordinates: + cmd.append("-x") + + if weights: + cmd.append("-w") + cmd.extend(weights) + + if start_alignments: + cmd.append("-s") + cmd.extend(start_alignments) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": list(Path(output_dir).glob("*.csv")) + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/app/paste-bio_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/app/paste-bio_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1804b1dad3ecaa9568d53d69a02d36d6c116a7b8 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/app/paste-bio_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/app/paste-bio_server.py') +SERVER_NAME = 'biosci_paste_bio' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..caad11f45d84ef4967add43d6f94293a581118ab --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-paste-bio: + build: . + image: mcp-paste-bio:latest + container_name: mcp-paste-bio + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=paste-bio + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..65185f3bf3b4ffd5d47c646a619be616cd9967ff --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - paste-bio + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_paste-bio/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e5d92854dd966f31a37af991d15205448a5d2360 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-bio-samtools via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-bio-samtools -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-bio-samtools_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-bio-samtools_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-bio-samtools_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/perl-bio-samtools_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/perl-bio-samtools_server.py new file mode 100644 index 0000000000000000000000000000000000000000..904ad8650b220779873780a84cbc6016fb1fb571 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/perl-bio-samtools_server.py @@ -0,0 +1,228 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Dict, Any, Union + +# NO NEED to import mcp as per instructions + +@mcp.tool() +def run_perl_script( + program_file: Optional[Path] = None, + program_arguments: List[str] = [], + program_lines: List[str] = [], + program_lines_extended: List[str] = [], + record_separator: Optional[str] = None, + autosplit_mode: bool = False, + unicode_features: Optional[str] = None, + check_syntax_only: bool = False, + debugger: Optional[str] = None, + debugging_flags: Optional[str] = None, + no_sitecustomize: bool = False, + split_pattern: Optional[str] = None, + edit_in_place_extension: Optional[str] = None, + include_directories: List[Path] = [], + line_ending_terminator: Optional[str] = None, + use_modules_m: List[str] = [], + use_modules_M: List[str] = [], + no_modules_M: List[str] = [], + loop_around_program: bool = False, + loop_and_print: bool = False, + rudimentary_switch_parsing: bool = False, + search_path_for_program: bool = False, + tainting_warnings: bool = False, + tainting_checks: bool = False, + dump_core: bool = False, + allow_unsafe_operations: bool = False, + print_version: bool = False, + print_config_summary: Optional[str] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + ignore_text_directory: Optional[Path] = None, + disable_all_warnings: bool = False, +) -> Dict[str, Any]: + """ + Executes a Perl script or program lines with various Perl interpreter options. + + This tool wraps the 'perl' interpreter, allowing users to run Perl scripts + or one-liners. It provides access to all standard Perl command-line switches. + If the execution environment includes 'perl-bio-samtools', Perl scripts + can utilize the Bio::SamTools module. + + Args: + program_file: Path to the Perl script file to execute. Required if no program_lines are provided. + program_arguments: Arguments to pass to the Perl script. + program_lines: One or more lines of Perl program code to execute (like -e). + Can be specified multiple times. + program_lines_extended: One or more lines of Perl program code to execute, + like -e, but enables all optional features (like -E). + Can be specified multiple times. + record_separator: Specify record separator (e.g., '0' for NUL, '0777' for paragraph mode). + If an empty string is provided, it uses '\0'. + autosplit_mode: Enable autosplit mode with -n or -p (splits $_ into @F). + unicode_features: Enables listed Unicode features (e.g., 'all', 'say'). + check_syntax_only: Check syntax only (runs BEGIN and CHECK blocks). + debugger: Run program under debugger. Optionally specify debugger (e.g., 'MyDebugger'). + If an empty string is provided, it enables the default debugger. + debugging_flags: Set debugging flags (argument is a bit mask or alphabets). + no_sitecustomize: Don't do $sitelib/sitecustomize.pl at startup. + split_pattern: split() pattern for -a switch (e.g., '/\\s+/'). + edit_in_place_extension: Edit <> files in place. Makes backup if extension supplied (e.g., '.bak'). + If an empty string is provided, it edits in place without making a backup. + include_directories: Specify @INC/#include directory (can be specified multiple times). + line_ending_terminator: Enable line ending processing, specifies line terminator (e.g., '0777'). + If an empty string is provided, it enables processing with the default terminator. + use_modules_m: List of modules to 'use' before executing program (like -m). + use_modules_M: List of modules to 'use' with 'import' before executing program (like -M). + no_modules_M: List of modules to 'no' before executing program (like -M-). + loop_around_program: Assume "while (<>) { ... }" loop around program (like -n). + loop_and_print: Assume loop like -n but print line also, like sed (like -p). + rudimentary_switch_parsing: Enable rudimentary parsing for switches after programfile (like -s). + search_path_for_program: Look for programfile using PATH environment variable (like -S). + tainting_warnings: Enable tainting warnings (like -t). + tainting_checks: Enable tainting checks (like -T). + dump_core: Dump core after parsing program (like -u). + allow_unsafe_operations: Allow unsafe operations (like -U). + print_version: Print version, patchlevel and license (like -v). + print_config_summary: Print configuration summary. Optionally specify a single Config.pm variable (like -V:variable). + If an empty string is provided, it prints the full summary. + enable_warnings: Enable many useful warnings (like -w). + enable_all_warnings: Enable all warnings (like -W). + ignore_text_directory: Ignore text before #!perl line. Optionally cd to directory (like -x). + If an empty string is provided, it ignores text without changing directory. + disable_all_warnings: Disable all warnings (like -X). + """ + # Input validation + if not program_file and not (program_lines or program_lines_extended): + raise ValueError( + "Either 'program_file' or at least one of 'program_lines' or 'program_lines_extended' must be provided." + ) + + if program_file: + if not program_file.is_file(): + raise FileNotFoundError(f"Program file not found: {program_file}") + + for d in include_directories: + if not d.is_dir(): + raise NotADirectoryError(f"Include directory not found or not a directory: {d}") + + if ignore_text_directory: + if not ignore_text_directory.is_dir(): + raise NotADirectoryError(f"Ignore text directory not found or not a directory: {ignore_text_directory}") + + # Mutually exclusive options + if loop_around_program and loop_and_print: + raise ValueError("Options 'loop_around_program' (-n) and 'loop_and_print' (-p) are mutually exclusive.") + + warning_options_count = sum([enable_warnings, enable_all_warnings, disable_all_warnings]) + if warning_options_count > 1: + raise ValueError( + "Only one of 'enable_warnings' (-w), 'enable_all_warnings' (-W), or 'disable_all_warnings' (-X) can be true." + ) + + command: List[str] = ["perl"] + + # Add switches + if record_separator is not None: + command.append(f"-0{record_separator}") + if autosplit_mode: + command.append("-a") + if unicode_features: + command.append(f"-C{unicode_features}") + if check_syntax_only: + command.append("-c") + if debugger is not None: + command.append(f"-d{':' + debugger if debugger else ''}") + if debugging_flags: + command.append(f"-D{debugging_flags}") + for line in program_lines: + command.extend(["-e", line]) + for line in program_lines_extended: + command.extend(["-E", line]) + if no_sitecustomize: + command.append("-f") + if split_pattern: + command.append(f"-F{split_pattern}") + if edit_in_place_extension is not None: + command.append(f"-i{edit_in_place_extension}") + for d in include_directories: + command.extend(["-I", str(d)]) + if line_ending_terminator is not None: + command.append(f"-l{line_ending_terminator}") + for m in use_modules_m: + command.extend(["-m", m]) + for m in use_modules_M: + command.extend(["-M", m]) + for m in no_modules_M: + command.extend(["-M-", m]) + if loop_around_program: + command.append("-n") + if loop_and_print: + command.append("-p") + if rudimentary_switch_parsing: + command.append("-s") + if search_path_for_program: + command.append("-S") + if tainting_warnings: + command.append("-t") + if tainting_checks: + command.append("-T") + if dump_core: + command.append("-u") + if allow_unsafe_operations: + command.append("-U") + if print_version: + command.append("-v") + if print_config_summary is not None: + command.append(f"-V{':' + print_config_summary if print_config_summary else ''}") + if enable_warnings: + command.append("-w") + if enable_all_warnings: + command.append("-W") + if ignore_text_directory is not None: + command.append(f"-x{str(ignore_text_directory) if ignore_text_directory else ''}") + if disable_all_warnings: + command.append("-X") + + # Add program file and its arguments + if program_file: + command.append(str(program_file)) + command.extend(program_arguments) + + command_executed = " ".join(str(arg) for arg in command) + stdout = "" + stderr = "" + output_files: List[Path] = [] + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + stdout = process.stdout + stderr = process.stderr + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "returncode": e.returncode, + "output_files": output_files, + } + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: Perl executable not found. Ensure Perl is installed and in your PATH.", + "error": "Perl executable not found.", + "returncode": 127, + "output_files": output_files, + } + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/perl-bio-samtools_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/perl-bio-samtools_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6bb5ebf191ad633577e8e3b90e2e155ce6e7e9d3 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/perl-bio-samtools_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/perl-bio-samtools_server.py') +SERVER_NAME = 'biosci_perl_bio_samtools' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..937bf65745e4825484c3bb95398404113772cf87 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-bio-samtools: + build: . + image: mcp-perl-bio-samtools:latest + container_name: mcp-perl-bio-samtools + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-bio-samtools + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d8784daee314a4c90b9e29192e2d045412aa48a5 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-bio-samtools + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-samtools/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ff4c85c245cdc2fdaa10c116403fcd4626bdc79a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-bio-searchio-hmmer via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-bio-searchio-hmmer -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-bio-searchio-hmmer_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-bio-searchio-hmmer_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-bio-searchio-hmmer_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/perl-bio-searchio-hmmer_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/perl-bio-searchio-hmmer_server.py new file mode 100644 index 0000000000000000000000000000000000000000..92ecf6cf92399a168b415ff3dcc77215d957a803 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/perl-bio-searchio-hmmer_server.py @@ -0,0 +1,184 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Literal, Dict, Any + +# The MCP decorator is assumed to be available in the execution environment. +# from mcp import tool as mcp_tool + +# For local testing, you can use a placeholder decorator: +def mcp_tool_placeholder(*args, **kwargs): + def decorator(func): + return func + return decorator + +mcp = type('mcp', (), {'tool': mcp_tool_placeholder}) + + +# The self-contained Perl script that uses the Bio::SearchIO::hmmer module. +# Dollar signs and at-signs are escaped for use in Python strings. +PERL_PARSER_SCRIPT = r""" +#!/usr/bin/env perl +use strict; +use warnings; +use Getopt::Long; +use Bio::SearchIO; +use JSON; + +# --- Command line arguments --- +my $input_file = ''; +my $format = 'tsv'; # Default format +GetOptions( + 'input=s' => \$input_file, + 'format=s' => \$format, +) or die "Error in command line arguments\n"; + +die "Usage: $0 --input --format [tsv|json]\n" unless $input_file; +die "Input file not found: $input_file\n" unless -e $input_file; +die "Invalid format: $format. Choose 'tsv' or 'json'.\n" unless $format eq 'tsv' or $format eq 'json'; + +# --- Main parsing logic --- +# The format 'hmmer' in Bio::SearchIO handles both HMMER2 and HMMER3 +my $searchio = Bio::SearchIO->new(-file => $input_file, -format => 'hmmer'); + +my @results_data; + +while (my $result = $searchio->next_result) { + # Skip if no hits + next unless $result->num_hits > 0; + + while (my $hit = $result->next_hit) { + while (my $hsp = $hit->next_hsp) { + # Use placeholders for potentially missing values to avoid errors + my $query_name = $result->query_name // 'N/A'; + my $query_len = $result->query_length // 0; + my $hit_name = $hit->name // 'N/A'; + my $hit_acc = $hit->accession // 'N/A'; + my $hit_desc = $hit->description // 'N/A'; + my $hit_len = $hit->length // 0; + my $evalue = $hit->significance // 'N/A'; + my $bits = $hsp->bits // 0; + + push @results_data, { + query_name => $query_name, + query_length => $query_len, + hit_name => $hit_name, + hit_accession => $hit_acc, + hit_description => $hit_desc, + hit_length => $hit_len, + significance => $evalue, + bits => $bits, + hsp_start_query => $hsp->start('query'), + hsp_end_query => $hsp->end('query'), + hsp_start_hit => $hsp->start('hit'), + hsp_end_hit => $hsp->end('hit'), + hsp_strand_query => $hsp->strand('query'), + hsp_strand_hit => $hsp->strand('hit'), + }; + } + } +} + +# --- Output generation --- +if ($format eq 'json') { + print to_json(\@results_data, { pretty => 1 }); +} else { # tsv + # Print header + my @headers = qw(query_name query_length hit_name hit_accession hit_description hit_length significance bits hsp_start_query hsp_end_query hsp_start_hit hsp_end_hit hsp_strand_query hsp_strand_hit); + print join("\t", @headers), "\n"; + # Print data + for my $data (@results_data) { + # Ensure all values are defined to avoid warnings + my @values = map { defined $_ ? $_ : '' } @{$data}{@headers}; + print join("\t", @values), "\n"; + } +} +""" + +@mcp.tool() +def perl_bio_searchio_hmmer( + hmmer_file: Path, + output_file: Path, + output_format: Literal["tsv", "json"] = "tsv", +) -> Dict[str, Any]: + """ + Parses HMMER2/HMMER3 output files using the Bio::SearchIO::hmmer Perl module. + + This tool acts as a command-line wrapper for the `perl-bio-searchio-hmmer` + library. It takes a standard HMMER output file (from hmmscan, hmmsearch, etc.) + and converts it into a structured format, either Tab-Separated Values (TSV) + or JSON, saving the result to a specified output file. + + Args: + hmmer_file: Path to the input HMMER output file. + output_file: Path to save the parsed results. + output_format: The desired format for the output file ('tsv' or 'json'). + Defaults to 'tsv'. + + Returns: + A dictionary containing the execution command, stdout, stderr, and a + list with the path to the generated output file. + """ + # Input validation + if not hmmer_file.exists(): + raise FileNotFoundError(f"Input file not found: {hmmer_file}") + if not hmmer_file.is_file(): + raise ValueError(f"Input path is not a file: {hmmer_file}") + if output_file.is_dir(): + raise ValueError(f"Output path cannot be a directory: {output_file}") + + # Create a temporary file for the Perl script + temp_script_path = None + try: + with tempfile.NamedTemporaryFile( + mode='w', delete=False, suffix=".pl", encoding='utf-8' + ) as temp_script: + temp_script.write(PERL_PARSER_SCRIPT) + temp_script_path = Path(temp_script.name) + + cmd = [ + "perl", + str(temp_script_path), + "--input", + str(hmmer_file), + "--format", + output_format, + ] + command_executed = " ".join(cmd) + + # Execute the command and write output directly to the specified file + with open(output_file, "w", encoding='utf-8') as f_out: + result = subprocess.run( + cmd, + stdout=f_out, + stderr=subprocess.PIPE, + text=True, + check=True, + encoding='utf-8' + ) + + stderr_content = result.stderr + + return { + "command_executed": command_executed, + "stdout": f"Output successfully written to {output_file}", + "stderr": stderr_content, + "output_files": [str(output_file)], + } + + except FileNotFoundError: + # This happens if 'perl' is not in the system's PATH + raise RuntimeError("Perl executable not found. Please ensure Perl and the required modules (Bio::SearchIO) are installed and in your PATH.") + except subprocess.CalledProcessError as e: + # The Perl script writes its own errors to stderr, which is captured here. + return { + "command_executed": command_executed, + "stdout": e.stdout or "", + "stderr": e.stderr or "Perl script failed with an unknown error.", + "output_files": [], + "error": f"Process failed with exit code {e.returncode}" + } + finally: + # Always clean up the temporary script + if temp_script_path and temp_script_path.exists(): + temp_script_path.unlink() diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/perl-bio-searchio-hmmer_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/perl-bio-searchio-hmmer_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..490a6b1d93cf3a94151bbcf9a0e38c8b51bc6560 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/perl-bio-searchio-hmmer_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/perl-bio-searchio-hmmer_server.py') +SERVER_NAME = 'biosci_perl_bio_searchio_hmmer' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..022502e983ce32be8d3fdccba9d07621a6d8042a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-bio-searchio-hmmer: + build: . + image: mcp-perl-bio-searchio-hmmer:latest + container_name: mcp-perl-bio-searchio-hmmer + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-bio-searchio-hmmer + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8e1545c2a159727a8767c8db73031f6d3857cad6 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-bio-searchio-hmmer + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-searchio-hmmer/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9fec5bbc371d1a90e56f77f2f0840b66c90ca4fe --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-bio-tools-run-alignment-clustalw via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-bio-tools-run-alignment-clustalw -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-bio-tools-run-alignment-clustalw_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-bio-tools-run-alignment-clustalw_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-bio-tools-run-alignment-clustalw_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/perl-bio-tools-run-alignment-clustalw_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/perl-bio-tools-run-alignment-clustalw_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d7226b1639e6505640e319bc855e6e0b37d5423f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/perl-bio-tools-run-alignment-clustalw_server.py @@ -0,0 +1,238 @@ +import subprocess +import tempfile +import shutil +from pathlib import Path +from typing import Optional, Literal + +# In a real MCP environment, the mcp package would be available. +# This is a placeholder for development and testing. +class _MCP: + def tool(self, *args, **kwargs): + def decorator(f): + return f + return decorator +mcp = _MCP() + + +@mcp.tool() +def run_clustalw( + infile: Path, + seq_type: Literal["PROTEIN", "DNA"], + outfile: Optional[Path] = None, + profile1: Optional[Path] = None, + profile2: Optional[Path] = None, + quicktree: bool = False, + negative: bool = False, + output_format: Optional[Literal["GCG", "GDE", "PHYLIP", "PIR", "NEXUS"]] = None, + output_tree: Optional[Literal["nj", "phylip", "dist", "nexus"]] = None, + outorder: Optional[Literal["INPUT", "ALIGNED"]] = "ALIGNED", + case: Optional[Literal["LOWER", "UPPER"]] = "UPPER", + seqnos: Optional[Literal["ON", "OFF"]] = "OFF", + bootstrap: Optional[int] = None, + # Pairwise Alignment Parameters + pwmatrix: Optional[Literal["BLOSUM", "PAM", "GONNET", "ID"]] = None, + pwdnamatrix: Optional[Literal["IUB", "CLUSTALW"]] = None, + pwgapopen: Optional[float] = None, + pwgapext: Optional[float] = None, + # Multiple Alignment Parameters + matrix: Optional[Literal["BLOSUM", "PAM", "GONNET", "ID"]] = None, + dnamatrix: Optional[Literal["IUB", "CLUSTALW"]] = None, + gapopen: Optional[float] = None, + gapext: Optional[float] = None, + maxdiv: Optional[int] = None, + gapdist: Optional[int] = None, + nopgap: bool = False, + nohgap: bool = False, + hgapresidues: Optional[str] = None, + endgaps: bool = False, + # Fast Pairwise Options + ktuple: Optional[int] = None, + window: Optional[int] = None, + score: Optional[Literal["PERCENT", "ABSOLUTE"]] = None, + topdiags: Optional[int] = None, + pairgap: Optional[int] = None, +) -> dict: + """ + Performs multiple sequence alignment using ClustalW. + + This tool is a wrapper for the ClustalW program, providing its core + functionality for aligning DNA or protein sequences. It supports standard + sequence alignment, profile alignment, and various parameter adjustments. + + Args: + infile: Input file containing sequences to be aligned (e.g., FASTA format). + seq_type: Type of sequences, either 'PROTEIN' or 'DNA'. + outfile: Optional path for the main output alignment file. If not provided, + it will be generated based on the input file name. + profile1: First profile for profile alignment. + profile2: Second profile for profile alignment. + quicktree: Use FAST algorithm for the guide tree. + negative: Protein weight matrix has negative values. + output_format: Format for the output alignment file. + output_tree: Format for the output guide tree file. + outorder: Order of sequences in the output alignment. + case: Set sequence case to LOWER or UPPER for output. + seqnos: Turn sequence numbers ON or OFF for output. + bootstrap: Number of bootstrap trials for the guide tree. + pwmatrix: Protein weight matrix for pairwise alignments. + pwdnamatrix: DNA weight matrix for pairwise alignments. + pwgapopen: Gap opening penalty for pairwise alignments. + pwgapext: Gap extension penalty for pairwise alignments. + matrix: Protein weight matrix for multiple alignments. + dnamatrix: DNA weight matrix for multiple alignments. + gapopen: Gap opening penalty for multiple alignments. + gapext: Gap extension penalty for multiple alignments. + maxdiv: Percent divergence for delaying alignment. + gapdist: Gap separation penalty range. + nopgap: Disable residue-specific gaps. + nohgap: Disable hydrophilic gaps. + hgapresidues: Custom list of hydrophilic residues (e.g., "GPSNDQERK"). + endgaps: Do not apply end gap separation penalty. + ktuple: Word size for FASTA-type alignments. + window: Window size for FASTA-type alignments. + score: Score type (PERCENT or ABSOLUTE) for FASTA-type alignments. + topdiags: Number of top diagonals for FASTA-type alignments. + pairgap: Gap penalty for FASTA-type alignments. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a + list of output file paths. + """ + # --- Input Validation --- + if not infile.is_file(): + raise FileNotFoundError(f"Input file not found: {infile}") + if (profile1 and not profile2) or (profile2 and not profile1): + raise ValueError("Both profile1 and profile2 must be provided for profile alignment.") + if profile1 and not profile1.is_file(): + raise FileNotFoundError(f"Profile 1 file not found: {profile1}") + if profile2 and not profile2.is_file(): + raise FileNotFoundError(f"Profile 2 file not found: {profile2}") + + if seq_type == "DNA": + if matrix or pwmatrix: + raise ValueError("Protein matrices (matrix, pwmatrix) cannot be used with seq_type='DNA'.") + elif seq_type == "PROTEIN": + if dnamatrix or pwdnamatrix: + raise ValueError("DNA matrices (dnamatrix, pwdnamatrix) cannot be used with seq_type='PROTEIN'.") + else: + raise ValueError(f"Invalid seq_type: {seq_type}. Must be 'PROTEIN' or 'DNA'.") + + if bootstrap is not None and bootstrap <= 0: + raise ValueError("Bootstrap value must be a positive integer.") + + cmd = ["clustalw"] + + with tempfile.TemporaryDirectory() as tempdir: + workdir = Path(tempdir) + + # Copy inputs to working directory to manage outputs cleanly + input_base_name = infile.stem + local_infile = workdir / infile.name + shutil.copy(infile, local_infile) + cmd.append(f"-INFILE={local_infile.name}") + + if profile1 and profile2: + local_profile1 = workdir / profile1.name + local_profile2 = workdir / profile2.name + shutil.copy(profile1, local_profile1) + shutil.copy(profile2, local_profile2) + cmd.extend([f"-PROFILE1={local_profile1.name}", f"-PROFILE2={local_profile2.name}"]) + + # --- Command Construction --- + cmd.append(f"-TYPE={seq_type}") + + if outfile: + # Use a predictable name inside tempdir, then move to final destination + output_aln_name = "alignment.out" + cmd.append(f"-OUTFILE={output_aln_name}") + else: + output_aln_name = f"{input_base_name}.aln" + + if quicktree: cmd.append("-QUICKTREE") + if negative: cmd.append("-NEGATIVE") + if output_format: cmd.append(f"-OUTPUT={output_format}") + if output_tree: cmd.append(f"-OUTPUTTREE={output_tree}") + if outorder: cmd.append(f"-OUTORDER={outorder}") + if case: cmd.append(f"-CASE={case}") + if seqnos: cmd.append(f"-SEQNOS={seqnos}") + if bootstrap: cmd.append(f"-BOOTSTRAP={bootstrap}") + + # Pairwise params + if pwmatrix: cmd.append(f"-PWMATRIX={pwmatrix}") + if pwdnamatrix: cmd.append(f"-PWDNAMATRIX={pwdnamatrix}") + if pwgapopen is not None: cmd.append(f"-PWGAPOPEN={pwgapopen}") + if pwgapext is not None: cmd.append(f"-PWGAPEXT={pwgapext}") + + # Multiple alignment params + if matrix: cmd.append(f"-MATRIX={matrix}") + if dnamatrix: cmd.append(f"-DNAMATRIX={dnamatrix}") + if gapopen is not None: cmd.append(f"-GAPOPEN={gapopen}") + if gapext is not None: cmd.append(f"-GAPEXT={gapext}") + if maxdiv is not None: cmd.append(f"-MAXDIV={maxdiv}") + if gapdist is not None: cmd.append(f"-GAPDIST={gapdist}") + if nopgap: cmd.append("-NOPGAP") + if nohgap: cmd.append("-NOHGAP") + if hgapresidues: cmd.append(f"-HGAPRESIDUES={hgapresidues}") + if endgaps: cmd.append("-ENDGAPS") + + # FastA params + if ktuple is not None: cmd.append(f"-KTUPLE={ktuple}") + if window is not None: cmd.append(f"-WINDOW={window}") + if score: cmd.append(f"-SCORE={score}") + if topdiags is not None: cmd.append(f"-TOPDIAGS={topdiags}") + if pairgap is not None: cmd.append(f"-PAIRGAP={pairgap}") + + # --- Subprocess Execution --- + try: + process = subprocess.run( + cmd, + cwd=workdir, + capture_output=True, + text=True, + check=True + ) + except FileNotFoundError: + raise RuntimeError("clustalw executable not found. Please ensure it is in your PATH.") + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(map(str, e.cmd)), + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "output_files": [] + } + + # --- Output File Handling --- + output_files = {} + output_dir = outfile.parent if outfile else Path.cwd() + + # 1. Alignment file (.aln) + temp_aln_path = workdir / output_aln_name + if temp_aln_path.exists(): + final_aln_path = outfile if outfile else output_dir / output_aln_name + shutil.move(temp_aln_path, final_aln_path) + output_files["alignment_file"] = str(final_aln_path) + + # 2. Guide tree file (.dnd) + dnd_filename = f"{input_base_name}.dnd" + temp_dnd_path = workdir / dnd_filename + if temp_dnd_path.exists(): + final_dnd_path = output_dir / dnd_filename + shutil.move(temp_dnd_path, final_dnd_path) + output_files["guide_tree_file"] = str(final_dnd_path) + + # 3. Phylip tree file (.ph) if bootstrap is used + ph_filename = f"{input_base_name}.ph" + temp_ph_path = workdir / ph_filename + if temp_ph_path.exists(): + final_ph_path = output_dir / ph_filename + shutil.move(temp_ph_path, final_ph_path) + output_files["phylip_tree_file"] = str(final_ph_path) + + return { + "command_executed": " ".join(map(str, cmd)), + "stdout": process.stdout, + "stderr": process.stderr, + "return_code": process.returncode, + "output_files": output_files + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/perl-bio-tools-run-alignment-clustalw_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/perl-bio-tools-run-alignment-clustalw_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c6fda60729483566fb772f6b4ff4a3f40edd65b1 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/perl-bio-tools-run-alignment-clustalw_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/perl-bio-tools-run-alignment-clustalw_server.py') +SERVER_NAME = 'biosci_perl_bio_tools_run_alignment_clustalw' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..7426bc559322011aca4859b662faa50fd35e7f63 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-bio-tools-run-alignment-clustalw: + build: . + image: mcp-perl-bio-tools-run-alignment-clustalw:latest + container_name: mcp-perl-bio-tools-run-alignment-clustalw + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-bio-tools-run-alignment-clustalw + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e8584abe5ba090ecb11c10cb19dfb23bd93595cc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-bio-tools-run-alignment-clustalw + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-bio-tools-run-alignment-clustalw/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..dc43e5287f8699dca5cecd622f5fb69a1c7daec5 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-cpan-meta-requirements via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-cpan-meta-requirements -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-cpan-meta-requirements_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-cpan-meta-requirements_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-cpan-meta-requirements_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/perl-cpan-meta-requirements_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/perl-cpan-meta-requirements_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ad48fdefbdaa24bebda8eb8bbde882d5c9696000 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/perl-cpan-meta-requirements_server.py @@ -0,0 +1,216 @@ +import subprocess +import json +from typing import Optional, Dict, List +from pathlib import Path + +@mcp.tool() +def check_version_satisfaction( + module: str, + requirement_string: str, + version_to_check: str, +) -> Dict: + """ + Checks if a specific version of a Perl module satisfies a given requirement string. + + Args: + module: The name of the Perl module (e.g., 'CPAN::Meta'). + requirement_string: The version requirement (e.g., '>= 1.20, != 1.25', '0'). + version_to_check: The version string to validate (e.g., '1.21'). + """ + # Input validation + if not module or not requirement_string or not version_to_check: + return {"error": "Module, requirement_string, and version_to_check are all required."} + + # Perl script to use the library + perl_code = f""" + use CPAN::Meta::Requirements; + my $req = CPAN::Meta::Requirements->new; + eval {{ + $req->add_string_requirement('{module}', '{requirement_string}'); + my $result = $req->accepts_module('{module}', '{version_to_check}'); + print $result ? "true" : "false"; + }}; + if ($@) {{ + die "Error processing requirements: $@"; + }} + """ + + try: + process = subprocess.run( + ["perl", "-e", perl_code], + capture_output=True, + text=True, + check=True + ) + + is_satisfied = process.stdout.strip() == "true" + + return { + "command_executed": f"perl -MCPAN::Meta::Requirements ...", + "module": module, + "requirement": requirement_string, + "version_checked": version_to_check, + "is_satisfied": is_satisfied, + "stdout": process.stdout, + "stderr": process.stderr + } + except subprocess.CalledProcessError as e: + return { + "error": "Perl execution failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": e.cmd + } + +@mcp.tool() +def merge_requirement_sets( + set_1: Dict[str, str], + set_2: Dict[str, str], +) -> Dict: + """ + Merges two sets of CPAN module requirements. If both sets have requirements for the + same module, they are combined (intersected) to satisfy both. + + Args: + set_1: A dictionary where keys are module names and values are requirement strings. + set_2: A second dictionary of module requirements to merge with the first. + """ + # Convert dicts to JSON for safe passing to Perl + json_1 = json.dumps(set_1) + json_2 = json.dumps(set_2) + + perl_code = """ + use CPAN::Meta::Requirements; + use JSON::PP; + + my $json = JSON::PP->new; + my $data1 = $json->decode($ARGV[0]); + my $data2 = $json->decode($ARGV[1]); + + my $req1 = CPAN::Meta::Requirements->from_string_hash($data1); + my $req2 = CPAN::Meta::Requirements->from_string_hash($data2); + + $req1->add_requirements($req2); + + print $json->encode($req1->as_string_hash); + """ + + try: + process = subprocess.run( + ["perl", "-MJSON::PP", "-MCPAN::Meta::Requirements", "-e", perl_code, json_1, json_2], + capture_output=True, + text=True, + check=True + ) + + merged_requirements = json.loads(process.stdout) + + return { + "command_executed": "perl -MCPAN::Meta::Requirements -MJSON::PP ...", + "merged_requirements": merged_requirements, + "stdout": process.stdout, + "stderr": process.stderr + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to merge requirements", + "stderr": e.stderr + } + except json.JSONDecodeError: + return { + "error": "Failed to parse Perl output as JSON", + "stdout": process.stdout + } + +@mcp.tool() +def validate_requirement_string( + requirement_string: str, +) -> Dict: + """ + Validates if a version requirement string is syntactically correct according to CPAN standards. + + Args: + requirement_string: The version requirement string to validate (e.g., '>= 1.0, < 2.0'). + """ + perl_code = f""" + use CPAN::Meta::Requirements; + eval {{ + my $req = CPAN::Meta::Requirements->new; + $req->add_string_requirement('Dummy::Module', '{requirement_string}'); + print "valid"; + }}; + if ($@) {{ + print "invalid: $@"; + }} + """ + + try: + process = subprocess.run( + ["perl", "-e", perl_code], + capture_output=True, + text=True, + check=True + ) + + output = process.stdout.strip() + is_valid = output == "valid" + + return { + "requirement": requirement_string, + "is_valid": is_valid, + "error_message": None if is_valid else output.replace("invalid: ", ""), + "stdout": process.stdout, + "stderr": process.stderr + } + except subprocess.CalledProcessError as e: + return { + "error": "Execution failed", + "stderr": e.stderr + } + +@mcp.tool() +def simplify_requirements( + requirements: Dict[str, str], +) -> Dict: + """ + Simplifies a set of requirements to their most concise form. + + Args: + requirements: A dictionary of module names and their requirement strings. + """ + req_json = json.dumps(requirements) + + perl_code = """ + use CPAN::Meta::Requirements; + use JSON::PP; + + my $json = JSON::PP->new; + my $data = $json->decode($ARGV[0]); + + my $req = CPAN::Meta::Requirements->from_string_hash($data); + # finalize() returns a new object with requirements simplified + my $final = $req->finalize; + + print $json->encode($final->as_string_hash); + """ + + try: + process = subprocess.run( + ["perl", "-MJSON::PP", "-MCPAN::Meta::Requirements", "-e", perl_code, req_json], + capture_output=True, + text=True, + check=True + ) + + simplified = json.loads(process.stdout) + + return { + "original": requirements, + "simplified": simplified, + "stdout": process.stdout + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to simplify requirements", + "stderr": e.stderr + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/perl-cpan-meta-requirements_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/perl-cpan-meta-requirements_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..466685d1c087e1b8ee2c00db3def18a4d303bc94 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/perl-cpan-meta-requirements_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/perl-cpan-meta-requirements_server.py') +SERVER_NAME = 'biosci_perl_cpan_meta_requirements' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..1a4efaa57d1562b07fa812dad3c4051897c982b7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-cpan-meta-requirements: + build: . + image: mcp-perl-cpan-meta-requirements:latest + container_name: mcp-perl-cpan-meta-requirements + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-cpan-meta-requirements + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d902f67eec6fd1e13587b4a161ebf7748a93518a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-cpan-meta-requirements + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-cpan-meta-requirements/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..99fdd84eab8d91799770d5cba86db2f2a1560f94 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-gd via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-gd -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-gd_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-gd_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-gd_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/__pycache__/perl-gd_server.cpython-310.pyc b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/__pycache__/perl-gd_server.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb379ca8677dd7e4c523c2db9ca9c4f71e00997b Binary files /dev/null and b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/__pycache__/perl-gd_server.cpython-310.pyc differ diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/perl-gd_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/perl-gd_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6160473ad6ead4df7c15bc8cc45c59bb8e79dd50 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/perl-gd_server.py @@ -0,0 +1,366 @@ +import subprocess +from pathlib import Path +from typing import Optional, List +import tempfile + +@mcp.tool() +def perl_execute( + program_file: Optional[str] = None, + one_liner: Optional[str] = None, + one_liner_with_features: Optional[str] = None, + modules: Optional[List[str]] = None, + include_dirs: Optional[List[str]] = None, + record_separator: Optional[str] = None, + unicode_features: Optional[str] = None, + check_syntax: bool = False, + debugger: bool = False, + debug_flags: Optional[str] = None, + no_site_customize: bool = False, + split_pattern: Optional[str] = None, + inplace_edit_ext: Optional[str] = None, + line_ending_proc: bool = False, + loop_while: bool = False, + loop_while_print: bool = False, + switch_parsing: bool = False, + search_path: bool = False, + taint_warnings: bool = False, + taint_checks: bool = False, + unsafe_ops: bool = False, + warnings: bool = False, + all_warnings: bool = False, + disable_warnings: bool = False, + arguments: Optional[List[str]] = None, +) -> dict: + """ + Execute the Perl interpreter with various switches. + This tool provides full access to the Perl environment where the GD library is installed. + + Args: + program_file: Path to a Perl script file to execute. + one_liner: One line of program (equivalent to -e). + one_liner_with_features: Like -e, but enables all optional features (equivalent to -E). + modules: Execute "use module..." before executing program (equivalent to -M). + include_dirs: Specify @INC/#include directory (equivalent to -I). + record_separator: Specify record separator (equivalent to -0). + unicode_features: Enables the listed Unicode features (equivalent to -C). + check_syntax: Check syntax only (equivalent to -c). + debugger: Run program under debugger (equivalent to -d). + debug_flags: Set debugging flags (equivalent to -D). + no_site_customize: Don't do $sitelib/sitecustomize.pl at startup (equivalent to -f). + split_pattern: split() pattern for -a switch (equivalent to -F). + inplace_edit_ext: Edit <> files in place, optionally making backup (equivalent to -i). + line_ending_proc: Enable line ending processing (equivalent to -l). + loop_while: Assume "while (<>) { ... }" loop around program (equivalent to -n). + loop_while_print: Assume loop like -n but print line also (equivalent to -p). + switch_parsing: Enable rudimentary parsing for switches after programfile (equivalent to -s). + search_path: Look for programfile using PATH environment variable (equivalent to -S). + taint_warnings: Enable tainting warnings (equivalent to -t). + taint_checks: Enable tainting checks (equivalent to -T). + unsafe_ops: Allow unsafe operations (equivalent to -U). + warnings: Enable many useful warnings (equivalent to -w). + all_warnings: Enable all warnings (equivalent to -W). + disable_warnings: Disable all warnings (equivalent to -X). + arguments: Arguments to pass to the Perl program. + """ + cmd = ["perl"] + + if record_separator is not None: + cmd.append(f"-0{record_separator}") + if unicode_features: + cmd.append(f"-C{unicode_features}") + if check_syntax: + cmd.append("-c") + if debugger: + cmd.append("-d") + if debug_flags: + cmd.append(f"-D{debug_flags}") + if no_site_customize: + cmd.append("-f") + if split_pattern: + cmd.append(f"-F{split_pattern}") + if inplace_edit_ext is not None: + cmd.append(f"-i{inplace_edit_ext}") + if line_ending_proc: + cmd.append("-l") + if loop_while: + cmd.append("-n") + if loop_while_print: + cmd.append("-p") + if switch_parsing: + cmd.append("-s") + if search_path: + cmd.append("-S") + if taint_warnings: + cmd.append("-t") + if taint_checks: + cmd.append("-T") + if unsafe_ops: + cmd.append("-U") + if warnings: + cmd.append("-w") + if all_warnings: + cmd.append("-W") + if disable_warnings: + cmd.append("-X") + + if include_dirs: + for d in include_dirs: + cmd.append(f"-I{d}") + if modules: + for m in modules: + cmd.append(f"-M{m}") + + if one_liner: + cmd.extend(["-e", one_liner]) + elif one_liner_with_features: + cmd.extend(["-E", one_liner_with_features]) + + if program_file: + p = Path(program_file) + if not p.exists(): + return {"error": f"Program file not found: {program_file}"} + cmd.append(str(p)) + + if arguments: + cmd.extend(arguments) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def perl_version() -> dict: + """Print version, patchlevel and license of the Perl interpreter.""" + cmd = ["perl", "-v"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return {"error": str(e)} + +@mcp.tool() +def perl_config(variable: Optional[str] = None) -> dict: + """ + Print configuration summary or a single Config.pm variable. + + Args: + variable: Optional single Config.pm variable to print. + """ + cmd = ["perl", "-V"] + if variable: + cmd.append(f":{variable}") + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return {"error": str(e)} + +@mcp.tool() +def perl_gd_version_info() -> dict: + """Get the version of the GD Perl module and the underlying libgd library.""" + perl_code = "use GD; print \"GD version: $GD::VERSION\\n\";" + cmd = ["perl", "-e", perl_code] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return {"error": str(e)} + +@mcp.tool() +def perl_gd_get_info( + input_file: str, +) -> dict: + """ + Retrieve metadata for an image file using the GD library. + + Args: + input_file: Path to the input image file. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file not found: {input_file}"} + + perl_code = ( + "use GD; " + "my $img = GD::Image->new($ARGV[0]) or die 'Could not load image'; " + "my ($w, $h) = $img->getBounds(); " + "my $tc = $img->isTrueColor() ? 'true' : 'false'; " + "my $colors = $img->colorsTotal(); " + "print \"width: $w\\nheight: $h\\ntruecolor: $tc\\ncolors: $colors\\n\";" + ) + + cmd = ["perl", "-e", perl_code, str(input_path)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "info": result.stdout.strip() + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def perl_gd_convert_image( + input_file: str, + output_file: str, + output_format: str = "png", +) -> dict: + """ + Convert an image from one format to another using the GD library. + + Args: + input_file: Path to the source image file. + output_file: Path where the converted image will be saved. + output_format: Target format. Supported: 'png', 'jpeg', 'gif', 'bmp', 'wbmp', 'tiff', 'webp'. + """ + input_path = Path(input_file) + output_path = Path(output_file) + + if not input_path.exists(): + return {"error": f"Input file not found: {input_file}"} + + valid_formats = ["png", "jpeg", "gif", "bmp", "wbmp", "tiff", "webp"] + if output_format.lower() not in valid_formats: + return {"error": f"Unsupported format: {output_format}. Must be one of {valid_formats}"} + + perl_code = ( + "use GD; " + "my $img = GD::Image->new($ARGV[0]) or die 'Could not load image'; " + "my $fmt = $ARGV[2]; " + "open my $fh, '>', $ARGV[1] or die \"Could not open output: $!\"; " + "binmode $fh; " + "print $fh $img->$fmt(); " + "close $fh;" + ) + + cmd = ["perl", "-e", perl_code, str(input_path), str(output_path), output_format.lower()] + + try: + subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "output_files": [str(output_path)], + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def perl_gd_create_simple_image( + output_file: str, + width: int = 100, + height: int = 100, + background_color: str = "white", + draw_example: bool = False, +) -> dict: + """ + Create a new image using GD. + + Args: + output_file: Path to save the image (PNG). + width: Width in pixels. + height: Height in pixels. + background_color: Background color (white, black, red, blue, green). + draw_example: If True, draws a simple rectangle and arc as an example. + """ + output_path = Path(output_file) + + colors_code = """ + my $white = $im->colorAllocate(255,255,255); + my $black = $im->colorAllocate(0,0,0); + my $red = $im->colorAllocate(255,0,0); + my $blue = $im->colorAllocate(0,0,255); + my $green = $im->colorAllocate(0,255,0); + """ + + bg_map = { + "white": "$white", + "black": "$black", + "red": "$red", + "blue": "$blue", + "green": "$green" + } + bg_var = bg_map.get(background_color.lower(), "$white") + + example_code = "" + if draw_example: + example_code = """ + $im->rectangle(0,0,$width-1,$height-1,$black); + $im->arc($width/2,$height/2,$width*0.9,$height*0.7,0,360,$blue); + $im->fill($width/2,$height/2,$red); + """ + + perl_code = f""" +use GD; +my $width = {width}; +my $height = {height}; +my $im = GD::Image->new($width, $height); +{colors_code} +$im->fill(0, 0, {bg_var}); +{example_code} +open my $fh, '>', $ARGV[0] or die $!; +binmode $fh; +print $fh $im->png; +close $fh; +""" + + with tempfile.NamedTemporaryFile(mode='w', suffix='.pl', delete=False) as tmp: + tmp.write(perl_code) + tmp_path = tmp.name + + cmd = ["perl", tmp_path, str(output_path)] + + try: + subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "output_files": [str(output_path)], + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + finally: + Path(tmp_path).unlink(missing_ok=True) \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/perl-gd_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/perl-gd_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f78594133cb845431e8fd93c08f1e2048df8acc5 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/perl-gd_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/perl-gd_server.py') +SERVER_NAME = 'biosci_perl_gd' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..f80b3e91385affa789925dfb242285dc3653d484 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-gd: + build: . + image: mcp-perl-gd:latest + container_name: mcp-perl-gd + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-gd + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..57ac6180c496d5f7c94585aba712e3aaeacdede3 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-gd + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-gd/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-getopt-long/app/perl-getopt-long_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-getopt-long/app/perl-getopt-long_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d501591ffa97eb7972561909de3ebb161ca8dde5 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-getopt-long/app/perl-getopt-long_server.py @@ -0,0 +1,262 @@ +from typing import List, Optional, Dict, Any +import subprocess +import json +import tempfile +from pathlib import Path + +@mcp.tool() +def perl_getopt_long_parse( + specifications: List[str], + arguments: List[str], + configuration: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Simulates the Getopt::Long::GetOptions function to test how command-line arguments + would be parsed in a Perl-based bioinformatics tool. + + Args: + specifications: List of option specifications (e.g., ["length=i", "file=s", "verbose+"]). + arguments: The command-line arguments to parse (e.g., ["--length", "100", "-v"]). + configuration: Optional list of Getopt::Long configuration strings (e.g., ["bundling", "ignore_case"]). + """ + # Input validation + if not specifications: + return {"error": "Specifications list cannot be empty"} + + config_list = configuration if configuration is not None else [] + + # Prepare the Perl script to simulate parsing + perl_script = """ +use Getopt::Long; +use JSON::PP; + +my $json_input = do { local $/; }; +my $input = decode_json($json_input); +my $specs = $input->{specs}; +my $config = $input->{config}; +my $args = $input->{args}; + +if (@$config) { + Getopt::Long::Configure(@$config); +} + +my %opts; +@ARGV = @$args; +# GetOptions returns true on success +my $result = GetOptions(\\%opts, @$specs); + +print encode_json({ + success => $result ? 1 : 0, + options => \\%opts, + remaining_args => [ @ARGV ] +}); +""" + + input_data = { + "specs": specifications, + "config": config_list, + "args": arguments + } + + try: + process = subprocess.run( + ["perl", "-e", perl_script], + input=json.dumps(input_data).encode(), + capture_output=True, + check=True + ) + + result = json.loads(process.stdout) + return { + "command_executed": f"Getopt::Long::GetOptions with {len(specifications)} specs", + "success": bool(result["success"]), + "parsed_options": result["options"], + "remaining_arguments": result["remaining_args"], + "stderr": process.stderr.decode() + } + except subprocess.CalledProcessError as e: + return { + "error": "Perl execution failed", + "stdout": e.stdout.decode(), + "stderr": e.stderr.decode() + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def perl_run_script( + script_path: str, + args: Optional[List[str]] = None, + include_paths: Optional[List[str]] = None, + warnings: bool = True, +) -> Dict[str, Any]: + """ + Executes a Perl script (common in bioinformatics pipelines) with specified arguments. + + Args: + script_path: Path to the .pl script. + args: List of command-line arguments to pass to the script. + include_paths: List of directories to add to @INC (Perl's include path). + warnings: Whether to enable Perl warnings (-w). + """ + path = Path(script_path) + if not path.exists(): + return {"error": f"Script not found: {script_path}"} + + cmd = ["perl"] + if warnings: + cmd.append("-w") + + if include_paths: + for p in include_paths: + cmd.extend(["-I", p]) + + cmd.append(str(path)) + + if args: + cmd.extend(args) + + try: + process = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "exit_code": process.returncode + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": "Script execution failed", + "stdout": e.stdout, + "stderr": e.stderr, + "exit_code": e.returncode + } + +@mcp.tool() +def perl_check_syntax( + script_path: str, + include_paths: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Checks the syntax of a Perl script without executing it (perl -c). + + Args: + script_path: Path to the .pl script. + include_paths: List of directories to add to @INC. + """ + path = Path(script_path) + if not path.exists(): + return {"error": f"Script not found: {script_path}"} + + cmd = ["perl", "-c"] + if include_paths: + for p in include_paths: + cmd.extend(["-I", p]) + cmd.append(str(path)) + + try: + process = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "status": "Syntax OK", + "stderr": process.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "status": "Syntax Error", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def perl_module_info( + module_name: str, +) -> Dict[str, Any]: + """ + Checks if a Perl module is installed and retrieves its version. + + Args: + module_name: The name of the module (e.g., "Bio::SeqIO" or "Getopt::Long"). + """ + # Validate module name format + if not all(c.isalnum() or c == ':' for c in module_name): + return {"error": "Invalid module name format"} + + perl_cmd = f"use {module_name}; print ${module_name}::VERSION // 'installed';" + + try: + process = subprocess.run( + ["perl", "-e", perl_cmd], + capture_output=True, + text=True, + check=True + ) + return { + "module": module_name, + "installed": True, + "version": process.stdout.strip(), + "stderr": process.stderr + } + except subprocess.CalledProcessError: + return { + "module": module_name, + "installed": False, + "error": f"Module {module_name} is not available in the current environment." + } + +@mcp.tool() +def perl_get_environment() -> Dict[str, Any]: + """ + Returns information about the Perl interpreter and environment, including version and @INC. + """ + try: + # Get version info + v_proc = subprocess.run(["perl", "-v"], capture_output=True, text=True, check=True) + + # Get @INC (include paths) + inc_proc = subprocess.run(["perl", "-e", "print join('\\n', @INC)"], capture_output=True, text=True, check=True) + + return { + "perl_version_info": v_proc.stdout.splitlines()[1].strip(), + "include_paths": inc_proc.stdout.splitlines(), + "executable": subprocess.run(["which", "perl"], capture_output=True, text=True).stdout.strip() + } + except subprocess.CalledProcessError as e: + return {"error": "Failed to retrieve Perl environment info", "stderr": e.stderr} + +@mcp.tool() +def perl_execute_inline( + code: str, + args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Executes a Perl one-liner (perl -e). Useful for quick data transformations. + + Args: + code: The Perl code to execute. + args: Optional arguments to pass to the code (accessible via @ARGV). + """ + if not code: + return {"error": "No code provided"} + + cmd = ["perl", "-e", code] + if args: + cmd.extend(["--"]) + cmd.extend(args) + + try: + process = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": f"perl -e '{code}'", + "stdout": process.stdout, + "stderr": process.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": f"perl -e '{code}'", + "error": "Execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b6ffa93526995cc4eaf7c192e2113b7515ffeac4 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-graph via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-graph -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-graph_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-graph_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-graph_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/perl-graph_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/perl-graph_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8ebce595f1e90b6f262f40dc23c57a01d9a4aa32 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/perl-graph_server.py @@ -0,0 +1,201 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict +import shlex + +# MCP decorator placeholder +def tool(func): + """A placeholder for the @mcp.tool decorator.""" + return func + +class mcp: + tool = staticmethod(tool) + +@mcp.tool +def run_perl( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + e_program: Optional[List[str]] = None, + E_program: Optional[List[str]] = None, + include_dirs: Optional[List[Path]] = None, + use_module: Optional[List[str]] = None, + use_module_no_import: Optional[List[str]] = None, + check_syntax_only: bool = False, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + disable_all_warnings: bool = False, + enable_taint_checks: bool = False, + enable_taint_warnings: bool = False, + print_version: bool = False, + print_config: Optional[str] = None, + loop_around_program: bool = False, + loop_and_print: bool = False, + autosplit: bool = False, + split_pattern: Optional[str] = None, + line_ending_processing: Optional[str] = None, + record_separator: Optional[str] = None, + in_place_edit: Optional[str] = None, + enable_switch_parsing: bool = False, + search_path: bool = False, + run_under_debugger: Optional[str] = None, + debugging_flags: Optional[str] = None, + unicode_features: Optional[str] = None, + no_sitecustomize: bool = False, + dump_core: bool = False, + allow_unsafe: bool = False, + ignore_text_before_perl: bool = False, + ignore_text_dir: Optional[Path] = None, +) -> Dict[str, any]: + """ + Executes the Perl interpreter. This tool is a wrapper for `perl`, the execution + environment for the `perl-graph` library. + + Args: + programfile: The Perl script file to execute. + arguments: A list of arguments to be passed to the programfile. + e_program: One or more lines of a program to be executed. Omit programfile. + E_program: Like -e, but enables all optional features. Omit programfile. + include_dirs: Specifies @INC/#include directories. + use_module: Executes "use module..." before the program. + use_module_no_import: Executes "use module..." without default imports. + check_syntax_only: Checks syntax only (runs BEGIN and CHECK blocks). + enable_warnings: Enables many useful warnings (-w). + enable_all_warnings: Enables all warnings (-W). + disable_all_warnings: Disables all warnings (-X). + enable_taint_checks: Enables tainting checks (-T). + enable_taint_warnings: Enables tainting warnings (-t). + print_version: Prints the version, patchlevel, and license. + print_config: Prints configuration summary. Provide a variable name for a single value, or an empty string for the full summary. + loop_around_program: Assumes a "while (<>) { ... }" loop around the program. + loop_and_print: Like loop_around_program, but also prints the line. + autosplit: Enables autosplit mode with -n or -p (splits $_ into @F). + split_pattern: Specifies the split() pattern for autosplit mode. + line_ending_processing: Enables line ending processing. Provide an octal value for a custom terminator, or an empty string for default handling. + record_separator: Specifies the record separator. Provide an octal value, or an empty string for null character. + in_place_edit: Edits <> files in place. Provide a file extension for backups, or an empty string for no backup. + enable_switch_parsing: Enables rudimentary parsing for switches after programfile. + search_path: Looks for programfile using the PATH environment variable. + run_under_debugger: Runs the program under the debugger. Provide a debugger name, or an empty string for the default. + debugging_flags: Sets debugging flags (argument is a bit mask or alphabets). + unicode_features: Enables the listed Unicode features. + no_sitecustomize: Prevents running $sitelib/sitecustomize.pl at startup. + dump_core: Dumps core after parsing the program. + allow_unsafe: Allows unsafe operations. + ignore_text_before_perl: Ignores text before the #!perl line. + ignore_text_dir: Optionally changes to this directory when using ignore_text_before_perl. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + execution_targets = [] + if programfile: execution_targets.append("programfile") + if e_program: execution_targets.append("e_program") + if E_program: execution_targets.append("E_program") + + is_info_command = print_version or (print_config is not None) + + if not is_info_command and not execution_targets: + raise ValueError("A 'programfile', 'e_program', or 'E_program' must be provided.") + + if len(execution_targets) > 1: + raise ValueError(f"The following parameters are mutually exclusive: {', '.join(execution_targets)}.") + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"Program file not found: {programfile}") + + if include_dirs: + for d in include_dirs: + if not d.is_dir(): + raise NotADirectoryError(f"Include directory not found: {d}") + + if ignore_text_dir and not ignore_text_before_perl: + raise ValueError("'ignore_text_dir' can only be used when 'ignore_text_before_perl' is True.") + + if ignore_text_dir and not ignore_text_dir.is_dir(): + raise NotADirectoryError(f"Directory for -x option not found: {ignore_text_dir}") + + # --- Command Construction --- + cmd = ["perl"] + + # Boolean flags + if check_syntax_only: cmd.append("-c") + if enable_warnings: cmd.append("-w") + if enable_all_warnings: cmd.append("-W") + if disable_all_warnings: cmd.append("-X") + if enable_taint_checks: cmd.append("-T") + if enable_taint_warnings: cmd.append("-t") + if print_version: cmd.append("-v") + if loop_around_program: cmd.append("-n") + if loop_and_print: cmd.append("-p") + if autosplit: cmd.append("-a") + if enable_switch_parsing: cmd.append("-s") + if search_path: cmd.append("-S") + if no_sitecustomize: cmd.append("-f") + if dump_core: cmd.append("-u") + if allow_unsafe: cmd.append("-U") + if ignore_text_before_perl and not ignore_text_dir: cmd.append("-x") + + # Flags with optional or required values + if record_separator is not None: cmd.append(f"-0{record_separator}") + if unicode_features is not None: cmd.append(f"-C{unicode_features}") + if split_pattern is not None: cmd.append(f"-F{split_pattern}") + if in_place_edit is not None: cmd.append(f"-i{in_place_edit}") + if line_ending_processing is not None: cmd.append(f"-l{line_ending_processing}") + if run_under_debugger is not None: cmd.append(f"-d{run_under_debugger}") + if debugging_flags is not None: cmd.append(f"-D{debugging_flags}") + if ignore_text_dir: cmd.extend(["-x", str(ignore_text_dir)]) + if print_config is not None: + cmd.append(f"-V:{print_config}" if print_config else "-V") + + # List-based flags + if include_dirs: + for d in include_dirs: cmd.extend(["-I", str(d)]) + if use_module: + for m in use_module: cmd.extend(["-M", m]) + if use_module_no_import: + for m in use_module_no_import: cmd.extend(["-m", m]) + if e_program: + for p in e_program: cmd.extend(["-e", p]) + if E_program: + for p in E_program: cmd.extend(["-E", p]) + + # Positional arguments + if programfile: + cmd.append(str(programfile)) + if arguments: + cmd.append("--") + cmd.extend(arguments) + + # --- Subprocess Execution --- + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + stdout = result.stdout + stderr = result.stderr + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Perl execution failed with return code {e.returncode}", + "output_files": [] + } + + # --- Output Handling --- + output_files = [] + if in_place_edit is not None and arguments: + # Files listed in arguments are modified in place + output_files.extend([str(Path(arg)) for arg in arguments if Path(arg).is_file()]) + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/perl-graph_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/perl-graph_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..318bbbc1c343cc3578726f0cc82a67bfcec28abd --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/perl-graph_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/perl-graph_server.py') +SERVER_NAME = 'biosci_perl_graph' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..55a03f150f284fc3e24f2535f578955be58ae181 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-graph: + build: . + image: mcp-perl-graph:latest + container_name: mcp-perl-graph + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-graph + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ef2f4a2d84a565096d570de12f60c08269a57696 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-graph + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-graph/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..75edbc5feff5bd62b8f13bd2a0314db8747d7410 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-html-formatter via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-html-formatter -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-html-formatter_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-html-formatter_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-html-formatter_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/perl-html-formatter_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/perl-html-formatter_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b27ed003e6c581dd18b68818a95a13e70c355da8 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/perl-html-formatter_server.py @@ -0,0 +1,215 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# In a real MCP environment, this would be imported. +class mcp: + @staticmethod + def tool(func): + return func + +@mcp.tool +def perl( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + execute_program: Optional[List[str]] = None, + execute_program_features: Optional[List[str]] = None, + check_syntax_only: bool = False, + print_version: bool = False, + print_config: Optional[str] = None, + record_separator: Optional[str] = None, + autosplit: bool = False, + split_pattern: Optional[str] = None, + loop_around_program: bool = False, + loop_and_print: bool = False, + line_ending_processing: Optional[str] = None, + in_place_edit: Optional[str] = None, + include_directories: Optional[List[Path]] = None, + use_module: Optional[List[str]] = None, + no_module: Optional[List[str]] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + disable_all_warnings: bool = False, + tainting_warnings: bool = False, + tainting_checks: bool = False, + unicode_features: Optional[str] = None, + run_under_debugger: Optional[str] = None, + debugging_flags: Optional[str] = None, + no_sitecustomize: bool = False, + parse_switches: bool = False, + search_path: bool = False, + dump_core: bool = False, + unsafe_operations: bool = False, + ignore_text_before_shebang: Optional[Path] = None, +) -> dict: + """ + Executes the Perl interpreter. This tool can run Perl scripts that may utilize + modules like HTML::Formatter, which is provided by the perl-html-formatter package. + + Args: + programfile: The Perl script file to execute. + arguments: A list of arguments to pass to the programfile. + execute_program: One or more lines of a program to execute directly from the command line. + execute_program_features: Like -e, but enables all optional features. + check_syntax_only: Checks syntax only, running BEGIN and CHECK blocks. Corresponds to -c. + print_version: Prints the version, patchlevel, and license of Perl. Corresponds to -v. + print_config: Prints configuration summary or a single Config.pm variable. Corresponds to -V[:variable]. + record_separator: Specifies the record separator character (octal or literal). Corresponds to -0. + autosplit: Enables autosplit mode with -n or -p, splitting $_ into @F. Corresponds to -a. + split_pattern: Specifies the split() pattern for the -a switch. Corresponds to -F. + loop_around_program: Assumes a "while (<>) { ... }" loop around the program. Corresponds to -n. + loop_and_print: Assumes a loop like -n but also prints the line. Corresponds to -p. + line_ending_processing: Enables line ending processing and specifies the terminator. Corresponds to -l. + in_place_edit: Edits files in place, creating a backup if an extension is supplied. Corresponds to -i. + include_directories: Specifies directories to add to @INC. Corresponds to -I. + use_module: Executes "use module..." before the program. Corresponds to -M. + no_module: Executes "no module..." before the program. Corresponds to -m. + enable_warnings: Enables many useful warnings. Corresponds to -w. + enable_all_warnings: Enables all warnings. Corresponds to -W. + disable_all_warnings: Disables all warnings. Corresponds to -X. + tainting_warnings: Enables tainting warnings. Corresponds to -t. + tainting_checks: Enables tainting checks. Corresponds to -T. + unicode_features: Enables listed Unicode features. Corresponds to -C. + run_under_debugger: Runs the program under a specified debugger. Corresponds to -d. + debugging_flags: Sets debugging flags. Corresponds to -D. + no_sitecustomize: Prevents running $sitelib/sitecustomize.pl at startup. Corresponds to -f. + parse_switches: Enables rudimentary parsing for switches after the programfile. Corresponds to -s. + search_path: Looks for the programfile using the PATH environment variable. Corresponds to -S. + dump_core: Dumps core after parsing the program. Corresponds to -u. + unsafe_operations: Allows unsafe operations. Corresponds to -U. + ignore_text_before_shebang: Ignores text before #!perl line, optionally changing to a directory. Corresponds to -x. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + # --- Input Validation --- + if not any([programfile, execute_program, execute_program_features, print_version, print_config is not None]): + raise ValueError("You must specify a program to run via 'programfile', 'execute_program', or 'execute_program_features', or an action like 'print_version' or 'print_config'.") + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"The specified program file does not exist: {programfile}") + + if include_directories: + for directory in include_directories: + if not directory.is_dir(): + raise NotADirectoryError(f"The include directory does not exist: {directory}") + + # --- Command Construction --- + cmd = ["perl"] + + if record_separator is not None: + cmd.append(f"-0{record_separator}") + if autosplit: + cmd.append("-a") + if unicode_features is not None: + cmd.append(f"-C{unicode_features}") + if check_syntax_only: + cmd.append("-c") + if run_under_debugger is not None: + cmd.append(f"-d:{run_under_debugger}" if run_under_debugger else "-d") + if debugging_flags is not None: + cmd.append(f"-D{debugging_flags}") + if execute_program: + for prog_line in execute_program: + cmd.extend(["-e", prog_line]) + if execute_program_features: + for prog_line in execute_program_features: + cmd.extend(["-E", prog_line]) + if no_sitecustomize: + cmd.append("-f") + if split_pattern is not None: + cmd.append(f"-F{split_pattern}") + if in_place_edit is not None: + cmd.append(f"-i{in_place_edit}") + if include_directories: + for directory in include_directories: + cmd.extend(["-I", str(directory)]) + if line_ending_processing is not None: + cmd.append(f"-l{line_ending_processing}") + if use_module: + for mod in use_module: + cmd.extend(["-M", mod]) + if no_module: + for mod in no_module: + cmd.extend(["-m", mod]) + if loop_around_program: + cmd.append("-n") + if loop_and_print: + cmd.append("-p") + if parse_switches: + cmd.append("-s") + if search_path: + cmd.append("-S") + if tainting_warnings: + cmd.append("-t") + if tainting_checks: + cmd.append("-T") + if dump_core: + cmd.append("-u") + if unsafe_operations: + cmd.append("-U") + if print_version: + cmd.append("-v") + if print_config is not None: + cmd.append(f"-V:{print_config}" if print_config else "-V") + if enable_warnings: + cmd.append("-w") + if enable_all_warnings: + cmd.append("-W") + if ignore_text_before_shebang is not None: + cmd.append(f"-x{str(ignore_text_before_shebang)}" if ignore_text_before_shebang.name else "-x") + if disable_all_warnings: + cmd.append("-X") + + if programfile: + cmd.append(str(programfile)) + + if arguments: + cmd.extend(arguments) + + # --- Identify Output Files --- + output_files_list = [] + if in_place_edit is not None and arguments: + # When using in-place edit (-i), the files listed in arguments are modified. + # We'll treat any argument that is an existing file as an output file. + for arg in arguments: + try: + p = Path(arg) + if p.is_file(): + output_files_list.append(str(p.resolve())) + except (TypeError, ValueError): + # Argument is not a valid path, ignore it. + pass + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + stdout = result.stdout + stderr = result.stderr + except FileNotFoundError: + # This error is raised if the 'perl' command is not found. + raise RuntimeError("Perl executable not found. Please ensure Perl is installed and in your system's PATH.") + except subprocess.CalledProcessError as e: + # This error is raised for non-zero exit codes. + return { + "error": f"Perl execution failed with return code {e.returncode}.", + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + + # --- Structured Result Return --- + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files_list + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/perl-html-formatter_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/perl-html-formatter_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a3c9cd2186c731856a78887192a6dd950b116a70 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/perl-html-formatter_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/perl-html-formatter_server.py') +SERVER_NAME = 'biosci_perl_html_formatter' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..daa22304a23dd01b3087bd2ee6fa9c2cb22f1512 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-html-formatter: + build: . + image: mcp-perl-html-formatter:latest + container_name: mcp-perl-html-formatter + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-html-formatter + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7209a1152f51da34009009ac349755c0787607ef --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-html-formatter + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-html-formatter/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d0191230145a5d527c79af261975410eb25bdf6c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-io-zlib via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-io-zlib -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-io-zlib_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-io-zlib_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-io-zlib_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/perl-io-zlib_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/perl-io-zlib_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3d99a0f5f0192aadbc01ec4a979607a8504ebcbe --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/perl-io-zlib_server.py @@ -0,0 +1,230 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, Dict, Any + +# In a real MCP environment, this would be: from mcp import tool +class mcp: + @staticmethod + def tool(func): + return func + +@mcp.tool +def compress_file( + input_file: Path, + output_file: Path, + compression_level: Optional[int] = None, + use_external_gzip: Optional[bool] = None, +) -> Dict[str, Any]: + """ + Compresses a file using the IO::Zlib Perl module. + + This tool provides the core writing/compression functionality of IO::Zlib, + allowing for specified compression levels and control over using an + external gzip executable. + + Args: + input_file: Path to the input file to be compressed. + output_file: Path to the output compressed file (e.g., file.gz). + compression_level: The compression level to use, from 1 (fastest) to 9 (best). + If not provided, the default level is used. + use_external_gzip: Explicitly control the use of an external gzip command. + If True, forces use of external gzip. + If False, forces use of the internal Compress::Zlib module. + If not provided (default), IO::Zlib will attempt to use + Compress::Zlib and fall back to external gzip if needed. + """ + # --- Input Validation --- + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + if compression_level is not None and not (1 <= compression_level <= 9): + raise ValueError("compression_level must be between 1 and 9.") + + # --- Perl Script Generation --- + mode = "wb" + if compression_level is not None: + mode = f"wb{compression_level}" + + use_line = "use IO::Zlib;" + if use_external_gzip is True: + use_line = "use IO::Zlib qw(:gzip_external 1);" + elif use_external_gzip is False: + use_line = "use IO::Zlib qw(:gzip_external 0);" + + perl_script_content = f""" + use strict; + use warnings; + {use_line} + + my ($input_path, $output_path, $open_mode) = @ARGV; + die "Usage: perl_script.pl \\n" unless @ARGV == 3; + + open(my $in_fh, '<:raw', $input_path) + or die "Cannot open input file '$input_path': $!\\n"; + + my $out_fh = IO::Zlib->new($output_path, $open_mode) + or die "Cannot open output file '$output_path' for compression: $!\\n"; + + my $buffer; + while (read($in_fh, $buffer, 8192)) {{ + print $out_fh $buffer; + }} + + close $in_fh; + close $out_fh; + """ + + # --- Subprocess Execution --- + command = [] + stdout_str, stderr_str = "", "" + script_path = None + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".pl", delete=False) as temp_script: + temp_script.write(perl_script_content) + script_path = temp_script.name + + command = [ + "perl", + script_path, + str(input_file), + str(output_file), + mode, + ] + + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + stdout_str = process.stdout + stderr_str = process.stderr + + except FileNotFoundError: + # This error is raised if 'perl' is not found in the system's PATH. + raise RuntimeError("Perl executable not found. Please ensure Perl is installed and in your PATH.") + except subprocess.CalledProcessError as e: + # This error is caught when the Perl script returns a non-zero exit code. + return { + "error": "Perl script execution failed.", + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + finally: + # Clean up the temporary script file + if script_path and Path(script_path).exists(): + Path(script_path).unlink() + + # --- Structured Result Return --- + return { + "command_executed": " ".join(command), + "stdout": stdout_str, + "stderr": stderr_str, + "output_files": [str(output_file)] if output_file.exists() else [], + } + +@mcp.tool +def decompress_file( + input_file: Path, + output_file: Path, + use_external_gzip: Optional[bool] = None, +) -> Dict[str, Any]: + """ + Decompresses a gzipped file using the IO::Zlib Perl module. + + This tool provides the core reading/decompression functionality of IO::Zlib, + allowing control over using an external gzip executable. + + Args: + input_file: Path to the input gzipped file (e.g., file.gz). + output_file: Path to the output decompressed file. + use_external_gzip: Explicitly control the use of an external gzip command. + If True, forces use of external gzip. + If False, forces use of the internal Compress::Zlib module. + If not provided (default), IO::Zlib will attempt to use + Compress::Zlib and fall back to external gzip if needed. + """ + # --- Input Validation --- + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + # --- Perl Script Generation --- + use_line = "use IO::Zlib;" + if use_external_gzip is True: + use_line = "use IO::Zlib qw(:gzip_external 1);" + elif use_external_gzip is False: + use_line = "use IO::Zlib qw(:gzip_external 0);" + + perl_script_content = f""" + use strict; + use warnings; + {use_line} + + my ($input_path, $output_path) = @ARGV; + die "Usage: perl_script.pl \\n" unless @ARGV == 2; + + my $in_fh = IO::Zlib->new($input_path, "rb") + or die "Cannot open input file '$input_path' for decompression: $!\\n"; + + open(my $out_fh, '>:raw', $output_path) + or die "Cannot open output file '$output_path': $!\\n"; + + my $buffer; + while (read($in_fh, $buffer, 8192)) {{ + print $out_fh $buffer; + }} + + close $in_fh; + close $out_fh; + """ + + # --- Subprocess Execution --- + command = [] + stdout_str, stderr_str = "", "" + script_path = None + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".pl", delete=False) as temp_script: + temp_script.write(perl_script_content) + script_path = temp_script.name + + command = [ + "perl", + script_path, + str(input_file), + str(output_file), + ] + + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + stdout_str = process.stdout + stderr_str = process.stderr + + except FileNotFoundError: + raise RuntimeError("Perl executable not found. Please ensure Perl is installed and in your PATH.") + except subprocess.CalledProcessError as e: + return { + "error": "Perl script execution failed.", + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + finally: + # Clean up the temporary script file + if script_path and Path(script_path).exists(): + Path(script_path).unlink() + + # --- Structured Result Return --- + return { + "command_executed": " ".join(command), + "stdout": stdout_str, + "stderr": stderr_str, + "output_files": [str(output_file)] if output_file.exists() else [], + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/perl-io-zlib_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/perl-io-zlib_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..03e8cf2bb784facc350297a7069ce7aa509baa2d --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/perl-io-zlib_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/perl-io-zlib_server.py') +SERVER_NAME = 'biosci_perl_io_zlib' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..9eac4a4a27edbca68b3f67c081ea4e17489add26 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-io-zlib: + build: . + image: mcp-perl-io-zlib:latest + container_name: mcp-perl-io-zlib + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-io-zlib + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..50042d93782825750588e3f23c5a6cdf59c24e4f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-io-zlib + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-io-zlib/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..29df605dc7a34ea9ee5a61031530e46728f62dfa --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-number-format via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-number-format -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-number-format_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-number-format_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-number-format_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/perl-number-format_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/perl-number-format_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2c0914ea18812e3bf424a48a3c46ce21b360e8ce --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/perl-number-format_server.py @@ -0,0 +1,165 @@ +import subprocess +from typing import Optional, List, Dict, Any +from pathlib import Path + +def _run_perl_format(method: str, *args: Any) -> Dict[str, Any]: + """ + Helper function to execute Number::Format methods via Perl one-liners. + """ + # Construct the Perl script + # We use Number::Format->new() and then call the requested method + # Arguments are passed via @ARGV to ensure safe handling + perl_script = ( + f"use Number::Format; " + f"my $formatter = Number::Format->new(); " + f"print $formatter->{method}(@ARGV);" + ) + + cmd = ["perl", "-MNumber::Format", "-e", perl_script, "--"] + [str(arg) for arg in args] + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout.strip(), + "stderr": result.stderr.strip(), + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout.strip(), + "stderr": e.stderr.strip(), + "status": "error", + "error_message": f"Perl execution failed: {e.stderr}" + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Perl or Number::Format module not found", + "status": "error", + "error_message": "Ensure perl and Number::Format are installed." + } + +@mcp.tool() +def format_number( + number: float, + precision: int = 2, + trailing_zeros: bool = True +) -> Dict[str, Any]: + """ + Formats a number with a specific precision and grouping separators (commas). + + :param number: The numeric value to format. + :param precision: Number of decimal places. + :param trailing_zeros: Whether to keep trailing zeros. + """ + if precision < 0: + precision = 0 + + # Perl's format_number(number, precision, trailing_zeros) + tz = 1 if trailing_zeros else 0 + return _run_perl_format("format_number", number, precision, tz) + +@mcp.tool() +def format_negative( + number: float, + picture: str = "(-)" +) -> Dict[str, Any]: + """ + Formats a negative number according to a specified format (picture). + + :param number: The numeric value to format. + :param picture: The format string (e.g., '(-)' for parentheses, '-' for prefix). + """ + if number >= 0: + return { + "command_executed": "None", + "stdout": str(number), + "stderr": "Warning: Number is not negative, returning as string.", + "status": "success" + } + + return _run_perl_format("format_negative", picture, number) + +@mcp.tool() +def format_currency( + number: float, + symbol: str = "$", + precision: int = 2 +) -> Dict[str, Any]: + """ + Formats a number as a currency string. + + :param number: The numeric value to format. + :param symbol: The currency symbol to use (e.g., '$', '£'). + :param precision: Number of decimal places. + """ + if precision < 0: + precision = 0 + + # Perl's format_currency(symbol, number, precision) + return _run_perl_format("format_currency", symbol, number, precision) + +@mcp.tool() +def format_bytes( + number: float, + precision: int = 2 +) -> Dict[str, Any]: + """ + Formats a number of bytes into a human-readable string (K, M, G, etc.). + + :param number: The number of bytes. + :param precision: Number of decimal places for the result. + """ + if number < 0: + return {"status": "error", "error_message": "Byte count cannot be negative."} + + return _run_perl_format("format_bytes", number, precision) + +@mcp.tool() +def format_percentage( + number: float, + precision: int = 2 +) -> Dict[str, Any]: + """ + Formats a number as a percentage string. + + :param number: The numeric value (e.g., 0.5 for 50%). + :param precision: Number of decimal places. + """ + return _run_perl_format("format_percentage", number, precision) + +@mcp.tool() +def unformat_number( + formatted_string: str +) -> Dict[str, Any]: + """ + Converts a formatted string back into a raw numeric value. + + :param formatted_string: The string to unformat (e.g., '1,234.56'). + """ + if not formatted_string: + return {"status": "error", "error_message": "Input string is empty."} + + return _run_perl_format("unformat_number", formatted_string) + +@mcp.tool() +def format_price( + number: float, + precision: int = 2 +) -> Dict[str, Any]: + """ + A specialized version of format_number often used for prices, ensuring + standard decimal formatting without currency symbols. + + :param number: The price value. + :param precision: Decimal precision. + """ + return _run_perl_format("format_price", number, precision) \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/perl-number-format_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/perl-number-format_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f87d7c871ecde868a6d6144d3ac9d19454b19013 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/perl-number-format_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/perl-number-format_server.py') +SERVER_NAME = 'biosci_perl_number_format' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..036a37ebcbf15c297a999072eed8b21930e4035a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-number-format/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-number-format: + build: . + image: mcp-perl-number-format:latest + container_name: mcp-perl-number-format + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-number-format + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ccae3e165318875a244a3b51936761aa0e8c73db --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-parse-recdescent via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-parse-recdescent -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-parse-recdescent_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-parse-recdescent_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-parse-recdescent_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/perl-parse-recdescent_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/perl-parse-recdescent_server.py new file mode 100644 index 0000000000000000000000000000000000000000..7632cde397ce484501c8a1ea1d912be823d2b2fa --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/perl-parse-recdescent_server.py @@ -0,0 +1,229 @@ +from typing import Optional, List +import subprocess +from pathlib import Path + +@mcp.tool() +def precompile_grammar( + grammar_file: str, + parser_class: str, + output_file: str, + use_warnings: bool = True, +) -> dict: + """ + Precompiles a Parse::RecDescent grammar into a standalone Perl module. + + This is the primary way to generate a recursive-descent parser from a grammar + specification file. The resulting file is a standard Perl module (.pm). + + Args: + grammar_file: Path to the file containing the RecDescent grammar specification. + parser_class: The name of the Perl class/package to be generated (e.g., 'My::Parser'). + output_file: Path where the generated Perl module (.pm) should be saved. + use_warnings: Whether to enable Perl warnings during the generation process. + """ + grammar_path = Path(grammar_file) + output_path = Path(output_file) + + if not grammar_path.exists(): + return {"error": f"Grammar file not found: {grammar_file}"} + + # Ensure output directory exists + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Construct the Perl command + # We use a one-liner to read the grammar from STDIN and call Precompile + perl_cmd = ["perl"] + if use_warnings: + perl_cmd.append("-w") + + perl_cmd.extend([ + "-MParse::RecDescent", + "-e", + "undef $/; my $grammar = ; Parse::RecDescent->Precompile($grammar, $ARGV[0]);", + "--", + parser_class + ]) + + try: + with open(grammar_path, "r") as f_in: + result = subprocess.run( + perl_cmd, + stdin=f_in, + capture_output=True, + text=True, + check=True + ) + + # Precompile prints the generated code to STDOUT + with open(output_path, "w") as f_out: + f_out.write(result.stdout) + + return { + "command_executed": " ".join(perl_cmd) + f" < {grammar_file} > {output_file}", + "stdout": "Parser generated successfully.", + "stderr": result.stderr, + "output_files": [str(output_path.absolute())] + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Failed to precompile grammar. Check if the grammar syntax is valid." + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def verify_grammar( + grammar_file: str, +) -> dict: + """ + Validates the syntax of a Parse::RecDescent grammar file. + + This tool attempts to instantiate a parser object from the grammar. + If the grammar contains errors, Parse::RecDescent will report them to STDERR. + + Args: + grammar_file: Path to the file containing the RecDescent grammar specification. + """ + grammar_path = Path(grammar_file) + + if not grammar_path.exists(): + return {"error": f"Grammar file not found: {grammar_file}"} + + # Perl command to check grammar validity + perl_cmd = [ + "perl", + "-MParse::RecDescent", + "-e", + "undef $/; my $grammar = ; my $parser = Parse::RecDescent->new($grammar); exit($parser ? 0 : 1);" + ] + + try: + with open(grammar_path, "r") as f_in: + result = subprocess.run( + perl_cmd, + stdin=f_in, + capture_output=True, + text=True, + check=True + ) + + return { + "command_executed": " ".join(perl_cmd) + f" < {grammar_file}", + "stdout": "Grammar is valid.", + "stderr": result.stderr, + "valid": True + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Grammar validation failed.", + "valid": False + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def run_parser_test( + grammar_file: str, + input_text: str, + start_rule: str = "start", +) -> dict: + """ + Tests a grammar against a specific input string without precompiling. + + Useful for rapid prototyping and debugging of grammars. + + Args: + grammar_file: Path to the grammar specification. + input_text: The text string to be parsed. + start_rule: The name of the top-level rule in the grammar to start parsing from. + """ + grammar_path = Path(grammar_file) + + if not grammar_path.exists(): + return {"error": f"Grammar file not found: {grammar_file}"} + + # Perl script to load grammar and parse input + # $ARGV[0] is the start rule, $ARGV[1] is the input text + perl_script = """ + use Parse::RecDescent; + undef $/; + my $grammar = ; + my $parser = Parse::RecDescent->new($grammar) or die "Invalid grammar"; + my $result = $parser->$ARGV[0]($ARGV[1]); + if (defined $result) { + print "Match successful.\\n"; + } else { + print "Match failed.\\n"; + exit 1; + } + """ + + perl_cmd = [ + "perl", + "-MParse::RecDescent", + "-e", + perl_script, + "--", + start_rule, + input_text + ] + + try: + with open(grammar_path, "r") as f_in: + result = subprocess.run( + perl_cmd, + stdin=f_in, + capture_output=True, + text=True, + check=True + ) + + return { + "command_executed": "perl -MParse::RecDescent ...", + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": "perl -MParse::RecDescent ...", + "stdout": e.stdout, + "stderr": e.stderr, + "status": "failed", + "error": "Parsing failed or grammar error." + } + +@mcp.tool() +def perl_recdescent_info() -> dict: + """ + Returns the version information for Perl and the Parse::RecDescent module. + """ + try: + perl_ver = subprocess.run(["perl", "-v"], capture_output=True, text=True, check=True) + mod_ver = subprocess.run( + ["perl", "-MParse::RecDescent", "-e", "print $Parse::RecDescent::VERSION"], + capture_output=True, + text=True, + check=True + ) + + return { + "perl_version": perl_ver.stdout.splitlines()[1] if len(perl_ver.stdout.splitlines()) > 1 else "Unknown", + "parse_recdescent_version": mod_ver.stdout, + "status": "installed" + } + except Exception as e: + return { + "status": "error", + "error": str(e), + "message": "Ensure perl and perl-parse-recdescent are installed in the environment." + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/perl-parse-recdescent_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/perl-parse-recdescent_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..52c27b5bdfd8f5d31a1be764904c9a4170e5d863 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/perl-parse-recdescent_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/perl-parse-recdescent_server.py') +SERVER_NAME = 'biosci_perl_parse_recdescent' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c041a9b282f3d9cafa7e8a8d3604efc2a5d3d857 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-parse-recdescent: + build: . + image: mcp-perl-parse-recdescent:latest + container_name: mcp-perl-parse-recdescent + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-parse-recdescent + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ce3a1c9fdb30ca3c049958bb407c047688974ce1 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-parse-recdescent + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-parse-recdescent/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..90c6424007c6f1ccfac3baba3c47a6c86c4faa78 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-role-tiny via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-role-tiny -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-role-tiny_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-role-tiny_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-role-tiny_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/perl-role-tiny_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/perl-role-tiny_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c0bf06fe733c34d9cc7a21d53a40b9974469ad9e --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/perl-role-tiny_server.py @@ -0,0 +1,216 @@ +from typing import Optional, List +import subprocess +from pathlib import Path + +@mcp.tool() +def perl_role_tiny_run( + script_path: Optional[str] = None, + one_liner: Optional[str] = None, + use_modules: Optional[List[str]] = None, + include_dirs: Optional[List[str]] = None, + warnings_mode: str = "none", + loop_mode: str = "none", + autosplit: bool = False, + split_pattern: Optional[str] = None, + inplace_suffix: Optional[str] = None, + unicode_features: Optional[str] = None, + taint_check: bool = False, + script_args: Optional[List[str]] = None, +): + """ + Execute Perl code using the perl interpreter. This environment includes the Role::Tiny module. + + Args: + script_path: Path to a Perl script file to execute. + one_liner: A string containing Perl code to run (equivalent to -e). + use_modules: List of modules to load before execution (equivalent to -M). + include_dirs: List of directories to add to @INC (equivalent to -I). + warnings_mode: Warning level: 'none', 'normal' (-w), 'all' (-W), or 'silent' (-X). + loop_mode: Loop behavior: 'none', 'n' (while(<>)), or 'p' (while(<>) { print }). + autosplit: Enable autosplit mode with -n or -p (equivalent to -a). + split_pattern: Pattern for autosplit (equivalent to -F). + inplace_suffix: Extension for in-place editing (equivalent to -i). + unicode_features: Enable Unicode features (equivalent to -C). + taint_check: Enable taint checks (equivalent to -T). + script_args: Arguments to pass to the Perl script or one-liner. + """ + cmd = ["perl"] + + # Handle Warnings + if warnings_mode == "normal": + cmd.append("-w") + elif warnings_mode == "all": + cmd.append("-W") + elif warnings_mode == "silent": + cmd.append("-X") + + # Handle Taint + if taint_check: + cmd.append("-T") + + # Handle Unicode + if unicode_features: + cmd.append(f"-C{unicode_features}") + + # Handle Include Directories + if include_dirs: + for d in include_dirs: + cmd.append(f"-I{d}") + + # Handle Modules + if use_modules: + for m in use_modules: + cmd.append(f"-M{m}") + + # Handle In-place editing + if inplace_suffix is not None: + cmd.append(f"-i{inplace_suffix}") + + # Handle Loops and Autosplit + if loop_mode == "n": + cmd.append("-n") + elif loop_mode == "p": + cmd.append("-p") + + if autosplit: + cmd.append("-a") + if split_pattern: + cmd.append(f"-F{split_pattern}") + + # Handle Execution Source + if one_liner: + cmd.extend(["-e", one_liner]) + elif script_path: + p = Path(script_path) + if not p.exists(): + return {"error": f"Script file not found: {script_path}"} + cmd.append(str(p)) + else: + # If neither is provided, we can't execute much unless it's a version check + # but we have a separate tool for that. + return {"error": "Either script_path or one_liner must be provided."} + + # Handle Script Arguments + if script_args: + cmd.extend(script_args) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "return_code": result.returncode + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "error": "Perl execution failed" + } + +@mcp.tool() +def perl_role_tiny_check_syntax( + script_path: str, + use_modules: Optional[List[str]] = None, + include_dirs: Optional[List[str]] = None, +): + """ + Check the syntax of a Perl script without executing it (equivalent to perl -c). + + Args: + script_path: Path to the Perl script to check. + use_modules: List of modules to load during the check. + include_dirs: List of directories to add to @INC. + """ + p = Path(script_path) + if not p.exists(): + return {"error": f"Script file not found: {script_path}"} + + cmd = ["perl", "-c"] + + if include_dirs: + for d in include_dirs: + cmd.append(f"-I{d}") + + if use_modules: + for m in use_modules: + cmd.append(f"-M{m}") + + cmd.append(str(p)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "Syntax OK" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Syntax check failed" + } + +@mcp.tool() +def perl_role_tiny_info( + verbose: bool = False, + config_variable: Optional[str] = None, +): + """ + Get version and configuration information for the Perl interpreter. + + Args: + verbose: If True, provides a full configuration summary (equivalent to -V). + If False, provides version info (equivalent to -v). + config_variable: Specific configuration variable to query (e.g., 'archname'). + Only used if verbose is True. + """ + if verbose: + if config_variable: + cmd = ["perl", f"-V:{config_variable}"] + else: + cmd = ["perl", "-V"] + else: + cmd = ["perl", "-v"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Failed to retrieve Perl info" + } + +@mcp.tool() +def perl_role_tiny_list_inc(): + """ + List the Perl include paths (@INC) to verify where modules are searched. + """ + cmd = ["perl", "-e", "print join('\\n', @INC)"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "include_paths": result.stdout.splitlines(), + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": "Failed to list @INC", + "stderr": e.stderr + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/perl-role-tiny_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/perl-role-tiny_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0f91dc29d1064360b559af792a8c5277800a22cf --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/perl-role-tiny_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/perl-role-tiny_server.py') +SERVER_NAME = 'biosci_perl_role_tiny' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..92888e30ffb40cd3f6e983511bd4ffb6e25ddea2 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-role-tiny: + build: . + image: mcp-perl-role-tiny:latest + container_name: mcp-perl-role-tiny + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-role-tiny + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..31de7372e7d4737f56ac691ecf8397be71d6eae8 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-role-tiny + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-role-tiny/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-set-intervaltree/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-set-intervaltree/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..80a693e4e1d785ba51843123c7c2186243c8ce9f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-set-intervaltree/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-set-intervaltree via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-set-intervaltree -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-set-intervaltree_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-set-intervaltree_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-set-intervaltree_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-set-intervaltree/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-set-intervaltree/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6694d34e706a2f2630df4f41a4dc4fab62d5d4bd --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-set-intervaltree/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-set-intervaltree + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b000ea84671cd9bd02f15e5b9b9f188fba657302 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-sub-install via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-sub-install -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-sub-install_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-sub-install_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-sub-install_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/__pycache__/perl-sub-install_server.cpython-310.pyc b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/__pycache__/perl-sub-install_server.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d112220043c0edca670da0380949160ac9fe0ddc Binary files /dev/null and b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/__pycache__/perl-sub-install_server.cpython-310.pyc differ diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/perl-sub-install_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/perl-sub-install_server.py new file mode 100644 index 0000000000000000000000000000000000000000..cdc701568828a4bf82fd28c23a43f33120cfc64e --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/perl-sub-install_server.py @@ -0,0 +1,216 @@ +from typing import Optional, List +import subprocess +from pathlib import Path + +@mcp.tool() +def perl_sub_install_run( + one_liner: Optional[str] = None, + program_file: Optional[str] = None, + use_all_features: bool = False, + autosplit: bool = False, + split_pattern: Optional[str] = None, + inplace_extension: Optional[str] = None, + include_dirs: Optional[List[str]] = None, + modules: Optional[List[str]] = None, + warnings_mode: str = "none", + script_args: Optional[List[str]] = None, +): + """ + Execute Perl code or a script using the perl interpreter. + This environment includes the perl-sub-install (Sub::Install) module. + + Args: + one_liner: One line of program (equivalent to -e). + program_file: Path to a Perl script file to execute. + use_all_features: If True, enables all optional features (equivalent to -E). + autosplit: Enable autosplit mode with -n or -p (equivalent to -a). + split_pattern: split() pattern for -a switch (equivalent to -F). + inplace_extension: Edit files in place, making backup if extension supplied (equivalent to -i). + include_dirs: List of directories to add to @INC (equivalent to -I). + modules: List of modules to execute "use module..." before the program (equivalent to -M). + warnings_mode: Warning level: 'none', 'useful' (-w), 'all' (-W), or 'disable' (-X). + script_args: Arguments to pass to the Perl program. + """ + cmd = ["perl"] + + # Handle warnings + if warnings_mode == "useful": + cmd.append("-w") + elif warnings_mode == "all": + cmd.append("-W") + elif warnings_mode == "disable": + cmd.append("-X") + + # Handle modules + if modules: + for module in modules: + cmd.append(f"-M{module}") + + # Handle include directories + if include_dirs: + for directory in include_dirs: + cmd.append(f"-I{directory}") + + # Handle autosplit + if autosplit: + cmd.append("-a") + if split_pattern: + cmd.append(f"-F{split_pattern}") + + # Handle inplace editing + if inplace_extension is not None: + cmd.append(f"-i{inplace_extension}") + + # Handle execution mode (one-liner vs file) + if one_liner: + flag = "-E" if use_all_features else "-e" + cmd.extend([flag, one_liner]) + elif program_file: + prog_path = Path(program_file) + if not prog_path.exists(): + return {"error": f"Program file not found: {program_file}"} + cmd.append(str(prog_path)) + else: + return {"error": "Either 'one_liner' or 'program_file' must be provided."} + + # Append script arguments + if script_args: + cmd.extend(script_args) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "failed" + } + +@mcp.tool() +def perl_sub_install_check_syntax( + program_file: str, + modules: Optional[List[str]] = None, + include_dirs: Optional[List[str]] = None, +): + """ + Check the syntax of a Perl script without executing it (equivalent to perl -c). + + Args: + program_file: Path to the Perl script to check. + modules: List of modules to load before checking. + include_dirs: List of directories to add to @INC. + """ + prog_path = Path(program_file) + if not prog_path.exists(): + return {"error": f"File not found: {program_file}"} + + cmd = ["perl", "-c"] + + if modules: + for module in modules: + cmd.append(f"-M{module}") + + if include_dirs: + for directory in include_dirs: + cmd.append(f"-I{directory}") + + cmd.append(str(prog_path)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "syntax_ok" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Syntax check failed", + "status": "failed" + } + +@mcp.tool() +def perl_sub_install_version(): + """ + Print the version and patchlevel of the Perl interpreter. + """ + cmd = ["perl", "-v"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e) + } + +@mcp.tool() +def perl_sub_install_config( + variable: Optional[str] = None +): + """ + Print a summary of Perl configuration or the value of a specific configuration variable. + + Args: + variable: Specific Config.pm variable to print (equivalent to -V:variable). + """ + arg = "-V" + if variable: + arg = f"-V:{variable}" + + cmd = ["perl", arg] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e) + } + +@mcp.tool() +def perl_sub_install_test_module(): + """ + Verify that the Sub::Install module is correctly installed and available in the environment. + """ + # Sub::Install is the module provided by the perl-sub-install package. + # We test it by attempting to load it and calling a simple version check or just '1'. + one_liner = "use Sub::Install; print 'Sub::Install is available.';" + cmd = ["perl", "-e", one_liner] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "available" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Sub::Install module not found or failed to load.", + "status": "unavailable" + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/perl-sub-install_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/perl-sub-install_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1d6851497e98abe073d07c70db17c74a12580b05 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/perl-sub-install_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/perl-sub-install_server.py') +SERVER_NAME = 'biosci_perl_sub_install' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..7608e6d638d3f44c7280aabb802e6b45cc7eba7a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-sub-install: + build: . + image: mcp-perl-sub-install:latest + container_name: mcp-perl-sub-install + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-sub-install + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..158b507ae3c238de583956d0b6b3b630620f9564 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-sub-install + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-install/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/app/perl-sub-quote_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/app/perl-sub-quote_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..5dcd6db242b719027c4fa9c275ca3929a1642de9 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/app/perl-sub-quote_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/app/perl-sub-quote_server.py') +SERVER_NAME = 'biosci_perl_sub_quote' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..92cfb2c600dcfca8cac558c2d683707fe5d15d72 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-quote/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-sub-quote + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6bfc3e627d159d944a5754a34e441b4c83e0dd91 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-sub-uplevel via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-sub-uplevel -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-sub-uplevel_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-sub-uplevel_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-sub-uplevel_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/perl-sub-uplevel_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/perl-sub-uplevel_server.py new file mode 100644 index 0000000000000000000000000000000000000000..7942fe2a02eb5b40fdaec4c19bf2db617edd72bf --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/perl-sub-uplevel_server.py @@ -0,0 +1,226 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Dict, Any + +# This is a placeholder for the MCP decorator. +# In a real MCP environment, this would be imported. +def tool(func): + """A dummy decorator to allow the code to be syntactically valid.""" + return func + +mcp = type("mcp", (), {"tool": tool}) + +@mcp.tool +def run_perl( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + e_program: Optional[List[str]] = None, + E_program: Optional[List[str]] = None, + include_dirs: Optional[List[Path]] = None, + module: Optional[List[str]] = None, + check_syntax_only: bool = False, + debugger: Optional[str] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + enable_tainting_checks: bool = False, + enable_tainting_warnings: bool = False, + loop_around_program: bool = False, + loop_and_print: bool = False, + autosplit: bool = False, + line_ending: Optional[str] = None, + split_pattern: Optional[str] = None, + inplace_edit_extension: Optional[str] = None, + parse_switches: bool = False, + search_path: bool = False, + print_version: bool = False, + print_config: Optional[str] = None, + unicode_features: Optional[str] = None, + debugging_flags: Optional[str] = None, + no_sitecustomize: bool = False, + dump_core: bool = False, + allow_unsafe: bool = False, + extract_from_text_dir: Optional[Path] = None, + disable_all_warnings: bool = False, + record_separator: Optional[str] = None, +) -> Dict[str, Any]: + """ + Executes a Perl script or command using the Perl interpreter. + + This tool is a wrapper for the `perl` command-line interpreter. The `perl-sub-uplevel` + package is a Perl module and does not have its own executable. + + Args: + programfile: Path to the Perl program file to execute. + arguments: A list of arguments to pass to the program file. + e_program: A list of strings, each being a line of a program. + E_program: Like -e, but enables all optional features. + include_dirs: A list of directories to add to @INC. Corresponds to the -I flag. + module: A list of modules to use/no before executing the program. Corresponds to the -M/-m flag. + check_syntax_only: Check syntax only (runs BEGIN and CHECK blocks). Corresponds to the -c flag. + debugger: Run the program under the specified debugger. Corresponds to the -d[:debugger] flag. + enable_warnings: Enable many useful warnings. Corresponds to the -w flag. + enable_all_warnings: Enable all warnings. Corresponds to the -W flag. + enable_tainting_checks: Enable tainting checks. Corresponds to the -T flag. + enable_tainting_warnings: Enable tainting warnings. Corresponds to the -t flag. + loop_around_program: Assume "while (<>) { ... }" loop around the program. Corresponds to the -n flag. + loop_and_print: Assume loop like -n but also print the line. Corresponds to the -p flag. + autosplit: Autosplit mode with -n or -p (splits $_ into @F). Corresponds to the -a flag. + line_ending: Enable line ending processing, specifies line terminator. Corresponds to the -l[octal] flag. + split_pattern: split() pattern for the -a switch. Corresponds to the -F/pattern/ flag. + inplace_edit_extension: Edit <> files in place, making a backup if an extension is supplied. Corresponds to the -i[extension] flag. + parse_switches: Enable rudimentary parsing for switches after the program file. Corresponds to the -s flag. + search_path: Look for the program file using the PATH environment variable. Corresponds to the -S flag. + print_version: Print version, patchlevel, and license. Corresponds to the -v flag. + print_config: Print configuration summary or a single Config.pm variable. Corresponds to the -V[:variable] flag. + unicode_features: Enables the listed Unicode features. Corresponds to the -C[number/list] flag. + debugging_flags: Set debugging flags. Corresponds to the -D[number/list] flag. + no_sitecustomize: Don't do $sitelib/sitecustomize.pl at startup. Corresponds to the -f flag. + dump_core: Dump core after parsing the program. Corresponds to the -u flag. + allow_unsafe: Allow unsafe operations. Corresponds to the -U flag. + extract_from_text_dir: Ignore text before #!perl line, optionally cd to the directory. Corresponds to the -x[directory] flag. + disable_all_warnings: Disable all warnings. Corresponds to the -X flag. + record_separator: Specify the record separator. Corresponds to the -0[octal] flag. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + if not any([programfile, e_program, E_program, print_version, print_config is not None]): + raise ValueError("Either a program file, an inline program (-e or -E), or an info flag (-v or -V) must be provided.") + + if programfile and (e_program or E_program): + raise ValueError("`programfile` cannot be used at the same time as `e_program` or `E_program`.") + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"The specified program file does not exist: {programfile}") + + if include_dirs: + for directory in include_dirs: + if not directory.is_dir(): + raise NotADirectoryError(f"The include directory does not exist: {directory}") + + if extract_from_text_dir and not extract_from_text_dir.is_dir(): + raise NotADirectoryError(f"The directory for -x does not exist: {extract_from_text_dir}") + + # --- Command Construction --- + cmd = ["perl"] + + if record_separator is not None: + cmd.append(f"-0{record_separator}") + if autosplit: + cmd.append("-a") + if unicode_features is not None: + cmd.append(f"-C{unicode_features}") + if check_syntax_only: + cmd.append("-c") + if debugger is not None: + cmd.append(f"-d:{debugger}" if debugger else "-d") + if debugging_flags is not None: + cmd.append(f"-D{debugging_flags}") + if no_sitecustomize: + cmd.append("-f") + if split_pattern is not None: + cmd.append(f"-F{split_pattern}") + if inplace_edit_extension is not None: + cmd.append(f"-i{inplace_edit_extension}") + if line_ending is not None: + cmd.append(f"-l{line_ending}") + if loop_around_program: + cmd.append("-n") + if loop_and_print: + cmd.append("-p") + if parse_switches: + cmd.append("-s") + if search_path: + cmd.append("-S") + if enable_tainting_warnings: + cmd.append("-t") + if enable_tainting_checks: + cmd.append("-T") + if dump_core: + cmd.append("-u") + if allow_unsafe: + cmd.append("-U") + if print_version: + cmd.append("-v") + if print_config is not None: + cmd.append(f"-V:{print_config}" if print_config else "-V") + if enable_warnings: + cmd.append("-w") + if enable_all_warnings: + cmd.append("-W") + if extract_from_text_dir is not None: + cmd.extend(["-x", str(extract_from_text_dir)]) + elif extract_from_text_dir is not None: # -x without directory + cmd.append("-x") + if disable_all_warnings: + cmd.append("-X") + + if include_dirs: + for directory in include_dirs: + cmd.extend(["-I", str(directory)]) + + if module: + for mod in module: + if mod.startswith('-'): # for 'no module' + cmd.extend(["-m", mod]) + else: + cmd.extend(["-M", mod]) + + if e_program: + for prog_line in e_program: + cmd.extend(["-e", prog_line]) + + if E_program: + for prog_line in E_program: + cmd.extend(["-E", prog_line]) + + if programfile: + cmd.append(str(programfile)) + + if arguments: + cmd.extend(arguments) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + stdout = result.stdout + stderr = result.stderr + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'perl' command not found. Please ensure Perl is installed and in your PATH.", + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + + # --- Structured Result Return --- + output_files = [] + # The -i flag modifies files in place. If used with a programfile and arguments, + # those arguments might be files that get modified. + if inplace_edit_extension is not None and arguments: + for arg in arguments: + p = Path(arg) + if p.is_file(): + output_files.append(str(p)) + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/perl-sub-uplevel_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/perl-sub-uplevel_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9a357909be2ee06a763151488bf8bf1bbd2e8e33 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/perl-sub-uplevel_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/perl-sub-uplevel_server.py') +SERVER_NAME = 'biosci_perl_sub_uplevel' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c830f8cbcf572935fb9d12691e9e1fe9f53929d0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-sub-uplevel: + build: . + image: mcp-perl-sub-uplevel:latest + container_name: mcp-perl-sub-uplevel + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-sub-uplevel + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fcd9a0f226b068f02f26750f04dbbf1c47a60995 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-sub-uplevel + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-sub-uplevel/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ae33589b1adbb38ca8215eb9e354e614f6649f03 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-test-deep via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-test-deep -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-test-deep_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-test-deep_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-test-deep_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/perl-test-deep_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/perl-test-deep_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0bfd3c6eb0fe528d2a13e19276a2670a0924579c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/perl-test-deep_server.py @@ -0,0 +1,225 @@ +from typing import List, Optional +import subprocess +from pathlib import Path + +@mcp.tool() +def perl_test_deep_run_script( + script_path: str, + arguments: Optional[List[str]] = None, + include_dirs: Optional[List[str]] = None, + modules: Optional[List[str]] = None, + warning_mode: str = "many", + taint_mode: str = "none", + syntax_check_only: bool = False, + autosplit: bool = False, + split_pattern: Optional[str] = None, + inplace_extension: Optional[str] = None, + line_ending_processing: Optional[str] = None, + loop_mode: str = "none", + unicode_features: Optional[str] = None, + no_site_customize: bool = False, + search_path: bool = False, + unsafe_operations: bool = False, + record_separator: Optional[str] = None, +): + """ + Execute a Perl script using the perl-test-deep environment. + This tool allows running complex deep comparison tests using the Test::Deep module. + + Args: + script_path: Path to the Perl script (.pl or .t) to execute. + arguments: List of command-line arguments to pass to the script. + include_dirs: List of directories to add to @INC (Perl's include path). + modules: List of additional modules to load (e.g., ["JSON", "YAML"]). Test::Deep is often loaded within the script. + warning_mode: Warning level: 'default', 'many' (-w), 'all' (-W), or 'none' (-X). + taint_mode: Taint checking: 'none', 'warnings' (-t), or 'checks' (-T). + syntax_check_only: If True, only check syntax without executing (-c). + autosplit: Enable autosplit mode (-a) with loop_mode. + split_pattern: Pattern for autosplit (-F). + inplace_extension: Edit files in place, optionally providing a backup extension (-i). + line_ending_processing: Enable line ending processing (-l). + loop_mode: Loop mode: 'none', 'n' (while(<>)), or 'p' (while(<>) { print }). + unicode_features: Enable Unicode features (-C). + no_site_customize: Don't run sitecustomize.pl at startup (-f). + search_path: Look for programfile using PATH environment variable (-S). + unsafe_operations: Allow unsafe operations (-U). + record_separator: Specify record separator (-0). + """ + # Input validation + script_file = Path(script_path) + if not script_file.exists(): + return {"error": f"Script file not found: {script_path}"} + + if warning_mode not in ["default", "many", "all", "none"]: + return {"error": "warning_mode must be one of: default, many, all, none"} + + if taint_mode not in ["none", "warnings", "checks"]: + return {"error": "taint_mode must be one of: none, warnings, checks"} + + if loop_mode not in ["none", "n", "p"]: + return {"error": "loop_mode must be one of: none, n, p"} + + # Build command + cmd = ["perl"] + + # Add switches + if warning_mode == "many": cmd.append("-w") + elif warning_mode == "all": cmd.append("-W") + elif warning_mode == "none": cmd.append("-X") + + if taint_mode == "warnings": cmd.append("-t") + elif taint_mode == "checks": cmd.append("-T") + + if syntax_check_only: cmd.append("-c") + if autosplit: cmd.append("-a") + if no_site_customize: cmd.append("-f") + if search_path: cmd.append("-S") + if unsafe_operations: cmd.append("-U") + + if record_separator is not None: cmd.append(f"-0{record_separator}") + if unicode_features is not None: cmd.append(f"-C{unicode_features}") + if split_pattern is not None: cmd.append(f"-F{split_pattern}") + if inplace_extension is not None: cmd.append(f"-i{inplace_extension}") + if line_ending_processing is not None: cmd.append(f"-l{line_ending_processing}") + + if loop_mode == "n": cmd.append("-n") + elif loop_mode == "p": cmd.append("-p") + + if include_dirs: + for d in include_dirs: + cmd.append(f"-I{d}") + + if modules: + for m in modules: + cmd.append(f"-M{m}") + + # Add script and its arguments + cmd.append(str(script_file)) + if arguments: + cmd.extend(arguments) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "status": "error", + "exit_code": e.returncode + } + +@mcp.tool() +def perl_test_deep_one_liner( + code: str, + arguments: Optional[List[str]] = None, + include_dirs: Optional[List[str]] = None, + use_test_deep: bool = True, + warning_mode: str = "many", + autosplit: bool = False, + loop_mode: str = "none", + unicode_features: Optional[str] = None, +): + """ + Execute a Perl one-liner using the perl-test-deep environment. + Useful for quick data structure comparisons or small test snippets. + + Args: + code: The Perl code to execute. + arguments: List of command-line arguments (e.g., input files). + include_dirs: List of directories to add to @INC. + use_test_deep: Automatically load the Test::Deep module (-MTest::Deep). + warning_mode: Warning level: 'default', 'many' (-w), 'all' (-W), or 'none' (-X). + autosplit: Enable autosplit mode (-a). + loop_mode: Loop mode: 'none', 'n' (while(<>)), or 'p' (while(<>) { print }). + unicode_features: Enable Unicode features (-C). + """ + if warning_mode not in ["default", "many", "all", "none"]: + return {"error": "warning_mode must be one of: default, many, all, none"} + + if loop_mode not in ["none", "n", "p"]: + return {"error": "loop_mode must be one of: none, n, p"} + + cmd = ["perl"] + + if use_test_deep: + cmd.append("-MTest::Deep") + + if warning_mode == "many": cmd.append("-w") + elif warning_mode == "all": cmd.append("-W") + elif warning_mode == "none": cmd.append("-X") + + if autosplit: cmd.append("-a") + if loop_mode == "n": cmd.append("-n") + elif loop_mode == "p": cmd.append("-p") + if unicode_features is not None: cmd.append(f"-C{unicode_features}") + + if include_dirs: + for d in include_dirs: + cmd.append(f"-I{d}") + + cmd.extend(["-e", code]) + + if arguments: + cmd.extend(arguments) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "status": "error", + "exit_code": e.returncode + } + +@mcp.tool() +def perl_test_deep_version_info( + verbose: bool = False, + config_variable: Optional[str] = None, +): + """ + Get version and configuration information for the Perl environment. + + Args: + verbose: If True, print configuration summary (-V). If False, print version (-v). + config_variable: Print the value of a specific configuration variable (e.g., 'installsitelib'). + """ + cmd = ["perl"] + + if config_variable: + cmd.append(f"-V:{config_variable}") + elif verbose: + cmd.append("-V") + else: + cmd.append("-v") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "status": "error", + "exit_code": e.returncode + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/perl-test-deep_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/perl-test-deep_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..40620d0a5ada477d4b50f221e94083ea06a1a6b7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/perl-test-deep_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/perl-test-deep_server.py') +SERVER_NAME = 'biosci_perl_test_deep' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..53ff855d2d208b1c0b0a31662cb9d9c2c2351308 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-test-deep: + build: . + image: mcp-perl-test-deep:latest + container_name: mcp-perl-test-deep + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-test-deep + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f387fe381f1a634c87ef05d5e1486a6fd7dcdc8d --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-test-deep + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-deep/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a73593138358727164adccda839539106af4531e --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-test-warn via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-test-warn -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-test-warn_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-test-warn_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-test-warn_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/perl-test-warn_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/perl-test-warn_server.py new file mode 100644 index 0000000000000000000000000000000000000000..62f25eecc4b3cdb12defa7c4e0b0a8b1a0b0406c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/perl-test-warn_server.py @@ -0,0 +1,213 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Union + +@mcp.tool() +def perl_interpreter( + program_file: Optional[str] = None, + arguments: List[str] = [], + one_liner: Optional[str] = None, + use_all_features: bool = False, + check_syntax_only: bool = False, + modules: List[str] = ["Test::Warn"], + include_directories: List[str] = [], + warning_mode: str = "useful", + taint_checks: bool = False, + taint_warnings: bool = False, + autosplit_mode: bool = False, + split_pattern: Optional[str] = None, + inplace_edit_extension: Optional[str] = None, + loop_assume_while: bool = False, + loop_assume_while_print: bool = False, + unicode_features: Optional[str] = None, + record_separator: Optional[str] = None, + line_ending_processing: Optional[str] = None, + unsafe_operations: bool = False, + ignore_text_before_perl_line: Optional[str] = None, +): + """ + Execute Perl scripts or commands, with built-in support for the Test::Warn module. + This tool provides access to the Perl interpreter and its various switches. + + Args: + program_file: Path to the Perl script to execute. + arguments: Arguments to pass to the Perl script. + one_liner: One line of program to execute (maps to -e). + use_all_features: Like one_liner, but enables all optional features (maps to -E). + check_syntax_only: Check syntax only without executing (maps to -c). + modules: List of modules to execute 'use module' before the program (maps to -M). Defaults to ['Test::Warn']. + include_directories: List of directories to add to @INC (maps to -I). + warning_mode: Warning level: 'useful' (-w), 'all' (-W), 'none' (-X), or 'default'. + taint_checks: Enable tainting checks (maps to -T). + taint_warnings: Enable tainting warnings (maps to -t). + autosplit_mode: Enable autosplit mode with -n or -p (maps to -a). + split_pattern: Pattern for autosplit mode (maps to -F). + inplace_edit_extension: Edit files in place, optionally providing a backup extension (maps to -i). + loop_assume_while: Assume 'while (<>) { ... }' loop around program (maps to -n). + loop_assume_while_print: Assume loop like -n but print line also (maps to -p). + unicode_features: Enable listed Unicode features (maps to -C). + record_separator: Specify record separator (maps to -0). + line_ending_processing: Enable line ending processing (maps to -l). + unsafe_operations: Allow unsafe operations (maps to -U). + ignore_text_before_perl_line: Ignore text before #!perl line, optionally cd to directory (maps to -x). + """ + cmd = ["perl"] + + # Handle switches + if record_separator is not None: + cmd.append(f"-0{record_separator}") + + if autosplit_mode: + cmd.append("-a") + + if unicode_features is not None: + cmd.append(f"-C{unicode_features}") + + if check_syntax_only: + cmd.append("-c") + + if one_liner: + cmd.append("-e") + cmd.append(one_liner) + elif use_all_features: + # Note: If both are provided, -E usually takes precedence or they are used together. + # Here we treat them as mutually exclusive for the primary code input. + cmd.append("-E") + cmd.append(use_all_features if isinstance(use_all_features, str) else "") + + if split_pattern: + cmd.append(f"-F{split_pattern}") + + if inplace_edit_extension is not None: + cmd.append(f"-i{inplace_edit_extension}") + + for directory in include_directories: + cmd.append(f"-I{directory}") + + if line_ending_processing is not None: + cmd.append(f"-l{line_ending_processing}") + + for module in modules: + cmd.append(f"-M{module}") + + if loop_assume_while: + cmd.append("-n") + + if loop_assume_while_print: + cmd.append("-p") + + if taint_warnings: + cmd.append("-t") + + if taint_checks: + cmd.append("-T") + + if unsafe_operations: + cmd.append("-U") + + if warning_mode == "useful": + cmd.append("-w") + elif warning_mode == "all": + cmd.append("-W") + elif warning_mode == "none": + cmd.append("-X") + + if ignore_text_before_perl_line is not None: + cmd.append(f"-x{ignore_text_before_perl_line}") + + # Handle program file and arguments + if program_file: + prog_path = Path(program_file) + if not prog_path.exists(): + return {"error": f"Program file not found: {program_file}"} + cmd.append(str(prog_path)) + + if arguments: + cmd.extend(arguments) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def perl_version_info( + config_variable: Optional[str] = None +): + """ + Print the Perl version, patchlevel, license, or configuration summary. + + Args: + config_variable: If provided, print the value of a specific Config.pm variable (maps to -V:variable). + If None, prints version and configuration summary. + """ + if config_variable: + cmd = ["perl", f"-V:{config_variable}"] + else: + cmd = ["perl", "-V"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def perl_test_warn_check( + test_code: str +): + """ + A specialized tool to run a snippet of Perl code specifically using Test::Warn. + This is useful for verifying if certain code blocks emit expected warnings. + + Args: + test_code: The Perl code snippet to test. It should ideally use Test::More and Test::Warn functions. + Example: 'use Test::More; use Test::Warn; warning_is { warn "foo" } "foo", "got foo warning"; done_testing();' + """ + # Ensure the code includes necessary modules if not present + if "use Test::Warn" not in test_code: + test_code = "use Test::Warn; " + test_code + if "use Test::More" not in test_code: + test_code = "use Test::More; " + test_code + if "done_testing" not in test_code: + test_code = test_code + "; done_testing();" + + cmd = ["perl", "-e", test_code] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Test failed or syntax error occurred.", + "status": "error" + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/perl-test-warn_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/perl-test-warn_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2e406fa97f7348a28bb4bc7329a7b25ce81828ca --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/perl-test-warn_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/perl-test-warn_server.py') +SERVER_NAME = 'biosci_perl_test_warn' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..f3db06bfdc2e1b9327248145ab86b4aa00720992 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-test-warn: + build: . + image: mcp-perl-test-warn:latest + container_name: mcp-perl-test-warn + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-test-warn + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e9bf0b1afeb3a81582154c969aa1a1898395041d --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-test-warn + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-test-warn/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3a8986669d7122c5b982c49a54fc7419ff1cc01e --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-tie-ixhash via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-tie-ixhash -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-tie-ixhash_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-tie-ixhash_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-tie-ixhash_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/perl-tie-ixhash_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/perl-tie-ixhash_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a15bffefd93c4a38fee02600cce1580a6fd2a8c4 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/perl-tie-ixhash_server.py @@ -0,0 +1,240 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Union +import tempfile + +@mcp.tool() +def perl_tie_ixhash_execute( + code: str, + input_files: Optional[List[str]] = None, + use_ixhash: bool = True, + autosplit: bool = False, + split_pattern: Optional[str] = None, + inplace_extension: Optional[str] = None, + line_ending_processing: bool = False, + assume_while_loop: bool = False, + assume_print_loop: bool = False, + warnings: bool = True, + all_warnings: bool = False, + include_dirs: Optional[List[str]] = None, + unicode_features: Optional[str] = None, +) -> dict: + """ + Execute Perl code (one-liner) with Tie::IxHash support. + + Args: + code: The Perl code to execute. + input_files: List of input files to process. + use_ixhash: If True, automatically prepends 'use Tie::IxHash;' to the code. + autosplit: Enable autosplit mode with -n or -p (splits $_ into @F). + split_pattern: Pattern for autosplit (-F). + inplace_extension: Edit files in place, optionally providing a backup extension. + line_ending_processing: Enable line ending processing (-l). + assume_while_loop: Assume "while (<>) { ... }" loop around program (-n). + assume_print_loop: Assume loop like -n but print line also (-p). + warnings: Enable many useful warnings (-w). + all_warnings: Enable all warnings (-W). + include_dirs: List of directories to add to @INC. + unicode_features: Enable listed Unicode features (-C). + """ + cmd = ["perl"] + + # Handle flags + if unicode_features: + cmd.append(f"-C{unicode_features}") + if warnings: + cmd.append("-w") + if all_warnings: + cmd.append("-W") + if autosplit: + cmd.append("-a") + if split_pattern: + cmd.append(f"-F{split_pattern}") + if inplace_extension is not None: + cmd.append(f"-i{inplace_extension}") + if line_ending_processing: + cmd.append("-l") + if assume_while_loop: + cmd.append("-n") + if assume_print_loop: + cmd.append("-p") + + if include_dirs: + for d in include_dirs: + cmd.append(f"-I{d}") + + # Prepare code + final_code = code + if use_ixhash: + final_code = "use Tie::IxHash; " + code + + cmd.extend(["-e", final_code]) + + # Handle input files + if input_files: + for f in input_files: + path = Path(f) + if not path.exists(): + return {"error": f"Input file not found: {f}"} + cmd.append(str(path)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def perl_tie_ixhash_run_script( + script_path: str, + args: Optional[List[str]] = None, + warnings: bool = True, + include_dirs: Optional[List[str]] = None, + taint_checks: bool = False, +) -> dict: + """ + Run a Perl script file that utilizes Tie::IxHash. + + Args: + script_path: Path to the Perl script (.pl). + args: Arguments to pass to the script. + warnings: Enable warnings (-w). + include_dirs: List of directories to add to @INC. + taint_checks: Enable tainting checks (-T). + """ + path = Path(script_path) + if not path.exists(): + return {"error": f"Script file not found: {script_path}"} + + cmd = ["perl"] + + if warnings: + cmd.append("-w") + if taint_checks: + cmd.append("-T") + if include_dirs: + for d in include_dirs: + cmd.append(f"-I{d}") + + cmd.append(str(path)) + + if args: + cmd.extend(args) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def perl_tie_ixhash_check_syntax( + script_path: str, + include_dirs: Optional[List[str]] = None, +) -> dict: + """ + Check the syntax of a Perl script without executing it. + + Args: + script_path: Path to the Perl script. + include_dirs: List of directories to add to @INC. + """ + path = Path(script_path) + if not path.exists(): + return {"error": f"Script file not found: {script_path}"} + + cmd = ["perl", "-c"] + if include_dirs: + for d in include_dirs: + cmd.append(f"-I{d}") + cmd.append(str(path)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "syntax_ok" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Syntax check failed", + "status": "syntax_error" + } + +@mcp.tool() +def perl_tie_ixhash_version_info() -> dict: + """ + Get Perl version and configuration information. + """ + try: + # Get basic version + v_result = subprocess.run(["perl", "-v"], capture_output=True, text=True, check=True) + # Check if Tie::IxHash is actually installed and get its version + ix_check = subprocess.run( + ["perl", "-MTie::IxHash", "-e", "print $Tie::IxHash::VERSION"], + capture_output=True, text=True + ) + + return { + "perl_version": v_result.stdout, + "tie_ixhash_version": ix_check.stdout if ix_check.returncode == 0 else "Not found", + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "error": str(e), + "stderr": e.stderr, + "status": "error" + } + +@mcp.tool() +def perl_tie_ixhash_example_usage() -> dict: + """ + Returns a code example showing how to use Tie::IxHash in Perl. + """ + example_code = """ +use Tie::IxHash; + +# Tie a hash to Tie::IxHash to maintain insertion order +tie my %hash, 'Tie::IxHash'; + +$hash{first} = 1; +$hash{second} = 2; +$hash{third} = 3; + +# Keys will be printed in the order they were inserted +foreach my $key (keys %hash) { + print "$key: $hash{$key}\\n"; +} +""" + return { + "example_perl_code": example_code, + "description": "This example demonstrates how to maintain key order in a Perl hash using Tie::IxHash." + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/perl-tie-ixhash_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/perl-tie-ixhash_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a2a4235a18389aded507ef3d4440ce933772d112 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/perl-tie-ixhash_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/perl-tie-ixhash_server.py') +SERVER_NAME = 'biosci_perl_tie_ixhash' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..555921a48b6e71df18ab1dea8a8fd96c8061e171 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-tie-ixhash: + build: . + image: mcp-perl-tie-ixhash:latest + container_name: mcp-perl-tie-ixhash + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-tie-ixhash + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1f7a590267a8087d582178dbca6a214ab59f88e8 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-tie-ixhash + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-tie-ixhash/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..25a0db90f0ee86f710258070adcbaecd574ff13d --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-www-robotrules via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-www-robotrules -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-www-robotrules_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-www-robotrules_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-www-robotrules_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/perl-www-robotrules_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/perl-www-robotrules_server.py new file mode 100644 index 0000000000000000000000000000000000000000..af8b795a8e1e8eb8b02c5c1532d2ba921c940a98 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/perl-www-robotrules_server.py @@ -0,0 +1,181 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, Dict, Any + +@mcp.tool() +def check_robots_url_allowed( + robots_txt_content: str, + user_agent: str, + url_to_check: str, + base_url: str = "http://localhost", +) -> Dict[str, Any]: + """ + Checks if a specific URL is allowed for a given user agent based on robots.txt content + using the Perl WWW::RobotRules module. + + Args: + robots_txt_content: The raw text content of the robots.txt file. + user_agent: The name of the robot/user-agent to check (e.g., 'Googlebot'). + url_to_check: The full URL or path to check for access permissions. + base_url: The base URL where the robots.txt is hosted (required by the parser). + """ + # Input validation + if not robots_txt_content.strip(): + return {"error": "robots_txt_content cannot be empty"} + if not user_agent.strip(): + return {"error": "user_agent cannot be empty"} + if not url_to_check.strip(): + return {"error": "url_to_check cannot be empty"} + + # Create a temporary file for the robots.txt content to avoid shell escaping issues + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as tmp: + tmp.write(robots_txt_content) + tmp_path = tmp.name + + # Construct the Perl script + # WWW::RobotRules->new($ua_name) + # $rules->parse($base_url, $content) + # $rules->allowed($url) + perl_script = f""" +use WWW::RobotRules; +my $ua = '{user_agent}'; +my $base = '{base_url}'; +my $url = '{url_to_check}'; +my $rules = WWW::RobotRules->new($ua); + +open(my $fh, '<', '{tmp_path}') or die "Could not open temp file: $!"; +my $content = do {{ local $/; <$fh> }}; +close($fh); + +$rules->parse($base, $content); +if ($rules->allowed($url)) {{ + print "ALLOWED"; +}} else {{ + print "DISALLOWED"; +}} +""" + + try: + result = subprocess.run( + ["perl", "-e", perl_script], + capture_output=True, + text=True, + check=True + ) + + is_allowed = result.stdout.strip() == "ALLOWED" + + return { + "command_executed": f"perl -e 'WWW::RobotRules->allowed(...)'", + "is_allowed": is_allowed, + "status": result.stdout.strip(), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "error": "Perl execution failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": e.cmd + } + finally: + # Cleanup + if Path(tmp_path).exists(): + Path(tmp_path).unlink() + +@mcp.tool() +def check_robots_url_allowed_from_file( + robots_txt_path: str, + user_agent: str, + url_to_check: str, + base_url: str = "http://localhost", +) -> Dict[str, Any]: + """ + Checks if a specific URL is allowed for a given user agent using a local robots.txt file. + + Args: + robots_txt_path: Path to the local robots.txt file. + user_agent: The name of the robot/user-agent to check. + url_to_check: The full URL or path to check for access permissions. + base_url: The base URL where the robots.txt is conceptually hosted. + """ + path_obj = Path(robots_txt_path) + if not path_obj.exists(): + return {"error": f"File not found: {robots_txt_path}"} + + try: + content = path_obj.read_text() + return check_robots_url_allowed( + robots_txt_content=content, + user_agent=user_agent, + url_to_check=url_to_check, + base_url=base_url + ) + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def get_robots_txt_agent_string( + user_agent: str +) -> Dict[str, Any]: + """ + Returns the agent string that the WWW::RobotRules parser will use for identification. + This is useful for verifying how the Perl module identifies your crawler. + + Args: + user_agent: The name of the robot/user-agent. + """ + perl_script = f""" +use WWW::RobotRules; +my $rules = WWW::RobotRules->new('{user_agent}'); +print $rules->agent(); +""" + try: + result = subprocess.run( + ["perl", "-e", perl_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": "perl -e 'WWW::RobotRules->agent()'", + "agent_string": result.stdout.strip(), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "error": "Perl execution failed", + "stderr": e.stderr + } + +@mcp.tool() +def validate_perl_robotrules_environment() -> Dict[str, Any]: + """ + Verifies that Perl and the WWW::RobotRules module are correctly installed in the environment. + """ + perl_script = "use WWW::RobotRules; print 'OK';" + try: + result = subprocess.run( + ["perl", "-e", perl_script], + capture_output=True, + text=True, + check=True + ) + return { + "status": "Ready", + "module_found": result.stdout.strip() == "OK", + "perl_version": subprocess.run(["perl", "-v"], capture_output=True, text=True).stdout.splitlines()[1] + } + except subprocess.CalledProcessError: + return { + "status": "Error", + "message": "WWW::RobotRules module not found. Please install perl-www-robotrules." + } + except FileNotFoundError: + return { + "status": "Error", + "message": "Perl interpreter not found in PATH." + } diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/perl-www-robotrules_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/perl-www-robotrules_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e1e8a5864e8def63072f293dfa6607483fd058c4 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/perl-www-robotrules_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/perl-www-robotrules_server.py') +SERVER_NAME = 'biosci_perl_www_robotrules' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..e92d138122bf09afea98701d4914441bb0abd469 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-www-robotrules: + build: . + image: mcp-perl-www-robotrules:latest + container_name: mcp-perl-www-robotrules + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-www-robotrules + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ec69db11a386bcf3fea1cf062fd0d0117813066c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-www-robotrules + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-www-robotrules/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-xml-dom-xpath/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-xml-dom-xpath/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-xml-dom-xpath/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..821e28ab9fc7a2248d3b6c99215247da8b11d037 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install proteomiqon-peptidedb via conda (e.g., from bioconda) +RUN conda install -c bioconda proteomiqon-peptidedb -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/proteomiqon-peptidedb_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/proteomiqon-peptidedb_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/proteomiqon-peptidedb_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/proteomiqon-peptidedb_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/proteomiqon-peptidedb_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6b15d62a81895a41cfcaaa912a85e21687404674 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/proteomiqon-peptidedb_server.py @@ -0,0 +1,166 @@ +import subprocess +import json +import os +from pathlib import Path +from typing import List, Optional, Literal +import tempfile + +@mcp.tool() +def create_peptide_database( + input_fasta_file: Path, + output_directory: Path, + name: str = "YourNameHere", + parse_protein_id_regex_pattern: str = "id", + protease: Literal["Trypsin"] = "Trypsin", + min_missed_cleavages: int = 0, + max_missed_cleavages: int = 2, + max_mass: float = 15000.0, + min_pep_length: int = 4, + max_pep_length: int = 65, + isotopic_mod: List[Literal["N15"]] = ["N15"], + mass_mode: Literal["Monoisotopic", "Average"] = "Monoisotopic", + fixed_mods: List[Literal[ + "Acetylation'ProtNTerm'", + "Carbamidomethyl'Cys'", + "Oxidation'Met'", + "Phosphorylation'Ser'Thr'Tyr'", + "Pyro_Glu'GluNterm'", + "Pyro_Glu'GlnNterm'" + ]] = [], + variable_mods: List[Literal[ + "Acetylation'ProtNTerm'", + "Carbamidomethyl'Cys'", + "Oxidation'Met'", + "Phosphorylation'Ser'Thr'Tyr'", + "Pyro_Glu'GluNterm'", + "Pyro_Glu'GlnNterm'" + ]] = ["Oxidation'Met'", "Acetylation'ProtNTerm'"], + var_mod_threshold: int = 4, +) -> dict: + """ + Creates a peptide database in SQLite format from FASTA proteome information + using ProteomIQon.PeptideDB. + + Parameters are provided via a JSON file, which is generated internally based + on the Python function arguments. + + Args: + input_fasta_file: Path to the input proteome FASTA file. + output_directory: Path to the directory where the SQLite peptide database + will be created. + name: Name of the database. + parse_protein_id_regex_pattern: Regex pattern for parsing protein IDs in the database. + protease: Protease used for the digestion of the proteins. Currently, only "Trypsin" is supported. + min_missed_cleavages: Minimal amount of missed cleavages a peptide can have. + max_missed_cleavages: Maximal amount of missed cleavages a peptide can have. + max_mass: Maximal mass of a peptide in Daltons. + min_pep_length: Minimal length of a peptide. + max_pep_length: Maximal length of a peptide. + isotopic_mod: List of isotopic modifications in the experiment. Currently, only "N15" is supported. + mass_mode: Method for mass calculation. + fixed_mods: List of fixed modifications of the proteins. + variable_mods: List of variable modifications of the proteins. + var_mod_threshold: Threshold for variable modifications. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any + generated output files. + """ + # Input validation + if not input_fasta_file.is_file(): + raise ValueError(f"Input FASTA file not found: {input_fasta_file}") + + if not output_directory.exists(): + output_directory.mkdir(parents=True, exist_ok=True) + if not output_directory.is_dir(): + raise ValueError(f"Output path is not a directory: {output_directory}") + + if min_missed_cleavages < 0: + raise ValueError("min_missed_cleavages must be non-negative.") + if max_missed_cleavages < min_missed_cleavages: + raise ValueError("max_missed_cleavages cannot be less than min_missed_cleavages.") + if max_mass <= 0: + raise ValueError("max_mass must be positive.") + if min_pep_length <= 0: + raise ValueError("min_pep_length must be positive.") + if max_pep_length < min_pep_length: + raise ValueError("max_pep_length cannot be less than min_pep_length.") + if var_mod_threshold < 0: + raise ValueError("var_mod_threshold must be non-negative.") + + # Construct parameters dictionary for JSON serialization + params = { + "Name": name, + "ParseProteinIDRegexPattern": parse_protein_id_regex_pattern, + "Protease": protease, + "MinMissedCleavages": min_missed_cleavages, + "MaxMissedCleavages": max_missed_cleavages, + "MaxMass": max_mass, + "MinPepLength": min_pep_length, + "MaxPepLength": max_pep_length, + "IsotopicMod": isotopic_mod, + "MassMode": mass_mode, + "FixedMods": fixed_mods, + "VariableMods": variable_mods, + "VarModThreshold": var_mod_threshold, + } + + temp_params_file: Optional[Path] = None + try: + # Create a temporary JSON file for parameters + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f: + json.dump(params, f, indent=4) + temp_params_file = Path(f.name) + + # Construct the command + command = [ + "proteomiqon-peptidedb", + "-i", str(input_fasta_file), + "-o", str(output_directory), + "-p", str(temp_params_file), + ] + + # Execute the command + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + + stdout = process.stdout + stderr = process.stderr + + # The tool creates a SQLite database in the output directory. + # The exact name is not specified, but it's usually derived from the input or 'Name' parameter. + # For now, we'll assume it's a .sqlite file in the output directory. + # A more robust solution might involve parsing stdout for the exact filename. + output_files = [str(f) for f in output_directory.glob("*.sqlite")] + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Tool execution failed with exit code {e.returncode}", + "output_files": [], + } + except Exception as e: + return { + "command_executed": " ".join(command) if 'command' in locals() else "N/A", + "stdout": "", + "stderr": str(e), + "error": "An unexpected error occurred", + "output_files": [], + } + finally: + # Clean up the temporary parameters file + if temp_params_file and temp_params_file.exists(): + os.remove(temp_params_file) \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/proteomiqon-peptidedb_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/proteomiqon-peptidedb_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..742f71babed8aa2371865e8f20d246dbeb15763a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/proteomiqon-peptidedb_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/proteomiqon-peptidedb_server.py') +SERVER_NAME = 'biosci_proteomiqon_peptidedb' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..6d339b247891e009ea5b07eec1f4b1ccef41e3cf --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-proteomiqon-peptidedb: + build: . + image: mcp-proteomiqon-peptidedb:latest + container_name: mcp-proteomiqon-peptidedb + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=proteomiqon-peptidedb + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..21c204dd7433ce319a4c838e0f9e56969be62320 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - proteomiqon-peptidedb + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_proteomiqon-peptidedb/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c2b444bf226db039b2628764dbac5b22f0e4693d --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install pubchempy via conda (e.g., from bioconda) +RUN conda install -c bioconda pubchempy -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/pubchempy_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/pubchempy_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/pubchempy_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/pubchempy_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/pubchempy_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a2240a27200ef7aba445114d6c944ca297709f4d --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/pubchempy_server.py @@ -0,0 +1,337 @@ +import subprocess +import json +import sys +from pathlib import Path +from typing import Optional, List, Union + +@mcp.tool() +def pubchem_get_compounds( + identifier: str, + namespace: str = "name", + searchtype: Optional[str] = None, + threshold: int = 90, + max_results: int = 10, +): + """ + Search the PubChem Compound database and return detailed compound records. + + Args: + identifier: The search query (e.g., "Aspirin", "CC(=O)OC1=CC=CC=C1C(=O)O", "CID2244"). + namespace: The type of identifier provided ('name', 'smiles', 'inchi', 'inchikey', 'formula', 'cid'). + searchtype: Optional search type for structural searches ('substructure', 'similarity', 'superstructure', 'identity'). + threshold: Similarity threshold (0-100) used when searchtype is 'similarity'. + max_results: Maximum number of compound records to return. + """ + if threshold < 0 or threshold > 100: + return {"error": "Threshold must be between 0 and 100"} + if max_results <= 0: + return {"error": "max_results must be a positive integer"} + + # Construct Python script to execute pubchempy + python_script = f""" +import pubchempy as pcp +import json +import sys + +try: + results = pcp.get_compounds( + identifier={repr(identifier)}, + namespace={repr(namespace)}, + searchtype={repr(searchtype)}, + threshold={threshold} + ) + # Convert compound objects to dictionaries and limit results + output = [c.to_dict() for c in results[:{max_results}]] + print(json.dumps(output)) +except Exception as e: + print(str(e), file=sys.stderr) + sys.exit(1) +""" + try: + process = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pubchempy.get_compounds({identifier}, {namespace}, ...)", + "stdout": process.stdout, + "stderr": process.stderr, + "results": json.loads(process.stdout) if process.stdout.strip() else [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": "pubchempy.get_compounds", + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Failed to retrieve compounds from PubChem" + } + +@mcp.tool() +def pubchem_get_substances( + identifier: str, + namespace: str = "name", + max_results: int = 10, +): + """ + Search the PubChem Substance database. Substances are often less processed than Compounds. + + Args: + identifier: The search query (e.g., "Glucose", "SID12345"). + namespace: The type of identifier ('name', 'sid'). + max_results: Maximum number of substance records to return. + """ + python_script = f""" +import pubchempy as pcp +import json +import sys + +try: + results = pcp.get_substances( + identifier={repr(identifier)}, + namespace={repr(namespace)} + ) + output = [s.to_dict() for s in results[:{max_results}]] + print(json.dumps(output)) +except Exception as e: + print(str(e), file=sys.stderr) + sys.exit(1) +""" + try: + process = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pubchempy.get_substances({identifier}, {namespace})", + "stdout": process.stdout, + "stderr": process.stderr, + "results": json.loads(process.stdout) if process.stdout.strip() else [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": "pubchempy.get_substances", + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Failed to retrieve substances from PubChem" + } + +@mcp.tool() +def pubchem_get_properties( + identifiers: str, + properties: str, + namespace: str = "cid", +): + """ + Retrieve specific calculated properties for a list of PubChem identifiers. + + Args: + identifiers: Comma-separated list of identifiers (e.g., "2244,1234" or "Aspirin,Caffeine"). + properties: Comma-separated list of property names (e.g., "MolecularFormula,MolecularWeight,XLogP,CanonicalSMILES"). + namespace: The type of identifiers provided ('cid', 'name', 'smiles', 'inchi', 'inchikey'). + """ + # Split strings into lists for the Python script + id_list = [i.strip() for i in identifiers.split(",")] + prop_list = [p.strip() for p in properties.split(",")] + + python_script = f""" +import pubchempy as pcp +import json +import sys + +try: + # get_properties returns a list of dictionaries + results = pcp.get_properties( + properties={repr(prop_list)}, + identifier={repr(id_list)}, + namespace={repr(namespace)} + ) + print(json.dumps(results)) +except Exception as e: + print(str(e), file=sys.stderr) + sys.exit(1) +""" + try: + process = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pubchempy.get_properties({properties}, {identifiers})", + "stdout": process.stdout, + "stderr": process.stderr, + "properties": json.loads(process.stdout) if process.stdout.strip() else [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": "pubchempy.get_properties", + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Failed to retrieve properties from PubChem" + } + +@mcp.tool() +def pubchem_get_synonyms( + identifier: str, + namespace: str = "name", +): + """ + Retrieve all known synonyms (trade names, IUPAC names, CAS numbers) for a given identifier. + + Args: + identifier: The search query (e.g., "Aspirin" or "2244"). + namespace: The type of identifier ('name', 'cid', 'sid'). + """ + python_script = f""" +import pubchempy as pcp +import json +import sys + +try: + # get_synonyms returns a list of dictionaries with 'CID' and 'Synonym' keys + results = pcp.get_synonyms( + identifier={repr(identifier)}, + namespace={repr(namespace)} + ) + print(json.dumps(results)) +except Exception as e: + print(str(e), file=sys.stderr) + sys.exit(1) +""" + try: + process = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pubchempy.get_synonyms({identifier})", + "stdout": process.stdout, + "stderr": process.stderr, + "synonyms": json.loads(process.stdout) if process.stdout.strip() else [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": "pubchempy.get_synonyms", + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Failed to retrieve synonyms from PubChem" + } + +@mcp.tool() +def pubchem_download( + identifier: str, + output_path: str, + outformat: str = "SDF", + namespace: str = "cid", + overwrite: bool = False, +): + """ + Download compound or substance records from PubChem in various file formats. + + Args: + identifier: The search query or identifier. + output_path: Local path where the file should be saved. + outformat: The file format ('SDF', 'JSON', 'XML', 'ASNT', 'ASNB', 'CSV', 'PNG'). + namespace: The type of identifier ('cid', 'sid', 'name', 'smiles', etc.). + overwrite: Whether to overwrite the file if it already exists. + """ + path_obj = Path(output_path) + if path_obj.exists() and not overwrite: + return {"error": f"File {output_path} already exists. Set overwrite=True to replace it."} + + python_script = f""" +import pubchempy as pcp +import sys + +try: + pcp.download( + outformat={repr(outformat)}, + path={repr(str(path_obj))}, + identifier={repr(identifier)}, + namespace={repr(namespace)}, + overwrite={overwrite} + ) + print(f"Successfully downloaded to {repr(str(path_obj))}") +except Exception as e: + print(str(e), file=sys.stderr) + sys.exit(1) +""" + try: + process = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pubchempy.download({outformat}, {output_path}, ...)", + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(path_obj)] if path_obj.exists() else [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": "pubchempy.download", + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Failed to download data from PubChem" + } + +@mcp.tool() +def pubchem_get_cids( + identifier: str, + namespace: str = "name", + searchtype: Optional[str] = None, +): + """ + Get a list of PubChem Compound Identifiers (CIDs) for a given input. + + Args: + identifier: The search query (e.g., "Aspirin"). + namespace: The type of identifier ('name', 'smiles', 'inchi', 'formula'). + searchtype: Optional search type ('substructure', 'similarity', 'superstructure', 'identity'). + """ + python_script = f""" +import pubchempy as pcp +import json +import sys + +try: + # get_cids returns a list of integers + results = pcp.get_cids( + identifier={repr(identifier)}, + namespace={repr(namespace)}, + searchtype={repr(searchtype)} + ) + print(json.dumps(results)) +except Exception as e: + print(str(e), file=sys.stderr) + sys.exit(1) +""" + try: + process = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pubchempy.get_cids({identifier})", + "stdout": process.stdout, + "stderr": process.stderr, + "cids": json.loads(process.stdout) if process.stdout.strip() else [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": "pubchempy.get_cids", + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Failed to retrieve CIDs from PubChem" + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/pubchempy_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/pubchempy_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f8602a98036ac18b0169cad4a09901f3a2e3d9e8 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/pubchempy_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/pubchempy_server.py') +SERVER_NAME = 'biosci_pubchempy' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..60421cba7e6fa2fbb1d55c8c9680257b341d3625 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-pubchempy: + build: . + image: mcp-pubchempy:latest + container_name: mcp-pubchempy + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=pubchempy + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e556ac2600e96e60945810715ddced604aea0729 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - pubchempy + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pubchempy/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pybigwig/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pybigwig/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8870b587f69b6f459956378a5ed96811e05c1598 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_pybigwig/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install pybigwig via conda (e.g., from bioconda) +RUN conda install -c bioconda pybigwig -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/pybigwig_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/pybigwig_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/pybigwig_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e9cb36742ecb3e7722c7286aa4f5dfe8c2ee27ad --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install r-acidbase via conda (e.g., from bioconda) +RUN conda install -c bioconda r-acidbase -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/r-acidbase_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/r-acidbase_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/r-acidbase_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/r-acidbase_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/r-acidbase_server.py new file mode 100644 index 0000000000000000000000000000000000000000..bc6d168e2e6b635b2296b20244b48a0939ba115d --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/r-acidbase_server.py @@ -0,0 +1,224 @@ +import subprocess +import json +from pathlib import Path +from typing import Optional, List, Union, Dict +import tempfile + +def _run_r_command(r_code: str) -> dict: + """ + Helper function to execute R code via Rscript. + """ + try: + # Ensure AcidBase is loaded + full_code = f"suppressPackageStartupMessages(library(AcidBase)); {r_code}" + result = subprocess.run( + ["Rscript", "-e", full_code], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"Rscript -e '{full_code}'", + "stdout": result.stdout.strip(), + "stderr": result.stderr.strip(), + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": f"Rscript -e '{r_code}'", + "stdout": e.stdout.strip() if e.stdout else "", + "stderr": e.stderr.strip() if e.stderr else str(e), + "status": "error" + } + except FileNotFoundError: + return { + "command_executed": "Rscript", + "stdout": "", + "stderr": "Rscript not found. Please ensure R is installed and in your PATH.", + "status": "error" + } + +@mcp.tool() +def detect_organism( + query: str +): + """ + Detect the organism from a character string (e.g., gene symbols, chromosome names, or Latin names). + + Args: + query: The string to analyze (e.g., 'Homo sapiens', 'ENSG000001', 'chr1'). + """ + # Input validation + if not query: + return {"error": "Query string cannot be empty"} + + r_code = f'cat(as.character(detectOrganism("{query}")))' + return _run_r_command(r_code) + +@mcp.tool() +def match_genes( + query: List[str], + subject: List[str] +): + """ + Match gene identifiers against a reference subject list. + + Args: + query: List of gene identifiers to match. + subject: List of reference gene identifiers. + """ + if not query or not subject: + return {"error": "Query and subject lists cannot be empty"} + + # Format lists for R vector syntax + query_r = "c(" + ",".join([f'"{q}"' for q in query]) + ")" + subject_r = "c(" + ",".join([f'"{s}"' for s in subject]) + ")" + + r_code = f'print(matchGenes(x = {query_r}, table = {subject_r}))' + return _run_r_command(r_code) + +@mcp.tool() +def init_project( + path: str, + force: bool = False +): + """ + Initialize a standard Acid Genomics project directory structure. + + Args: + path: Directory path where the project should be initialized. + force: Whether to overwrite existing files. + """ + project_path = Path(path) + # Validation + if not project_path.parent.exists(): + return {"error": f"Parent directory {project_path.parent} does not exist"} + + force_r = "TRUE" if force else "FALSE" + r_code = f'initProject(path = "{str(project_path)}", force = {force_r})' + return _run_r_command(r_code) + +@mcp.tool() +def check_dependencies( + packages: List[str] +): + """ + Check if required R packages are installed and meet version requirements. + + Args: + packages: List of R package names to check. + """ + if not packages: + return {"error": "Package list cannot be empty"} + + pkgs_r = "c(" + ",".join([f'"{p}"' for p in packages]) + ")" + r_code = f'checkDependencies({pkgs_r})' + return _run_r_command(r_code) + +@mcp.tool() +def render_report( + input_file: str, + output_dir: Optional[str] = None, + quiet: bool = False +): + """ + Render an RMarkdown report using Acid Genomics templates. + + Args: + input_file: Path to the .Rmd file. + output_dir: Directory to save the rendered report. + quiet: Whether to suppress RMarkdown rendering output. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} not found"} + + out_dir_arg = f'"{output_dir}"' if output_dir else "NULL" + quiet_r = "TRUE" if quiet else "FALSE" + + r_code = f'renderReport(input = "{str(input_path)}", outputDir = {out_dir_arg}, quiet = {quiet_r})' + return _run_r_command(r_code) + +@mcp.tool() +def is_url( + path: str +): + """ + Check if a string is a valid URL. + + Args: + path: The string to check. + """ + r_code = f'cat(isUrl("{path}"))' + return _run_r_command(r_code) + +@mcp.tool() +def is_existing_file( + path: str +): + """ + Check if a file exists and is not a directory. + + Args: + path: File path to check. + """ + r_code = f'cat(isExistingFile("{path}"))' + return _run_r_command(r_code) + +@mcp.tool() +def get_session_info(): + """ + Get the current R session information including AcidBase version and dependencies. + """ + r_code = 'print(sessionInfo())' + return _run_r_command(r_code) + +@mcp.tool() +def acidbase_system_check(): + """ + Run low-level system environment checks (OS, GUI, ANSI support). + """ + r_code = ( + 'cat(paste0("OS: ", .Platform$OS.type, "\\n", ' + '"Unix: ", isUnix(), "\\n", ' + '"Windows: ", isWindows(), "\\n", ' + '"Mac: ", isMac(), "\\n", ' + '"ANSI Support: ", isAnsiSupported(), "\\n", ' + '"Interactive: ", isInteractive()))' + ) + return _run_r_command(r_code) + +@mcp.tool() +def make_names( + names: List[str], + unique: bool = True +): + """ + Make syntactically valid names for R (cleaning up strings for column names, etc.). + + Args: + names: List of strings to clean. + unique: Whether to ensure the resulting names are unique. + """ + if not names: + return {"error": "Names list cannot be empty"} + + names_r = "c(" + ",".join([f'"{n}"' for n in names]) + ")" + unique_r = "TRUE" if unique else "FALSE" + r_code = f'cat(makeNames({names_r}, unique = {unique_r}), sep = ", ")' + return _run_r_command(r_code) + +@mcp.tool() +def paste_url( + base_url: str, + path: str +): + """ + Safely concatenate a base URL and a path, ensuring correct slash handling. + + Args: + base_url: The base URL (e.g., 'https://acidgenomics.com'). + path: The path to append. + """ + r_code = f'cat(pasteURL("{base_url}", "{path}"))' + return _run_r_command(r_code) \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/r-acidbase_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/r-acidbase_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9444bf11193ab0b870ec8d8e1df2c319549ca87f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/r-acidbase_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/r-acidbase_server.py') +SERVER_NAME = 'biosci_r_acidbase' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..42081d51e349cec76287749d056a820b006c2091 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-r-acidbase: + build: . + image: mcp-r-acidbase:latest + container_name: mcp-r-acidbase + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=r-acidbase + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ec2a69adbb27db017ab3e62b538f4178f0f480aa --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - r-acidbase + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-acidbase/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4fe16d19726de622200dc23a9d15203ecb07ea36 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install r-sceasy via conda (e.g., from bioconda) +RUN conda install -c bioconda r-sceasy -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/r-sceasy_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/r-sceasy_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/r-sceasy_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/app/r-sceasy_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/app/r-sceasy_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8cdcab3d0a8a06821567e33eb4ed2f04f0c20c45 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/app/r-sceasy_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/app/r-sceasy_server.py') +SERVER_NAME = 'biosci_r_sceasy' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c5a5d80ed8a3179820255a84a492c79becccf0e0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-r-sceasy: + build: . + image: mcp-r-sceasy:latest + container_name: mcp-r-sceasy + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=r-sceasy + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a24c7819a38a3eae96ae31e8c20eae98c006647c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - r-sceasy + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_r-sceasy/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9c0fb300cedafcd8356f1e1197a2c780ba55da9f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install repeatmasker via conda (e.g., from bioconda) +RUN conda install -c bioconda repeatmasker -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/repeatmasker_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/repeatmasker_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/repeatmasker_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/repeatmasker_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/repeatmasker_server.py new file mode 100644 index 0000000000000000000000000000000000000000..eb6ce2e5c82ef3b45eef3554afb44a9c2e30b01a --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/repeatmasker_server.py @@ -0,0 +1,238 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, Literal, List, Dict, Any + +# No need to import mcp as per instructions + +def _run_command(cmd: List[str], cwd: Optional[Path] = None) -> Dict[str, Any]: + """ + Executes a shell command and captures its output. + """ + try: + process = subprocess.run( + cmd, + cwd=cwd, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "returncode": process.returncode, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "returncode": e.returncode, + "error": str(e), + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Error: repeatmasker executable not found. Please ensure it is in your PATH.", + "returncode": 127, + "error": "Executable not found", + } + +@mcp.tool() +def repeatmasker( + input_fasta: Path, + output_dir: Optional[Path] = None, + custom_library: Optional[Path] = None, + species: Optional[str] = None, + search_engine: Optional[Literal["crossmatch", "rmblast", "wublast", "abblast", "hmmer"]] = None, + gff3_output: bool = False, + alignment_output: bool = False, + uncurated: bool = False, + soft_masking: bool = False, +) -> Dict[str, Any]: + """ + Screens DNA sequences for interspersed repeats and low complexity DNA sequences. + + Args: + input_fasta: Path to the input DNA sequence file in FASTA format. + output_dir: Optional path to the directory where output files will be written. + If not provided, outputs will be written to the current working directory. + custom_library: Optional path to a custom repeat library file in FASTA format. + If provided, RepeatMasker will use this library instead of the default. + species: Optional species name to guide the repeat search. E.g., "human", "mouse", "mammals". + This option was mentioned in release notes regarding its interpretation. + search_engine: Optional search engine to use. If not specified, RepeatMasker will use its configured default. + Supported engines: "crossmatch", "rmblast", "wublast", "abblast", "hmmer". + This option was mentioned in release notes regarding configuration. + gff3_output: If True, generate output in GFF3 format in addition to the default formats. + This option was mentioned in release notes for version 4.1.3. + alignment_output: If True, generate a detailed alignment file (*.align) in addition to other outputs. + This option was mentioned in release notes for version 4.2.1. + uncurated: If True, include uncurated Dfam families in the search (requires Dfam database). + This flag was mentioned in release notes for version 4.1.6. + soft_masking: If True, softmask repetitive sequences in the output FASTA file. + This feature was mentioned in release notes for version 4.1.6 regarding NCBIBlastSearchEngine.pm. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of generated output files. + """ + # Input validation + if not input_fasta.exists(): + raise ValueError(f"Input FASTA file not found: {input_fasta}") + if not input_fasta.is_file(): + raise ValueError(f"Input FASTA path is not a file: {input_fasta}") + + if custom_library: + if not custom_library.exists(): + raise ValueError(f"Custom library file not found: {custom_library}") + if not custom_library.is_file(): + raise ValueError(f"Custom library path is not a file: {custom_library}") + + # Determine output directory + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + else: + output_dir = Path.cwd() + + cmd = ["RepeatMasker"] + + # Add input file + cmd.append(str(input_fasta)) + + # Add optional parameters + if output_dir: + cmd.extend(["-dir", str(output_dir)]) # Mentioned in 4.0.9 bugfix + if custom_library: + cmd.extend(["-lib", str(custom_library)]) # Explicitly mentioned in installation section + if species: + cmd.extend(["-species", species]) # Explicitly mentioned in release notes + if search_engine: + cmd.extend(["-engine", search_engine]) # Explicitly mentioned in release notes + if gff3_output: + cmd.append("-gff3") # Explicitly mentioned in 4.1.3 release notes + if alignment_output: + cmd.append("-a") # Explicitly mentioned in 4.2.1 release notes + if uncurated: + cmd.append("--uncurated") # Explicitly mentioned in 4.1.6 release notes + if soft_masking: + cmd.append("-s") # Implied by "Added softmasking support to NCBIBlastSearchEngine.pm" in 4.1.6 + + # Execute the command + result = _run_command(cmd) + + # Collect output files + output_files = [] + if result.get("returncode") == 0: + base_name = input_fasta.name + + # Remove common compression suffixes for base name + if base_name.endswith((".gz", ".bgz")): + base_name = base_name[:-3] + elif base_name.endswith(".bz2"): + base_name = base_name[:-4] + + # Remove common sequence file extensions (e.g., .fasta, .fa, .fna) + # This assumes the last dot-separated part is the extension + parts = base_name.split(".") + if len(parts) > 1 and parts[-1].lower() in ["fasta", "fa", "fna"]: + base_name = ".".join(parts[:-1]) + + # Expected output files based on RepeatMasker's typical behavior + expected_suffixes = [".out", ".masked", ".tbl"] + if alignment_output: + expected_suffixes.append(".align") + if gff3_output: + expected_suffixes.append(".gff") # RepeatMasker typically generates .gff for -gff3 + + for suffix in expected_suffixes: + output_file = output_dir / (base_name + suffix) + if output_file.exists(): + output_files.append(str(output_file)) + + # RepeatMasker also creates a log file + log_file = output_dir / (base_name + ".log") + if log_file.exists(): + output_files.append(str(log_file)) + + result["output_files"] = output_files + return result + +@mcp.tool() +def repeat_protein_mask( + input_fasta: Path, + output_dir: Optional[Path] = None, + search_engine: Optional[Literal["rmblastx", "wublastx", "hmmer"]] = None, +) -> Dict[str, Any]: + """ + Screens DNA sequences for repeats using protein-level homology. + + Args: + input_fasta: Path to the input DNA sequence file in FASTA format. + output_dir: Optional path to the directory where output files will be written. + If not provided, outputs will be written to the current working directory. + search_engine: Optional search engine to use for protein masking. + Supported engines: "rmblastx", "wublastx", "hmmer". + Inferred from documentation mentioning `NCBIBlastXSearchEngine.pm`, + `WUBlastXSearchEngine.pm` in the GitHub repository, and `HMMER` as a general engine. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of generated output files. + """ + # Input validation + if not input_fasta.exists(): + raise ValueError(f"Input FASTA file not found: {input_fasta}") + if not input_fasta.is_file(): + raise ValueError(f"Input FASTA path is not a file: {input_fasta}") + + # Determine output directory + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + else: + output_dir = Path.cwd() + + cmd = ["RepeatProteinMask"] + + # Add input file + cmd.append(str(input_fasta)) + + # Add optional parameters + if output_dir: + cmd.extend(["-dir", str(output_dir)]) + if search_engine: + cmd.extend(["-engine", search_engine]) # Mentioned in 4.1.7 release notes + + # Execute the command + result = _run_command(cmd) + + # Collect output files + output_files = [] + if result.get("returncode") == 0: + base_name = input_fasta.name + + # Remove common compression suffixes for base name + if base_name.endswith((".gz", ".bgz")): + base_name = base_name[:-3] + elif base_name.endswith(".bz2"): + base_name = base_name[:-4] + + # Remove common sequence file extensions (e.g., .fasta, .fa, .fna) + parts = base_name.split(".") + if len(parts) > 1 and parts[-1].lower() in ["fasta", "fa", "fna"]: + base_name = ".".join(parts[:-1]) + + # RepeatProteinMask typically produces .out, .masked, .tbl + expected_suffixes = [".out", ".masked", ".tbl"] + + for suffix in expected_suffixes: + output_file = output_dir / (base_name + suffix) + if output_file.exists(): + output_files.append(str(output_file)) + + log_file = output_dir / (base_name + ".log") + if log_file.exists(): + output_files.append(str(log_file)) + + result["output_files"] = output_files + return result \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/repeatmasker_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/repeatmasker_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6a2c406729ab1909ffe19c3bc66ca43e5542b3ac --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/repeatmasker_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/repeatmasker_server.py') +SERVER_NAME = 'biosci_repeatmasker' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c13ac7c6a69edd05d7168b3afcf4db9fa3a1f60c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-repeatmasker: + build: . + image: mcp-repeatmasker:latest + container_name: mcp-repeatmasker + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=repeatmasker + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..79920d007f30448e17a7b23a278af16bae4ba467 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - repeatmasker + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_repeatmasker/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..43e33da9f5b7740c84bc549a0c7da3bc6f2f8f6f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install rsa via conda (e.g., from bioconda) +RUN conda install -c bioconda rsa -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/rsa_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/rsa_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/rsa_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/rsa_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/rsa_server.py new file mode 100644 index 0000000000000000000000000000000000000000..bdbda35337ac5781ed0da4dcccabe8b4709bb559 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/rsa_server.py @@ -0,0 +1,301 @@ +import subprocess +from pathlib import Path +from typing import Optional + +@mcp.tool() +def rsa_keygen( + bits: int = 2048, + private_key_out: str = "private.pem", + public_key_out: str = "public.pem", +): + """ + Generates a new RSA key pair. + + :param bits: The number of bits for the key (e.g., 1024, 2048, 4096). + :param private_key_out: Path to save the generated private key. + :param public_key_out: Path to save the generated public key. + """ + if bits < 512: + return {"error": "Key size must be at least 512 bits."} + + cmd = [ + "pyrsa-keygen", + "--out", private_key_out, + "--pubout", public_key_out, + str(bits) + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [private_key_out, public_key_out] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def rsa_encrypt( + input_file: str, + public_key_file: str, + output_file: str, + keyform: str = "PEM", +): + """ + Encrypts a file using an RSA public key. + + :param input_file: Path to the file to be encrypted. + :param public_key_file: Path to the RSA public key file. + :param output_file: Path to save the encrypted output. + :param keyform: Key format (PEM or DER). Default is PEM. + """ + input_path = Path(input_file) + pubkey_path = Path(public_key_file) + + if not input_path.exists(): + return {"error": f"Input file {input_file} not found."} + if not pubkey_path.exists(): + return {"error": f"Public key file {public_key_file} not found."} + if keyform not in ["PEM", "DER"]: + return {"error": "keyform must be either 'PEM' or 'DER'."} + + cmd = [ + "pyrsa-encrypt", + "--keyform", keyform, + "--out", output_file, + str(pubkey_path) + ] + + try: + # pyrsa-encrypt reads the message from stdin + with open(input_path, "rb") as f_in: + result = subprocess.run(cmd, stdin=f_in, capture_output=True, text=True, check=True) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_file] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def rsa_decrypt( + input_file: str, + private_key_file: str, + output_file: str, + keyform: str = "PEM", +): + """ + Decrypts a file using an RSA private key. + + :param input_file: Path to the encrypted file. + :param private_key_file: Path to the RSA private key file. + :param output_file: Path to save the decrypted output. + :param keyform: Key format (PEM or DER). Default is PEM. + """ + input_path = Path(input_file) + privkey_path = Path(private_key_file) + + if not input_path.exists(): + return {"error": f"Input file {input_file} not found."} + if not privkey_path.exists(): + return {"error": f"Private key file {private_key_file} not found."} + if keyform not in ["PEM", "DER"]: + return {"error": "keyform must be either 'PEM' or 'DER'."} + + cmd = [ + "pyrsa-decrypt", + "--keyform", keyform, + "--out", output_file, + str(privkey_path) + ] + + try: + # pyrsa-decrypt reads the ciphertext from stdin + with open(input_path, "rb") as f_in: + result = subprocess.run(cmd, stdin=f_in, capture_output=True, text=True, check=True) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_file] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def rsa_sign( + input_file: str, + private_key_file: str, + output_file: str, + hash_method: str = "SHA-256", + keyform: str = "PEM", +): + """ + Signs a file using an RSA private key. + + :param input_file: Path to the file to be signed. + :param private_key_file: Path to the RSA private key file. + :param output_file: Path to save the signature. + :param hash_method: Hash method to use (MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512). + :param keyform: Key format (PEM or DER). Default is PEM. + """ + input_path = Path(input_file) + privkey_path = Path(private_key_file) + valid_hashes = ["MD5", "SHA-1", "SHA-224", "SHA-256", "SHA-384", "SHA-512"] + + if not input_path.exists(): + return {"error": f"Input file {input_file} not found."} + if not privkey_path.exists(): + return {"error": f"Private key file {private_key_file} not found."} + if hash_method not in valid_hashes: + return {"error": f"Invalid hash method. Choose from: {', '.join(valid_hashes)}"} + if keyform not in ["PEM", "DER"]: + return {"error": "keyform must be either 'PEM' or 'DER'."} + + cmd = [ + "pyrsa-sign", + "--keyform", keyform, + "--hash-method", hash_method, + "--out", output_file, + str(privkey_path) + ] + + try: + # pyrsa-sign reads the message from stdin + with open(input_path, "rb") as f_in: + result = subprocess.run(cmd, stdin=f_in, capture_output=True, text=True, check=True) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_file] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def rsa_verify( + input_file: str, + signature_file: str, + public_key_file: str, + keyform: str = "PEM", +): + """ + Verifies a signature against a file using an RSA public key. + + :param input_file: Path to the original file that was signed. + :param signature_file: Path to the signature file. + :param public_key_file: Path to the RSA public key file. + :param keyform: Key format (PEM or DER). Default is PEM. + """ + input_path = Path(input_file) + sig_path = Path(signature_file) + pubkey_path = Path(public_key_file) + + if not input_path.exists(): + return {"error": f"Input file {input_file} not found."} + if not sig_path.exists(): + return {"error": f"Signature file {signature_file} not found."} + if not pubkey_path.exists(): + return {"error": f"Public key file {public_key_file} not found."} + if keyform not in ["PEM", "DER"]: + return {"error": "keyform must be either 'PEM' or 'DER'."} + + cmd = [ + "pyrsa-verify", + "--keyform", keyform, + str(pubkey_path), + str(sig_path) + ] + + try: + # pyrsa-verify reads the message from stdin + with open(input_path, "rb") as f_in: + result = subprocess.run(cmd, stdin=f_in, capture_output=True, text=True, check=True) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "verification_status": "Success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": "Verification failed", + "stdout": e.stdout, + "stderr": e.stderr, + "verification_status": "Failed" + } + +@mcp.tool() +def rsa_priv2pub( + private_key_file: str, + public_key_out: str, + keyform: str = "PEM", +): + """ + Extracts the public key from a private key. + + :param private_key_file: Path to the RSA private key file. + :param public_key_out: Path to save the extracted public key. + :param keyform: Key format (PEM or DER). Default is PEM. + """ + privkey_path = Path(private_key_file) + + if not privkey_path.exists(): + return {"error": f"Private key file {private_key_file} not found."} + if keyform not in ["PEM", "DER"]: + return {"error": "keyform must be either 'PEM' or 'DER'."} + + cmd = [ + "pyrsa-priv2pub", + "--keyform", keyform, + "--out", public_key_out + ] + + try: + # pyrsa-priv2pub reads the private key from stdin + with open(privkey_path, "rb") as f_in: + result = subprocess.run(cmd, stdin=f_in, capture_output=True, text=True, check=True) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [public_key_out] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/rsa_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/rsa_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..bf8fe988fecf7d2ffc67e24600d9c2c4410b827c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/rsa_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/app/rsa_server.py') +SERVER_NAME = 'biosci_rsa' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..832e48c1dc3c876a436a61c48bbb9b920c7e9420 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-rsa: + build: . + image: mcp-rsa:latest + container_name: mcp-rsa + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=rsa + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..802d2518edc3c55107e8bc6759da2c4935e633db --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - rsa + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_rsa/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..fd6b77e08df44d2159e374c14c9dfef5bfeee1d2 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install sevenbridges-python via conda (e.g., from bioconda) +RUN conda install -c bioconda sevenbridges-python -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/sevenbridges-python_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/sevenbridges-python_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/sevenbridges-python_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/sevenbridges-python_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/sevenbridges-python_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ba39b68d732d010289a60b907501f5aea7fd4630 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/sevenbridges-python_server.py @@ -0,0 +1,527 @@ +import subprocess +import json +from pathlib import Path +from typing import Optional, List, Dict, Any +import os +import tempfile + +# --- Start of sevenbridges-python library placeholder --- +# This section simulates the sevenbridges-python library for demonstration purposes. +# In a real MCP environment, the actual 'sevenbridges' library would be installed +# and imported directly. This placeholder allows the code to be runnable +# without the actual library being present. + +class SbgError(Exception): + """Base exception for Seven Bridges API errors.""" + def __init__(self, message: str, status_code: Optional[int] = None, response_data: Optional[Dict[str, Any]] = None): + super().__init__(message) + self.status_code = status_code + self.response_data = response_data + def __str__(self): + status_str = f" (Status: {self.status_code})" if self.status_code else "" + return f"Seven Bridges API Error{status_str}: {self.args[0]}" + +class Config: + def __init__(self, url: Optional[str] = None, token: Optional[str] = None, profile: Optional[str] = None): + self.url = url + self.token = token + self.profile = profile + + if profile: + # Simulate loading from a config file based on profile + if profile == 'cgc': + self.url = 'https://cgc-api.sbgenomics.com/v2' + self.token = 'MOCK_CGC_TOKEN_FROM_PROFILE' + elif profile == 'cavatica': + self.url = 'https://cavatica-api.sbgenomics.com/v2' + self.token = 'MOCK_CAVATICA_TOKEN_FROM_PROFILE' + elif profile == 'default': + self.url = 'https://api.sbgenomics.com/v2' + self.token = 'MOCK_DEFAULT_TOKEN_FROM_PROFILE' + else: + raise SbgError(f"Unknown profile: {profile}. Valid profiles are 'default', 'cgc', 'cavatica'.") + elif not self.url or not self.token: + # Attempt to load from environment variables if no explicit params or profile + self.url = os.environ.get('SB_API_ENDPOINT', self.url) + self.token = os.environ.get('SB_AUTH_TOKEN', self.token) + if not self.url or not self.token: + raise SbgError("API URL and token must be provided explicitly, via environment variables (SB_API_ENDPOINT, SB_AUTH_TOKEN), or a profile specified.") + +class User: + def __init__(self, id: str, username: str, email: str): + self.id = id + self.username = username + self.email = email + + def to_dict(self) -> Dict[str, str]: + return {"id": self.id, "username": self.username, "email": self.email} + +class Project: + def __init__(self, id: str, name: str, owner: str, description: Optional[str] = None): + self.id = id + self.name = name + self.owner = owner + self.description = description + self._files: List['File'] = [] # Simulate files for this project + + def to_dict(self) -> Dict[str, Optional[str]]: + return { + "id": self.id, + "name": self.name, + "owner": self.owner, + "description": self.description + } + + def get_files(self, limit: int = 100, offset: int = 0) -> List['File']: + # Simulate fetching files for a project + if not self._files: + # Populate some mock files if empty + self._files = [ + File(f"file-{self.id}-{i}", f"sample_data_{i}.fastq.gz", 1024 * (i + 1), "FASTQ", self.id) + for i in range(5) + ] + return self._files[offset:offset + limit] + +class File: + def __init__(self, id: str, name: str, size: int, file_type: str, project_id: str): + self.id = id + self.name = name + self.size = size + self.type = file_type + self.project_id = project_id + + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.id, + "name": self.name, + "size": self.size, + "type": self.type, + "project_id": self.project_id + } + + def download(self, path: Path) -> Path: + # Simulate file download + try: + with open(path, 'w') as f: + f.write(f"Mock content for file {self.name} (ID: {self.id}) from project {self.project_id}\n") + return path + except IOError as e: + raise SbgError(f"Failed to write file to {path}: {e}") + +class Users: + def me(self) -> User: + # Simulate fetching current user + return User("user-123", "testuser", "test@example.com") + +class Projects: + def __init__(self): + self._projects = [ + Project("proj-abc", "My First Project", "testuser", "A project for testing."), + Project("proj-def", "Genomics Data", "testuser", "Contains various genomics datasets."), + Project("proj-ghi", "RNA-seq Analysis", "anotheruser", "RNA-seq results."), + ] + + def query(self, limit: int = 100, offset: int = 0) -> List[Project]: + # Simulate querying projects + return self._projects[offset:offset + limit] + + def get(self, project_id: str) -> Project: + for p in self._projects: + if p.id == project_id: + return p + raise SbgError(f"Project with ID '{project_id}' not found.", status_code=404) + +class FilesManager: # Renamed to avoid conflict with File class + def __init__(self, projects_manager: Projects): + self._projects_manager = projects_manager + self._all_files: List[File] = [] + # Populate mock files from projects + for p in self._projects_manager._projects: + self._all_files.extend(p.get_files()) + + def get(self, file_id: str) -> File: + for f in self._all_files: + if f.id == file_id: + return f + raise SbgError(f"File with ID '{file_id}' not found.", status_code=404) + +class Api: + def __init__(self, config: Optional[Config] = None, url: Optional[str] = None, token: Optional[str] = None): + if config: + self.config = config + else: + self.config = Config(url=url, token=token) + + # Simulate API resources + self.users = Users() + self.projects = Projects() + self.files = FilesManager(self.projects) # Use FilesManager + +# Alias for the simulated library +sevenbridges = Api +sevenbridges.Config = Config +sevenbridges.Api = Api +sevenbridges.SbgError = SbgError +# --- End of sevenbridges-python library placeholder --- + + +def _initialize_sevenbridges_api(api_url: Optional[str], auth_token: Optional[str], profile: Optional[str]) -> sevenbridges.Api: + """ + Helper function to initialize the Seven Bridges API object. + Handles different authentication methods. + """ + if profile: + if api_url is not None or auth_token is not None: + raise ValueError("Cannot specify both 'profile' and explicit 'api_url'/'auth_token'.") + config = sevenbridges.Config(profile=profile) + elif api_url is not None and auth_token is not None: + config = sevenbridges.Config(url=api_url, token=auth_token) + else: + # Attempt to use environment variables if no explicit params or profile + config = sevenbridges.Config() # This will try to load from env vars internally + + return sevenbridges.Api(config=config) + + +@mcp.tool() +def sbg_get_current_user( + api_url: Optional[str] = None, + auth_token: Optional[str] = None, + profile: Optional[str] = None, +) -> Dict[str, Any]: + """ + Retrieves information about the currently authenticated Seven Bridges Platform user. + + Authentication can be provided explicitly via `api_url` and `auth_token`, + by specifying a `profile` from the $HOME/.sevenbridges/credentials file, + or implicitly via environment variables (SB_API_ENDPOINT, SB_AUTH_TOKEN). + """ + stdout_lines: List[str] = [] + stderr_lines: List[str] = [] + command_executed = "sevenbridges.Api().users.me()" + + try: + api = _initialize_sevenbridges_api(api_url, auth_token, profile) + user = api.users.me() + user_info = user.to_dict() + stdout_lines.append(json.dumps(user_info, indent=2)) + return { + "command_executed": command_executed, + "stdout": "\n".join(stdout_lines), + "stderr": "", + "output_files": [], + "user_info": user_info # Structured output + } + except ValueError as e: + stderr_lines.append(f"Input validation error: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e) + } + except sevenbridges.SbgError as e: + stderr_lines.append(f"Seven Bridges API Error: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e), + "status_code": e.status_code, + "response_data": e.response_data + } + except Exception as e: + stderr_lines.append(f"An unexpected error occurred: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e) + } + + +@mcp.tool() +def sbg_list_projects( + api_url: Optional[str] = None, + auth_token: Optional[str] = None, + profile: Optional[str] = None, + limit: int = 100, + offset: int = 0, +) -> Dict[str, Any]: + """ + Lists projects accessible to the authenticated Seven Bridges Platform user. + + Authentication can be provided explicitly via `api_url` and `auth_token`, + by specifying a `profile` from the $HOME/.sevenbridges/credentials file, + or implicitly via environment variables (SB_API_ENDPOINT, SB_AUTH_TOKEN). + + Args: + limit: The maximum number of projects to return. Must be a positive integer. + offset: The number of projects to skip before starting to return results. Must be a non-negative integer. + """ + stdout_lines: List[str] = [] + stderr_lines: List[str] = [] + command_executed = f"sevenbridges.Api().projects.query(limit={limit}, offset={offset})" + + # Input validation + if not isinstance(limit, int) or limit <= 0: + stderr_lines.append("Error: 'limit' must be a positive integer.") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": "'limit' must be a positive integer." + } + if not isinstance(offset, int) or offset < 0: + stderr_lines.append("Error: 'offset' must be a non-negative integer.") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": "'offset' must be a non-negative integer." + } + + try: + api = _initialize_sevenbridges_api(api_url, auth_token, profile) + projects = api.projects.query(limit=limit, offset=offset) + project_list = [p.to_dict() for p in projects] + stdout_lines.append(json.dumps(project_list, indent=2)) + return { + "command_executed": command_executed, + "stdout": "\n".join(stdout_lines), + "stderr": "", + "output_files": [], + "projects": project_list # Structured output + } + except ValueError as e: + stderr_lines.append(f"Input validation error: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e) + } + except sevenbridges.SbgError as e: + stderr_lines.append(f"Seven Bridges API Error: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e), + "status_code": e.status_code, + "response_data": e.response_data + } + except Exception as e: + stderr_lines.append(f"An unexpected error occurred: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e) + } + + +@mcp.tool() +def sbg_list_project_files( + project_id: str, + api_url: Optional[str] = None, + auth_token: Optional[str] = None, + profile: Optional[str] = None, + limit: int = 100, + offset: int = 0, +) -> Dict[str, Any]: + """ + Lists files within a specific Seven Bridges Platform project. + + Authentication can be provided explicitly via `api_url` and `auth_token`, + by specifying a `profile` from the $HOME/.sevenbridges/credentials file, + or implicitly via environment variables (SB_API_ENDPOINT, SB_AUTH_TOKEN). + + Args: + project_id: The ID of the project to list files from. + limit: The maximum number of files to return. Must be a positive integer. + offset: The number of files to skip before starting to return results. Must be a non-negative integer. + """ + stdout_lines: List[str] = [] + stderr_lines: List[str] = [] + command_executed = f"sevenbridges.Api().projects.get('{project_id}').get_files(limit={limit}, offset={offset})" + + # Input validation + if not project_id: + stderr_lines.append("Error: 'project_id' cannot be empty.") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": "'project_id' cannot be empty." + } + if not isinstance(limit, int) or limit <= 0: + stderr_lines.append("Error: 'limit' must be a positive integer.") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": "'limit' must be a positive integer." + } + if not isinstance(offset, int) or offset < 0: + stderr_lines.append("Error: 'offset' must be a non-negative integer.") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": "'offset' must be a non-negative integer." + } + + try: + api = _initialize_sevenbridges_api(api_url, auth_token, profile) + project = api.projects.get(project_id) + files = project.get_files(limit=limit, offset=offset) + file_list = [f.to_dict() for f in files] + stdout_lines.append(json.dumps(file_list, indent=2)) + return { + "command_executed": command_executed, + "stdout": "\n".join(stdout_lines), + "stderr": "", + "output_files": [], + "files": file_list # Structured output + } + except ValueError as e: + stderr_lines.append(f"Input validation error: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e) + } + except sevenbridges.SbgError as e: + stderr_lines.append(f"Seven Bridges API Error: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e), + "status_code": e.status_code, + "response_data": e.response_data + } + except Exception as e: + stderr_lines.append(f"An unexpected error occurred: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e) + } + + +@mcp.tool() +def sbg_download_file( + file_id: str, + output_path: Path, + api_url: Optional[str] = None, + auth_token: Optional[str] = None, + profile: Optional[str] = None, +) -> Dict[str, Any]: + """ + Downloads a specific file from the Seven Bridges Platform to a local path. + + Authentication can be provided explicitly via `api_url` and `auth_token`, + by specifying a `profile` from the $HOME/.sevenbridges/credentials file, + or implicitly via environment variables (SB_API_ENDPOINT, SB_AUTH_TOKEN). + + Args: + file_id: The ID of the file to download. + output_path: The local path where the file should be saved. + """ + stdout_lines: List[str] = [] + stderr_lines: List[str] = [] + command_executed = f"sevenbridges.Api().files.get('{file_id}').download(path='{output_path}')" + + # Input validation + if not file_id: + stderr_lines.append("Error: 'file_id' cannot be empty.") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": "'file_id' cannot be empty." + } + if not isinstance(output_path, Path): + stderr_lines.append("Error: 'output_path' must be a pathlib.Path object.") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": "'output_path' must be a pathlib.Path object." + } + + # Ensure parent directory exists + try: + output_path.parent.mkdir(parents=True, exist_ok=True) + except OSError as e: + stderr_lines.append(f"Error creating output directory '{output_path.parent}': {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": f"Failed to create output directory: {e}" + } + + try: + api = _initialize_sevenbridges_api(api_url, auth_token, profile) + sbg_file = api.files.get(file_id) + downloaded_path = sbg_file.download(output_path) + + stdout_lines.append(f"Successfully downloaded file '{sbg_file.name}' (ID: {file_id}) to '{downloaded_path}'") + return { + "command_executed": command_executed, + "stdout": "\n".join(stdout_lines), + "stderr": "", + "output_files": [str(downloaded_path)], + "downloaded_file_info": sbg_file.to_dict() # Structured output + } + except ValueError as e: + stderr_lines.append(f"Input validation error: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e) + } + except sevenbridges.SbgError as e: + stderr_lines.append(f"Seven Bridges API Error: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e), + "status_code": e.status_code, + "response_data": e.response_data + } + except Exception as e: + stderr_lines.append(f"An unexpected error occurred: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "\n".join(stderr_lines), + "output_files": [], + "error": str(e) + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/sevenbridges-python_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/sevenbridges-python_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..efaecde982d8fd2961cb37add5e995598144353b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/sevenbridges-python_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/app/sevenbridges-python_server.py') +SERVER_NAME = 'biosci_sevenbridges_python' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..ad4c84661142bc17289402afebba682a46c84d66 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-sevenbridges-python: + build: . + image: mcp-sevenbridges-python:latest + container_name: mcp-sevenbridges-python + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=sevenbridges-python + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..10825cf72a342f167eadae131285722cae1ca25c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - sevenbridges-python + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sevenbridges-python/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2badc059aa16bc8124a8a198d3c2a1c4f7dd58fe --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install sina via conda (e.g., from bioconda) +RUN conda install -c bioconda sina -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/sina_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/sina_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/sina_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/sina_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/sina_server.py new file mode 100644 index 0000000000000000000000000000000000000000..4d31706328771fa16aa0623341867076b14a2238 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/sina_server.py @@ -0,0 +1,244 @@ +from pathlib import Path +import subprocess +from typing import Optional, List, Union + +@mcp.tool() +def sina( + in_file: str, + out_file: str, + db: str, + threads: int = 1, + search: bool = False, + search_db: Optional[str] = None, + lca_tax: bool = False, + lca_quorum: float = 0.8, + min_identity: float = 0.7, + turn: str = "yes", + prealigned: bool = False, + intype: str = "fasta", + outtype: str = "fasta", + fs_min_identity: float = 0.7, + fs_mismatch: int = 2, + fs_gap_open: int = 5, + fs_gap_extend: int = 2, + overhang: str = "attach", + insertion: str = "all", + remove_unaligned: bool = False, + sample_id: Optional[str] = None, +): + """ + SINA: Reference-based multiple sequence alignment, homology search, and LCA-based classification. + + SINA aligns nucleotide sequences to match a pre-existing MSA using a graph-based alignment algorithm. + It is particularly optimized for ribosomal RNA genes (SSU/LSU). + + Args: + in_file: Path to the input sequence file (FASTA, FASTQ, or ARB). + out_file: Path to the output file. + db: Path to the reference database (ARB format or FASTA). + threads: Number of threads to use for processing. + search: Enable homology search against the reference database. + search_db: Path to a separate search database (if different from alignment db). + lca_tax: Enable LCA (Lowest Common Ancestor) based taxonomic classification. + lca_quorum: Fraction of search results that must agree for a classification (0.0-1.0). + min_identity: Minimum identity threshold for search results. + turn: Try reverse complement of sequences. Options: 'yes', 'no', 'all'. + prealigned: Set to True if input sequences are already aligned. + intype: Input file format ('fasta', 'fastq', 'arb'). + outtype: Output file format ('fasta', 'arb'). + fs_min_identity: Minimum identity for the alignment stage. + fs_mismatch: Mismatch penalty for alignment. + fs_gap_open: Gap open penalty for alignment. + fs_gap_extend: Gap extension penalty for alignment. + overhang: How to handle overhangs ('attach', 'edge', 'remove'). + insertion: How to handle insertions ('all', 'none', 'upper'). + remove_unaligned: If True, sequences that fail to align will be excluded from output. + sample_id: Optional identifier for the run. + """ + # Input validation + input_path = Path(in_file) + if not input_path.exists(): + return {"error": f"Input file not found: {in_file}"} + + db_path = Path(db) + if not db_path.exists(): + return {"error": f"Reference database not found: {db}"} + + output_path = Path(out_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Construct command + cmd = [ + "sina", + "--in", str(input_path), + "--out", str(output_path), + "--db", str(db_path), + "--threads", str(threads), + "--intype", intype, + "--outtype", outtype, + "--turn", turn, + "--overhang", overhang, + "--insertion", insertion, + "--min-identity", str(min_identity), + "--fs-min-identity", str(fs_min_identity), + "--fs-mismatch", str(fs_mismatch), + "--fs-gap-open", str(fs_gap_open), + "--fs-gap-extend", str(fs_gap_extend) + ] + + # Boolean flags + if search: + cmd.append("--search") + if lca_tax: + cmd.append("--lca-tax") + if prealigned: + cmd.append("--prealigned") + if remove_unaligned: + cmd.append("--remove-unaligned") + + # Optional parameters with values + if search_db: + sdb_path = Path(search_db) + if not sdb_path.exists(): + return {"error": f"Search database not found: {search_db}"} + cmd.extend(["--search-db", str(sdb_path)]) + + if lca_tax: + cmd.extend(["--lca-quorum", str(lca_quorum)]) + + if sample_id: + cmd.extend(["--sample-id", sample_id]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def sina_search( + in_file: str, + db: str, + out_file: str, + threads: int = 1, + min_identity: float = 0.7, + max_parents: int = 10, + search_kmer_candidates: int = 1000, + lca_tax: bool = False, +): + """ + Perform only a homology search and/or classification using SINA without full alignment. + + Args: + in_file: Path to the input sequence file. + db: Path to the reference database (ARB or FASTA). + out_file: Path to save search results. + threads: Number of threads. + min_identity: Minimum identity for search hits. + max_parents: Maximum number of search neighbors to report. + search_kmer_candidates: Number of k-mer candidates to consider. + lca_tax: Enable LCA classification based on search results. + """ + input_path = Path(in_file) + db_path = Path(db) + output_path = Path(out_file) + + if not input_path.exists(): + return {"error": f"Input file not found: {in_file}"} + if not db_path.exists(): + return {"error": f"Database not found: {db}"} + + cmd = [ + "sina", + "--in", str(input_path), + "--out", str(output_path), + "--db", str(db_path), + "--threads", str(threads), + "--search", + "--no-align", # Skip alignment stage + "--min-identity", str(min_identity), + "--search-max-parents", str(max_parents), + "--search-kmer-candidates", str(search_kmer_candidates) + ] + + if lca_tax: + cmd.append("--lca-tax") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def sina_convert( + in_file: str, + out_file: str, + intype: str = "arb", + outtype: str = "fasta", + filter_aligned: bool = False, +): + """ + Use SINA to convert between sequence formats (ARB, FASTA, FASTQ). + + Args: + in_file: Input file path. + out_file: Output file path. + intype: Input format ('arb', 'fasta', 'fastq'). + outtype: Output format ('arb', 'fasta', 'fastq'). + filter_aligned: If True, only export sequences that have an alignment. + """ + input_path = Path(in_file) + output_path = Path(out_file) + + if not input_path.exists(): + return {"error": f"Input file not found: {in_file}"} + + cmd = [ + "sina", + "--in", str(input_path), + "--out", str(output_path), + "--intype", intype, + "--outtype", outtype, + "--no-align", + "--no-search" + ] + + if filter_aligned: + cmd.append("--filter-aligned") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/sina_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/sina_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..78c24f511041bae34b82b976636decff07273abd --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/sina_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/app/sina_server.py') +SERVER_NAME = 'biosci_sina' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..3024c6eb460f5a2949619f05f07d167f8caeffc7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-sina: + build: . + image: mcp-sina:latest + container_name: mcp-sina + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=sina + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..aa22f5bf431ed27a18928d711f2145146afe1631 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - sina + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_sina/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ce7f53e49fdc692cb4c73fb4e48ea0cd409aa0f3 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install spagrn via conda (e.g., from bioconda) +RUN conda install -c bioconda spagrn -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/spagrn_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/spagrn_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/spagrn_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..2d3691d383cfb0707cb493e5df9ca8a6445940e0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-spagrn: + build: . + image: mcp-spagrn:latest + container_name: mcp-spagrn + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=spagrn + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1b7c436119b56ae5247dd2701eb0de9d05473d60 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - spagrn + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_spagrn/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_thapbi-pict/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_thapbi-pict/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_thapbi-pict/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f6bce29a056e3950fbfed381b2b60b53663b281b --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install ucsc-twobittofa via conda (e.g., from bioconda) +RUN conda install -c bioconda ucsc-twobittofa -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/ucsc-twobittofa_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/ucsc-twobittofa_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/ucsc-twobittofa_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/ucsc-twobittofa_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/ucsc-twobittofa_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6b1c56557f1f4bdbd72c18fd9091b828339da7db --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/ucsc-twobittofa_server.py @@ -0,0 +1,157 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +# Assume mcp is imported from a library, no need to import it here as per instructions. +# from mcp import tool + +@mcp.tool() +def twobittofa( + input_2bit_file: Path, + output_fasta_file: Path, + sequence_name: Optional[str] = None, + chromosome: Optional[str] = None, + start_coordinate: Optional[int] = None, + end_coordinate: Optional[int] = None, + hard_mask: bool = False, + soft_mask: bool = False, + reverse_complement: bool = False, + no_names: bool = False, + to_upper: bool = False, + to_lower: bool = False, + verbose_level: int = 0, +) -> Dict[str, Any]: + """ + Convert all or part of a .2bit file to FASTA format. + + This tool converts a UCSC .2bit sequence file into a FASTA file. + It supports extracting specific sequences or regions, and various + masking and case conversion options. + + Note: The exact command-line parameters for ucsc-twobittofa were not + available in the provided documentation. Parameters are inferred based + on common conventions for UCSC command-line tools and the tool's description. + + Args: + input_2bit_file: Path to the input .2bit file. + output_fasta_file: Path to the output FASTA file. + sequence_name: Optional. Extract a specific sequence by name. Cannot be + used with `chromosome`, `start_coordinate`, or `end_coordinate`. + chromosome: Optional. Extract a specific chromosome. Requires `start_coordinate` + and `end_coordinate` if provided. Cannot be used with `sequence_name`. + start_coordinate: Optional. Start coordinate (0-based) for region extraction. + Requires `chromosome` and `end_coordinate`. + end_coordinate: Optional. End coordinate (0-based, exclusive) for region extraction. + Requires `chromosome` and `start_coordinate`. + hard_mask: If True, mask N's in the output. + soft_mask: If True, mask lower-case letters in the output. Cannot be used with `hard_mask`. + reverse_complement: If True, output the reverse complement of the sequence. + no_names: If True, do not include sequence names in the output FASTA headers. + to_upper: If True, convert all output sequence to upper case. Cannot be used with `to_lower`. + to_lower: If True, convert all output sequence to lower case. Cannot be used with `to_upper`. + verbose_level: Set the verbosity level (0-3, where 0 is silent). + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + + Raises: + FileNotFoundError: If the input .2bit file does not exist. + ValueError: If invalid parameter combinations or values are provided. + subprocess.CalledProcessError: If the ucsc-twobittofa command fails. + """ + # --- Input Validation --- + if not input_2bit_file.is_file(): + raise FileNotFoundError(f"Input .2bit file not found: {input_2bit_file}") + + if output_fasta_file.suffix.lower() not in [".fa", ".fasta", ".fna"]: + raise ValueError(f"Output file '{output_fasta_file}' must have a FASTA extension (.fa, .fasta, .fna).") + + output_fasta_file.parent.mkdir(parents=True, exist_ok=True) + + # Region/Sequence selection validation + region_params_set = any(p is not None for p in [chromosome, start_coordinate, end_coordinate]) + if sequence_name and region_params_set: + raise ValueError("Cannot specify both 'sequence_name' and region parameters ('chromosome', 'start_coordinate', 'end_coordinate').") + + if chromosome: + if start_coordinate is None or end_coordinate is None: + raise ValueError("If 'chromosome' is specified, 'start_coordinate' and 'end_coordinate' must also be provided.") + if start_coordinate < 0: + raise ValueError("'start_coordinate' must be a non-negative integer.") + if end_coordinate <= start_coordinate: + raise ValueError("'end_coordinate' must be greater than 'start_coordinate'.") + elif (start_coordinate is not None or end_coordinate is not None): + raise ValueError("If 'start_coordinate' or 'end_coordinate' is specified, 'chromosome' must also be provided.") + + # Masking options validation + if hard_mask and soft_mask: + raise ValueError("Cannot specify both 'hard_mask' and 'soft_mask'.") + + # Case conversion options validation + if to_upper and to_lower: + raise ValueError("Cannot specify both 'to_upper' and 'to_lower'.") + + if not (0 <= verbose_level <= 3): + raise ValueError("Verbose level must be between 0 and 3.") + + # --- Construct Command --- + cmd = ["ucsc-twobittofa"] + + if sequence_name: + cmd.append(f"-seq={sequence_name}") + elif chromosome: + cmd.append(f"-chrom={chromosome}") + cmd.append(f"-start={start_coordinate}") + cmd.append(f"-end={end_coordinate}") + + if hard_mask: + cmd.append("-hardmask") + if soft_mask: + cmd.append("-softmask") + if reverse_complement: + cmd.append("-rev") + if no_names: + cmd.append("-noNames") + if to_upper: + cmd.append("-upper") + if to_lower: + cmd.append("-lower") + + if verbose_level > 0: + cmd.append(f"-verbose={verbose_level}") + + cmd.extend([str(input_2bit_file), str(output_fasta_file)]) + + command_executed = " ".join(cmd) + stdout = "" + stderr = "" + output_files: List[Path] = [] + + try: + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + stdout = process.stdout + stderr = process.stderr + output_files.append(output_fasta_file) + + except FileNotFoundError: + stderr = f"Error: 'ucsc-twobittofa' command not found. Please ensure it is installed and in your PATH." + raise + except subprocess.CalledProcessError as e: + stdout = e.stdout + stderr = e.stderr + raise RuntimeError(f"Command failed with exit code {e.returncode}: {e.cmd}\nStdout: {stdout}\nStderr: {stderr}") from e + except Exception as e: + stderr = f"An unexpected error occurred: {e}" + raise + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/ucsc-twobittofa_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/ucsc-twobittofa_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..903d96a02a1331908a20f446233d71abfab28492 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/ucsc-twobittofa_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/app/ucsc-twobittofa_server.py') +SERVER_NAME = 'biosci_ucsc_twobittofa' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..2611cca3c1f976f99a40cfceefbe9ae8b1a6c26c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-ucsc-twobittofa: + build: . + image: mcp-ucsc-twobittofa:latest + container_name: mcp-ucsc-twobittofa + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=ucsc-twobittofa + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..676b7bfcaaa8362db3989567d60345a099d498f7 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - ucsc-twobittofa + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_ucsc-twobittofa/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..854a653ba11b048b3ca89c8e386cecf4387e3431 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install vcontact2 via conda (e.g., from bioconda) +RUN conda install -c bioconda vcontact2 -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/vcontact2_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/vcontact2_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/vcontact2_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/app/vcontact2_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/app/vcontact2_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b60d52a57b0711958f26e715ea7f9a5c2895ba71 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/app/vcontact2_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/app/vcontact2_server.py') +SERVER_NAME = 'biosci_vcontact2' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..3ba89e17a4f5599a5e13034e12864a6a860a9510 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-vcontact2: + build: . + image: mcp-vcontact2:latest + container_name: mcp-vcontact2 + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=vcontact2 + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e04f1267149c78b5b6e26a515334fef77edfa56f --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - vcontact2 + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_vcontact2/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/Dockerfile b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..09bfbd83ebff7ba78ac19660c6716ce8e5d060e3 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install zol via conda (e.g., from bioconda) +RUN conda install -c bioconda zol -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/zol_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/zol_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/zol_server.py"] + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/zol_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/zol_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ec414b8472716977e25322c17ad227a0408afb8c --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/zol_server.py @@ -0,0 +1,562 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Dict, Any + +# Note: The 'mcp' import is omitted as per the instructions. + +def _run_zol_suite_command( + program: str, + args: List[str], + output_directory: Path, + input_files_to_validate: Optional[List[Path]] = None, +) -> Dict[str, Any]: + """ + Helper function to execute zol-suite commands. + + Args: + program: The zol-suite subcommand to run (e.g., "prepTG", "fai"). + args: A list of command-line arguments specific to the subcommand. + output_directory: The directory where output files will be written. + input_files_to_validate: An optional list of input file paths to validate existence. + + Returns: + A dictionary containing execution details (command, stdout, stderr, output_files, error status). + """ + if not output_directory.is_dir(): + output_directory.mkdir(parents=True, exist_ok=True) + + command = ["zol-suite", program] + args + + # Validate input files + if input_files_to_validate: + for f in input_files_to_validate: + if not f.is_file(): + return { + "command_executed": " ".join(command), + "stdout": "", + "stderr": f"Error: Input file not found: {f}", + "output_files": [], + "error": True, + "message": f"Input file not found: {f}", + } + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + cwd=output_directory, # Run in output directory to capture generated files + ) + stdout = process.stdout + stderr = process.stderr + + # List files created in the output_directory. + # This is a generic approach; specific tools might have known output patterns. + output_files = [str(f.resolve()) for f in output_directory.iterdir() if f.is_file()] + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + "error": False, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + "error": True, + "message": f"Command failed with exit code {e.returncode}", + } + except FileNotFoundError: + return { + "command_executed": " ".join(command), + "stdout": "", + "stderr": "Error: 'zol-suite' command not found. Is zol installed and in PATH?", + "output_files": [], + "error": True, + "message": "'zol-suite' command not found.", + } + + +@mcp.tool() +def prep_tg( + input_genomes: List[Path], + output_database_dir: Path, + reference_proteome: Optional[Path] = None, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Prepare a database of target genomes for searches using fai. + + This tool processes and performs gene-calling or gene-mapping on an input set of genomes. + It can take in as input either FASTA files or GenBank files with CDS features. + If FASTA files are provided, it will by default use pyrodigal to make gene calls. + If genomes are eukaryotic and a reference proteome is provided, miniprot will be used + to perform gene mapping. + + Note: Detailed command-line parameters for 'prepTG' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'prepTG' subcommand. + """ + # Input validation + if not input_genomes: + return { + "command_executed": "zol-suite prepTG ...", + "stdout": "", + "stderr": "Error: At least one input genome file is required.", + "output_files": [], + "error": True, + "message": "At least one input genome file is required.", + } + for genome_path in input_genomes: + if not genome_path.is_file(): + return { + "command_executed": "zol-suite prepTG ...", + "stdout": "", + "stderr": f"Error: Input genome file not found: {genome_path}", + "output_files": [], + "error": True, + "message": f"Input genome file not found: {genome_path}", + } + if reference_proteome and not reference_proteome.is_file(): + return { + "command_executed": "zol-suite prepTG ...", + "stdout": "", + "stderr": f"Error: Reference proteome file not found: {reference_proteome}", + "output_files": [], + "error": True, + "message": f"Reference proteome file not found: {reference_proteome}", + } + + args = [] + for genome_path in input_genomes: + args.extend(["-g", str(genome_path)]) # Assuming -g for genome input + args.extend(["-o", str(output_database_dir)]) # Assuming -o for output directory + if reference_proteome: + args.extend(["--ref-proteome", str(reference_proteome)]) # Inferred flag + + if cli_args: + args.extend(cli_args) + + input_files_to_validate = input_genomes + if reference_proteome: + input_files_to_validate.append(reference_proteome) + + return _run_zol_suite_command("prepTG", args, output_database_dir, input_files_to_validate) + + +@mcp.tool() +def fai( + query_gene_cluster: Path, + target_database: Path, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Find additional instances of gene clusters in a genome database. + + This tool searches for homologous/orthologous instances of a query gene cluster + in a database of (meta-)genomes, leveraging DIAMOND alignment. It can assess + syntenic similarity, allow looser criteria for fragmented clusters, and filter + secondary neighborhoods. + + Note: Detailed command-line parameters for 'fai' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'fai' subcommand. + """ + # Input validation + if not query_gene_cluster.is_file(): + return { + "command_executed": "zol-suite fai ...", + "stdout": "", + "stderr": f"Error: Query gene cluster file not found: {query_gene_cluster}", + "output_files": [], + "error": True, + "message": f"Query gene cluster file not found: {query_gene_cluster}", + } + if not target_database.is_dir(): # Assuming target_database is a directory created by prepTG + return { + "command_executed": "zol-suite fai ...", + "stdout": "", + "stderr": f"Error: Target database directory not found: {target_database}", + "output_files": [], + "error": True, + "message": f"Target database directory not found: {target_database}", + } + + args = [ + "-q", str(query_gene_cluster), # Inferred flag for query + "-d", str(target_database), # Inferred flag for database + "-o", str(output_directory / "fai_results"), # Inferred output prefix/file + ] + if cli_args: + args.extend(cli_args) + + return _run_zol_suite_command("fai", args, output_directory, [query_gene_cluster]) + + +@mcp.tool() +def zol( + input_gene_clusters: Path, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Perform comparative gene cluster analysis. + + This tool creates table reports showing ortholog group conservation, annotation, + and evolutionary statistics for gene clusters of interest. It performs de novo + ortholog group inference and can filter incomplete gene cluster instances. + It also has options for dereplication using skani and re-inflation with CD-HIT. + + Note: Detailed command-line parameters for 'zol' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'zol' subcommand. + """ + # Input validation + if not input_gene_clusters.is_file(): + return { + "command_executed": "zol-suite zol ...", + "stdout": "", + "stderr": f"Error: Input gene clusters file not found: {input_gene_clusters}", + "output_files": [], + "error": True, + "message": f"Input gene clusters file not found: {input_gene_clusters}", + } + + args = [ + "-i", str(input_gene_clusters), # Inferred flag for input + "-o", str(output_directory / "zol_report"), # Inferred output prefix/file + ] + if cli_args: + args.extend(cli_args) + + return _run_zol_suite_command("zol", args, output_directory, [input_gene_clusters]) + + +@mcp.tool() +def cgc( + input_zol_results: Path, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Visualize zol results along a consensus gene cluster sequence. + + This tool generates publication-quality figures, producing a collapsed gene cluster + with bar plots for quantitative statistics computed in zol atop a consensus + gene cluster representation. + + Note: Detailed command-line parameters for 'cgc' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'cgc' subcommand. + """ + # Input validation + if not input_zol_results.is_file(): + return { + "command_executed": "zol-suite cgc ...", + "stdout": "", + "stderr": f"Error: Input zol results file not found: {input_zol_results}", + "output_files": [], + "error": True, + "message": f"Input zol results file not found: {input_zol_results}", + } + + args = [ + "-i", str(input_zol_results), # Inferred flag for input + "-o", str(output_directory / "cgc_figure"), # Inferred output prefix/file + ] + if cli_args: + args.extend(cli_args) + + return _run_zol_suite_command("cgc", args, output_directory, [input_zol_results]) + + +@mcp.tool() +def cgcg( + input_zol_results: Path, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Visualize zol results as a graphical network. + + This tool generates publication-quality figures, producing a network visual of + ortholog groups (nodes) where edges represent information on syntenic ordering. + + Note: Detailed command-line parameters for 'cgcg' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'cgcg' subcommand. + """ + # Input validation + if not input_zol_results.is_file(): + return { + "command_executed": "zol-suite cgcg ...", + "stdout": "", + "stderr": f"Error: Input zol results file not found: {input_zol_results}", + "output_files": [], + "error": True, + "message": f"Input zol results file not found: {input_zol_results}", + } + + args = [ + "-i", str(input_zol_results), # Inferred flag for input + "-o", str(output_directory / "cgcg_network"), # Inferred output prefix/file + ] + if cli_args: + args.extend(cli_args) + + return _run_zol_suite_command("cgcg", args, output_directory, [input_zol_results]) + + +@mcp.tool() +def abon( + input_genome: Path, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Automated analysis of conservation/novelty for a sample's biosynthetic gene clusters (BGCs). + + This tool allows users to check if BGCs from their favorite strain/genome are + conserved or novel relative to other genomes available from the isolate's + respective genus. + + Note: Detailed command-line parameters for 'abon' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'abon' subcommand. + """ + # Input validation + if not input_genome.is_file(): + return { + "command_executed": "zol-suite abon ...", + "stdout": "", + "stderr": f"Error: Input genome file not found: {input_genome}", + "output_files": [], + "error": True, + "message": f"Input genome file not found: {input_genome}", + } + + args = [ + "-g", str(input_genome), # Inferred flag for genome input + "-o", str(output_directory / "abon_results"), # Inferred output prefix/file + ] + if cli_args: + args.extend(cli_args) + + return _run_zol_suite_command("abon", args, output_directory, [input_genome]) + + +@mcp.tool() +def atpoc( + input_genome: Path, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Automated analysis of conservation/novelty for a sample's prophages. + + This tool allows users to check if temperate phages from their favorite strain/genome + are conserved or novel relative to other genomes available from the isolate's + respective genus. + + Note: Detailed command-line parameters for 'atpoc' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'atpoc' subcommand. + """ + # Input validation + if not input_genome.is_file(): + return { + "command_executed": "zol-suite atpoc ...", + "stdout": "", + "stderr": f"Error: Input genome file not found: {input_genome}", + "output_files": [], + "error": True, + "message": f"Input genome file not found: {input_genome}", + } + + args = [ + "-g", str(input_genome), # Inferred flag for genome input + "-o", str(output_directory / "atpoc_results"), # Inferred output prefix/file + ] + if cli_args: + args.extend(cli_args) + + return _run_zol_suite_command("atpoc", args, output_directory, [input_genome]) + + +@mcp.tool() +def apos( + input_genome: Path, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Automated analysis of conservation/novelty for a sample's plasmids. + + This tool allows users to check if plasmids from their favorite strain/genome + are conserved or novel relative to other genomes available from the isolate's + respective genus. + + Note: Detailed command-line parameters for 'apos' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'apos' subcommand. + """ + # Input validation + if not input_genome.is_file(): + return { + "command_executed": "zol-suite apos ...", + "stdout": "", + "stderr": f"Error: Input genome file not found: {input_genome}", + "output_files": [], + "error": True, + "message": f"Input genome file not found: {input_genome}", + } + + args = [ + "-g", str(input_genome), # Inferred flag for genome input + "-o", str(output_directory / "apos_results"), # Inferred output prefix/file + ] + if cli_args: + args.extend(cli_args) + + return _run_zol_suite_command("apos", args, output_directory, [input_genome]) + + +@mcp.tool() +def salt( + input_fai_results: Path, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Assess support for lateral/horizontal transfer of gene clusters (experimental). + + This tool reports statistics such as codon usage dissimilarity, presence of + plasmid/virus-associated proteins on the same scaffold, and distance from transposons + for gene cluster instances detected by fai. + + Note: Detailed command-line parameters for 'salt' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'salt' subcommand. + """ + # Input validation + if not input_fai_results.is_file(): + return { + "command_executed": "zol-suite salt ...", + "stdout": "", + "stderr": f"Error: Input fai results file not found: {input_fai_results}", + "output_files": [], + "error": True, + "message": f"Input fai results file not found: {input_fai_results}", + } + + args = [ + "-i", str(input_fai_results), # Inferred flag for input + "-o", str(output_directory / "salt_report"), # Inferred output prefix/file + ] + if cli_args: + args.extend(cli_args) + + return _run_zol_suite_command("salt", args, output_directory, [input_fai_results]) + + +@mcp.tool() +def regex( + input_genome: Path, + scaffold_id: str, + start_coordinate: int, + end_coordinate: int, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Extract a genomic region (as GenBank) from a genome file (FASTA or GenBank) + based on scaffold and coordinate inputs (experimental). + + Note: Detailed command-line parameters for 'regex' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'regex' subcommand. + """ + # Input validation + if not input_genome.is_file(): + return { + "command_executed": "zol-suite regex ...", + "stdout": "", + "stderr": f"Error: Input genome file not found: {input_genome}", + "output_files": [], + "error": True, + "message": f"Input genome file not found: {input_genome}", + } + if not scaffold_id: + return { + "command_executed": "zol-suite regex ...", + "stdout": "", + "stderr": "Error: Scaffold ID cannot be empty.", + "output_files": [], + "error": True, + "message": "Scaffold ID cannot be empty.", + } + if not (0 <= start_coordinate <= end_coordinate): + return { + "command_executed": "zol-suite regex ...", + "stdout": "", + "stderr": "Error: Invalid coordinates. Start must be <= End and both non-negative.", + "output_files": [], + "error": True, + "message": "Invalid coordinates. Start must be <= End and both non-negative.", + } + + args = [ + "-g", str(input_genome), + "-s", scaffold_id, + "-t", f"{start_coordinate}-{end_coordinate}", # Inferred format for coordinates + "-o", str(output_directory / f"{scaffold_id}_{start_coordinate}-{end_coordinate}.gbk"), # Inferred output file + ] + if cli_args: + args.extend(cli_args) + + return _run_zol_suite_command("regex", args, output_directory, [input_genome]) + + +@mcp.tool() +def zol_scape( + bigscape_results_directory: Path, + output_directory: Path, + cli_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Run zol analysis on BiG-SCAPE results. + + This tool is a simple wrapper to run zol for all Gene Cluster Families (GCFs) + determined by BiG-SCAPE, complementing CORASON visual results. + + Note: Detailed command-line parameters for 'zol-scape' are not available in the provided + documentation. The 'cli_args' parameter allows passing additional, undocumented + arguments directly to the 'zol-scape' subcommand. + """ + # Input validation + if not bigscape_results_directory.is_dir(): + return { + "command_executed": "zol-suite zol-scape ...", + "stdout": "", + "stderr": f"Error: BiG-SCAPE results directory not found: {bigscape_results_directory}", + "output_files": [], + "error": True, + "message": f"BiG-SCAPE results directory not found: {bigscape_results_directory}", + } + + args = [ + "-i", str(bigscape_results_directory), # Inferred flag for input directory + "-o", str(output_directory / "zol_scape_results"), # Inferred output prefix/directory + ] + if cli_args: + args.extend(cli_args) + + # No specific input files to validate, as it's a directory. + return _run_zol_suite_command("zol-scape", args, output_directory) diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/zol_shim_server.py b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/zol_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9bd242db3659f0a75737c860fee6bf6976aaa3ee --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/zol_shim_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/app/zol_server.py') +SERVER_NAME = 'biosci_zol' + + +class _ShimMCP: + @staticmethod + def tool(): + def _decorator(fn): + return fn + return _decorator + + +def _load_functions(): + code = SOURCE_SERVER.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(SOURCE_SERVER)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(SOURCE_SERVER), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/docker-compose.yml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..01651afaf40422b6dd079ac2442f64712c117050 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-zol: + build: . + image: mcp-zol:latest + container_name: mcp-zol + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=zol + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/environment.yaml b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6608f192d97acabe5d2cf7b27a6c291b8762c816 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - zol + - python=3.10 + \ No newline at end of file diff --git a/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/requirements.txt b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_zol/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp