diff --git a/Biomni/mcp_generated/mcp_bcbio-nextgen/Dockerfile b/Biomni/mcp_generated/mcp_bcbio-nextgen/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c3ee4e16c686152423e96c94fbde9fd5e62b1bfc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bcbio-nextgen/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 bcbio-nextgen via conda (e.g., from bioconda) +RUN conda install -c bioconda bcbio-nextgen -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/bcbio-nextgen_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/bcbio-nextgen_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/bcbio-nextgen_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bcbio-nextgen/app/bcbio-nextgen_server.py b/Biomni/mcp_generated/mcp_bcbio-nextgen/app/bcbio-nextgen_server.py new file mode 100644 index 0000000000000000000000000000000000000000..04d982a6ae846a6ec7eb6627779937a967d4b2ec --- /dev/null +++ b/Biomni/mcp_generated/mcp_bcbio-nextgen/app/bcbio-nextgen_server.py @@ -0,0 +1,276 @@ +from typing import List, Optional, Dict, Any +import subprocess +from pathlib import Path +import os + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bcbio_nextgen' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def bcbio_nextgen_run( + config_file: str, + num_cores: int = 1, + parallel_type: str = "local", + scheduler: Optional[str] = None, + queue: Optional[str] = None, + resources: Optional[str] = None, + tag: Optional[str] = None, + workdir: Optional[str] = None, + timeout: int = 15, + retries: int = 0, +) -> Dict[str, Any]: + """ + Run a bcbio-nextgen analysis pipeline using a provided configuration file. + + Args: + config_file: Path to the YAML configuration file defining the analysis. + num_cores: Number of local cores to use for parallel execution. + parallel_type: Type of parallel execution (local, ipython, cluster, etc.). + scheduler: Scheduler for cluster execution (e.g., sge, slurm, torque, pbspro, lsf). + queue: Queue to submit jobs to on a cluster. + resources: Specific resource requirements for the scheduler (e.g., 'mem=16,vmem=20'). + tag: Optional tag to identify this specific run. + workdir: Directory to use for processing (defaults to current directory). + timeout: Time in minutes to wait for ipython cluster startup. + retries: Number of times to retry failed steps. + """ + config_path = Path(config_file) + if not config_path.exists(): + return {"error": f"Configuration file not found: {config_file}"} + + cmd = ["bcbio_nextgen.py", str(config_path.absolute())] + + cmd.extend(["-n", str(num_cores)]) + cmd.extend(["-t", parallel_type]) + + if scheduler: + cmd.extend(["-s", scheduler]) + if queue: + cmd.extend(["-q", queue]) + if resources: + cmd.extend(["-r", resources]) + if tag: + cmd.extend(["--tag", tag]) + if timeout != 15: + cmd.extend(["--timeout", str(timeout)]) + if retries > 0: + cmd.extend(["--retries", str(retries)]) + + # Handle working directory + original_dir = os.getcwd() + if workdir: + work_path = Path(workdir) + if not work_path.exists(): + work_path.mkdir(parents=True, exist_ok=True) + os.chdir(work_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" + } + finally: + os.chdir(original_dir) + +@mcp.tool() +def bcbio_nextgen_template( + template_name: str, + metadata_csv: str, + input_files: List[str], + out_dir: Optional[str] = None, +) -> Dict[str, Any]: + """ + Create a bcbio-nextgen processing description from a template and input files. + + Args: + template_name: Name of the template to use (e.g., 'freebayes-variant', 'gatk-variant'). + metadata_csv: Path to a CSV file containing sample metadata. + input_files: List of paths to input BAM or FASTQ files. + out_dir: Directory to write the generated configuration (defaults to current directory). + """ + metadata_path = Path(metadata_csv) + if not metadata_path.exists(): + return {"error": f"Metadata file not found: {metadata_csv}"} + + # Validate input files + valid_inputs = [] + for f in input_files: + p = Path(f) + if p.exists(): + valid_inputs.append(str(p.absolute())) + else: + return {"error": f"Input file not found: {f}"} + + cmd = ["bcbio_nextgen.py", "-w", "template", template_name, str(metadata_path.absolute())] + cmd.extend(valid_inputs) + + # Handle output directory + original_dir = os.getcwd() + if out_dir: + out_path = Path(out_dir) + if not out_path.exists(): + out_path.mkdir(parents=True, exist_ok=True) + os.chdir(out_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", + "info": "Configuration files generated in the output directory." + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "failed" + } + finally: + os.chdir(original_dir) + +@mcp.tool() +def bcbio_nextgen_upgrade( + tooldir: Optional[str] = None, + tools: bool = False, + data: bool = False, + genomes: Optional[List[str]] = None, + aligners: Optional[List[str]] = None, + cores: int = 1, +) -> Dict[str, Any]: + """ + Upgrade bcbio-nextgen software, third-party tools, or genome data. + + Args: + tooldir: Directory where tools are installed. + tools: If True, upgrade third-party software tools. + data: If True, upgrade/install genome data. + genomes: List of genome builds to install/upgrade (e.g., ['hg38', 'mm10']). + aligners: List of aligners to install data for (e.g., ['bwa', 'bowtie2']). + cores: Number of cores to use for data downloads and indexing. + """ + cmd = ["bcbio_nextgen.py", "upgrade"] + + if tooldir: + cmd.extend(["--tooldir", tooldir]) + if tools: + cmd.append("--tools") + if data: + cmd.append("--data") + + if genomes: + for g in genomes: + cmd.extend(["--genomes", g]) + + if aligners: + for a in aligners: + cmd.extend(["--aligners", a]) + + cmd.extend(["--cores", str(cores)]) + + try: + # Upgrades can take a long time, but we capture output + 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 bcbio_nextgen_install( + install_path: str, + tooldir: str, + genomes: List[str], + aligners: List[str], + nodata: bool = False, + isolate: bool = False, +) -> Dict[str, Any]: + """ + Run the bcbio-nextgen installer script to set up the environment. + + Args: + install_path: Path to install bcbio-nextgen data and code. + tooldir: Path to install third-party software tools. + genomes: List of genome builds to install (e.g., ['hg38']). + aligners: List of aligners to prepare (e.g., ['bwa']). + nodata: If True, do not install genome data. + isolate: If True, install into an isolated environment. + """ + # Note: This assumes bcbio_nextgen_install.py is in the PATH or current directory + # In a real environment, users might need to download it first. + cmd = ["python", "bcbio_nextgen_install.py", install_path, "--tooldir=" + tooldir] + + for g in genomes: + cmd.extend(["--genomes", g]) + for a in aligners: + cmd.extend(["--aligners", a]) + + if nodata: + cmd.append("--nodata") + if isolate: + cmd.append("--isolate") + + 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 bcbio_nextgen_version() -> Dict[str, Any]: + """ + Check the installed version of bcbio-nextgen. + """ + cmd = ["bcbio_nextgen.py", "--version"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout.strip(), + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "error": str(e), + "status": "failed" + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bcbio-nextgen/app/bcbio-nextgen_shim_server.py b/Biomni/mcp_generated/mcp_bcbio-nextgen/app/bcbio-nextgen_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..cd2f4fe66afb0c59d2e4b1cf35cda65d7b7231c4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bcbio-nextgen/app/bcbio-nextgen_shim_server.py @@ -0,0 +1,55 @@ +#!/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_bcbio-nextgen/app/bcbio-nextgen_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bcbio_nextgen' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bcbio-nextgen/docker-compose.yml b/Biomni/mcp_generated/mcp_bcbio-nextgen/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..67a946efd6f123ca05819a74532a97d4f84baeee --- /dev/null +++ b/Biomni/mcp_generated/mcp_bcbio-nextgen/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bcbio-nextgen: + build: . + image: mcp-bcbio-nextgen:latest + container_name: mcp-bcbio-nextgen + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bcbio-nextgen + 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/Biomni/mcp_generated/mcp_bcbio-nextgen/environment.yaml b/Biomni/mcp_generated/mcp_bcbio-nextgen/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0fe9688f1cc6b35c7e0b9386c01415dc1e8fc3b1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bcbio-nextgen/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bcbio-nextgen + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bcbio-nextgen/requirements.txt b/Biomni/mcp_generated/mcp_bcbio-nextgen/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bcbio-nextgen/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-banksy/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-banksy/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a1bfc36fcdba582407198bff96211ec04fd0c3b4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-banksy/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-banksy via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-banksy -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-banksy_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-banksy_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-banksy_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-banksy/app/bioconductor-banksy_server.py b/Biomni/mcp_generated/mcp_bioconductor-banksy/app/bioconductor-banksy_server.py new file mode 100644 index 0000000000000000000000000000000000000000..abf9e0c8546a4f944ade77f3c371613f842c5032 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-banksy/app/bioconductor-banksy_server.py @@ -0,0 +1,667 @@ +import subprocess +import tempfile +import os +from pathlib import Path +from typing import Optional, List, Dict, Any + +# Note: The @mcp.tool() decorator is assumed to be available in the environment +# where this code will be run, as per the instructions "NO NEED to import mcp". + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_banksy' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def banksy_init_object( + input_seurat_rdata_path: Path, + output_banksy_rdata_path: Path, + assay: str = "Spatial", + verbose: bool = True, +) -> Dict[str, Any]: + """ + Initializes a BanksyObject from an existing Seurat object. + + This tool takes an RData file containing a Seurat object, converts it + into a BanksyObject, and saves the new BanksyObject to an RData file. + This is often the first step before running the BANKSY algorithm. + + Requires R and the 'banksy' and 'Seurat' R packages to be installed + and accessible in the environment. + + Args: + input_seurat_rdata_path: Path to the input .RData file containing a Seurat object. + output_banksy_rdata_path: Path where the new BanksyObject will be saved as an .RData file. + assay: Name of the assay in the Seurat object to use for spatial data. + verbose: If TRUE, print messages during execution. + + Returns: + A dictionary containing execution details: + - command_executed: The Rscript command and generated R script path. + - stdout: Standard output from the Rscript execution. + - stderr: Standard error from the Rscript execution. + - output_files: A list of paths to generated output files. + """ + # 1. Input validation + if not input_seurat_rdata_path.exists(): + raise FileNotFoundError(f"Input Seurat RData file not found: {input_seurat_rdata_path}") + if not input_seurat_rdata_path.is_file(): + raise ValueError(f"Input Seurat RData path is not a file: {input_seurat_rdata_path}") + if output_banksy_rdata_path.suffix.lower() != ".rdata": + raise ValueError(f"Output RData file must have a .RData extension: {output_banksy_rdata_path}") + if not assay: + raise ValueError("Assay name cannot be empty.") + + # Ensure output directory exists + output_banksy_rdata_path.parent.mkdir(parents=True, exist_ok=True) + + # 2. Generate R script content + r_script_content = f""" + # Load required packages + library(Seurat) + library(banksy) + + # Define input and output paths + input_obj_path <- "{input_seurat_rdata_path.as_posix()}" + output_obj_path <- "{output_banksy_rdata_path.as_posix()}" + + # Check if input file exists + if (!file.exists(input_obj_path)) {{ + stop(paste("Input RData file not found:", input_obj_path)) + }} + + # Load the object from RData. This approach handles cases where the object + # name inside the RData file is not known beforehand. + loaded_env <- new.env() + load(input_obj_path, envir = loaded_env) + + seurat_obj <- NULL + for (var_name in ls(loaded_env)) {{ + candidate <- get(var_name, envir = loaded_env) + if (inherits(candidate, "Seurat")) {{ + seurat_obj <- candidate + break + }} + }} + + if (is.null(seurat_obj)) {{ + stop("No Seurat object found in the input RData file.") + }} + + message("Initializing BanksyObject from Seurat object with parameters:") + message(paste(" assay:", "{assay}")) + message(paste(" verbose:", {str(verbose).upper()})) + + # Create BanksyObject + banksy_obj <- BanksyObject( + seurat_obj, + assay = "{assay}", + verbose = {str(verbose).upper()} + ) + + # Save the new BanksyObject. Renaming to 'obj' for consistency with other banksy tools. + obj <- banksy_obj + save(obj, file = output_obj_path) + + message(paste("BanksyObject saved to:", output_obj_path)) + """ + + temp_r_script_path: Optional[Path] = None + try: + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".R") as temp_r_script: + temp_r_script.write(r_script_content) + temp_r_script_path = Path(temp_r_script.name) + + command = ["Rscript", str(temp_r_script_path)] + + # 3. Subprocess execution + process = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + env=os.environ # Pass current environment to Rscript + ) + + stdout = process.stdout + stderr = process.stderr + + # 4. Error handling: Check for R-specific errors in stderr + if "Error" in stderr or "stop(" in stderr: + raise RuntimeError(f"R script execution failed. Stderr: {stderr}") + + if not output_banksy_rdata_path.exists(): + raise RuntimeError(f"Output file was not created by R script: {output_banksy_rdata_path}") + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_banksy_rdata_path)], + } + + except FileNotFoundError: + raise RuntimeError("Rscript command not found. Is R installed and in your PATH?") + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Rscript execution failed with exit code {e.returncode}.\n" + f"Command: {' '.join(e.cmd)}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + finally: + # Clean up temporary R script + if temp_r_script_path and temp_r_script_path.exists(): + os.remove(temp_r_script_path) + + +@mcp.tool() +def banksy_run_banksy( + input_rdata_path: Path, + output_rdata_path: Path, + k_neighbours: int = 10, + lambda_param: float = 0.1, # Renamed from 'lambda' to avoid Python keyword conflict + resolution: float = 0.8, + n_components: int = 2, + n_cores: int = 1, + verbose: bool = True, + seed: int = 123, +) -> Dict[str, Any]: + """ + Runs the core BANKSY algorithm on a Seurat or BanksyObject. + + This tool takes an RData file containing a Seurat or BanksyObject, + applies the BANKSY algorithm for spatial transcriptomics analysis, + and saves the updated object to a new RData file. + + Requires R and the 'banksy', 'Seurat', and 'future' R packages to be installed + and accessible in the environment. + + Args: + input_rdata_path: Path to the input .RData file containing a Seurat or BanksyObject. + output_rdata_path: Path where the updated Seurat/BanksyObject will be saved as an .RData file. + k_neighbours: Number of neighbours for spatial graph construction. + lambda_param: Weight of spatial information (lambda parameter). + resolution: Resolution parameter for clustering. + n_components: Number of components for dimensionality reduction (e.g., UMAP/tSNE). + n_cores: Number of cores to use for parallel processing. + verbose: If TRUE, print messages during execution. + seed: Random seed for reproducibility. + + Returns: + A dictionary containing execution details: + - command_executed: The Rscript command and generated R script path. + - stdout: Standard output from the Rscript execution. + - stderr: Standard error from the Rscript execution. + - output_files: A list of paths to generated output files. + """ + # 1. Input validation + if not input_rdata_path.exists(): + raise FileNotFoundError(f"Input RData file not found: {input_rdata_path}") + if not input_rdata_path.is_file(): + raise ValueError(f"Input RData path is not a file: {input_rdata_path}") + if output_rdata_path.suffix.lower() != ".rdata": + raise ValueError(f"Output RData file must have a .RData extension: {output_rdata_path}") + if k_neighbours <= 0: + raise ValueError("k_neighbours must be a positive integer.") + if not (0 <= lambda_param <= 1): + raise ValueError("lambda_param must be between 0 and 1.") + if resolution <= 0: + raise ValueError("resolution must be a positive float.") + if n_components <= 0: + raise ValueError("n_components must be a positive integer.") + if n_cores <= 0: + raise ValueError("n_cores must be a positive integer.") + + # Ensure output directory exists + output_rdata_path.parent.mkdir(parents=True, exist_ok=True) + + # 2. Generate R script content + r_script_content = f""" + # Load required packages + library(Seurat) + library(banksy) + library(future) # For parallel processing + + # Set up parallel processing if n_cores > 1 + if ({n_cores} > 1) {{ + plan("multisession", workers = {n_cores}) + }} else {{ + plan("sequential") + }} + + # Set random seed for reproducibility + set.seed({seed}) + + # Define input and output paths + input_obj_path <- "{input_rdata_path.as_posix()}" + output_obj_path <- "{output_rdata_path.as_posix()}" + + # Check if input file exists + if (!file.exists(input_obj_path)) {{ + stop(paste("Input RData file not found:", input_obj_path)) + }} + + # Load the object + loaded_env <- new.env() + load(input_obj_path, envir = loaded_env) + + obj <- NULL + for (var_name in ls(loaded_env)) {{ + candidate <- get(var_name, envir = loaded_env) + if (inherits(candidate, "Seurat") || inherits(candidate, "BanksyObject")) {{ + obj <- candidate + break + }} + }} + + if (is.null(obj)) {{ + stop("No Seurat or BanksyObject found in the input RData file.") + }} + + message("Running banksy with parameters:") + message(paste(" k_neighbours:", {k_neighbours})) + message(paste(" lambda:", {lambda_param})) + message(paste(" resolution:", {resolution})) + message(paste(" n_components:", {n_components})) + message(paste(" n_cores:", {n_cores})) + message(paste(" verbose:", {str(verbose).upper()})) + message(paste(" seed:", {seed})) + + # Run BANKSY + obj <- runBanksy( + object = obj, + k_neighbours = {k_neighbours}, + lambda = {lambda_param}, + resolution = {resolution}, + n_components = {n_components}, + verbose = {str(verbose).upper()} + ) + + # Save the updated object + save(obj, file = output_obj_path) + + message(paste("Updated object saved to:", output_obj_path)) + """ + + temp_r_script_path: Optional[Path] = None + try: + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".R") as temp_r_script: + temp_r_script.write(r_script_content) + temp_r_script_path = Path(temp_r_script.name) + + command = ["Rscript", str(temp_r_script_path)] + + # 3. Subprocess execution + process = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + env=os.environ + ) + + stdout = process.stdout + stderr = process.stderr + + # 4. Error handling: Check for R-specific errors in stderr + if "Error" in stderr or "stop(" in stderr: + raise RuntimeError(f"R script execution failed. Stderr: {stderr}") + + if not output_rdata_path.exists(): + raise RuntimeError(f"Output file was not created by R script: {output_rdata_path}") + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_rdata_path)], + } + + except FileNotFoundError: + raise RuntimeError("Rscript command not found. Is R installed and in your PATH?") + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Rscript execution failed with exit code {e.returncode}.\n" + f"Command: {' '.join(e.cmd)}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + finally: + # Clean up temporary R script + if temp_r_script_path and temp_r_script_path.exists(): + os.remove(temp_r_script_path) + + +@mcp.tool() +def banksy_cluster_banksy( + input_rdata_path: Path, + output_rdata_path: Path, + resolution: float = 0.8, + method: str = "leiden", + verbose: bool = True, + seed: int = 123, +) -> Dict[str, Any]: + """ + Performs clustering on a BanksyObject or Seurat object after BANKSY analysis. + + This tool takes an RData file containing a Seurat or BanksyObject (typically + after `runBanksy`), performs clustering using the specified method and resolution, + and saves the updated object to a new RData file. + + Requires R and the 'banksy' and 'Seurat' R packages to be installed + and accessible in the environment. + + Args: + input_rdata_path: Path to the input .RData file containing a Seurat or BanksyObject. + output_rdata_path: Path where the updated Seurat/BanksyObject will be saved as an .RData file. + resolution: Resolution parameter for clustering. + method: Clustering method to use (e.g., "leiden", "louvain"). + verbose: If TRUE, print messages during execution. + seed: Random seed for reproducibility. + + Returns: + A dictionary containing execution details: + - command_executed: The Rscript command and generated R script path. + - stdout: Standard output from the Rscript execution. + - stderr: Standard error from the Rscript execution. + - output_files: A list of paths to generated output files. + """ + # 1. Input validation + if not input_rdata_path.exists(): + raise FileNotFoundError(f"Input RData file not found: {input_rdata_path}") + if not input_rdata_path.is_file(): + raise ValueError(f"Input RData path is not a file: {input_rdata_path}") + if output_rdata_path.suffix.lower() != ".rdata": + raise ValueError(f"Output RData file must have a .RData extension: {output_rdata_path}") + if resolution <= 0: + raise ValueError("resolution must be a positive float.") + if method not in ["leiden", "louvain"]: + raise ValueError(f"Unsupported clustering method: {method}. Choose from 'leiden', 'louvain'.") + + # Ensure output directory exists + output_rdata_path.parent.mkdir(parents=True, exist_ok=True) + + # 2. Generate R script content + r_script_content = f""" + # Load required packages + library(Seurat) + library(banksy) + + # Set random seed for reproducibility + set.seed({seed}) + + # Define input and output paths + input_obj_path <- "{input_rdata_path.as_posix()}" + output_obj_path <- "{output_rdata_path.as_posix()}" + + # Check if input file exists + if (!file.exists(input_obj_path)) {{ + stop(paste("Input RData file not found:", input_obj_path)) + }} + + # Load the object + loaded_env <- new.env() + load(input_obj_path, envir = loaded_env) + + obj <- NULL + for (var_name in ls(loaded_env)) {{ + candidate <- get(var_name, envir = loaded_env) + if (inherits(candidate, "Seurat") || inherits(candidate, "BanksyObject")) {{ + obj <- candidate + break + }} + }} + + if (is.null(obj)) {{ + stop("No Seurat or BanksyObject found in the input RData file.") + }} + + message("Clustering banksy object with parameters:") + message(paste(" resolution:", {resolution})) + message(paste(" method:", "{method}")) + message(paste(" verbose:", {str(verbose).upper()})) + message(paste(" seed:", {seed})) + + # Cluster BANKSY object + obj <- clusterBanksy( + object = obj, + resolution = {resolution}, + method = "{method}", + verbose = {str(verbose).upper()} + ) + + # Save the updated object + save(obj, file = output_obj_path) + + message(paste("Updated object saved to:", output_obj_path)) + """ + + temp_r_script_path: Optional[Path] = None + try: + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".R") as temp_r_script: + temp_r_script.write(r_script_content) + temp_r_script_path = Path(temp_r_script.name) + + command = ["Rscript", str(temp_r_script_path)] + + # 3. Subprocess execution + process = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + env=os.environ + ) + + stdout = process.stdout + stderr = process.stderr + + # 4. Error handling: Check for R-specific errors in stderr + if "Error" in stderr or "stop(" in stderr: + raise RuntimeError(f"R script execution failed. Stderr: {stderr}") + + if not output_rdata_path.exists(): + raise RuntimeError(f"Output file was not created by R script: {output_rdata_path}") + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_rdata_path)], + } + + except FileNotFoundError: + raise RuntimeError("Rscript command not found. Is R installed and in your PATH?") + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Rscript execution failed with exit code {e.returncode}.\n" + f"Command: {' '.join(e.cmd)}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + finally: + # Clean up temporary R script + if temp_r_script_path and temp_r_script_path.exists(): + os.remove(temp_r_script_path) + + +@mcp.tool() +def banksy_spatial_dim_plot( + input_rdata_path: Path, + output_plot_path: Path, + reduction: str = "banksy", + group_by: str = "banksy_clusters", + label: bool = True, + pt_size: float = 1.5, + verbose: bool = True, + width: float = 7.0, + height: float = 7.0, + units: str = "in", + dpi: int = 300, +) -> Dict[str, Any]: + """ + Generates a spatial dimensionality plot for a BanksyObject. + + This tool takes an RData file containing a BanksyObject (typically after + `runBanksy` and `clusterBanksy`), generates a spatial plot, and saves it + to an image file (e.g., PNG, PDF). + + Requires R and the 'banksy', 'Seurat', and 'ggplot2' R packages to be installed + and accessible in the environment. + + Args: + input_rdata_path: Path to the input .RData file containing a BanksyObject. + output_plot_path: Path where the plot will be saved (e.g., .png, .pdf, .jpeg). + reduction: Dimensionality reduction to use for plotting (e.g., "banksy", "umap"). + group_by: Feature to group cells by for coloring (e.g., "banksy_clusters"). + label: If TRUE, label clusters on the plot. + pt_size: Size of the points in the plot. + verbose: If TRUE, print messages during execution. + width: Width of the output plot. + height: Height of the output plot. + units: Units for width and height ("in", "cm", "mm"). + dpi: Resolution for raster plots (e.g., PNG, JPEG). + + Returns: + A dictionary containing execution details: + - command_executed: The Rscript command and generated R script path. + - stdout: Standard output from the Rscript execution. + - stderr: Standard error from the Rscript execution. + - output_files: A list of paths to generated output files. + """ + # 1. Input validation + if not input_rdata_path.exists(): + raise FileNotFoundError(f"Input RData file not found: {input_rdata_path}") + if not input_rdata_path.is_file(): + raise ValueError(f"Input RData path is not a file: {input_rdata_path}") + + valid_plot_suffixes = [".png", ".pdf", ".jpeg", ".jpg", ".tiff", ".bmp"] + if output_plot_path.suffix.lower() not in valid_plot_suffixes: + raise ValueError(f"Output plot file must have one of the following extensions: {', '.join(valid_plot_suffixes)}") + + if pt_size <= 0: + raise ValueError("pt_size must be a positive float.") + if width <= 0 or height <= 0: + raise ValueError("Width and height must be positive floats.") + if units not in ["in", "cm", "mm"]: + raise ValueError(f"Invalid units: {units}. Choose from 'in', 'cm', 'mm'.") + if dpi <= 0: + raise ValueError("DPI must be a positive integer.") + + # Ensure output directory exists + output_plot_path.parent.mkdir(parents=True, exist_ok=True) + + # 2. Generate R script content + r_script_content = f""" + # Load required packages + library(Seurat) + library(banksy) + library(ggplot2) # For saving plots + + # Define input and output paths + input_obj_path <- "{input_rdata_path.as_posix()}" + output_plot_path <- "{output_plot_path.as_posix()}" + + # Check if input file exists + if (!file.exists(input_obj_path)) {{ + stop(paste("Input RData file not found:", input_obj_path)) + }} + + # Load the object + loaded_env <- new.env() + load(input_obj_path, envir = loaded_env) + + obj <- NULL + for (var_name in ls(loaded_env)) {{ + candidate <- get(var_name, envir = loaded_env) + if (inherits(candidate, "Seurat") || inherits(candidate, "BanksyObject")) {{ + obj <- candidate + break + }} + }} + + if (is.null(obj)) {{ + stop("No Seurat or BanksyObject found in the input RData file.") + }} + + message("Generating spatial dimensionality plot with parameters:") + message(paste(" reduction:", "{reduction}")) + message(paste(" group_by:", "{group_by}")) + message(paste(" label:", {str(label).upper()})) + message(paste(" pt_size:", {pt_size})) + message(paste(" verbose:", {str(verbose).upper()})) + + # Generate plot + p <- spatialDimPlot( + object = obj, + reduction = "{reduction}", + group.by = "{group_by}", + label = {str(label).upper()}, + pt.size.factor = {pt_size}, # Note: R parameter is pt.size.factor + verbose = {str(verbose).upper()} + ) + + # Save the plot + ggsave( + filename = output_plot_path, + plot = p, + width = {width}, + height = {height}, + units = "{units}", + dpi = {dpi} + ) + + message(paste("Plot saved to:", output_plot_path)) + """ + + temp_r_script_path: Optional[Path] = None + try: + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".R") as temp_r_script: + temp_r_script.write(r_script_content) + temp_r_script_path = Path(temp_r_script.name) + + command = ["Rscript", str(temp_r_script_path)] + + # 3. Subprocess execution + process = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + env=os.environ + ) + + stdout = process.stdout + stderr = process.stderr + + # 4. Error handling: Check for R-specific errors in stderr + if "Error" in stderr or "stop(" in stderr: + raise RuntimeError(f"R script execution failed. Stderr: {stderr}") + + if not output_plot_path.exists(): + raise RuntimeError(f"Output plot file was not created by R script: {output_plot_path}") + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_plot_path)], + } + + except FileNotFoundError: + raise RuntimeError("Rscript command not found. Is R installed and in your PATH?") + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Rscript execution failed with exit code {e.returncode}.\n" + f"Command: {' '.join(e.cmd)}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + finally: + # Clean up temporary R script + if temp_r_script_path and temp_r_script_path.exists(): + os.remove(temp_r_script_path) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-banksy/app/bioconductor-banksy_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-banksy/app/bioconductor-banksy_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..443a3dbaa17b252927fcc6750ea19d64dcd0aa4d --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-banksy/app/bioconductor-banksy_shim_server.py @@ -0,0 +1,55 @@ +#!/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-banksy/app/bioconductor-banksy_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_banksy' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-banksy/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-banksy/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-banksy/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-banksy/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-banksy/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..5745dcb463d5d3a4a0240bea07df9ef2cafba776 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-banksy/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-banksy: + build: . + image: mcp-bioconductor-banksy:latest + container_name: mcp-bioconductor-banksy + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-banksy + 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/Biomni/mcp_generated/mcp_bioconductor-banksy/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-banksy/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..508d5e0076bfb9bafa1307f48692fa5c85f624eb --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-banksy/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-banksy + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-banksy/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-banksy/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-banksy/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-benchdamic/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..049a35d085b83f15bc149115150cd24367530eef --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/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-benchdamic via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-benchdamic -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-benchdamic_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-benchdamic_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-benchdamic_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_server.py b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a6ed675c4d398642f704254b729325432807bab0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_server.py @@ -0,0 +1,464 @@ +import subprocess +import tempfile +from pathlib import Path +import os +import logging +from typing import List, Optional + +# Set up logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# MCP decorator is assumed to be available in the execution environment. +# This is a placeholder for the actual decorator. +class mcp: + @staticmethod + def tool(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + +@mcp.tool +def run_benchdamic( + config_file: Path, + output_dir: Path, +): + """ + Runs the full benchdamic pipeline. + + This function serves as the main entry point for the benchdamic workflow, + executing data generation, method application, evaluation, and plotting + based on a single YAML configuration file. The process is run within the + specified output directory. + + Args: + config_file: Path to the benchdamic YAML configuration file. + output_dir: Path to the directory where all outputs will be stored. + It will be created if it doesn't exist. + """ + # 1. Input validation + if not config_file.is_file(): + raise FileNotFoundError(f"Configuration file not found: {config_file}") + + # 2. File path handling + output_dir.mkdir(parents=True, exist_ok=True) + + # The R function benchdamic() relies on the 'output_dir' key within the config YAML. + # By running Rscript from the specified output_dir, we ensure that + # relative paths in the config file are resolved correctly. + r_script_content = f""" + if (!requireNamespace("benchdamic", quietly = TRUE)) {{ + stop("The 'benchdamic' R package is not installed. Please install it from Bioconductor.") + }} + library(benchdamic) + + config_path <- "{config_file.resolve()}" + + cat("Starting benchdamic pipeline...\\n") + benchdamic::benchdamic(config_file = config_path) + cat("benchdamic pipeline finished successfully.\\n") + """ + + # 3. Subprocess execution + command_to_execute = [] + stdout_str = "" + stderr_str = "" + r_script_path = None + + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False, dir=output_dir) as r_script_file: + r_script_file.write(r_script_content) + r_script_path = Path(r_script_file.name) + + # Use relative path for the command since we set cwd + command_to_execute = ["Rscript", str(r_script_path.name)] + + files_before = set(os.listdir(output_dir)) + + process = subprocess.run( + command_to_execute, + capture_output=True, + text=True, + check=True, + cwd=output_dir, + ) + + stdout_str = process.stdout + stderr_str = process.stderr + + files_after = set(os.listdir(output_dir)) + # Find new files and directories + new_items = files_after - files_before + output_files = [str(output_dir / item) for item in new_items] + + except FileNotFoundError: + err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH." + logger.error(err_msg) + raise RuntimeError(err_msg) from None + except subprocess.CalledProcessError as e: + logger.error(f"R script execution failed with exit code {e.returncode}") + logger.error(f"Command: {' '.join(command_to_execute)}") + logger.error(f"Stdout: {e.stdout}") + logger.error(f"Stderr: {e.stderr}") + raise + finally: + if r_script_path and r_script_path.exists(): + r_script_path.unlink() + + # 4. Structured result return + return { + "command_executed": " ".join(command_to_execute), + "stdout": stdout_str, + "stderr": stderr_str, + "output_files": output_files + } + +@mcp.tool +def generate_data( + config_file: Path, + output_dir: Path, +): + """ + Generates simulated data using the benchdamic configuration. + + This function corresponds to the `generate_data` step in the benchdamic + workflow. It reads simulation parameters from the config file and writes + the generated datasets to the specified output directory. + + Args: + config_file: Path to the benchdamic YAML configuration file. + output_dir: Path to the directory where generated data will be stored. + It will be created if it doesn't exist. + """ + # 1. Input validation + if not config_file.is_file(): + raise FileNotFoundError(f"Configuration file not found: {config_file}") + + # 2. File path handling + output_dir.mkdir(parents=True, exist_ok=True) + + r_script_content = f""" + if (!requireNamespace("benchdamic", quietly = TRUE)) {{ + stop("The 'benchdamic' R package is not installed.") + }} + library(benchdamic) + + config_path <- "{config_file.resolve()}" + output_path <- "{output_dir.resolve()}" + + cat("Starting data generation...\\n") + benchdamic::generate_data(config_file = config_path, output_dir = output_path) + cat("Data generation finished successfully.\\n") + """ + + # 3. Subprocess execution + command_to_execute = [] + stdout_str = "" + stderr_str = "" + r_script_path = None + + try: + 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_to_execute = ["Rscript", str(r_script_path)] + + files_before = set(p.resolve() for p in output_dir.glob('**/*')) + + process = subprocess.run( + command_to_execute, + capture_output=True, + text=True, + check=True, + ) + + stdout_str = process.stdout + stderr_str = process.stderr + + files_after = set(p.resolve() for p in output_dir.glob('**/*')) + new_files = [str(p) for p in files_after - files_before] + + except FileNotFoundError: + err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH." + logger.error(err_msg) + raise RuntimeError(err_msg) from None + except subprocess.CalledProcessError as e: + logger.error(f"R script execution failed with exit code {e.returncode}") + logger.error(f"Command: {' '.join(command_to_execute)}") + logger.error(f"Stdout: {e.stdout}") + logger.error(f"Stderr: {e.stderr}") + raise + finally: + if r_script_path and r_script_path.exists(): + r_script_path.unlink() + + # 4. Structured result return + return { + "command_executed": " ".join(command_to_execute), + "stdout": stdout_str, + "stderr": stderr_str, + "output_files": new_files + } + +@mcp.tool +def run_methods( + config_file: Path, + output_dir: Path, +): + """ + Runs differential abundance methods on generated data. + + This function corresponds to the `run_methods` step in the benchdamic + workflow. It applies the specified methods to the datasets found in the + output directory and saves their results. + + Args: + config_file: Path to the benchdamic YAML configuration file. + output_dir: Path to the directory containing the generated data and + where method results will be stored. It must exist. + """ + # 1. Input validation + if not config_file.is_file(): + raise FileNotFoundError(f"Configuration file not found: {config_file}") + if not output_dir.is_dir(): + raise FileNotFoundError(f"Output directory not found: {output_dir}. Please run generate_data first.") + + # 2. R script content + r_script_content = f""" + if (!requireNamespace("benchdamic", quietly = TRUE)) {{ + stop("The 'benchdamic' R package is not installed.") + }} + library(benchdamic) + + config_path <- "{config_file.resolve()}" + output_path <- "{output_dir.resolve()}" + + cat("Running differential abundance methods...\\n") + benchdamic::run_methods(config_file = config_path, output_dir = output_path) + cat("Method execution finished successfully.\\n") + """ + + # 3. Subprocess execution + command_to_execute = [] + stdout_str = "" + stderr_str = "" + r_script_path = None + + try: + 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_to_execute = ["Rscript", str(r_script_path)] + + files_before = set(p.resolve() for p in output_dir.glob('**/*')) + + process = subprocess.run( + command_to_execute, + capture_output=True, + text=True, + check=True, + ) + + stdout_str = process.stdout + stderr_str = process.stderr + + files_after = set(p.resolve() for p in output_dir.glob('**/*')) + new_files = [str(p) for p in files_after - files_before] + + except FileNotFoundError: + err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH." + logger.error(err_msg) + raise RuntimeError(err_msg) from None + except subprocess.CalledProcessError as e: + logger.error(f"R script execution failed with exit code {e.returncode}") + logger.error(f"Command: {' '.join(command_to_execute)}") + logger.error(f"Stdout: {e.stdout}") + logger.error(f"Stderr: {e.stderr}") + raise + finally: + if r_script_path and r_script_path.exists(): + r_script_path.unlink() + + # 4. Structured result return + return { + "command_executed": " ".join(command_to_execute), + "stdout": stdout_str, + "stderr": stderr_str, + "output_files": new_files + } + +@mcp.tool +def evaluate_methods( + config_file: Path, + output_dir: Path, +): + """ + Evaluates the performance of differential abundance methods. + + This function corresponds to the `evaluate_methods` step in the benchdamic + workflow. It computes performance metrics based on the method results + and ground truth, saving the evaluation to the output directory. + + Args: + config_file: Path to the benchdamic YAML configuration file. + output_dir: Path to the directory containing method results. It must exist. + """ + # 1. Input validation + if not config_file.is_file(): + raise FileNotFoundError(f"Configuration file not found: {config_file}") + if not output_dir.is_dir(): + raise FileNotFoundError(f"Output directory not found: {output_dir}.") + + # 2. R script content + r_script_content = f""" + if (!requireNamespace("benchdamic", quietly = TRUE)) {{ + stop("The 'benchdamic' R package is not installed.") + }} + library(benchdamic) + + config_path <- "{config_file.resolve()}" + output_path <- "{output_dir.resolve()}" + + cat("Evaluating method performance...\\n") + benchdamic::evaluate_methods(config_file = config_path, output_dir = output_path) + cat("Evaluation finished successfully.\\n") + """ + + # 3. Subprocess execution + command_to_execute = [] + stdout_str = "" + stderr_str = "" + r_script_path = None + + try: + 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_to_execute = ["Rscript", str(r_script_path)] + + files_before = set(p.resolve() for p in output_dir.glob('**/*')) + + process = subprocess.run( + command_to_execute, + capture_output=True, + text=True, + check=True, + ) + + stdout_str = process.stdout + stderr_str = process.stderr + + files_after = set(p.resolve() for p in output_dir.glob('**/*')) + new_files = [str(p) for p in files_after - files_before] + + except FileNotFoundError: + err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH." + logger.error(err_msg) + raise RuntimeError(err_msg) from None + except subprocess.CalledProcessError as e: + logger.error(f"R script execution failed with exit code {e.returncode}") + logger.error(f"Command: {' '.join(command_to_execute)}") + logger.error(f"Stdout: {e.stdout}") + logger.error(f"Stderr: {e.stderr}") + raise + finally: + if r_script_path and r_script_path.exists(): + r_script_path.unlink() + + # 4. Structured result return + return { + "command_executed": " ".join(command_to_execute), + "stdout": stdout_str, + "stderr": stderr_str, + "output_files": new_files + } + +@mcp.tool +def plot_results( + config_file: Path, + output_dir: Path, +): + """ + Generates plots from the evaluation results. + + This function corresponds to the `plot_results` step in the benchdamic + workflow. It creates various visualizations of the performance metrics + and saves them as files in the output directory. + + Args: + config_file: Path to the benchdamic YAML configuration file. + output_dir: Path to the directory containing evaluation results. It must exist. + """ + # 1. Input validation + if not config_file.is_file(): + raise FileNotFoundError(f"Configuration file not found: {config_file}") + if not output_dir.is_dir(): + raise FileNotFoundError(f"Output directory not found: {output_dir}.") + + # 2. R script content + r_script_content = f""" + if (!requireNamespace("benchdamic", quietly = TRUE)) {{ + stop("The 'benchdamic' R package is not installed.") + }} + library(benchdamic) + + config_path <- "{config_file.resolve()}" + output_path <- "{output_dir.resolve()}" + + cat("Generating plots...\\n") + benchdamic::plot_results(config_file = config_path, output_dir = output_path) + cat("Plot generation finished successfully.\\n") + """ + + # 3. Subprocess execution + command_to_execute = [] + stdout_str = "" + stderr_str = "" + r_script_path = None + + try: + 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_to_execute = ["Rscript", str(r_script_path)] + + files_before = set(p.resolve() for p in output_dir.glob('**/*')) + + process = subprocess.run( + command_to_execute, + capture_output=True, + text=True, + check=True, + ) + + stdout_str = process.stdout + stderr_str = process.stderr + + files_after = set(p.resolve() for p in output_dir.glob('**/*')) + new_files = [str(p) for p in files_after - files_before] + + except FileNotFoundError: + err_msg = "Rscript not found. Please ensure R is installed and 'Rscript' is in your system's PATH." + logger.error(err_msg) + raise RuntimeError(err_msg) from None + except subprocess.CalledProcessError as e: + logger.error(f"R script execution failed with exit code {e.returncode}") + logger.error(f"Command: {' '.join(command_to_execute)}") + logger.error(f"Stdout: {e.stdout}") + logger.error(f"Stderr: {e.stderr}") + raise + finally: + if r_script_path and r_script_path.exists(): + r_script_path.unlink() + + # 4. Structured result return + return { + "command_executed": " ".join(command_to_execute), + "stdout": stdout_str, + "stderr": stderr_str, + "output_files": new_files + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..81c55f4d646eb595c0b00feb64ae68cf28d60fcd --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/app/bioconductor-benchdamic_shim_server.py @@ -0,0 +1,55 @@ +#!/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-benchdamic/app/bioconductor-benchdamic_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_benchdamic' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-benchdamic/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..65f8f9f6c1b78e6c31e9ba07e6510d513ea8cfaf --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-benchdamic: + build: . + image: mcp-bioconductor-benchdamic:latest + container_name: mcp-bioconductor-benchdamic + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-benchdamic + 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/Biomni/mcp_generated/mcp_bioconductor-benchdamic/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..69f3813eec164585774531f51cba29fd74c0d74a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-benchdamic + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-benchdamic/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-benchdamic/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-catscradle/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-catscradle/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d4d8e2ec0016e59226eb9e474ee2211b6eda3866 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-catscradle/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-catscradle via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-catscradle -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-catscradle_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-catscradle_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-catscradle_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-catscradle/app/bioconductor-catscradle_server.py b/Biomni/mcp_generated/mcp_bioconductor-catscradle/app/bioconductor-catscradle_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2e9c4bdfb1741a2a019aa1e13eb3df700381e079 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-catscradle/app/bioconductor-catscradle_server.py @@ -0,0 +1,312 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Union +import tempfile + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_catscradle' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def catscradle_build_neighborhoods( + input_rds: str, + output_rds: str, + radius: float = 50.0, + cell_type_col: str = "cell_type", + assay: str = "Spatial", + slot: str = "counts", + min_cells: int = 1 +): + """ + Identifies tissue neighborhoods in spatial transcriptomics data and produces a Seurat object + where individual elements are neighborhoods rather than cells. + + Args: + input_rds: Path to the input Seurat or SpatialExperiment object (RDS format). + output_rds: Path where the resulting neighborhood Seurat object will be saved. + radius: The radius (in spatial units) defining the contiguous region surrounding individual cells. + cell_type_col: Metadata column name containing cell type annotations. + assay: Assay to use from the input object. + slot: Slot to use (e.g., counts, data). + min_cells: Minimum number of cells required to define a neighborhood. + """ + input_path = Path(input_rds) + output_path = Path(output_rds) + + if not input_path.exists(): + return {"error": f"Input file not found: {input_rds}"} + + if radius <= 0: + return {"error": "Radius must be a positive float."} + + # R script to perform neighborhood construction + r_code = f""" + library(CatsCradle) + library(Seurat) + library(SpatialExperiment) + + # Load data + input_data <- readRDS("{input_path}") + + # Check if it's SpatialExperiment and convert if necessary + if (inherits(input_data, "SpatialExperiment")) {{ + # CatsCradle handles SE/SPE, but we ensure Seurat compatibility if needed + # Internal CatsCradle functions typically take these objects + }} + + # Core CatsCradle logic for neighborhood construction + # Based on package description: "produces Seurat objects whose individual elements are neighborhoods" + # We use the primary constructor function (inferred from documentation) + neighborhood_obj <- CatsCradle::spatialToNeighborhoods( + object = input_data, + radius = {radius}, + cell_type_col = "{cell_type_col}", + assay = "{assay}", + slot = "{slot}", + min_cells = {min_cells} + ) + + saveRDS(neighborhood_obj, "{output_path}") + """ + + try: + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_code) + tmp_path = tmp.name + + 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(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": " ".join(e.cmd) + } + finally: + if 'tmp_path' in locals() and Path(tmp_path).exists(): + Path(tmp_path).unlink() + +@mcp.tool() +def catscradle_gene_centric_analysis( + input_rds: str, + output_rds: str, + assay: str = "RNA", + slot: str = "data", + variable_features_only: bool = True, + n_features: int = 2000 +): + """ + Enables the categorisation and annotation of genes by producing Seurat objects + whose elements are genes rather than cells. + + Args: + input_rds: Path to the input Seurat object (RDS format). + output_rds: Path where the gene-centric Seurat object will be saved. + assay: Assay to use for gene expression data. + slot: Slot to use (e.g., data, scale.data). + variable_features_only: Whether to only include highly variable features. + n_features: Number of features to select if variable_features_only is True. + """ + input_path = Path(input_rds) + output_path = Path(output_rds) + + if not input_path.exists(): + return {"error": f"Input file not found: {input_rds}"} + + r_code = f""" + library(CatsCradle) + library(Seurat) + + input_data <- readRDS("{input_path}") + + # Core CatsCradle logic for gene-centric Seurat object creation + gene_obj <- CatsCradle::geneCentricSeurat( + object = input_data, + assay = "{assay}", + slot = "{slot}", + variable_features_only = {"TRUE" if variable_features_only else "FALSE"}, + n_features = {n_features} + ) + + saveRDS(gene_obj, "{output_path}") + """ + + try: + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_code) + tmp_path = tmp.name + + 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(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": " ".join(e.cmd) + } + finally: + if 'tmp_path' in locals() and Path(tmp_path).exists(): + Path(tmp_path).unlink() + +@mcp.tool() +def catscradle_tag_neighborhoods( + neighborhood_rds: str, + output_rds: str, + tag_by: str = "cell_type", + threshold: float = 0.1 +): + """ + Categorizes neighborhoods by the cell types contained in them or the genes expressed in them. + + Args: + neighborhood_rds: Path to the neighborhood Seurat object created by catscradle_build_neighborhoods. + output_rds: Path to save the tagged neighborhood object. + tag_by: Attribute to tag by ('cell_type' or 'gene_expression'). + threshold: Minimum proportion or expression level to consider a tag present in a neighborhood. + """ + input_path = Path(neighborhood_rds) + output_path = Path(output_rds) + + if not input_path.exists(): + return {"error": f"Input file not found: {neighborhood_rds}"} + + r_code = f""" + library(CatsCradle) + library(Seurat) + + nb_obj <- readRDS("{input_path}") + + # Categorize neighborhoods + nb_obj <- CatsCradle::tagNeighborhoods( + object = nb_obj, + tag_by = "{tag_by}", + threshold = {threshold} + ) + + saveRDS(nb_obj, "{output_path}") + """ + + try: + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_code) + tmp_path = tmp.name + + 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(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": " ".join(e.cmd) + } + finally: + if 'tmp_path' in locals() and Path(tmp_path).exists(): + Path(tmp_path).unlink() + +@mcp.tool() +def catscradle_spatial_vignette_pipeline( + input_rds: str, + output_prefix: str, + radius: float = 50.0, + cell_type_col: str = "cell_type" +): + """ + Runs a standard CatsCradle spatial analysis pipeline, including neighborhood construction + and gene clustering, as described in the package vignettes. + + Args: + input_rds: Path to the input spatial transcriptomics RDS file. + output_prefix: Prefix for output files (e.g., 'results/sample1'). + radius: Radius for neighborhood definition. + cell_type_col: Metadata column for cell types. + """ + input_path = Path(input_rds) + out_dir = Path(output_prefix).parent + if not out_dir.exists(): + out_dir.mkdir(parents=True, exist_ok=True) + + r_code = f""" + library(CatsCradle) + library(Seurat) + + # 1. Load Data + spatial_data <- readRDS("{input_path}") + + # 2. Build Neighborhoods + nb_obj <- CatsCradle::spatialToNeighborhoods( + object = spatial_data, + radius = {radius}, + cell_type_col = "{cell_type_col}" + ) + saveRDS(nb_obj, paste0("{output_prefix}", "_neighborhoods.rds")) + + # 3. Gene Centric Analysis + gene_obj <- CatsCradle::geneCentricSeurat(spatial_data) + saveRDS(gene_obj, paste0("{output_prefix}", "_gene_centric.rds")) + + # 4. Discover Gene Clusters (Internal CatsCradle method) + # This typically involves standard Seurat clustering on the gene-centric object + gene_obj <- Seurat::FindVariableFeatures(gene_obj) + gene_obj <- Seurat::ScaleData(gene_obj) + gene_obj <- Seurat::RunPCA(gene_obj) + gene_obj <- Seurat::FindNeighbors(gene_obj) + gene_obj <- Seurat::FindClusters(gene_obj) + saveRDS(gene_obj, paste0("{output_prefix}", "_gene_clusters.rds")) + """ + + try: + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_code) + tmp_path = tmp.name + + 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": [ + f"{output_prefix}_neighborhoods.rds", + f"{output_prefix}_gene_centric.rds", + f"{output_prefix}_gene_clusters.rds" + ] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": " ".join(e.cmd) + } + finally: + if 'tmp_path' in locals() and Path(tmp_path).exists(): + Path(tmp_path).unlink() + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-catscradle/app/bioconductor-catscradle_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-catscradle/app/bioconductor-catscradle_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c604bab269e08d1147d431d0970acc6a49f34d81 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-catscradle/app/bioconductor-catscradle_shim_server.py @@ -0,0 +1,55 @@ +#!/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-catscradle/app/bioconductor-catscradle_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_catscradle' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-catscradle/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-catscradle/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-catscradle/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-catscradle/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-catscradle/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d730384b0f7f5e35a4f9c96890d4ba2612be1eaa --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-catscradle/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-catscradle: + build: . + image: mcp-bioconductor-catscradle:latest + container_name: mcp-bioconductor-catscradle + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-catscradle + 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/Biomni/mcp_generated/mcp_bioconductor-catscradle/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-catscradle/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c46cd1636494f83da0ae619d694ce46850c60680 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-catscradle/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-catscradle + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-catscradle/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-catscradle/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-catscradle/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-clustifyr/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..52ec0d803f99f7a08118a664f84fa502c5ecfe8e --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/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-clustifyr via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-clustifyr -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 bioconductor-clustifyr_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-clustifyr_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-clustifyr_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-clustifyr/app/bioconductor-clustifyr_server.py b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/app/bioconductor-clustifyr_server.py new file mode 100644 index 0000000000000000000000000000000000000000..479e8abb69c6c5096a5ae81bf9bd02ea73ae4f78 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/app/bioconductor-clustifyr_server.py @@ -0,0 +1,127 @@ +import subprocess +import shlex +from pathlib import Path +from typing import Optional, List, Dict, Any + +# @mcp.tool() is a placeholder for the actual decorator in the MCP framework. +# The code is written to be compatible with it. + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_clustifyr' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def clustifyr_rscript( + script_file: Optional[Path] = None, + expressions: Optional[List[str]] = None, + script_args: Optional[List[str]] = None, + verbose: bool = False, + default_packages: Optional[str] = None, + save: bool = False, + no_environ: bool = False, + no_site_file: bool = False, + no_init_file: bool = False, + restore: bool = False, + vanilla: bool = False, +) -> Dict[str, Any]: + """ + Executes an R script using Rscript, for running Bioconductor packages like clustifyr. + + This tool serves as a wrapper around the Rscript command-line interpreter, + allowing the execution of R scripts that utilize the clustifyr library or other + Bioconductor packages. You can either provide an R script file or a list of + R expressions to execute. + + Args: + script_file: Path to the R script file to be executed. Mutually exclusive with 'expressions'. + expressions: A list of R expressions to execute. Mutually exclusive with 'script_file'. + script_args: A list of arguments to be passed to the R script itself. + verbose: If True, print information on progress. + default_packages: A comma-separated list of package names to be loaded by default (e.g., "clustifyr,Seurat"). + save: If True, save the workspace at the end of the session. Ignored if 'vanilla' is True. + no_environ: If True, do not read the site and user environment files. Ignored if 'vanilla' is True. + no_site_file: If True, do not read the site-wide Rprofile. Ignored if 'vanilla' is True. + no_init_file: If True, do not read the user R profile. Ignored if 'vanilla' is True. + restore: If True, restore previously saved objects at startup. Ignored if 'vanilla' is True. + vanilla: If True, combines --no-save, --no-restore, --no-site-file, --no-init-file, and --no-environ. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + Note: Output files are not automatically detected and must be known from the R script's logic. + """ + # 1. Input Validation + if not script_file and not expressions: + raise ValueError("Either 'script_file' or 'expressions' must be provided.") + if script_file and expressions: + raise ValueError("'script_file' and 'expressions' are mutually exclusive and cannot be used together.") + if script_file: + if not script_file.is_file(): + raise FileNotFoundError(f"The specified script file does not exist: {script_file}") + + # 2. Command Construction + cmd = ["Rscript"] + + # The --vanilla option is a shortcut for several other flags. + if vanilla: + cmd.append("--vanilla") + else: + if save: + cmd.append("--save") + if no_environ: + cmd.append("--no-environ") + if no_site_file: + cmd.append("--no-site-file") + if no_init_file: + cmd.append("--no-init-file") + if restore: + cmd.append("--restore") + + if verbose: + cmd.append("--verbose") + + if default_packages: + cmd.extend(["--default-packages", default_packages]) + + # Add the script file or expressions to execute + if script_file: + cmd.append(str(script_file)) + elif expressions: + for expr in expressions: + cmd.extend(["-e", expr]) + + # Add any additional arguments for the R script itself + if script_args: + cmd.extend(script_args) + + command_executed = shlex.join(cmd) + + # 3. Subprocess Execution + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + # 4. Structured Result Return + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] # Rscript does not have a defined output file parameter. + } + except FileNotFoundError: + raise RuntimeError("Rscript not found. Please ensure R is installed and in your system's PATH.") + except subprocess.CalledProcessError as e: + # Handle cases where the R script fails + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Rscript execution failed with return code {e.returncode}", + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-clustifyr/app/bioconductor-clustifyr_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/app/bioconductor-clustifyr_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9a8dc5da9264cbcef08a93d1d7a705cca6df9336 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/app/bioconductor-clustifyr_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_bioconductor-clustifyr/app/bioconductor-clustifyr_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_clustifyr' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-clustifyr/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..815f8ab58e324137dfb25d93e4ee371b34fc90af --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-clustifyr: + build: . + image: mcp-bioconductor-clustifyr:latest + container_name: mcp-bioconductor-clustifyr + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-clustifyr + 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/Biomni/mcp_generated/mcp_bioconductor-clustifyr/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..34e35a6e37b07334f789ad2eaa09c886ac0f1f7e --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-clustifyr + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-clustifyr/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-clustifyr/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9ce330750f20d1fec8196e0bc3c8a1de5fbd3e99 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/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-genomicfeatures via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-genomicfeatures -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-genomicfeatures_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-genomicfeatures_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-genomicfeatures_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/app/bioconductor-genomicfeatures_server.py b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/app/bioconductor-genomicfeatures_server.py new file mode 100644 index 0000000000000000000000000000000000000000..363bdf78d73781b0dd864e5ad440d07ec8286ac6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/app/bioconductor-genomicfeatures_server.py @@ -0,0 +1,178 @@ +import subprocess +import tempfile +import shlex +from pathlib import Path +from typing import Optional, Dict, List + +# @mcp.tool() is a placeholder for the actual decorator. +# The code is written to be compatible with the MCP framework. + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_genomicfeatures' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def make_txdb_from_gff( + gff_file: Path, + output_db_path: Path, + format: Optional[str] = None, + data_source: Optional[str] = None, + organism: Optional[str] = None, + taxonomy_id: Optional[int] = None, + chrominfo_file: Optional[Path] = None, + metadata_file: Optional[Path] = None, + pruning_mode: str = "coarse", + on_foreign_chrom: str = "abort", + infer_cds: bool = False, + infer_utr: bool = True, +) -> Dict: + """ + Creates a TranscriptDb (TxDb) SQLite database from a GFF or GTF file. + + This tool serves as a wrapper for the `makeTxDbFromGFF` function from the + R/Bioconductor GenomicFeatures package. It parses a gene annotation file + (GFF3 or GTF) and stores the transcript metadata in a structured SQLite + database, which is highly efficient for downstream analysis. + + Args: + gff_file: Path to the input GFF3 or GTF file. + output_db_path: Path where the output SQLite TxDb file will be saved. + format: The format of the input file. Can be 'gff3' or 'gtf'. + If not provided, the tool attempts to infer it from the file extension. + data_source: A string describing the source of the data (e.g., "Ensembl v104"). + organism: The scientific name of the organism (e.g., "Homo sapiens"). + taxonomy_id: The NCBI Taxonomy ID for the organism. + chrominfo_file: Optional path to a two-column, tab-separated file containing + chromosome names and their lengths. + metadata_file: Optional path to a two-column, tab-separated file containing + metadata as name-value pairs to be stored in the database. + pruning_mode: Sets the strictness for handling problematic transcripts. + Must be one of 'coarse', 'fine', or 'strict'. + on_foreign_chrom: Action to take for features on chromosomes not listed in + `chrominfo_file`. Must be one of 'abort', 'drop', or 'as.is'. + infer_cds: If TRUE, infer CDS features from annotated stop codons. + infer_utr: If TRUE, infer UTR features (5' and 3') when not explicitly provided. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a + mapping to the generated output database file. + """ + # 1. Input Validation + if not gff_file.is_file(): + raise FileNotFoundError(f"Input GFF/GTF file not found: {gff_file}") + + if chrominfo_file and not chrominfo_file.is_file(): + raise FileNotFoundError(f"Chrominfo file not found: {chrominfo_file}") + + if metadata_file and not metadata_file.is_file(): + raise FileNotFoundError(f"Metadata file not found: {metadata_file}") + + if format and format.lower() not in ["gff3", "gtf"]: + raise ValueError("Parameter 'format' must be either 'gff3' or 'gtf'.") + + allowed_pruning = ["coarse", "fine", "strict"] + if pruning_mode not in allowed_pruning: + raise ValueError(f"Parameter 'pruning_mode' must be one of {allowed_pruning}.") + + allowed_foreign_chrom = ["abort", "drop", "as.is"] + if on_foreign_chrom not in allowed_foreign_chrom: + raise ValueError(f"Parameter 'on_foreign_chrom' must be one of {allowed_foreign_chrom}.") + + output_db_path.parent.mkdir(parents=True, exist_ok=True) + + # 2. R Script Generation + # Use R-friendly paths (forward slashes) for cross-platform compatibility + gff_file_r = str(gff_file).replace('\\', '/') + output_db_path_r = str(output_db_path).replace('\\', '/') + + r_script_lines = [ + "suppressPackageStartupMessages(library(GenomicFeatures))", + "", + "# Build the argument list for makeTxDbFromGFF", + f'txdb_args <- list(file = "{gff_file_r}")', + ] + + # Add optional arguments to the list + if format: + r_script_lines.append(f'txdb_args$format <- "{format.lower()}"') + if data_source: + r_script_lines.append(f'txdb_args$dataSource <- "{data_source}"') + if organism: + r_script_lines.append(f'txdb_args$organism <- "{organism}"') + if taxonomy_id is not None: + r_script_lines.append(f'txdb_args$taxonomyId <- {taxonomy_id}') + + r_script_lines.append(f'txdb_args$pruning.mode <- "{pruning_mode}"') + r_script_lines.append(f'txdb_args$on.foreign.chrom <- "{on_foreign_chrom}"') + r_script_lines.append(f'txdb_args$infer_cds <- {"TRUE" if infer_cds else "FALSE"}') + r_script_lines.append(f'txdb_args$infer_utr <- {"TRUE" if infer_utr else "FALSE"}') + + if chrominfo_file: + chrominfo_file_r = str(chrominfo_file).replace('\\', '/') + r_script_lines.extend([ + f'chrominfo_df <- read.table("{chrominfo_file_r}", sep="\\t", header=FALSE, col.names=c("chrom", "length"), stringsAsFactors=FALSE)', + 'txdb_args$chrominfo <- chrominfo_df' + ]) + + if metadata_file: + metadata_file_r = str(metadata_file).replace('\\', '/') + r_script_lines.extend([ + f'metadata_df <- read.table("{metadata_file_r}", sep="\\t", header=FALSE, col.names=c("name", "value"), stringsAsFactors=FALSE)', + 'txdb_args$metadata <- metadata_df' + ]) + + r_script_lines.extend([ + "", + "# Execute the function with the constructed arguments", + "txdb <- do.call(makeTxDbFromGFF, txdb_args)", + "", + "# Save the resulting database", + f'saveDb(txdb, file="{output_db_path_r}")', + 'message("TxDb database created successfully.")' + ]) + + r_script_content = "\n".join(r_script_lines) + + script_path = None + try: + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".R", encoding='utf-8') as r_script_file: + r_script_file.write(r_script_content) + script_path = r_script_file.name + + # 3. Subprocess Execution + cmd = ["Rscript", script_path] + command_executed = shlex.join(cmd) + + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + encoding='utf-8' + ) + + # 4. Structured Result Return + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": {"txdb_database": str(output_db_path)} + } + + except subprocess.CalledProcessError as e: + # R often prints errors to stdout, so combine them for a comprehensive message + error_message = ( + f"R script execution failed with exit code {e.returncode}.\n" + f"STDOUT:\n{e.stdout}\n" + f"STDERR:\n{e.stderr}\n" + ) + raise RuntimeError(error_message) from e + + finally: + # Clean up the temporary script file + if script_path and Path(script_path).exists(): + Path(script_path).unlink() + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/app/bioconductor-genomicfeatures_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/app/bioconductor-genomicfeatures_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1a1da4f2d1e41d1807c9abd124c323c7e5bad914 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/app/bioconductor-genomicfeatures_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_bioconductor-genomicfeatures/app/bioconductor-genomicfeatures_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_genomicfeatures' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..0ed64c15246a6e42f3ed448bcf13f62762db3b12 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-genomicfeatures: + build: . + image: mcp-bioconductor-genomicfeatures:latest + container_name: mcp-bioconductor-genomicfeatures + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-genomicfeatures + 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/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..077be5894b7e52c31a6f405538213f1090d401b7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-genomicfeatures + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-genomicfeatures/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-hdf5array/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f2c0510d772e52fc655c301f1f3fd7380e937f81 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/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-hdf5array via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-hdf5array -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-hdf5array_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-hdf5array_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-hdf5array_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-hdf5array/app/bioconductor-hdf5array_server.py b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/app/bioconductor-hdf5array_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a4048de346ad70d73d03a0fc0339fb993b097ab8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/app/bioconductor-hdf5array_server.py @@ -0,0 +1,460 @@ +import subprocess +import tempfile +import textwrap +from pathlib import Path +from typing import Dict, List, Optional, Union + +# MCP decorator is not defined in this context, so we'll use a placeholder. +# In a real MCP environment, this would be: from mcp import tool +class mcp: + def tool(func): + return func + +@mcp.tool +def write_hdf5_array( + input_file: Path, + output_hdf5: Path, + dataset_name: str, + input_format: str = "csv", + chunk_dim: Optional[str] = None, + compression_level: int = 6, + as_integer: bool = False, + csv_header: bool = True, + csv_row_names_col: Optional[int] = 1, +) -> Dict[str, Union[str, Dict[str, str]]]: + """ + Converts a data file (CSV, TSV, or RDS) into an HDF5 file using HDF5Array. + + This tool is a wrapper around the HDF5Array::writeHDF5Array R function. + It requires R and the Bioconductor 'HDF5Array' package to be installed. + """ + # 1. Input validation + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + if not output_hdf5.parent.exists(): + raise FileNotFoundError(f"Output directory does not exist: {output_hdf5.parent}") + if input_format not in ["csv", "tsv", "rds"]: + raise ValueError("input_format must be one of 'csv', 'tsv', or 'rds'.") + if not (0 <= compression_level <= 9): + raise ValueError("compression_level must be between 0 and 9.") + if chunk_dim: + try: + [int(d) for d in chunk_dim.split(',')] + except ValueError: + raise ValueError("chunk_dim must be a comma-separated string of integers (e.g., '100,1000').") + + # 2. R script for execution + r_script_content = textwrap.dedent(""" + suppressPackageStartupMessages({ + library(optparse) + library(HDF5Array) + }) + + option_list <- list( + make_option("--input", type="character", help="Input file path (CSV, TSV, or RDS)."), + make_option("--output", type="character", help="Output HDF5 file path."), + make_option("--name", type="character", help="Name for the dataset within the HDF5 file."), + make_option("--format", type="character", default="csv", help="Input file format [csv, tsv, rds]."), + make_option("--chunkdim", type="character", default=NULL, help="Comma-separated chunk dimensions."), + make_option("--level", type="integer", default=6, help="Compression level (0-9)."), + make_option("--as_integer", action="store_true", default=FALSE, help="Coerce data to integer."), + make_option("--header", action="store_true", default=TRUE, help="Does the CSV/TSV have a header?"), + make_option("--rownames", type="integer", default=NULL, help="Column number for row names in CSV/TSV.") + ) + + args <- parse_args(OptionParser(option_list=option_list)) + + # Read input data + message("Reading input file: ", args$input) + if (args$format == "rds") { + data_matrix <- readRDS(args$input) + } else if (args$format == "csv") { + data_matrix <- read.csv(args$input, header=args$header, row.names=args$rownames, check.names=FALSE) + } else if (args$format == "tsv") { + data_matrix <- read.delim(args$input, header=args$header, row.names=args$rownames, check.names=FALSE) + } else { + stop("Unsupported format: ", args$format) + } + + if (!is.matrix(data_matrix)) { + message("Input data is not a matrix, coercing...") + data_matrix <- as.matrix(data_matrix) + } + + if (args$as_integer) { + message("Coercing data to integer...") + storage.mode(data_matrix) <- "integer" + } + + # Parse chunk dimensions + chunk_dims <- NULL + if (!is.null(args$chunkdim)) { + chunk_dims <- as.integer(strsplit(args$chunkdim, ",")[[1]]) + message("Using chunk dimensions: ", paste(chunk_dims, collapse=", ")) + } + + # Write HDF5 array + message("Writing HDF5 array to: ", args$output) + writeHDF5Array( + x = data_matrix, + filepath = args$output, + name = args$name, + chunkdim = chunk_dims, + level = args$level, + with.dimnames = TRUE, + verbose = TRUE + ) + message("Successfully created HDF5 file.") + """) + + # 3. Subprocess execution + cmd = [ + "Rscript", "-", + "--input", str(input_file), + "--output", str(output_hdf5), + "--name", dataset_name, + "--format", input_format, + "--level", str(compression_level), + ] + if chunk_dim: + cmd.extend(["--chunkdim", chunk_dim]) + if as_integer: + cmd.append("--as_integer") + if csv_header: + cmd.append("--header") + if csv_row_names_col is not None: + cmd.extend(["--rownames", str(csv_row_names_col)]) + + try: + with tempfile.NamedTemporaryFile(mode='w', delete=True, suffix=".R") as r_script_file: + r_script_file.write(r_script_content) + r_script_file.flush() + + # Rebuild command to use the script file + cmd[1] = r_script_file.name + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"output_hdf5": str(output_hdf5)}, + } + except FileNotFoundError: + raise RuntimeError("Rscript not found. Please ensure R is installed and in your PATH.") + except subprocess.CalledProcessError as e: + error_message = ( + f"HDF5Array R script failed with exit code {e.returncode}.\n" + f"This may be due to missing R packages ('optparse', 'HDF5Array').\n" + f"Please install them in R using:\n" + f"install.packages('optparse')\n" + f"if (!requireNamespace('BiocManager', quietly = TRUE)) install.packages('BiocManager')\n" + f"BiocManager::install('HDF5Array')\n\n" + f"STDOUT:\n{e.stdout}\n" + f"STDERR:\n{e.stderr}" + ) + raise RuntimeError(error_message) from e + + +@mcp.tool +def save_hdf5_summarized_experiment( + input_rds: Path, + output_dir: Path, + prefix: Optional[str] = None, + replace: bool = False, +) -> Dict[str, Union[str, Dict[str, str]]]: + """ + Saves a SummarizedExperiment object from an RDS file to an HDF5-backed format. + + This tool wraps the HDF5Array::saveHDF5SummarizedExperiment R function. + It requires R and the Bioconductor packages 'HDF5Array' and 'SummarizedExperiment'. + """ + # 1. Input validation + if not input_rds.exists(): + raise FileNotFoundError(f"Input RDS file not found: {input_rds}") + if output_dir.exists() and not output_dir.is_dir(): + raise ValueError(f"Output path exists but is not a directory: {output_dir}") + if output_dir.exists() and any(output_dir.iterdir()) and not replace: + raise FileExistsError(f"Output directory {output_dir} is not empty. Use replace=True to overwrite.") + + output_dir.mkdir(parents=True, exist_ok=True) + + # 2. R script for execution + r_script_content = textwrap.dedent(""" + suppressPackageStartupMessages({ + library(optparse) + library(HDF5Array) + library(SummarizedExperiment) + }) + + option_list <- list( + make_option("--input", type="character", help="Input RDS file containing a SummarizedExperiment object."), + make_option("--outdir", type="character", help="Output directory to save the HDF5SummarizedExperiment."), + make_option("--prefix", type="character", default=NULL, help="Optional prefix for file names."), + make_option("--replace", action="store_true", default=FALSE, help="Replace existing directory content.") + ) + + args <- parse_args(OptionParser(option_list=option_list)) + + message("Reading SummarizedExperiment object from: ", args$input) + se <- readRDS(args$input) + + if (!is(se, "SummarizedExperiment")) { + stop("The object in the RDS file is not a SummarizedExperiment.") + } + + message("Saving HDF5SummarizedExperiment to: ", args$outdir) + saveHDF5SummarizedExperiment( + x = se, + dir = args$outdir, + prefix = args$prefix, + replace = args$replace, + verbose = TRUE + ) + message("Successfully saved HDF5SummarizedExperiment.") + """) + + # 3. Subprocess execution + cmd = [ + "Rscript", "-", + "--input", str(input_rds), + "--outdir", str(output_dir), + ] + if prefix: + cmd.extend(["--prefix", prefix]) + if replace: + cmd.append("--replace") + + try: + with tempfile.NamedTemporaryFile(mode='w', delete=True, suffix=".R") as r_script_file: + r_script_file.write(r_script_content) + r_script_file.flush() + + cmd[1] = r_script_file.name + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"output_directory": str(output_dir)}, + } + except FileNotFoundError: + raise RuntimeError("Rscript not found. Please ensure R is installed and in your PATH.") + except subprocess.CalledProcessError as e: + error_message = ( + f"HDF5Array R script failed with exit code {e.returncode}.\n" + f"This may be due to missing R packages ('optparse', 'HDF5Array', 'SummarizedExperiment').\n" + f"Please install them in R using BiocManager.\n\n" + f"STDOUT:\n{e.stdout}\n" + f"STDERR:\n{e.stderr}" + ) + raise RuntimeError(error_message) from e + + +@mcp.tool +def export_hdf5_summarized_experiment( + input_dir: Path, + output_dir: Path, + assay_names: Optional[str] = None, + export_coldata: bool = True, + export_rowdata: bool = True, +) -> Dict[str, Union[str, Dict[str, str]]]: + """ + Loads an HDF5SummarizedExperiment and exports its components to CSV files. + + This tool wraps HDF5Array::loadHDF5SummarizedExperiment and exports data. + Requires R and the Bioconductor packages 'HDF5Array' and 'SummarizedExperiment'. + """ + # 1. Input validation + if not input_dir.is_dir(): + raise FileNotFoundError(f"Input directory not found: {input_dir}") + if output_dir.exists() and not output_dir.is_dir(): + raise ValueError(f"Output path exists but is not a directory: {output_dir}") + + output_dir.mkdir(parents=True, exist_ok=True) + + # 2. R script for execution + r_script_content = textwrap.dedent(""" + suppressPackageStartupMessages({ + library(optparse) + library(HDF5Array) + library(SummarizedExperiment) + }) + + option_list <- list( + make_option("--indir", type="character", help="Input directory of the HDF5SummarizedExperiment."), + make_option("--outdir", type="character", help="Output directory for exported CSV files."), + make_option("--assays", type="character", default=NULL, help="Comma-separated list of assays to export (default: all)."), + make_option("--coldata", action="store_true", default=FALSE, help="Export colData to CSV."), + make_option("--rowdata", action="store_true", default=FALSE, help="Export rowData to CSV.") + ) + + args <- parse_args(OptionParser(option_list=option_list)) + + message("Loading HDF5SummarizedExperiment from: ", args$indir) + se <- loadHDF5SummarizedExperiment(dir = args$indir) + + assays_to_export <- assayNames(se) + if (!is.null(args$assays)) { + assays_to_export <- intersect(assays_to_export, strsplit(args$assays, ",")[[1]]) + } + + if (length(assays_to_export) > 0) { + for (aname in assays_to_export) { + message("Exporting assay: ", aname) + out_path <- file.path(args$outdir, paste0("assay_", aname, ".csv")) + mat <- as.matrix(assay(se, aname)) + write.csv(mat, file=out_path, quote=FALSE, row.names=TRUE) + } + } + + if (args$coldata) { + message("Exporting colData") + out_path <- file.path(args$outdir, "coldata.csv") + write.csv(as.data.frame(colData(se)), file=out_path, quote=FALSE, row.names=TRUE) + } + + if (args$rowdata) { + message("Exporting rowData") + out_path <- file.path(args$outdir, "rowdata.csv") + write.csv(as.data.frame(rowData(se)), file=out_path, quote=FALSE, row.names=TRUE) + } + + message("Export complete.") + """) + + # 3. Subprocess execution + cmd = [ + "Rscript", "-", + "--indir", str(input_dir), + "--outdir", str(output_dir), + ] + if assay_names: + cmd.extend(["--assays", assay_names]) + if export_coldata: + cmd.append("--coldata") + if export_rowdata: + cmd.append("--rowdata") + + try: + with tempfile.NamedTemporaryFile(mode='w', delete=True, suffix=".R") as r_script_file: + r_script_file.write(r_script_content) + r_script_file.flush() + + cmd[1] = r_script_file.name + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + output_files = [str(p) for p in output_dir.glob("*.csv")] + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"output_directory": str(output_dir), "exported_files": output_files}, + } + except FileNotFoundError: + raise RuntimeError("Rscript not found. Please ensure R is installed and in your PATH.") + except subprocess.CalledProcessError as e: + error_message = ( + f"HDF5Array R script failed with exit code {e.returncode}.\n" + f"This may be due to missing R packages or an invalid HDF5SummarizedExperiment directory.\n\n" + f"STDOUT:\n{e.stdout}\n" + f"STDERR:\n{e.stderr}" + ) + raise RuntimeError(error_message) from e + + +@mcp.tool +def inspect_hdf5_array( + input_hdf5: Path, + dataset_name: str, +) -> Dict[str, str]: + """ + Inspects an HDF5 file and prints properties of a specific HDF5Array dataset. + + This tool provides information like dimensions and chunking without loading data. + Requires R and the Bioconductor 'HDF5Array' package. + """ + # 1. Input validation + if not input_hdf5.exists(): + raise FileNotFoundError(f"Input HDF5 file not found: {input_hdf5}") + + # 2. R script for execution + r_script_content = textwrap.dedent(""" + suppressPackageStartupMessages({ + library(optparse) + library(HDF5Array) + }) + + option_list <- list( + make_option("--file", type="character", help="Input HDF5 file path."), + make_option("--name", type="character", help="Name of the dataset to inspect.") + ) + + args <- parse_args(OptionParser(option_list=option_list)) + + # Load the HDF5 array seed + HDF5_dataset <- HDF5Array(args$file, args$name) + + # Print properties + cat("Dataset:", name(HDF5_dataset), "\\n") + cat("Filepath:", path(HDF5_dataset), "\\n") + cat("Dimensions:", paste(dim(HDF5_dataset), collapse=" x "), "\\n") + cat("Datatype:", class(HDF5_dataset@seed@first_val), "\\n") + cat("Chunking:", paste(chunkdim(HDF5_dataset), collapse=" x "), "\\n") + """) + + # 3. Subprocess execution + cmd = [ + "Rscript", "-", + "--file", str(input_hdf5), + "--name", dataset_name, + ] + + try: + with tempfile.NamedTemporaryFile(mode='w', delete=True, suffix=".R") as r_script_file: + r_script_file.write(r_script_content) + r_script_file.flush() + + cmd[1] = r_script_file.name + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + } + except FileNotFoundError: + raise RuntimeError("Rscript not found. Please ensure R is installed and in your PATH.") + except subprocess.CalledProcessError as e: + error_message = ( + f"HDF5Array R script failed with exit code {e.returncode}.\n" + f"This may be due to a missing R package, an incorrect file path, or a non-existent dataset name ('{dataset_name}').\n\n" + f"STDOUT:\n{e.stdout}\n" + f"STDERR:\n{e.stderr}" + ) + raise RuntimeError(error_message) from e \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-hdf5array/app/bioconductor-hdf5array_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/app/bioconductor-hdf5array_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c526f717b855e99523f2e5f0f4750bdb38529269 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/app/bioconductor-hdf5array_shim_server.py @@ -0,0 +1,55 @@ +#!/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-hdf5array/app/bioconductor-hdf5array_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_hdf5array' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-hdf5array/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-hdf5array/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..97bfa9f0817812b9b034da837a26e1ca56d332f8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-hdf5array: + build: . + image: mcp-bioconductor-hdf5array:latest + container_name: mcp-bioconductor-hdf5array + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-hdf5array + 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/Biomni/mcp_generated/mcp_bioconductor-hdf5array/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0d5fae2ca90a31c0f18e23fab27e79852206c4dc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-hdf5array + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-hdf5array/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-hdf5array/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-irisfgm/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e30f96079a8c672ae33bcfb5e72ab1ed0a42a4cc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/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-irisfgm via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-irisfgm -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-irisfgm_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-irisfgm_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-irisfgm_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-irisfgm/app/bioconductor-irisfgm_server.py b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/app/bioconductor-irisfgm_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c798a7de6725fc156b1af9cf963158a092131d4e --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/app/bioconductor-irisfgm_server.py @@ -0,0 +1,183 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_irisfgm' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_irisfgm_script( + r_script_content: str, + output_dir: Path = Path("./"), + r_environment_setup_script: Optional[str] = None, +) -> Dict[str, Any]: + """ + Executes an R script that utilizes the bioconductor-irisfgm package. + + The bioconductor-irisfgm tool is an R package designed for comprehensive + analysis of gene interactivity networks based on single-cell RNA-Seq data. + As the provided documentation does not expose a direct command-line + interface with specific subcommands and parameters, this MCP tool provides + a generic entry point to execute arbitrary R code that leverages the IRISFGM + package. + + Users are expected to provide an R script that loads the IRISFGM package + and calls its internal functions as needed. The R script should be written + to save any desired output files to the directory specified by the + 'OUTPUT_DIR' environment variable, which corresponds to the `output_dir` + parameter. + + Example R script content: + ```R + # Load the IRISFGM package + library(IRISFGM) + + # --- Placeholder for actual IRISFGM function calls --- + # In a real scenario, you would load your actual scRNA-seq data + # (e.g., from a CSV, TSV, or a Seurat object) and then apply IRISFGM functions. + # + # Example: Create a dummy SingleCellExperiment object for demonstration + # library(SingleCellExperiment) + # counts <- matrix(rnbinom(1000, mu=10, size=1), ncol=10) + # rownames(counts) <- paste0("gene", 1:100) + # colnames(counts) <- paste0("cell", 1:10) + # sce <- SingleCellExperiment(assays=list(counts=counts)) + # + # Example of a hypothetical IRISFGM function call (replace with actual functions): + # fgms_result <- IRISFGM::identifyFGMs(sce_object, ...) + # + # Print some output to stdout + message("IRISFGM R script executed successfully.") + # + # Save results to a file in the output directory + # output_dir <- Sys.getenv("OUTPUT_DIR") + # if (!is.null(output_dir) && dir.exists(output_dir)) { + # write.csv(data.frame(gene="gene1", value=10), file = file.path(output_dir, "example_output.csv")) + # message(paste("Saved example_output.csv to", output_dir)) + # } else { + # warning("OUTPUT_DIR environment variable not set or directory does not exist.") + # } + ``` + + Args: + r_script_content: A string containing the R script to be executed. + This script should include calls to IRISFGM functions. + output_dir: The directory where any output files generated by the R script + should be stored. The R script should be written to save + files to this directory (e.g., using `file.path(Sys.getenv("OUTPUT_DIR"), "filename")`). + Defaults to the current working directory. + r_environment_setup_script: Optional R script content to run before the main + `r_script_content`. This can be used for + environment setup, installing packages, etc., + if not handled by the container. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list + of any output files generated. + """ + if not r_script_content: + raise ValueError("R script content cannot be empty.") + + # Ensure the output directory exists + output_dir.mkdir(parents=True, exist_ok=True) + + command_executed: List[str] = [] + stdout: str = "" + stderr: str = "" + output_files: List[Path] = [] + + with tempfile.TemporaryDirectory() as tmpdir: + temp_dir_path = Path(tmpdir) + r_script_path = temp_dir_path / "main_script.R" + setup_script_path: Optional[Path] = None + + # Write the main R script content to a temporary file + with open(r_script_path, "w") as f: + f.write(r_script_content) + + # Prepare environment variables for the R process + env_vars = {"OUTPUT_DIR": str(output_dir)} + + # If an R environment setup script is provided, execute it first + if r_environment_setup_script: + setup_script_path = temp_dir_path / "setup_script.R" + with open(setup_script_path, "w") as f: + f.write(r_environment_setup_script) + setup_command = ["Rscript", str(setup_script_path)] + command_executed.append(" ".join(setup_command)) + try: + setup_result = subprocess.run( + setup_command, + capture_output=True, + text=True, + check=True, + env={**subprocess.os.environ, **env_vars} # Merge with current environment + ) + stdout += f"Setup script stdout:\n{setup_result.stdout}\n" + stderr += f"Setup script stderr:\n{setup_result.stderr}\n" + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(setup_command), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"R setup script failed with exit code {e.returncode}", + "output_files": [], + } + except FileNotFoundError: + return { + "command_executed": " ".join(setup_command), + "stdout": "", + "stderr": "Error: Rscript command not found. Is R installed and in PATH?", + "error": "Rscript not found for setup script", + "output_files": [], + } + + # Execute the main R script + r_command = ["Rscript", str(r_script_path)] + command_executed.append(" ".join(r_command)) + + try: + result = subprocess.run( + r_command, + capture_output=True, + text=True, + check=True, + env={**subprocess.os.environ, **env_vars} # Merge with current environment + ) + stdout += result.stdout + stderr += result.stderr + + # Collect output files from the specified output_dir + for f in output_dir.iterdir(): + if f.is_file(): + output_files.append(f) + + except subprocess.CalledProcessError as e: + return { + "command_executed": "\n".join(command_executed), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"R script failed with exit code {e.returncode}", + "output_files": [], + } + except FileNotFoundError: + return { + "command_executed": "\n".join(command_executed), + "stdout": "", + "stderr": "Error: Rscript command not found. Is R installed and in PATH?", + "error": "Rscript not found for main script", + "output_files": [], + } + + return { + "command_executed": "\n".join(command_executed), + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-irisfgm/app/bioconductor-irisfgm_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/app/bioconductor-irisfgm_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..157ce2bc46e4b72dfc669514aeb94d7cfd931a8e --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/app/bioconductor-irisfgm_shim_server.py @@ -0,0 +1,55 @@ +#!/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-irisfgm/app/bioconductor-irisfgm_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_irisfgm' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-irisfgm/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..6ec8f6a1ced45007183fe46de3573b1d37550d46 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-irisfgm: + build: . + image: mcp-bioconductor-irisfgm:latest + container_name: mcp-bioconductor-irisfgm + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-irisfgm + 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/Biomni/mcp_generated/mcp_bioconductor-irisfgm/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ca7f25d8bc352e903a8fc4e77bb5c0eba53afd37 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-irisfgm + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-irisfgm/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-irisfgm/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-merfishdata/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-merfishdata/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..7823c41076fc0b5b374ed7430f528d63e59978fe --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-merfishdata/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-merfishdata via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-merfishdata -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-merfishdata_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-merfishdata_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-merfishdata_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-merfishdata/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-merfishdata/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-merfishdata/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-merfishdata/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-merfishdata/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-merfishdata/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ccec41ddd0d80bd47367f90dfd32979fcab14fc9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/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-moleculeexperiment via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-moleculeexperiment -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-moleculeexperiment_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-moleculeexperiment_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-moleculeexperiment_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/app/bioconductor-moleculeexperiment_server.py b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/app/bioconductor-moleculeexperiment_server.py new file mode 100644 index 0000000000000000000000000000000000000000..fae47e1184e790b367ff82c8ea0bccd7e8786cd4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/app/bioconductor-moleculeexperiment_server.py @@ -0,0 +1,378 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Union +import tempfile +import json + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_moleculeexperiment' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def read_xenium_to_molecule_experiment( + data_dir: str, + output_rds: str, + keep_unassigned: bool = True, +): + """ + Read 10X Xenium spatial transcriptomics data into a MoleculeExperiment RDS file. + + Args: + data_dir: Path to the directory containing Xenium output files (e.g., transcripts.csv.gz, cell_boundaries.csv.gz). + output_rds: Path where the resulting MoleculeExperiment object will be saved (.rds). + keep_unassigned: Whether to keep molecules not assigned to any cell. + """ + data_path = Path(data_dir) + if not data_path.is_dir(): + return {"error": f"Data directory {data_dir} does not exist."} + + r_code = f""" + library(MoleculeExperiment) + tryCatch({{ + me <- readXenium(data_dir = "{data_dir}", keep_unassigned = {str(keep_unassigned).upper()}) + saveRDS(me, "{output_rds}") + cat("Successfully saved MoleculeExperiment to {output_rds}") + }}, error = function(e) {{ + stop(e) + }}) + """ + + try: + result = subprocess.run( + ["Rscript", "-e", r_code], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"readXenium(data_dir='{data_dir}')", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def read_cosmx_to_molecule_experiment( + data_dir: str, + output_rds: str, + keep_unassigned: bool = True, +): + """ + Read Nanostring Cosmx SMI spatial transcriptomics data into a MoleculeExperiment RDS file. + + Args: + data_dir: Path to the directory containing Cosmx output files. + output_rds: Path where the resulting MoleculeExperiment object will be saved (.rds). + keep_unassigned: Whether to keep molecules not assigned to any cell. + """ + data_path = Path(data_dir) + if not data_path.is_dir(): + return {"error": f"Data directory {data_dir} does not exist."} + + r_code = f""" + library(MoleculeExperiment) + tryCatch({{ + me <- readCosmx(data_dir = "{data_dir}", keep_unassigned = {str(keep_unassigned).upper()}) + saveRDS(me, "{output_rds}") + cat("Successfully saved MoleculeExperiment to {output_rds}") + }}, error = function(e) {{ + stop(e) + }}) + """ + + try: + result = subprocess.run( + ["Rscript", "-e", r_code], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"readCosmx(data_dir='{data_dir}')", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def read_merscope_to_molecule_experiment( + data_dir: str, + output_rds: str, + keep_unassigned: bool = True, +): + """ + Read Vizgen Merscope spatial transcriptomics data into a MoleculeExperiment RDS file. + + Args: + data_dir: Path to the directory containing Merscope output files. + output_rds: Path where the resulting MoleculeExperiment object will be saved (.rds). + keep_unassigned: Whether to keep molecules not assigned to any cell. + """ + data_path = Path(data_dir) + if not data_path.is_dir(): + return {"error": f"Data directory {data_dir} does not exist."} + + r_code = f""" + library(MoleculeExperiment) + tryCatch({{ + me <- readMerscope(data_dir = "{data_dir}", keep_unassigned = {str(keep_unassigned).upper()}) + saveRDS(me, "{output_rds}") + cat("Successfully saved MoleculeExperiment to {output_rds}") + }}, error = function(e) {{ + stop(e) + }}) + """ + + try: + result = subprocess.run( + ["Rscript", "-e", r_code], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"readMerscope(data_dir='{data_dir}')", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def count_molecules( + input_rds: str, + output_rds: str, + assay_name: str = "counts", +): + """ + Aggregate molecules in a MoleculeExperiment object into a SpatialExperiment object (cell x gene counts). + + Args: + input_rds: Path to the input MoleculeExperiment RDS file. + output_rds: Path where the resulting SpatialExperiment object will be saved (.rds). + assay_name: Name of the assay to create in the output object. + """ + if not Path(input_rds).exists(): + return {"error": f"Input file {input_rds} does not exist."} + + r_code = f""" + library(MoleculeExperiment) + library(SpatialExperiment) + tryCatch({{ + me <- readRDS("{input_rds}") + se <- countMolecules(me, assay_name = "{assay_name}") + saveRDS(se, "{output_rds}") + cat("Successfully aggregated molecules into SpatialExperiment") + }}, error = function(e) {{ + stop(e) + }}) + """ + + try: + result = subprocess.run( + ["Rscript", "-e", r_code], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"countMolecules(me, assay_name='{assay_name}')", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def filter_molecules_by_gene( + input_rds: str, + output_rds: str, + genes: List[str], +): + """ + Filter a MoleculeExperiment object to keep only specific genes. + + Args: + input_rds: Path to the input MoleculeExperiment RDS file. + output_rds: Path where the filtered MoleculeExperiment object will be saved (.rds). + genes: List of gene names to keep. + """ + if not Path(input_rds).exists(): + return {"error": f"Input file {input_rds} does not exist."} + + genes_r = 'c("' + '", "'.join(genes) + '")' + + r_code = f""" + library(MoleculeExperiment) + tryCatch({{ + me <- readRDS("{input_rds}") + # Assuming molecules are stored in a way that allows standard subsetting + # MoleculeExperiment typically uses a list-like structure for molecules + me_filtered <- me[me$gene %in% {genes_r}, ] + saveRDS(me_filtered, "{output_rds}") + cat("Successfully filtered molecules") + }}, error = function(e) {{ + stop(e) + }}) + """ + + try: + result = subprocess.run( + ["Rscript", "-e", r_code], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"me[me$gene %in% {genes_r}, ]", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def plot_molecules_spatial( + input_rds: str, + output_image: str, + genes: Optional[List[str]] = None, + sample_id: Optional[str] = None, + point_size: float = 0.1, +): + """ + Visualize molecule locations from a MoleculeExperiment object. + + Args: + input_rds: Path to the input MoleculeExperiment RDS file. + output_image: Path to save the output plot (e.g., .png, .pdf). + genes: Optional list of specific genes to plot. If None, plots all. + sample_id: Optional sample identifier if multiple samples exist. + point_size: Size of the points in the plot. + """ + if not Path(input_rds).exists(): + return {"error": f"Input file {input_rds} does not exist."} + + genes_r = 'NULL' + if genes: + genes_r = 'c("' + '", "'.join(genes) + '")' + + sample_id_r = f'"{sample_id}"' if sample_id else "NULL" + + r_code = f""" + library(MoleculeExperiment) + library(ggplot2) + tryCatch({{ + me <- readRDS("{input_rds}") + p <- plotMolecules(me, genes = {genes_r}, sample_id = {sample_id_r}, point_size = {point_size}) + ggsave("{output_image}", plot = p, width = 10, height = 10) + cat("Successfully saved plot to {output_image}") + }}, error = function(e) {{ + stop(e) + }}) + """ + + try: + result = subprocess.run( + ["Rscript", "-e", r_code], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": "plotMolecules(...)", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_image] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def create_molecule_experiment_from_df( + molecules_csv: str, + output_rds: str, + boundaries_csv: Optional[str] = None, +): + """ + Create a MoleculeExperiment object from custom CSV files. + + Args: + molecules_csv: Path to a CSV containing molecule data (must have x, y, and gene columns). + output_rds: Path where the resulting MoleculeExperiment object will be saved (.rds). + boundaries_csv: Optional path to a CSV containing cell boundary coordinates. + """ + if not Path(molecules_csv).exists(): + return {"error": f"Molecules file {molecules_csv} does not exist."} + + r_code = f""" + library(MoleculeExperiment) + library(data.table) + tryCatch({{ + mols <- fread("{molecules_csv}") + bounds <- NULL + if ("{boundaries_csv}" != "None" && file.exists("{boundaries_csv}")) {{ + bounds <- fread("{boundaries_csv}") + }} + + me <- MoleculeExperiment(molecules = mols, boundaries = bounds) + saveRDS(me, "{output_rds}") + cat("Successfully created MoleculeExperiment from CSV") + }}, error = function(e) {{ + stop(e) + }}) + """ + + try: + result = subprocess.run( + ["Rscript", "-e", r_code], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": "MoleculeExperiment(molecules=mols, boundaries=bounds)", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_rds] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/app/bioconductor-moleculeexperiment_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/app/bioconductor-moleculeexperiment_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..010e51c97c310dae596632accb9878aef25bdb4f --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/app/bioconductor-moleculeexperiment_shim_server.py @@ -0,0 +1,55 @@ +#!/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-moleculeexperiment/app/bioconductor-moleculeexperiment_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_moleculeexperiment' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b8391b5a42b71f56f0fbadd46a4ebf46bb3be01b --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-moleculeexperiment: + build: . + image: mcp-bioconductor-moleculeexperiment:latest + container_name: mcp-bioconductor-moleculeexperiment + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-moleculeexperiment + 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/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..915487a01bd02c680dcac5ec49ea08b92d891c21 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-moleculeexperiment + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-moleculeexperiment/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-multtest/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-multtest/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..50b12bc8047f8991312c0813274193fff3435c15 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-multtest/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-multtest via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-multtest -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-multtest_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-multtest_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-multtest_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-multtest/app/bioconductor-multtest_server.py b/Biomni/mcp_generated/mcp_bioconductor-multtest/app/bioconductor-multtest_server.py new file mode 100644 index 0000000000000000000000000000000000000000..cd1c7a7ebc81b5bbb403fa2a32ae0930805c557a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-multtest/app/bioconductor-multtest_server.py @@ -0,0 +1,238 @@ +import subprocess +import tempfile +import logging +from pathlib import Path +from typing import List, Optional, Literal + +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Define allowed choices for parameters based on the multtest R package documentation. +MTP_TEST_CHOICES = Literal[ + "t.onesamp", "t.twosamp.equalvar", "t.twosamp.unequalvar", + "f", "pairt", "blockf" +] +MTP_ALTERNATIVE_CHOICES = Literal["two.sided", "less", "greater"] +MTP_TYPEONE_CHOICES = Literal["fwer", "gfwer", "tppfp", "fdr"] +MTP_NULLDIST_CHOICES = Literal["boot.cs", "boot.ctr", "perm"] +MTP_METHOD_CHOICES = Literal["ss.maxT", "ss.minP", "sd.maxT", "sd.minP"] + +ADJP_PROCEDURE_CHOICES = Literal[ + "Bonferroni", "Holm", "Hochberg", "SidakSS", "SidakSD", + "BH", "BY", "ABH", "TSBH" +] + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_multtest' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def multtest_mtp( + data_file: Path, + class_labels_file: Path, + output_prefix: str, + test: MTP_TEST_CHOICES = "t.twosamp.equalvar", + alternative: MTP_ALTERNATIVE_CHOICES = "two.sided", + typeone: MTP_TYPEONE_CHOICES = "fwer", + alpha: float = 0.05, + nulldist: MTP_NULLDIST_CHOICES = "boot.cs", + b: int = 10000, + method: MTP_METHOD_CHOICES = "ss.maxT", + seed: Optional[int] = None, +) -> dict: + """ + Performs a Multiple Testing Procedure (MTP) using the bioconductor-multtest package. + + This function is a wrapper for the main `MTP` function in the `multtest` R package. + It takes a data matrix and class labels to perform hypothesis testing, controlling + for a specified type of error rate (e.g., FWER, FDR). + + Args: + data_file: Path to the input data file (CSV format). Rows should be features/genes, + and columns should be samples. A header with sample names and a column + with feature names are expected. + class_labels_file: Path to a single-column CSV file containing class labels for + the samples. The order must match the columns in data_file. + No header is expected. + output_prefix: Prefix for the output files (e.g., 'my_analysis'). + test: The statistical test to use. + alternative: Specifies the alternative hypothesis. + typeone: The type of error rate to control. + alpha: The significance level for the error rate. + nulldist: Method for estimating the null distribution. + b: The number of bootstrap or permutation resamples. + method: The multiple testing procedure to use for adjusting p-values. + seed: An optional integer seed for random number generation to ensure reproducibility. + + Returns: + A dictionary containing the executed command, stdout, stderr, and paths to output files. + """ + # --- Input Validation --- + if not data_file.is_file(): + raise FileNotFoundError(f"Data file not found: {data_file}") + if not class_labels_file.is_file(): + raise FileNotFoundError(f"Class labels file not found: {class_labels_file}") + if not (0 < alpha < 1): + raise ValueError("alpha must be between 0 and 1.") + if b <= 0: + raise ValueError("Number of resamples (b) must be a positive integer.") + + output_dir = Path.cwd() + adjp_output_file = output_dir / f"{output_prefix}_adjp.csv" + reject_output_file = output_dir / f"{output_prefix}_rejected_indices.csv" + + # --- R Script Generation --- + # Handle optional seed value for R script + r_seed_code = f"set.seed({seed})" if seed is not None else "" + + r_script_content = f""" + # Load the library + library(multtest) + + # Set seed for reproducibility if provided + {r_seed_code} + + # Read input data + # Using tryCatch for better error reporting from R + tryCatch({{ + X <- as.matrix(read.csv('{data_file}', header=TRUE, row.names=1, check.names=FALSE)) + Y <- read.csv('{class_labels_file}', header=FALSE)$V1 + + # Run the Multiple Testing Procedure + mtp_results <- MTP(X=X, Y=Y, test='{test}', alternative='{alternative}', + typeone='{typeone}', alpha={alpha}, nulldist='{nulldist}', + B={b}, method='{method}') + + # Write adjusted p-values to output file + write.csv(mtp_results@adjp, file='{adjp_output_file}', row.names=TRUE) + + # Write indices of rejected hypotheses + rejected_indices <- which(mtp_results@reject) + write.csv(rejected_indices, file='{reject_output_file}', row.names=FALSE) + + cat("MTP analysis completed successfully.\\n") + }}, error = function(e) {{ + stop("R script failed with error: ", e$message) + }}) + """ + + with tempfile.TemporaryDirectory() as temp_dir: + r_script_path = Path(temp_dir) / "run_mtp.R" + with open(r_script_path, "w") as f: + f.write(r_script_content) + + command = ["Rscript", str(r_script_path)] + command_str = " ".join(command) + logging.info(f"Executing command: {command_str}") + + try: + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": { + "adjusted_p_values": str(adjp_output_file), + "rejected_indices": str(reject_output_file) + } + } + except subprocess.CalledProcessError as e: + logging.error(f"Error executing R script for MTP: {e.stderr}") + raise RuntimeError(f"R script execution failed: {e.stderr}") from e + + +@mcp.tool() +def multtest_adjust_pvalues( + raw_pvalues_file: Path, + procedures: List[ADJP_PROCEDURE_CHOICES], + output_prefix: str, +) -> dict: + """ + Computes adjusted p-values from a set of raw (unadjusted) p-values. + + This function is a wrapper for the `mt.rawp2adjp` function in the `multtest` R package. + It applies one or more specified adjustment procedures to a given list of p-values. + + Args: + raw_pvalues_file: Path to a CSV file of raw p-values. The file should have a header + and the first column should contain feature/gene names. + procedures: A list of adjustment procedures to apply. + output_prefix: Prefix for the output file (e.g., 'my_analysis_adjp'). + + Returns: + A dictionary containing the executed command, stdout, stderr, and the path to the output file. + """ + # --- Input Validation --- + if not raw_pvalues_file.is_file(): + raise FileNotFoundError(f"Raw p-values file not found: {raw_pvalues_file}") + if not procedures: + raise ValueError("At least one adjustment procedure must be specified.") + + output_dir = Path.cwd() + output_file = output_dir / f"{output_prefix}_adjusted_pvalues.csv" + + # --- R Script Generation --- + # Format the list of procedures into an R character vector string, e.g., c("BH", "Bonferroni") + r_proc_vector = 'c(' + ', '.join(f'"{p}"' for p in procedures) + ')' + + r_script_content = f""" + # Load the library + library(multtest) + + # Read input data + tryCatch({{ + rawp <- as.matrix(read.csv('{raw_pvalues_file}', header=TRUE, row.names=1, check.names=FALSE)) + + # Define the adjustment procedures + procs <- {r_proc_vector} + + # Run the p-value adjustment + adj_results <- mt.rawp2adjp(rawp, proc=procs) + + # Write the adjusted p-values to the output file + # The result 'adjp' contains the adjusted p-values, with original p-values in the first column + write.csv(adj_results$adjp, file='{output_file}', row.names=TRUE) + + cat("P-value adjustment completed successfully.\\n") + }}, error = function(e) {{ + stop("R script failed with error: ", e$message) + }}) + """ + + with tempfile.TemporaryDirectory() as temp_dir: + r_script_path = Path(temp_dir) / "run_adjp.R" + with open(r_script_path, "w") as f: + f.write(r_script_content) + + command = ["Rscript", str(r_script_path)] + command_str = " ".join(command) + logging.info(f"Executing command: {command_str}") + + try: + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": { + "adjusted_p_values": str(output_file) + } + } + except subprocess.CalledProcessError as e: + logging.error(f"Error executing R script for p-value adjustment: {e.stderr}") + raise RuntimeError(f"R script execution failed: {e.stderr}") from e + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-multtest/app/bioconductor-multtest_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-multtest/app/bioconductor-multtest_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2b6107d58c37cffbce0d4e92f23c92cbb1de34d5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-multtest/app/bioconductor-multtest_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_bioconductor-multtest/app/bioconductor-multtest_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_multtest' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-multtest/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-multtest/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-multtest/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-multtest/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-multtest/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..6bc264787603248074bf059e0399068f89fd919a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-multtest/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-multtest: + build: . + image: mcp-bioconductor-multtest:latest + container_name: mcp-bioconductor-multtest + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-multtest + 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/Biomni/mcp_generated/mcp_bioconductor-multtest/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-multtest/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f686d7fe95f83931ab22a680f054decfb3dd5756 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-multtest/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-multtest + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-multtest/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-multtest/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-multtest/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-orfhunter/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ef6617906b8f7a0a1eedb1ed96648f36004ded13 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/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-orfhunter via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-orfhunter -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-orfhunter_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-orfhunter_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-orfhunter_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-orfhunter/app/bioconductor-orfhunter_server.py b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/app/bioconductor-orfhunter_server.py new file mode 100644 index 0000000000000000000000000000000000000000..58d501afdf7a36ed397172cfdf6854fa407c105a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/app/bioconductor-orfhunter_server.py @@ -0,0 +1,291 @@ +import subprocess +from pathlib import Path +from typing import Optional, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_orfhunter' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def predict_orf( + fasta_input: str, + output_csv: str, + model_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Predict the most likely open reading frame (ORF) in nucleotide sequences using a random forest model. + This is the primary tool for automatic determination and annotation of ORFs in RNA molecules. + + Args: + fasta_input: Path to the input FASTA file containing nucleotide sequences. + output_csv: Path where the predicted ORFs and their characteristics will be saved (CSV format). + model_path: Optional path to a custom pre-trained random forest model (.rds file). + If not provided, the default human-specific model included in the package is used. + """ + input_path = Path(fasta_input).resolve() + output_path = Path(output_csv).resolve() + + if not input_path.exists(): + return {"error": f"Input file not found: {fasta_input}"} + + output_path.parent.mkdir(parents=True, exist_ok=True) + + model_loading = "model <- NULL" + if model_path: + m_path = Path(model_path).resolve() + if not m_path.exists(): + return {"error": f"Model file not found: {model_path}"} + model_loading = f"model <- readRDS('{m_path}')" + + r_script = f""" + library(ORFhunteR) + library(Biostrings) + {model_loading} + sequences <- readDNAStringSet('{input_path}') + # predictORF returns a data.frame with predicted ORFs + result <- predictORF(sequences = sequences, model = model) + write.csv(result, '{output_path}', row.names = FALSE) + """ + + try: + process = subprocess.run( + ["Rscript", "-e", r_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"ORFhunteR::predictORF on {fasta_input}", + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def find_orfs( + fasta_input: str, + output_csv: str, +) -> Dict[str, Any]: + """ + Identify all potential open reading frames (ORFs) in a set of nucleotide sequences. + This tool finds every sequence starting with a start codon and ending with a stop codon. + + Args: + fasta_input: Path to the input FASTA file containing nucleotide sequences. + output_csv: Path where the identified ORFs and their coordinates will be saved (CSV format). + """ + input_path = Path(fasta_input).resolve() + output_path = Path(output_csv).resolve() + + if not input_path.exists(): + return {"error": f"Input file not found: {fasta_input}"} + + output_path.parent.mkdir(parents=True, exist_ok=True) + + r_script = f""" + library(ORFhunteR) + library(Biostrings) + library(data.table) + sequences <- readDNAStringSet('{input_path}') + # findORFs returns a list of data.tables + result_list <- findORFs(sequences = sequences) + # Combine list into a single data table with transcript IDs + result_df <- rbindlist(result_list, idcol = "transcript_id") + write.csv(result_df, '{output_path}', row.names = FALSE) + """ + + try: + process = subprocess.run( + ["Rscript", "-e", r_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"ORFhunteR::findORFs on {fasta_input}", + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def classify_orfs( + fasta_input: str, + output_csv: str, + model_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Classify a set of nucleotide sequences as coding or non-coding ORFs using a random forest model. + + Args: + fasta_input: Path to a FASTA file containing the ORF sequences to be classified. + output_csv: Path where the classification probabilities will be saved (CSV format). + model_path: Optional path to a custom pre-trained random forest model (.rds file). + """ + input_path = Path(fasta_input).resolve() + output_path = Path(output_csv).resolve() + + if not input_path.exists(): + return {"error": f"Input file not found: {fasta_input}"} + + output_path.parent.mkdir(parents=True, exist_ok=True) + + model_loading = "model <- NULL" + if model_path: + m_path = Path(model_path).resolve() + if not m_path.exists(): + return {"error": f"Model file not found: {model_path}"} + model_loading = f"model <- readRDS('{m_path}')" + + r_script = f""" + library(ORFhunteR) + library(Biostrings) + {model_loading} + sequences <- as.character(readDNAStringSet('{input_path}')) + # classifyORFs returns a vector of probabilities + probs <- classifyORFs(ORFs = sequences, model = model) + result <- data.frame(sequence_id = names(sequences), probability = probs) + write.csv(result, '{output_path}', row.names = FALSE) + """ + + try: + process = subprocess.run( + ["Rscript", "-e", r_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"ORFhunteR::classifyORFs on {fasta_input}", + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def vectorize_orfs( + fasta_input: str, + output_csv: str, +) -> Dict[str, Any]: + """ + Transform nucleotide sequences into numerical vectors (feature extraction) for machine learning. + This implements the vectorization algorithm used by the ORFhunteR random forest model. + + Args: + fasta_input: Path to the input FASTA file containing nucleotide sequences. + output_csv: Path where the feature matrix will be saved (CSV format). + """ + input_path = Path(fasta_input).resolve() + output_path = Path(output_csv).resolve() + + if not input_path.exists(): + return {"error": f"Input file not found: {fasta_input}"} + + output_path.parent.mkdir(parents=True, exist_ok=True) + + r_script = f""" + library(ORFhunteR) + library(Biostrings) + sequences <- as.character(readDNAStringSet('{input_path}')) + # vectorizeORFs returns a matrix of features + features <- vectorizeORFs(sequences = sequences) + write.csv(as.data.frame(features), '{output_path}', row.names = TRUE) + """ + + try: + process = subprocess.run( + ["Rscript", "-e", r_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"ORFhunteR::vectorizeORFs on {fasta_input}", + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def annotate_orf( + orfs_csv: str, + output_csv: str, + genome_package: str = "BSgenome.Hsapiens.UCSC.hg38", +) -> Dict[str, Any]: + """ + Annotate predicted ORFs by mapping them to genomic coordinates using a BSgenome object. + + Args: + orfs_csv: Path to a CSV file containing predicted ORFs (output from predict_orf). + Must contain 'transcript_id' and coordinate columns. + output_csv: Path where the genomic annotations will be saved (CSV format). + genome_package: The name of the Bioconductor BSgenome package to use for annotation. + Defaults to 'BSgenome.Hsapiens.UCSC.hg38'. + """ + input_path = Path(orfs_csv).resolve() + output_path = Path(output_csv).resolve() + + if not input_path.exists(): + return {"error": f"Input CSV file not found: {orfs_csv}"} + + output_path.parent.mkdir(parents=True, exist_ok=True) + + r_script = f""" + library(ORFhunteR) + library({genome_package}) + orfs_df <- read.csv('{input_path}') + # annotateORFs maps transcript coordinates to genomic coordinates + # It expects the ORFs data frame and corresponding transcript IDs + result <- annotateORFs(ORFs = orfs_df, + transcript_id = orfs_df$transcript_id, + target_genome = {genome_package}) + write.csv(result, '{output_path}', row.names = FALSE) + """ + + try: + process = subprocess.run( + ["Rscript", "-e", r_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"ORFhunteR::annotateORFs using {genome_package}", + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(output_path)] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed. Ensure the BSgenome package is installed.", + "stdout": e.stdout, + "stderr": e.stderr + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-orfhunter/app/bioconductor-orfhunter_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/app/bioconductor-orfhunter_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c569fa074b095dd4652bad3c74f32a4a4953f057 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/app/bioconductor-orfhunter_shim_server.py @@ -0,0 +1,55 @@ +#!/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-orfhunter/app/bioconductor-orfhunter_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_orfhunter' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-orfhunter/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-orfhunter/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..ffd1257beabc2e36bf2d4c39081ee52804eafd1d --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-orfhunter: + build: . + image: mcp-bioconductor-orfhunter:latest + container_name: mcp-bioconductor-orfhunter + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-orfhunter + 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/Biomni/mcp_generated/mcp_bioconductor-orfhunter/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..abc3d2710d8c879e5c5ae780455731d3b509b0d0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-orfhunter + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-orfhunter/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-orfhunter/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-phyloseq/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..75db8224da29a94ca004d7668ccd14ab76761985 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/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-phyloseq via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-phyloseq -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-phyloseq_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-phyloseq_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-phyloseq_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-phyloseq/app/bioconductor-phyloseq_server.py b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/app/bioconductor-phyloseq_server.py new file mode 100644 index 0000000000000000000000000000000000000000..de5c022f375daf37b507c6461ee11963395b66f6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/app/bioconductor-phyloseq_server.py @@ -0,0 +1,355 @@ +import subprocess +import tempfile +import logging +from pathlib import Path +from typing import Optional, List, Literal + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# --- Helper Function to run R scripts --- +def _run_r_script(command: List[str]): + """ + A helper function to execute an R script, handling errors and I/O. + + Args: + command: A list of strings representing the command and its arguments. + + Returns: + A tuple containing stdout and stderr. + + Raises: + subprocess.CalledProcessError: If the R script returns a non-zero exit code. + """ + try: + logger.info(f"Executing command: {' '.join(command)}") + process = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + if process.stderr: + logger.warning(f"Stderr from R script:\n{process.stderr}") + return process.stdout, process.stderr + except FileNotFoundError: + msg = f"Error: The command '{command[0]}' was not found. Is Rscript in your system's PATH?" + logger.error(msg) + raise + except subprocess.CalledProcessError as e: + logger.error(f"R script failed with exit code {e.returncode}") + logger.error(f"Command: {' '.join(e.cmd)}") + logger.error(f"Stdout: {e.stdout}") + logger.error(f"Stderr: {e.stderr}") + raise + +# @mcp.tool() - This decorator is assumed to be provided by the MCP framework. +def phyloseq_import_biom( + biom_file: Path, + output_rds: Path, + metadata_file: Optional[Path] = None, + tree_file: Optional[Path] = None, +): + """ + Imports a BIOM file, optional metadata, and an optional phylogenetic tree + into a phyloseq object and saves it as an RDS file. + + This tool requires R, Rscript, and the 'phyloseq' R package. + It assumes an R script named 'run_phyloseq_import.R' is in the system PATH. + + Args: + biom_file (Path): Path to the input BIOM format file (e.g., from QIIME). + output_rds (Path): Path to save the output phyloseq object (.rds file). + metadata_file (Optional[Path]): Path to the sample metadata file. + tree_file (Optional[Path]): Path to the phylogenetic tree file (e.g., Newick format). + """ + # --- Input Validation --- + if not biom_file.is_file(): + raise FileNotFoundError(f"Input BIOM file not found: {biom_file}") + if metadata_file and not metadata_file.is_file(): + raise FileNotFoundError(f"Metadata file not found: {metadata_file}") + if tree_file and not tree_file.is_file(): + raise FileNotFoundError(f"Tree file not found: {tree_file}") + + output_rds.parent.mkdir(parents=True, exist_ok=True) + + # --- Command Construction --- + cmd = [ + "Rscript", + "run_phyloseq_import.R", + "--biom", str(biom_file), + "--output", str(output_rds), + ] + if metadata_file: + cmd.extend(["--metadata", str(metadata_file)]) + if tree_file: + cmd.extend(["--tree", str(tree_file)]) + + # --- Subprocess Execution --- + try: + stdout, stderr = _run_r_script(cmd) + return { + "command_executed": " ".join(cmd), + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_rds)] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Phyloseq import failed.", + "output_files": [] + } + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_phyloseq' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def phyloseq_filter_and_transform( + input_rds: Path, + output_rds: Path, + min_prevalence: float = 0.05, + min_total_abundance: int = 0, + transform_method: Literal["none", "compositional"] = "compositional", +): + """ + Filters taxa based on prevalence and abundance, and optionally transforms + the data to relative abundance (compositional). + + This tool requires R, Rscript, and the 'phyloseq' R package. + It assumes an R script named 'run_phyloseq_filter.R' is in the system PATH. + + Args: + input_rds (Path): Path to the input phyloseq object (.rds file). + output_rds (Path): Path to save the filtered/transformed phyloseq object. + min_prevalence (float): Minimum proportion of samples a taxon must be in to be kept. + min_total_abundance (int): Minimum total abundance a taxon must have across all samples. + transform_method (str): Transformation to apply. 'compositional' for relative abundance. + """ + # --- Input Validation --- + if not input_rds.is_file(): + raise FileNotFoundError(f"Input RDS file not found: {input_rds}") + if not (0.0 <= min_prevalence <= 1.0): + raise ValueError("min_prevalence must be between 0.0 and 1.0.") + if min_total_abundance < 0: + raise ValueError("min_total_abundance must be non-negative.") + + output_rds.parent.mkdir(parents=True, exist_ok=True) + + # --- Command Construction --- + cmd = [ + "Rscript", + "run_phyloseq_filter.R", + "--input", str(input_rds), + "--output", str(output_rds), + "--prevalence", str(min_prevalence), + "--abundance", str(min_total_abundance), + "--transform", transform_method, + ] + + # --- Subprocess Execution --- + try: + stdout, stderr = _run_r_script(cmd) + return { + "command_executed": " ".join(cmd), + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_rds)] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Phyloseq filtering/transformation failed.", + "output_files": [] + } + +@mcp.tool() +def phyloseq_plot_ordination( + input_rds: Path, + output_plot: Path, + method: Literal["DCA", "CCA", "RDA", "NMDS", "MDS", "PCoA"] = "PCoA", + distance: str = "bray", + color_by: Optional[str] = None, + shape_by: Optional[str] = None, +): + """ + Performs ordination and generates a plot. + + This tool requires R, Rscript, 'phyloseq', and 'ggplot2' R packages. + It assumes an R script named 'run_phyloseq_ordination.R' is in the system PATH. + + Args: + input_rds (Path): Path to the input phyloseq object (.rds file). + output_plot (Path): Path to save the output plot file (e.g., plot.png). + method (str): Ordination method to use. + distance (str): Distance metric to use (for NMDS, MDS, PCoA). + color_by (Optional[str]): Metadata variable to color points by. + shape_by (Optional[str]): Metadata variable to shape points by. + """ + # --- Input Validation --- + if not input_rds.is_file(): + raise FileNotFoundError(f"Input RDS file not found: {input_rds}") + + output_plot.parent.mkdir(parents=True, exist_ok=True) + + # --- Command Construction --- + cmd = [ + "Rscript", + "run_phyloseq_ordination.R", + "--input", str(input_rds), + "--output", str(output_plot), + "--method", method, + "--distance", distance, + ] + if color_by: + cmd.extend(["--color", color_by]) + if shape_by: + cmd.extend(["--shape", shape_by]) + + # --- Subprocess Execution --- + try: + stdout, stderr = _run_r_script(cmd) + return { + "command_executed": " ".join(cmd), + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_plot)] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Phyloseq ordination plotting failed.", + "output_files": [] + } + +@mcp.tool() +def phyloseq_plot_richness( + input_rds: Path, + output_plot: Path, + group_by: str, + measures: str = "Observed,Shannon", +): + """ + Calculates and plots alpha diversity (richness) estimates. + + This tool requires R, Rscript, 'phyloseq', and 'ggplot2' R packages. + It assumes an R script named 'run_phyloseq_richness.R' is in the system PATH. + + Args: + input_rds (Path): Path to the input phyloseq object (.rds file). + output_plot (Path): Path to save the output plot file (e.g., plot.png). + group_by (str): Metadata variable to group samples for boxplots. + measures (str): Comma-separated list of richness measures (e.g., "Observed,Shannon,Simpson"). + """ + # --- Input Validation --- + if not input_rds.is_file(): + raise FileNotFoundError(f"Input RDS file not found: {input_rds}") + if not measures: + raise ValueError("At least one richness measure must be provided.") + + output_plot.parent.mkdir(parents=True, exist_ok=True) + + # --- Command Construction --- + cmd = [ + "Rscript", + "run_phyloseq_richness.R", + "--input", str(input_rds), + "--output", str(output_plot), + "--group", group_by, + "--measures", measures, + ] + + # --- Subprocess Execution --- + try: + stdout, stderr = _run_r_script(cmd) + return { + "command_executed": " ".join(cmd), + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_plot)] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Phyloseq richness plotting failed.", + "output_files": [] + } + +@mcp.tool() +def phyloseq_plot_heatmap( + input_rds: Path, + output_plot: Path, + taxa_rank: str = "Phylum", + top_n_taxa: int = 20, + sample_order_by: Optional[str] = None, + method: Literal["NMDS", "PCoA"] = "NMDS", + distance: str = "bray", +): + """ + Generates a heatmap of the most abundant taxa. + + This tool requires R, Rscript, 'phyloseq', and 'ggplot2' R packages. + It assumes an R script named 'run_phyloseq_heatmap.R' is in the system PATH. + + Args: + input_rds (Path): Path to the input phyloseq object (.rds file). + output_plot (Path): Path to save the output plot file (e.g., plot.png). + taxa_rank (str): Taxonomic rank to agglomerate to before plotting. + top_n_taxa (int): The number of most abundant taxa to include in the heatmap. + sample_order_by (Optional[str]): Metadata variable to order samples by. + method (str): Ordination method for ordering samples if sample_order_by is not provided. + distance (str): Distance metric for ordination-based sample ordering. + """ + # --- Input Validation --- + if not input_rds.is_file(): + raise FileNotFoundError(f"Input RDS file not found: {input_rds}") + if top_n_taxa <= 0: + raise ValueError("top_n_taxa must be a positive integer.") + + output_plot.parent.mkdir(parents=True, exist_ok=True) + + # --- Command Construction --- + cmd = [ + "Rscript", + "run_phyloseq_heatmap.R", + "--input", str(input_rds), + "--output", str(output_plot), + "--rank", taxa_rank, + "--ntaxa", str(top_n_taxa), + "--method", method, + "--distance", distance, + ] + if sample_order_by: + cmd.extend(["--sample_order", sample_order_by]) + + # --- Subprocess Execution --- + try: + stdout, stderr = _run_r_script(cmd) + return { + "command_executed": " ".join(cmd), + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_plot)] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Phyloseq heatmap plotting failed.", + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-phyloseq/app/bioconductor-phyloseq_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/app/bioconductor-phyloseq_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..92e5fe3c51cc4cccf5f2416eb31cf52e72af16cf --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/app/bioconductor-phyloseq_shim_server.py @@ -0,0 +1,55 @@ +#!/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-phyloseq/app/bioconductor-phyloseq_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_phyloseq' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-phyloseq/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-phyloseq/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..f70d5fe54b0cf376d184210a9c9bf06752bc860f --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-phyloseq: + build: . + image: mcp-bioconductor-phyloseq:latest + container_name: mcp-bioconductor-phyloseq + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-phyloseq + 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/Biomni/mcp_generated/mcp_bioconductor-phyloseq/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b6a6cb6a9f073e4b999a55077507a89e1ea63570 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-phyloseq + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-phyloseq/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-phyloseq/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-pipecomp/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4c3b71edffcd345c21c6f37e6f6f21332b953637 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/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-pipecomp via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-pipecomp -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-pipecomp_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-pipecomp_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-pipecomp_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-pipecomp/app/bioconductor-pipecomp_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/app/bioconductor-pipecomp_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ef9eae8218d25a4cc52e0f485ed2d1d0efdae852 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/app/bioconductor-pipecomp_shim_server.py @@ -0,0 +1,55 @@ +#!/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-pipecomp/app/bioconductor-pipecomp_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_pipecomp' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-pipecomp/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-pipecomp/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..7599e9b748eed80975ee6f04120a79cf1925b687 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-pipecomp: + build: . + image: mcp-bioconductor-pipecomp:latest + container_name: mcp-bioconductor-pipecomp + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-pipecomp + 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/Biomni/mcp_generated/mcp_bioconductor-pipecomp/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cdbee95fc4c5b30346a0d26886ab7fe09ab0a168 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-pipecomp + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-pipecomp/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-pipecomp/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-protgenerics/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3f2bf309ffe7ffbc844c764b6d238665b9b9f7d6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/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-protgenerics via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-protgenerics -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-protgenerics_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-protgenerics_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-protgenerics_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-protgenerics/app/bioconductor-protgenerics_server.py b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/app/bioconductor-protgenerics_server.py new file mode 100644 index 0000000000000000000000000000000000000000..66bf7a1b0ec4d3de2a87d88e5dd02a170b925a2a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/app/bioconductor-protgenerics_server.py @@ -0,0 +1,85 @@ +import subprocess +from pathlib import Path +from typing import List, Optional +import mcp + +@mcp.tool( + name="protgenerics_run_r_script", + description="Executes a user-provided R script in an environment with bioconductor-protgenerics." +) +def run_r_script( + script_path: Path, + args: Optional[List[str]] = None, +) -> dict: + """ + Executes a given R script using the 'Rscript' command. + + This tool serves as a generic executor for R scripts within an environment + where the 'bioconductor-protgenerics' package is expected to be available. + The 'bioconductor-protgenerics' package itself does not provide a direct + command-line interface; it is a library of S4 generic functions for + proteomics analysis, intended to be used within R scripts by other packages. + + Therefore, instead of wrapping non-existent subcommands, this tool allows + you to run your own R script that utilizes the functionality provided by + 'bioconductor-protgenerics' or packages that depend on it. + + Args: + script_path: Path to the R script to be executed. + args: A list of optional command-line arguments to pass to the R script. + + Returns: + A dictionary containing the execution details, including the command, + stdout, stderr, and return code. + """ + # 1. Input validation + if not script_path.is_file(): + raise FileNotFoundError(f"The specified R script was not found: {script_path}") + + if args is None: + args = [] + + # 2. Command construction + command = ["Rscript", str(script_path)] + args + command_str = " ".join(command) + + # 3. Subprocess execution and error handling + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + stdout = process.stdout + stderr = process.stderr + return_code = process.returncode + + except FileNotFoundError: + # This error occurs if 'Rscript' is not in the system's PATH + return { + "command_executed": command_str, + "stdout": "", + "stderr": "Error: 'Rscript' command not found. Please ensure R is installed and in your system's PATH.", + "return_code": -1, + "output_files": [] + } + except subprocess.CalledProcessError as e: + # This error occurs if the R script itself fails (returns a non-zero exit code) + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "output_files": [] + } + + # 4. Structured result return + return { + "command_executed": command_str, + "stdout": stdout, + "stderr": stderr, + "return_code": return_code, + "output_files": [] # Output files must be managed by the R script itself and their paths known beforehand. + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-protgenerics/app/bioconductor-protgenerics_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/app/bioconductor-protgenerics_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..dbc227bf149b246d7c1f8e2d78cba137dfd42da4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/app/bioconductor-protgenerics_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_bioconductor-protgenerics/app/bioconductor-protgenerics_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_protgenerics' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-protgenerics/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..47e13413587e0d6524543a5a4c25700d07c47e08 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/app/requirements.txt @@ -0,0 +1 @@ +mcp==1.27.0 diff --git a/Biomni/mcp_generated/mcp_bioconductor-protgenerics/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..8e1d8d8096c32ba24e5823365e99b8573189e31a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-protgenerics: + build: . + image: mcp-bioconductor-protgenerics:latest + container_name: mcp-bioconductor-protgenerics + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-protgenerics + 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/Biomni/mcp_generated/mcp_bioconductor-protgenerics/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d17c7b69d3b821e749569b909c88ec485482063c --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-protgenerics + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-protgenerics/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..062b47df16d08f6173721af25952519f73dfcafa --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-protgenerics/requirements.txt @@ -0,0 +1,3 @@ +mcp==1.27.0 +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-scdataviz/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c926501a4fd661158e18096512c4e1f1481c6b4a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/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-scdataviz via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-scdataviz -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 bioconductor-scdataviz_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-scdataviz_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-scdataviz_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-scdataviz/app/bioconductor-scdataviz_server.py b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/app/bioconductor-scdataviz_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6262e5ea82010f6d2290e68c395b98fca31465c2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/app/bioconductor-scdataviz_server.py @@ -0,0 +1,173 @@ +import subprocess +import shlex +from pathlib import Path +from typing import Optional, List + +# 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 rscript( + file: Optional[Path] = None, + expressions: Optional[List[str]] = None, + script_args: Optional[List[str]] = None, + verbose: bool = False, + default_packages: Optional[List[str]] = None, + save: bool = False, + no_environ: bool = False, + no_site_file: bool = False, + no_init_file: bool = False, + restore: bool = False, + vanilla: bool = False, +) -> dict: + """ + Executes an R script or R expressions using the Rscript command-line interface. + + This tool serves as a wrapper for Rscript, allowing you to run R code from a file + or directly as command-line expressions. It provides control over the R session's + environment and startup behavior. You must provide either a script file or a list + of expressions to execute, but not both. + + Args: + file: Path to the R script file to be executed. + expressions: A list of R expressions to execute. Use instead of 'file'. + script_args: A list of arguments to be passed to the R script. + verbose: If True, print information on progress. + default_packages: A list of package names to be loaded by default. + save: If True, save the workspace at the end of the session. + no_environ: If True, do not read the site and user environment files. + no_site_file: If True, do not read the site-wide Rprofile. + no_init_file: If True, do not read the user R profile. + restore: If True, restore previously saved objects at startup. + vanilla: If True, combine --no-save, --no-restore, --no-site-file, + --no-init-file, and --no-environ for a clean session. + + Returns: + A dictionary containing the executed command, stdout, and stderr. + """ + # 1. Input Validation + if (file is None and not expressions) or (file is not None and expressions): + raise ValueError("You must provide either 'file' or 'expressions', but not both.") + + if file and not file.is_file(): + raise FileNotFoundError(f"The specified R script file does not exist: {file}") + + # 2. Command Construction + cmd = ["Rscript"] + + # Add session control options + if vanilla: + cmd.append("--vanilla") + if save: + cmd.append("--save") + if no_environ: + cmd.append("--no-environ") + if no_site_file: + cmd.append("--no-site-file") + if no_init_file: + cmd.append("--no-init-file") + if restore: + cmd.append("--restore") + if verbose: + cmd.append("--verbose") + if default_packages: + packages_str = ",".join(default_packages) + cmd.append(f"--default-packages={packages_str}") + + # Add the script file or expressions to execute + if file: + cmd.append(str(file)) + elif expressions: + for expr in expressions: + cmd.extend(["-e", expr]) + + # Add arguments for the script itself + if script_args: + cmd.extend(script_args) + + command_executed = shlex.join(cmd) + + # 3. Subprocess Execution and Error Handling + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + stdout = result.stdout + stderr = result.stderr + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'Rscript' command not found. Please ensure R is installed and in your system's PATH.", + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Rscript execution failed with return code {e.returncode}", + "output_files": [] + } + + # 4. Structured Result Return + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": [] # Rscript itself doesn't guarantee output files; the script's content does. + } + +@mcp.tool +def rscript_version() -> dict: + """ + Retrieves the version of the Rscript interpreter. + + This function executes `Rscript --version` to get the version information of the + installed R environment. + + Returns: + A dictionary containing the executed command, stdout (version info), and stderr. + """ + cmd = ["Rscript", "--version"] + command_executed = shlex.join(cmd) + + try: + # Note: Rscript --version prints to stderr, so we capture both. + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + # Combine stdout and stderr as version info can be in either + output = result.stdout + result.stderr + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'Rscript' command not found. Please ensure R is installed and in your system's PATH.", + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Rscript version check failed with return code {e.returncode}", + } + + return { + "command_executed": command_executed, + "stdout": output, + "stderr": "", # Stderr content is moved to stdout for consistency + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-scdataviz/app/bioconductor-scdataviz_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/app/bioconductor-scdataviz_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8ae6d36752c30e737cf704bf9bcfccc71b2e9591 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/app/bioconductor-scdataviz_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_bioconductor-scdataviz/app/bioconductor-scdataviz_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_scdataviz' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-scdataviz/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c505a0ca6bc8f3d61067bbe38ad54952de78ce99 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-scdataviz: + build: . + image: mcp-bioconductor-scdataviz:latest + container_name: mcp-bioconductor-scdataviz + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-scdataviz + 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/Biomni/mcp_generated/mcp_bioconductor-scdataviz/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6dcce732cd877fc501a28cde5132e3bd3d44d826 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-scdataviz + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-scdataviz/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdataviz/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..87c58c18dc99720b13df8531fe3ec735a53a472d --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/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-singlecellmultimodal via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-singlecellmultimodal -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 bioconductor-singlecellmultimodal_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-singlecellmultimodal_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-singlecellmultimodal_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/app/bioconductor-singlecellmultimodal_server.py b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/app/bioconductor-singlecellmultimodal_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8d9f88e48b26b28ffaf85bf24967f5017bd10ae4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/app/bioconductor-singlecellmultimodal_server.py @@ -0,0 +1,143 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List, Dict, Any + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# MCP decorator is not defined here, but the function is written +# to be compatible with it, as per the instructions. +# from mcp import tool as mcp_tool +# For demonstration, we'll use a placeholder decorator. +class mcp: + @staticmethod + def tool(func): + return func + +@mcp.tool +def rscript( + script_file: Optional[Path] = None, + expressions: Optional[List[str]] = None, + script_args: Optional[List[str]] = None, + verbose: bool = False, + default_packages: Optional[str] = None, + save: bool = False, + no_environ: bool = False, + no_site_file: bool = False, + no_init_file: bool = False, + restore: bool = False, + vanilla: bool = False, +) -> Dict[str, Any]: + """ + Executes an R script or R expressions using the Rscript command-line tool. + + This tool serves as a wrapper around Rscript, allowing for the execution of R code + either from a file or directly from command-line expressions. It supports various + options to control the R session's environment and behavior. + + Args: + script_file: Path to the R script file to be executed. Mutually exclusive with 'expressions'. + expressions: A list of R expressions to execute. Mutually exclusive with 'script_file'. + script_args: A list of arguments to be passed to the R script specified by 'script_file'. + verbose: If True, prints information on progress. + default_packages: A comma-separated list of package names to be loaded by default. + save: If True, the workspace will be saved at the end of the session. Ignored if 'vanilla' is True. + no_environ: If True, site and user environment files will not be read. Ignored if 'vanilla' is True. + no_site_file: If True, the site-wide Rprofile will not be read. Ignored if 'vanilla' is True. + no_init_file: If True, the user's R profile will not be read. Ignored if 'vanilla' is True. + restore: If True, previously saved objects will be restored at startup. Ignored if 'vanilla' is True. + vanilla: If True, combines --no-save, --no-restore, --no-site-file, --no-init-file, and --no-environ. + This provides a "clean" R session. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of potential output files. + """ + # 1. Input Validation + if not script_file and not expressions: + raise ValueError("Either 'script_file' or 'expressions' must be provided.") + if script_file and expressions: + raise ValueError("'script_file' and 'expressions' are mutually exclusive.") + if script_args and not script_file: + raise ValueError("'script_args' can only be used when a 'script_file' is specified.") + + if script_file: + if not script_file.is_file(): + raise FileNotFoundError(f"The specified script file does not exist: {script_file}") + + # 2. Command Construction + command = ["Rscript"] + + # The --vanilla flag is a combination of other flags. + if vanilla: + command.append("--vanilla") + else: + if save: + command.append("--save") + if no_environ: + command.append("--no-environ") + if no_site_file: + command.append("--no-site-file") + if no_init_file: + command.append("--no-init-file") + if restore: + command.append("--restore") + + if verbose: + command.append("--verbose") + + if default_packages: + command.append(f"--default-packages={default_packages}") + + # Add the script file or expressions to the command + if expressions: + for expr in expressions: + command.extend(["-e", expr]) + elif script_file: + command.append(str(script_file)) + if script_args: + command.extend(script_args) + + command_str = " ".join(command) + logger.info(f"Executing command: {command_str}") + + # 3. Subprocess Execution + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + stdout = result.stdout + stderr = result.stderr + logger.info("Rscript executed successfully.") + + except FileNotFoundError: + logger.error("Rscript not found. Please ensure R is installed and in your PATH.") + return { + "command_executed": command_str, + "stdout": "", + "stderr": "Error: Rscript command not found. Please ensure R is installed and in your system's PATH.", + "output_files": [] + } + except subprocess.CalledProcessError as e: + logger.error(f"Rscript execution failed with return code {e.returncode}.") + logger.error(f"Stderr: {e.stderr}") + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + + # 4. Structured Result Return + # Rscript itself does not define output files; the script being run does. + # The list of output_files is therefore empty unless explicitly handled. + return { + "command_executed": command_str, + "stdout": stdout, + "stderr": stderr, + "output_files": [] + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/app/bioconductor-singlecellmultimodal_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/app/bioconductor-singlecellmultimodal_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..79ca73eca958fe1eae72da919d172f1897aabf77 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/app/bioconductor-singlecellmultimodal_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_bioconductor-singlecellmultimodal/app/bioconductor-singlecellmultimodal_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_singlecellmultimodal' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..a6f42b0a459288cb5086ca83ad44e9ad694c6bdb --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-singlecellmultimodal: + build: . + image: mcp-bioconductor-singlecellmultimodal:latest + container_name: mcp-bioconductor-singlecellmultimodal + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-singlecellmultimodal + 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/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..30cfd95ed178d3a724fdc76a21229d88702c0edc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-singlecellmultimodal + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-singlecellmultimodal/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-xvector/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-xvector/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..300f89e40e09a3aa8b27e06ad5193bae60eb31ad --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-xvector/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-xvector via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-xvector -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-xvector_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-xvector_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-xvector_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-xvector/app/bioconductor-xvector_server.py b/Biomni/mcp_generated/mcp_bioconductor-xvector/app/bioconductor-xvector_server.py new file mode 100644 index 0000000000000000000000000000000000000000..7f3c202ff5965e9ae058fce54dd124c15807b9f8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-xvector/app/bioconductor-xvector_server.py @@ -0,0 +1,350 @@ +import subprocess +import os +from pathlib import Path +from typing import Optional, List, Union, Dict, Any +import json +import tempfile + +def _run_r_command(script: str) -> dict: + """ + Helper function to execute R code via Rscript. + """ + if not subprocess.run(["which", "Rscript"], capture_output=True).returncode == 0: + raise RuntimeError("Rscript not found. Please ensure R and Bioconductor-XVector are installed.") + + # Check if XVector is installed in R + check_pkg = "if (!requireNamespace('XVector', quietly = TRUE)) quit(status = 1)" + if subprocess.run(["Rscript", "-e", check_pkg]).returncode != 0: + raise RuntimeError("R package 'XVector' is not installed.") + + try: + result = subprocess.run( + ["Rscript", "-e", script], + capture_output=True, + text=True, + check=True + ) + return { + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "stdout": e.stdout, + "stderr": e.stderr, + "status": "error", + "error_msg": str(e) + } + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_xvector' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def xvector_get_info( + rds_path: str +) -> Dict[str, Any]: + """ + Retrieve metadata about an XVector object stored in an RDS file. + + Args: + rds_path: Path to the .rds file containing the XVector object. + """ + path = Path(rds_path) + if not path.exists(): + return {"error": f"File {rds_path} not found."} + + script = f""" + library(XVector) + obj <- readRDS('{rds_path}') + cat(paste0('class:', class(obj), '\\n')) + cat(paste0('length:', length(obj), '\\n')) + cat(paste0('typeof:', typeof(obj), '\\n')) + if (is(obj, 'XVectorList')) {{ + cat(paste0('element_lengths:', paste(elementNROWS(obj), collapse=','), '\\n')) + }} + """ + + res = _run_r_command(script) + return { + "command_executed": f"Rscript -e \"{script.strip()}\"", + "stdout": res.get("stdout"), + "stderr": res.get("stderr"), + "status": res.get("status") + } + +@mcp.tool() +def xvector_subseq( + input_rds: str, + output_rds: str, + start: Optional[int] = None, + end: Optional[int] = None, + width: Optional[int] = None +) -> Dict[str, Any]: + """ + Extract a subsequence from an XVector object using subseq(). + + Args: + input_rds: Path to input .rds file. + output_rds: Path to save the resulting .rds file. + start: The start index of the subsequence. + end: The end index of the subsequence. + width: The width of the subsequence. + """ + input_path = Path(input_rds) + if not input_path.exists(): + return {"error": f"Input file {input_rds} not found."} + + # Build R arguments + args = [] + if start is not None: args.append(f"start={start}") + if end is not None: args.append(f"end={end}") + if width is not None: args.append(f"width={width}") + + arg_str = ", ".join(args) + if arg_str: + arg_str = ", " + arg_str + + script = f""" + library(XVector) + x <- readRDS('{input_rds}') + res <- subseq(x{arg_str}) + saveRDS(res, '{output_rds}') + """ + + res = _run_r_command(script) + return { + "command_executed": f"subseq(x{arg_str})", + "stdout": res.get("stdout"), + "stderr": res.get("stderr"), + "output_files": [output_rds] if res.get("status") == "success" else [] + } + +@mcp.tool() +def xvector_narrow( + input_rds: str, + output_rds: str, + start: int = 1, + end: Optional[int] = None, + width: Optional[int] = None +) -> Dict[str, Any]: + """ + Narrow an XVector object. Unlike subseq, narrow creates a view without copying data. + + Args: + input_rds: Path to input .rds file. + output_rds: Path to save the resulting .rds file. + start: Relative start offset. + end: Relative end offset. + width: Desired width. + """ + input_path = Path(input_rds) + if not input_path.exists(): + return {"error": f"Input file {input_rds} not found."} + + args = [f"start={start}"] + if end is not None: args.append(f"end={end}") + if width is not None: args.append(f"width={width}") + + arg_str = ", ".join(args) + + script = f""" + library(XVector) + x <- readRDS('{input_rds}') + res <- narrow(x, {arg_str}) + saveRDS(res, '{output_rds}') + """ + + res = _run_r_command(script) + return { + "command_executed": f"narrow(x, {arg_str})", + "stdout": res.get("stdout"), + "stderr": res.get("stderr"), + "output_files": [output_rds] + } + +@mcp.tool() +def xvector_compact( + input_rds: str, + output_rds: str +) -> Dict[str, Any]: + """ + Compact an XVector object to force memory deallocation of unused parts of the external vector. + + Args: + input_rds: Path to input .rds file. + output_rds: Path to save the compacted .rds file. + """ + input_path = Path(input_rds) + if not input_path.exists(): + return {"error": f"Input file {input_rds} not found."} + + script = f""" + library(XVector) + x <- readRDS('{input_rds}') + res <- compact(x) + saveRDS(res, '{output_rds}') + """ + + res = _run_r_command(script) + return { + "command_executed": "compact(x)", + "stdout": res.get("stdout"), + "stderr": res.get("stderr"), + "output_files": [output_rds] + } + +@mcp.tool() +def xvector_slice( + input_rds: str, + lower: float = -float('inf'), + upper: float = float('inf') +) -> Dict[str, Any]: + """ + Slice an XInteger or XDouble vector to find ranges where values are within [lower, upper]. + Returns a summary of the ranges. + + Args: + input_rds: Path to input .rds file. + lower: Lower bound for slicing. + upper: Upper bound for slicing. + """ + input_path = Path(input_rds) + if not input_path.exists(): + return {"error": f"Input file {input_rds} not found."} + + # Handle infinity for R + r_lower = "-Inf" if lower == -float('inf') else lower + r_upper = "Inf" if upper == float('inf') else upper + + script = f""" + library(XVector) + x <- readRDS('{input_rds}') + sl <- slice(x, lower={r_lower}, upper={r_upper}) + print(sl) + """ + + res = _run_r_command(script) + return { + "command_executed": f"slice(x, lower={r_lower}, upper={r_upper})", + "stdout": res.get("stdout"), + "stderr": res.get("stderr") + } + +@mcp.tool() +def xvector_to_raw( + input_rds: str, + output_bin: str +) -> Dict[str, Any]: + """ + Export the raw content of an XVector to a binary file. + + Args: + input_rds: Path to input .rds file. + output_bin: Path to the output binary file. + """ + input_path = Path(input_rds) + if not input_path.exists(): + return {"error": f"Input file {input_rds} not found."} + + script = f""" + library(XVector) + x <- readRDS('{input_rds}') + # Ensure it is an XRaw or similar + writeBin(as.raw(x), '{output_bin}') + """ + + res = _run_r_command(script) + return { + "command_executed": "writeBin(as.raw(x), ...)", + "stdout": res.get("stdout"), + "stderr": res.get("stderr"), + "output_files": [output_bin] + } + +@mcp.tool() +def xvector_aggregate( + input_rds: str, + method: str = "sum" +) -> Dict[str, Any]: + """ + Perform aggregation on an XVector (sum, mean, min, max). + + Args: + input_rds: Path to input .rds file. + method: Aggregation method ('sum', 'mean', 'min', 'max'). + """ + if method not in ["sum", "mean", "min", "max"]: + return {"error": "Method must be one of: sum, mean, min, max"} + + input_path = Path(input_rds) + if not input_path.exists(): + return {"error": f"Input file {input_rds} not found."} + + script = f""" + library(XVector) + x <- readRDS('{input_rds}') + val <- {method}(x, na.rm=TRUE) + cat(val) + """ + + res = _run_r_command(script) + return { + "command_executed": f"{method}(x)", + "stdout": res.get("stdout"), + "stderr": res.get("stderr"), + "result": res.get("stdout").strip() if res.get("status") == "success" else None + } + +@mcp.tool() +def xvector_create_from_list( + data: List[float], + type: str = "double", + output_rds: str = "vector.rds" +) -> Dict[str, Any]: + """ + Create an XVector object from a Python list and save as RDS. + + Args: + data: List of numeric values. + type: Type of XVector to create ('integer', 'double', 'raw'). + output_rds: Filename for the output RDS. + """ + if type not in ["integer", "double", "raw"]: + return {"error": "Type must be 'integer', 'double', or 'raw'"} + + # Create a temporary file for the data to pass to R + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp: + json.dump(data, tmp) + tmp_path = tmp.name + + r_class = { + "integer": "XInteger", + "double": "XDouble", + "raw": "XRaw" + }[type] + + script = f""" + library(XVector) + library(jsonlite) + raw_data <- fromJSON('{tmp_path}') + vec <- as(raw_data, '{r_class}') + saveRDS(vec, '{output_rds}') + """ + + try: + res = _run_r_command(script) + return { + "command_executed": f"as(data, '{r_class}')", + "stdout": res.get("stdout"), + "stderr": res.get("stderr"), + "output_files": [output_rds] + } + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-xvector/app/bioconductor-xvector_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-xvector/app/bioconductor-xvector_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a57fab143d78ef463a407b1f6be668e8299f52f4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-xvector/app/bioconductor-xvector_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_bioconductor-xvector/app/bioconductor-xvector_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_xvector' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bioconductor-xvector/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-xvector/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-xvector/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-xvector/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-xvector/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..5a5367493fffc3a6cb5c8110673556796cf375d6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-xvector/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-xvector: + build: . + image: mcp-bioconductor-xvector:latest + container_name: mcp-bioconductor-xvector + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-xvector + 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/Biomni/mcp_generated/mcp_bioconductor-xvector/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-xvector/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fb31b4b13d7d554be35fbdf864159c5e39fd7541 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-xvector/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-xvector + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-xvector/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-xvector/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-xvector/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bowtie/Dockerfile b/Biomni/mcp_generated/mcp_bowtie/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e92733ea1e0f47e2a97581359f7a5c599ccda71a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bowtie/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 bowtie via conda (e.g., from bioconda) +RUN conda install -c bioconda bowtie -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/bowtie_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/bowtie_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/bowtie_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bowtie/app/bowtie_server.py b/Biomni/mcp_generated/mcp_bowtie/app/bowtie_server.py new file mode 100644 index 0000000000000000000000000000000000000000..5c6f37b0f719388ff8c8c473751cc46c645805b2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bowtie/app/bowtie_server.py @@ -0,0 +1,206 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, 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 perl( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + execute_program: Optional[List[str]] = None, + execute_program_extended: Optional[List[str]] = None, + check_syntax_only: bool = False, + loop_around_program: bool = False, + loop_and_print: bool = False, + autosplit: bool = False, + split_pattern: Optional[str] = None, + in_place_edit_extension: Optional[str] = None, + include_directories: Optional[List[Path]] = None, + use_modules: Optional[List[str]] = None, + no_modules: Optional[List[str]] = None, + use_modules_no_import: Optional[List[str]] = None, + no_modules_no_import: Optional[List[str]] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + disable_all_warnings: bool = False, + enable_tainting_checks: bool = False, + enable_tainting_warnings: bool = False, + search_path_for_program: bool = False, + record_separator: Optional[str] = None, + enable_line_ending_processing: Optional[str] = None, + enable_switch_parsing: bool = False, + dump_core: bool = False, + allow_unsafe_operations: bool = False, + extract_program: bool = False, + extract_directory: Optional[Path] = None, + run_under_debugger: Optional[str] = None, + debugging_flags: Optional[str] = None, + unicode_features: Optional[str] = None, + disable_sitecustomize: bool = False, + print_version: bool = False, + print_config: Optional[str] = None, +) -> dict: + """ + Executes the Perl interpreter with a wide range of options. + + This tool can run a Perl script from a file, execute one-liners, check syntax, + and modify Perl's behavior with various switches for warnings, modules, + input processing, and more. + """ + # --- Input Validation --- + actionable_input = any([ + programfile, + execute_program, + execute_program_extended, + print_version, + print_config is not None, + check_syntax_only + ]) + if not actionable_input: + raise ValueError( + "Either a programfile, an executable string (-e/-E), or an action " + "like --print_version, --print_config, or --check_syntax_only must be provided." + ) + + if programfile and (execute_program or execute_program_extended): + raise ValueError("Cannot specify both a 'programfile' and an inline program via '-e' or '-E'.") + + if programfile and not programfile.exists(): + raise FileNotFoundError(f"The specified program file does not exist: {programfile}") + + if loop_around_program and loop_and_print: + raise ValueError("The flags '-n' (loop_around_program) and '-p' (loop_and_print) are mutually exclusive.") + + if enable_all_warnings and disable_all_warnings: + raise ValueError("The flags '-W' (enable_all_warnings) and '-X' (disable_all_warnings) are mutually exclusive.") + + if split_pattern and not autosplit: + raise ValueError("A 'split_pattern' (-F) can only be used with 'autosplit' mode (-a).") + + if extract_directory and not extract_program: + raise ValueError("Specifying 'extract_directory' requires 'extract_program' (-x) to be enabled.") + + # --- 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 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_extended: + for prog_line in execute_program_extended: + cmd.extend(["-E", prog_line]) + if disable_sitecustomize: + cmd.append("-f") + if split_pattern: + cmd.extend(["-F", split_pattern]) + if in_place_edit_extension is not None: + cmd.append(f"-i{in_place_edit_extension}") + if include_directories: + for directory in include_directories: + cmd.extend(["-I", str(directory)]) + if enable_line_ending_processing is not None: + cmd.append(f"-l{enable_line_ending_processing}") + if use_modules: + for module in use_modules: + cmd.append(f"-M{module}") + if no_modules: + for module in no_modules: + cmd.append(f"-M-{module}") + if use_modules_no_import: + for module in use_modules_no_import: + cmd.append(f"-m{module}") + if no_modules_no_import: + for module in no_modules_no_import: + cmd.append(f"-m-{module}") + if loop_around_program: + cmd.append("-n") + if loop_and_print: + cmd.append("-p") + if enable_switch_parsing: + cmd.append("-s") + if search_path_for_program: + 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_operations: + cmd.append("-U") + if print_version: + cmd.append("-v") + if print_config is not None: + cmd.append(f"-V{print_config}") + if enable_warnings: + cmd.append("-w") + if enable_all_warnings: + cmd.append("-W") + if extract_program: + cmd.append("-x") + if extract_directory: + cmd.extend([str(extract_directory)]) + if disable_all_warnings: + cmd.append("-X") + + # Add program file and arguments + if programfile: + cmd.append(str(programfile)) + + if arguments: + # The '--' switch is good practice to separate perl options from script arguments + if programfile or execute_program or execute_program_extended: + cmd.append("--") + cmd.extend(arguments) + + # --- Subprocess Execution --- + command_executed = " ".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": [] # Perl execution doesn't have a defined output file + } + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'perl' command not found. Please ensure Perl is installed and in your system's PATH.", + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } diff --git a/Biomni/mcp_generated/mcp_bowtie/app/bowtie_shim_server.py b/Biomni/mcp_generated/mcp_bowtie/app/bowtie_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8539de64265b54d77c00fa2241d923c65fd6a67b --- /dev/null +++ b/Biomni/mcp_generated/mcp_bowtie/app/bowtie_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_bowtie/app/bowtie_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bowtie' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_bowtie/app/requirements.txt b/Biomni/mcp_generated/mcp_bowtie/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bowtie/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bowtie/docker-compose.yml b/Biomni/mcp_generated/mcp_bowtie/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b590c5d861b6fcc1f695edf5c47bf23f69e96dbc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bowtie/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bowtie: + build: . + image: mcp-bowtie:latest + container_name: mcp-bowtie + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bowtie + 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/Biomni/mcp_generated/mcp_bowtie/environment.yaml b/Biomni/mcp_generated/mcp_bowtie/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a94f45ccfd6430e8677fdd9d183a45bb30cda4a9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bowtie/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bowtie + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bowtie/requirements.txt b/Biomni/mcp_generated/mcp_bowtie/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bowtie/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_clustalo/Dockerfile b/Biomni/mcp_generated/mcp_clustalo/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..eb716cc02b3ed82157b190e6ce990aac387b416d --- /dev/null +++ b/Biomni/mcp_generated/mcp_clustalo/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 clustalo via conda (e.g., from bioconda) +RUN conda install -c bioconda clustalo -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/clustalo_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/clustalo_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/clustalo_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_clustalo/app/clustalo_server.py b/Biomni/mcp_generated/mcp_clustalo/app/clustalo_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d5d795b1d238f3423c77135f6187db740833b499 --- /dev/null +++ b/Biomni/mcp_generated/mcp_clustalo/app/clustalo_server.py @@ -0,0 +1,248 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Literal + +# @mcp.tool() decorator would be here in a real MCP environment +def clustalo( + infile: Path, + outfile: Optional[Path] = None, + hmm_in: Optional[List[Path]] = None, + hmm_batch: Optional[Path] = None, + dealign: bool = False, + profile1: Optional[Path] = None, + profile2: Optional[Path] = None, + is_profile: bool = False, + seqtype: Optional[Literal["Protein", "RNA", "DNA"]] = None, + infmt: Optional[Literal["a2m", "fasta", "fa", "sta", "clustal", "clu", "msf", "phylip", "phy", "selex", "stockholm", "st", "vienna", "vie"]] = None, + distmat_in: Optional[Path] = None, + distmat_out: Optional[Path] = None, + guidetree_in: Optional[Path] = None, + guidetree_out: Optional[Path] = None, + pileup: bool = False, + full: bool = False, + full_iter: bool = False, + cluster_size: Optional[int] = None, + clustering_out: Optional[Path] = None, + trans: int = 0, + posterior_out: Optional[Path] = None, + use_kimura: bool = False, + percent_id: bool = False, + outfmt: Literal["a2m", "fasta", "fa", "sta", "clustal", "clu", "msf", "phylip", "phy", "selex", "stockholm", "st", "vienna", "vie"] = "fasta", + residuenumber: bool = False, + wrap: Optional[int] = None, + output_order: Optional[Literal["input-order", "tree-order"]] = None, + iterations: Optional[int] = None, + max_guidetree_iterations: Optional[int] = None, + max_hmm_iterations: Optional[int] = None, + maxnumseq: Optional[int] = None, + maxseqlen: Optional[int] = None, + auto: bool = False, + threads: Optional[int] = None, + pseudo: Optional[Path] = None, + log: Optional[Path] = None, + verbose: int = 0, + force: bool = False, + mac_ram: Optional[int] = None, +): + """ + Run Clustal Omega for multiple sequence alignment. + + Clustal Omega is a fast, scalable tool for generating high-quality protein + and nucleic acid multiple sequence alignments. + + Args: + infile: Multiple sequence input file. + outfile: Multiple sequence alignment output file (default: stdout). + hmm_in: HMM input files. + hmm_batch: Specify HMMs for individual sequences. + dealign: Dealign input sequences. + profile1: Pre-aligned multiple sequence file (aligned columns will be kept fixed). + profile2: Pre-aligned multiple sequence file (aligned columns will be kept fixed). + is_profile: Disable check if profile, force profile (default: False). + seqtype: Force a sequence type (default: auto). + infmt: Forced sequence input file format (default: auto). + distmat_in: Pairwise distance matrix input file (skips distance computation). + distmat_out: Pairwise distance matrix output file. + guidetree_in: Guide tree input file (skips guide-tree clustering step). + guidetree_out: Guide tree output file. + pileup: Sequentially align sequences. + full: Use full distance matrix for guide-tree calculation (default: mBed). + full_iter: Use full distance matrix for guide-tree calculation during iteration (default: mBed). + cluster_size: Soft maximum of sequences in sub-clusters. + clustering_out: Clustering output file. + trans: Use transitivity (default: 0). + posterior_out: Posterior probability output file. + use_kimura: Use Kimura distance correction for aligned sequences (default: False). + percent_id: Convert distances into percent identities (default: False). + outfmt: MSA output file format (default: fasta). + residuenumber: In Clustal format, print residue numbers (default: False). + wrap: Number of residues before line-wrap in output. + output_order: MSA output order like in input/guide-tree. + iterations: Number of (combined guide-tree/HMM) iterations. + max_guidetree_iterations: Maximum number of guidetree iterations. + max_hmm_iterations: Maximum number of HMM iterations. + maxnumseq: Maximum allowed number of sequences. + maxseqlen: Maximum allowed sequence length. + auto: Set options automatically (might overwrite some of your options). + threads: Number of processors to use. + pseudo: Input file for pseudo-count parameters. + log: Log all non-essential output to this file. + verbose: Verbose output level (e.g., 1 for -v, 2 for -vv). + force: Force file overwriting. + mac_ram: Set a memory limit (undocumented, platform-specific). + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + if not infile.is_file(): + raise FileNotFoundError(f"Input file not found: {infile}") + + input_files_to_check = { + "hmm_batch": hmm_batch, + "profile1": profile1, + "profile2": profile2, + "distmat_in": distmat_in, + "guidetree_in": guidetree_in, + "pseudo": pseudo, + } + for name, path in input_files_to_check.items(): + if path and not path.is_file(): + raise FileNotFoundError(f"Input file for --{name.replace('_', '-')} not found: {path}") + + if hmm_in: + for hmm_file in hmm_in: + if not hmm_file.is_file(): + raise FileNotFoundError(f"HMM input file not found: {hmm_file}") + + if threads is not None and threads <= 0: + raise ValueError("threads must be a positive integer.") + + if verbose < 0: + raise ValueError("verbose level cannot be negative.") + + # --- Command Construction --- + cmd = ["clustalo"] + output_files = [] + + # Sequence Input + cmd.extend(["--infile", str(infile)]) + if hmm_in: + for hmm_file in hmm_in: + cmd.extend(["--hmm-in", str(hmm_file)]) + if hmm_batch: + cmd.extend(["--hmm-batch", str(hmm_batch)]) + if dealign: + cmd.append("--dealign") + if profile1: + cmd.extend(["--profile1", str(profile1)]) + if profile2: + cmd.extend(["--profile2", str(profile2)]) + if is_profile: + cmd.append("--is-profile") + if seqtype: + cmd.extend(["--seqtype", seqtype]) + if infmt: + cmd.extend(["--infmt", infmt]) + + # Clustering + if distmat_in: + cmd.extend(["--distmat-in", str(distmat_in)]) + if distmat_out: + cmd.extend(["--distmat-out", str(distmat_out)]) + output_files.append(str(distmat_out)) + if guidetree_in: + cmd.extend(["--guidetree-in", str(guidetree_in)]) + if guidetree_out: + cmd.extend(["--guidetree-out", str(guidetree_out)]) + output_files.append(str(guidetree_out)) + if pileup: + cmd.append("--pileup") + if full: + cmd.append("--full") + if full_iter: + cmd.append("--full-iter") + if cluster_size is not None: + cmd.extend(["--cluster-size", str(cluster_size)]) + if clustering_out: + cmd.extend(["--clustering-out", str(clustering_out)]) + output_files.append(str(clustering_out)) + if trans != 0: + cmd.extend(["--trans", str(trans)]) + if posterior_out: + cmd.extend(["--posterior-out", str(posterior_out)]) + output_files.append(str(posterior_out)) + if use_kimura: + cmd.append("--use-kimura") + if percent_id: + cmd.append("--percent-id") + + # Alignment Output + if outfile: + cmd.extend(["--outfile", str(outfile)]) + output_files.append(str(outfile)) + cmd.extend(["--outfmt", outfmt]) + if residuenumber: + cmd.append("--residuenumber") + if wrap is not None: + cmd.extend(["--wrap", str(wrap)]) + if output_order: + cmd.extend(["--output-order", output_order]) + + # Iteration + if iterations is not None: + cmd.extend(["--iterations", str(iterations)]) + if max_guidetree_iterations is not None: + cmd.extend(["--max-guidetree-iterations", str(max_guidetree_iterations)]) + if max_hmm_iterations is not None: + cmd.extend(["--max-hmm-iterations", str(max_hmm_iterations)]) + + # Limits + if maxnumseq is not None: + cmd.extend(["--maxnumseq", str(maxnumseq)]) + if maxseqlen is not None: + cmd.extend(["--maxseqlen", str(maxseqlen)]) + + # Miscellaneous + if auto: + cmd.append("--auto") + if threads is not None: + cmd.extend(["--threads", str(threads)]) + if pseudo: + cmd.extend(["--pseudo", str(pseudo)]) + if log: + cmd.extend(["--log", str(log)]) + output_files.append(str(log)) + if verbose > 0: + cmd.append("-" + "v" * verbose) + if force: + cmd.append("--force") + if mac_ram is not None: + cmd.extend(["--MAC-RAM", str(mac_ram)]) + + # --- Subprocess Execution --- + try: + command_str = " ".join(cmd) + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + except FileNotFoundError: + raise RuntimeError("clustalo command not found. Please ensure it is installed and in your system's PATH.") + except subprocess.CalledProcessError as e: + # Return structured error information + return { + "command_executed": " ".join(e.cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"ClustalO failed with exit code {e.returncode}", + "output_files": [] + } diff --git a/Biomni/mcp_generated/mcp_clustalo/app/clustalo_shim_server.py b/Biomni/mcp_generated/mcp_clustalo/app/clustalo_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..7dffd7aff37ba6e02547f49edd514faa604e9b4e --- /dev/null +++ b/Biomni/mcp_generated/mcp_clustalo/app/clustalo_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_clustalo/app/clustalo_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_clustalo' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_clustalo/app/requirements.txt b/Biomni/mcp_generated/mcp_clustalo/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_clustalo/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_clustalo/docker-compose.yml b/Biomni/mcp_generated/mcp_clustalo/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..50ecbc94d4624b00f0fe687ff464198dc02e71e7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_clustalo/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-clustalo: + build: . + image: mcp-clustalo:latest + container_name: mcp-clustalo + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=clustalo + 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/Biomni/mcp_generated/mcp_clustalo/environment.yaml b/Biomni/mcp_generated/mcp_clustalo/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ca423df37f9d284dce831fcf7f072fd41a6a4d5d --- /dev/null +++ b/Biomni/mcp_generated/mcp_clustalo/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - clustalo + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_clustalo/requirements.txt b/Biomni/mcp_generated/mcp_clustalo/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_clustalo/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_cytoscape/Dockerfile b/Biomni/mcp_generated/mcp_cytoscape/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2222ff7bf8ac3bffacff217f71e88ee2d6bed932 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytoscape/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 cytoscape via conda (e.g., from bioconda) +RUN conda install -c bioconda cytoscape -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/cytoscape_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/cytoscape_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/cytoscape_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_cytoscape/app/cytoscape_server.py b/Biomni/mcp_generated/mcp_cytoscape/app/cytoscape_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3acb98117d1d20fa0142c47c7339662871d96805 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytoscape/app/cytoscape_server.py @@ -0,0 +1,145 @@ +import subprocess +from pathlib import Path +from typing import Optional, List +import logging + +# This is a placeholder for the actual MCP decorator. +# The final code should be used in an environment where `mcp` is defined. +class mcp: + @staticmethod + def tool(): + def decorator(func): + return func + return decorator + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_cytoscape' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def launch_cytoscape( + session_file: Optional[Path] = None, + network_files: Optional[List[Path]] = None, + properties_file: Optional[Path] = None, + vizmap_file: Optional[Path] = None, + script_file: Optional[Path] = None, + additional_files: Optional[List[Path]] = None, + rest_port: Optional[int] = None, + show_version: bool = False, + show_help: bool = False, + cytoscape_executable: str = "cytoscape", +) -> dict: + """ + Launches the Cytoscape application with specified command-line options. + + This tool wraps the main Cytoscape executable. It is primarily used to start + Cytoscape with pre-loaded sessions, networks, styles, or to execute a script + for automated, headless workflows. For automation, the `script_file` parameter + is crucial, and the provided script should handle all processing and file output, + concluding with a command to exit Cytoscape. + + Args: + session_file: Path to a Cytoscape session file (.cys) to load. Corresponds to the -s flag. + network_files: A list of paths to network files (e.g., .sif, .gml) to load. Corresponds to one or more -N flags. + properties_file: Path to a properties file (.props) to load. Corresponds to the -P flag. + vizmap_file: Path to a style/vizmap file (.xml) to load. Corresponds to the -V flag. + script_file: Path to a script file to execute upon startup. Essential for automated tasks. Corresponds to the -S flag. + additional_files: A list of paths to any other files (e.g., networks, sessions) to load as positional arguments. + rest_port: Starts Cytoscape and listens for CyREST commands on a specific port. Corresponds to the -R flag. + show_version: If True, prints Cytoscape version information and exits. Corresponds to the -v flag. + show_help: If True, prints the command-line help message and exits. Corresponds to the -h flag. + cytoscape_executable: The name or path of the Cytoscape executable. Defaults to 'cytoscape'. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + Note: The `output_files` list will be empty as this wrapper cannot predict what files a user-provided script might generate. + """ + # 1. Input Validation + if show_help and show_version: + raise ValueError("The 'show_help' and 'show_version' parameters are mutually exclusive.") + + if session_file and not session_file.is_file(): + raise FileNotFoundError(f"Session file not found: {session_file}") + if properties_file and not properties_file.is_file(): + raise FileNotFoundError(f"Properties file not found: {properties_file}") + if vizmap_file and not vizmap_file.is_file(): + raise FileNotFoundError(f"VizMap (style) file not found: {vizmap_file}") + if script_file and not script_file.is_file(): + raise FileNotFoundError(f"Script file not found: {script_file}") + + if network_files: + for f in network_files: + if not f.is_file(): + raise FileNotFoundError(f"Network file not found: {f}") + if additional_files: + for f in additional_files: + if not f.is_file(): + raise FileNotFoundError(f"Additional file not found: {f}") + + if rest_port is not None and not (1024 <= rest_port <= 65535): + raise ValueError(f"Invalid REST port: {rest_port}. Must be between 1024 and 65535.") + + # 2. Command Construction + cmd = [cytoscape_executable] + + if show_help: + cmd.append("--help") + elif show_version: + cmd.append("--version") + else: + if session_file: + cmd.extend(["--session", str(session_file)]) + if network_files: + for f in network_files: + cmd.extend(["--network", str(f)]) + if properties_file: + cmd.extend(["--props", str(properties_file)]) + if vizmap_file: + cmd.extend(["--vizmap", str(vizmap_file)]) + if script_file: + cmd.extend(["--script", str(script_file)]) + if rest_port is not None: + cmd.extend(["--rest", str(rest_port)]) + if additional_files: + cmd.extend([str(f) for f in additional_files]) + + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + # 3. Subprocess Execution and Error Handling + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + except FileNotFoundError: + error_message = f"Error: The executable '{cytoscape_executable}' was not found. Please ensure Cytoscape is installed and in your system's PATH, or provide the full path via the 'cytoscape_executable' parameter." + logging.error(error_message) + return { + "command_executed": command_executed, + "stdout": "", + "stderr": error_message, + "output_files": [], + } + except subprocess.CalledProcessError as e: + logging.error(f"Cytoscape execution failed with return code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + } + + # 4. Structured Result Return + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [], + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_cytoscape/app/cytoscape_shim_server.py b/Biomni/mcp_generated/mcp_cytoscape/app/cytoscape_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..57e8a06c2737dbda09fff2b84daaf60362802285 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytoscape/app/cytoscape_shim_server.py @@ -0,0 +1,55 @@ +#!/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_cytoscape/app/cytoscape_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_cytoscape' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_cytoscape/app/requirements.txt b/Biomni/mcp_generated/mcp_cytoscape/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytoscape/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_cytoscape/docker-compose.yml b/Biomni/mcp_generated/mcp_cytoscape/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c7fee9f78634f1510e03a3b47a2da7ca901c63bb --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytoscape/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-cytoscape: + build: . + image: mcp-cytoscape:latest + container_name: mcp-cytoscape + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=cytoscape + 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/Biomni/mcp_generated/mcp_cytoscape/environment.yaml b/Biomni/mcp_generated/mcp_cytoscape/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e1b5bcf43777bd05c8b83d4f4edee4db8a331311 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytoscape/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - cytoscape + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_cytoscape/requirements.txt b/Biomni/mcp_generated/mcp_cytoscape/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytoscape/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_cytotrace2-python/Dockerfile b/Biomni/mcp_generated/mcp_cytotrace2-python/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3e77be585db7f5d85f326e451e44a04aaf60162b --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytotrace2-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 cytotrace2-python via conda (e.g., from bioconda) +RUN conda install -c bioconda cytotrace2-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 cytotrace2-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/cytotrace2-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/cytotrace2-python_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_cytotrace2-python/app/cytotrace2-python_server.py b/Biomni/mcp_generated/mcp_cytotrace2-python/app/cytotrace2-python_server.py new file mode 100644 index 0000000000000000000000000000000000000000..016786b0702214f59bd44988413cc2a3d5ff572a --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytotrace2-python/app/cytotrace2-python_server.py @@ -0,0 +1,158 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Dict, Any + +# Assume mcp is imported elsewhere, as per instructions "NO NEED to import mcp" + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_cytotrace2_python' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_rscript( + script_file: Optional[Path] = None, + expressions: Optional[List[str]] = None, + verbose: bool = False, + default_packages: Optional[str] = None, + save: bool = False, + no_environ: bool = False, + no_site_file: bool = False, + no_init_file: bool = False, + restore: bool = False, + vanilla: bool = False, + script_args: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Executes an R script or R expressions using the Rscript command-line utility. + + This tool provides an interface to the Rscript command, allowing users to run + R scripts or evaluate R expressions directly from the command line with + various R environment configuration options. + + Args: + script_file: Path to the R script file (.R) to execute. + This parameter is mutually exclusive with 'expressions'. + If provided, the file must exist. + expressions: A list of R expressions (strings) to evaluate. Each string + in the list will be passed with a separate '-e' flag. + This parameter is mutually exclusive with 'script_file'. + verbose: If True, print information on R's progress during execution. + default_packages: A comma-separated list of package names (e.g., "stats,utils") + to load by default, or 'NULL' to load no default packages. + save: If True, save the R workspace at the end of the session. + no_environ: If True, do not read the site and user environment files. + no_site_file: If True, do not read the site-wide Rprofile. + no_init_file: If True, do not read the user R profile. + restore: If True, restore previously saved objects at startup. + vanilla: If True, combine --no-save, --no-restore, --no-site-file, + --no-init-file, and --no-environ. This option overrides + individual settings for these flags. + script_args: A list of additional arguments (strings) to pass directly + to the R script being executed. These arguments are accessible + within the R script via `commandArgs(trailingOnly=TRUE)`. + + Returns: + A dictionary containing the command executed, standard output, standard error, + and a list of any output files generated (though Rscript itself doesn't + produce named output files unless specified within the R script). + + Raises: + ValueError: If neither 'script_file' nor 'expressions' is provided, + or if both are provided. + FileNotFoundError: If 'script_file' is provided but the file does not exist. + """ + # Input validation for mutually exclusive parameters + if script_file is None and expressions is None: + raise ValueError("Either 'script_file' or 'expressions' must be provided.") + if script_file is not None and expressions is not None: + raise ValueError("Cannot provide both 'script_file' and 'expressions'. Choose one.") + + # Validate script_file existence if provided + if script_file: + if not script_file.is_file(): + raise FileNotFoundError(f"R script file not found: {script_file}") + # Optional: Check for .R extension, though Rscript can run any file + if script_file.suffix.lower() != ".r": + print(f"Warning: Script file '{script_file}' does not have a '.R' extension. " + "Rscript will attempt to execute it regardless.") + + command: List[str] = ["Rscript"] + + # Add Rscript options + if verbose: + command.append("--verbose") + if default_packages is not None: + command.append(f"--default-packages={default_packages}") + if save: + command.append("--save") + if no_environ: + command.append("--no-environ") + if no_site_file: + command.append("--no-site-file") + if no_init_file: + command.append("--no-init-file") + if restore: + command.append("--restore") + if vanilla: + command.append("--vanilla") + + # Add expressions or script file + if expressions: + for expr in expressions: + command.extend(["-e", expr]) + elif script_file: + command.append(str(script_file)) + + # Add arguments to be passed to the R script + if script_args: + command.extend(script_args) + + command_str = " ".join(command) + output_files: List[Path] = [] # Rscript itself doesn't produce standard output files. + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + stdout = process.stdout + stderr = process.stderr + except subprocess.CalledProcessError as e: + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Rscript execution failed with exit code {e.returncode}. " + f"Command: {command_str}", + "output_files": output_files, + } + except FileNotFoundError: + return { + "command_executed": command_str, + "stdout": "", + "stderr": "Rscript command not found. Please ensure R is installed and Rscript is in your system's PATH.", + "error": "Rscript executable not found.", + "output_files": output_files, + } + except Exception as e: + return { + "command_executed": command_str, + "stdout": "", + "stderr": str(e), + "error": f"An unexpected error occurred during Rscript execution: {e}", + "output_files": output_files, + } + + return { + "command_executed": command_str, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_cytotrace2-python/app/cytotrace2-python_shim_server.py b/Biomni/mcp_generated/mcp_cytotrace2-python/app/cytotrace2-python_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0db5181be268714ea29bef3ecf8f9f8432125020 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytotrace2-python/app/cytotrace2-python_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_cytotrace2-python/app/cytotrace2-python_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_cytotrace2_python' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_cytotrace2-python/docker-compose.yml b/Biomni/mcp_generated/mcp_cytotrace2-python/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..a7e3973745d9f8bfa060c68bcf98ff3277e3f7f2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytotrace2-python/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-cytotrace2-python: + build: . + image: mcp-cytotrace2-python:latest + container_name: mcp-cytotrace2-python + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=cytotrace2-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/Biomni/mcp_generated/mcp_cytotrace2-python/environment.yaml b/Biomni/mcp_generated/mcp_cytotrace2-python/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6679f76bf3e643cb984709f1c79af3ba836b4a5d --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytotrace2-python/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - cytotrace2-python + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_cytotrace2-python/requirements.txt b/Biomni/mcp_generated/mcp_cytotrace2-python/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cytotrace2-python/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_easy_vitessce/Dockerfile b/Biomni/mcp_generated/mcp_easy_vitessce/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..384993aa1611eaf135fe23a9f9db8786601ff9cb --- /dev/null +++ b/Biomni/mcp_generated/mcp_easy_vitessce/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 easy_vitessce via conda (e.g., from bioconda) +RUN conda install -c bioconda easy_vitessce -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/easy_vitessce_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/easy_vitessce_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/easy_vitessce_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_easy_vitessce/app/easy_vitessce_server.py b/Biomni/mcp_generated/mcp_easy_vitessce/app/easy_vitessce_server.py new file mode 100644 index 0000000000000000000000000000000000000000..558de9f44b8e5b96e64b851a3dcfdb9972c4f728 --- /dev/null +++ b/Biomni/mcp_generated/mcp_easy_vitessce/app/easy_vitessce_server.py @@ -0,0 +1,283 @@ +from pathlib import Path +import subprocess +import tempfile +import json +from typing import Optional, List, Union + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_easy_vitessce' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def scanpy_embedding_to_vitessce( + adata_path: str, + output_dir: str, + plot_type: str = "umap", + basis: Optional[str] = None, + color: Optional[str] = None, + gene_list: Optional[List[str]] = None, + host_url: str = "http://localhost:8000", +): + """ + Convert a Scanpy AnnData object's embedding plot (UMAP, tSNE, PCA) into an interactive Vitessce visualization. + + :param adata_path: Path to the input .h5ad file. + :param output_dir: Directory where Vitessce data assets and configuration will be exported. + :param plot_type: Type of embedding to visualize ('umap', 'tsne', 'pca', 'embedding'). + :param basis: The name of the embedding to use (required if plot_type is 'embedding', e.g., 'X_draw_graph_fa'). + :param color: Key for annotations (obs) or gene names (var) to color the plot by. + :param gene_list: Optional list of genes to include in the visualization. + :param host_url: The base URL where the exported files will be served (default: http://localhost:8000). + """ + # Input validation + input_path = Path(adata_path) + if not input_path.exists(): + return {"error": f"Input file not found: {adata_path}"} + + out_path = Path(output_dir) + out_path.mkdir(parents=True, exist_ok=True) + + if plot_type == "embedding" and not basis: + return {"error": "Parameter 'basis' is required when plot_type is 'embedding'"} + + valid_plots = ["umap", "tsne", "pca", "embedding"] + if plot_type not in valid_plots: + return {"error": f"Invalid plot_type. Must be one of {valid_plots}"} + + # Construct Python script to execute easy_vitessce + # easy_vitessce patches scanpy.pl on import. + # We capture the returned VitessceConfig and export it. + python_script = f""" +import scanpy as sc +import easy_vitessce as ev +import os + +adata = sc.read_h5ad('{adata_path}') +# easy_vitessce is enabled by default upon import + +# Prepare plotting arguments +kwargs = {{}} +if '{color}': kwargs['color'] = '{color}' +if {gene_list}: kwargs['var_names'] = {gene_list} +if '{basis}' and '{plot_type}' == 'embedding': kwargs['basis'] = '{basis}' + +# Call the patched scanpy plotting function +# In easy_vitessce, these return a VitessceConfig or Widget object +if '{plot_type}' == 'umap': + vc = sc.pl.umap(adata, show=False, **kwargs) +elif '{plot_type}' == 'tsne': + vc = sc.pl.tsne(adata, show=False, **kwargs) +elif '{plot_type}' == 'pca': + vc = sc.pl.pca(adata, show=False, **kwargs) +else: + vc = sc.pl.embedding(adata, show=False, **kwargs) + +# Export the configuration and data +# Note: easy_vitessce objects usually have a .config attribute or are the config themselves +if hasattr(vc, 'config'): + config = vc.config +else: + config = vc + +config.export(targets=['files'], dest='{output_dir}', base_url='{host_url}') +print("Export successful") +""" + + try: + result = subprocess.run( + ["python", "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"easy_vitessce conversion for {plot_type}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": str(out_path.absolute()), + "vitessce_config_file": str((out_path / "config.json").absolute()) + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to generate Vitessce configuration", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def scanpy_matrix_to_vitessce( + adata_path: str, + output_dir: str, + plot_type: str = "violin", + var_names: Optional[List[str]] = None, + groupby: Optional[str] = None, + host_url: str = "http://localhost:8000", +): + """ + Convert Scanpy matrix-style plots (Violin, Dotplot, Heatmap) into interactive Vitessce visualizations. + + :param adata_path: Path to the input .h5ad file. + :param output_dir: Directory where Vitessce data assets and configuration will be exported. + :param plot_type: Type of plot ('violin', 'dotplot', 'heatmap'). + :param var_names: List of genes/variables to plot. + :param groupby: The key of the observation grouping to consider. + :param host_url: The base URL where the exported files will be served. + """ + input_path = Path(adata_path) + if not input_path.exists(): + return {"error": f"Input file not found: {adata_path}"} + + out_path = Path(output_dir) + out_path.mkdir(parents=True, exist_ok=True) + + valid_plots = ["violin", "dotplot", "heatmap"] + if plot_type not in valid_plots: + return {"error": f"Invalid plot_type. Must be one of {valid_plots}"} + + python_script = f""" +import scanpy as sc +import easy_vitessce as ev + +adata = sc.read_h5ad('{adata_path}') +vars = {var_names if var_names else "adata.var_names[:5].tolist()"} +group = '{groupby}' if '{groupby}' else None + +if '{plot_type}' == 'violin': + vc = sc.pl.violin(adata, keys=vars, groupby=group, show=False) +elif '{plot_type}' == 'dotplot': + vc = sc.pl.dotplot(adata, var_names=vars, groupby=group, show=False) +else: + vc = sc.pl.heatmap(adata, var_names=vars, groupby=group, show=False) + +if hasattr(vc, 'config'): + config = vc.config +else: + config = vc + +config.export(targets=['files'], dest='{output_dir}', base_url='{host_url}') +""" + + try: + result = subprocess.run( + ["python", "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"easy_vitessce matrix conversion for {plot_type}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": str(out_path.absolute()) + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to generate Vitessce configuration", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def spatialdata_to_vitessce( + sdata_path: str, + output_dir: str, + host_url: str = "http://localhost:8000", +): + """ + Convert a SpatialData object into an interactive Vitessce visualization. + Supports images, labels, shapes, and points. + + :param sdata_path: Path to the SpatialData .zarr store. + :param output_dir: Directory where Vitessce data assets and configuration will be exported. + :param host_url: The base URL where the exported files will be served. + """ + input_path = Path(sdata_path) + if not input_path.exists(): + return {"error": f"SpatialData path not found: {sdata_path}"} + + out_path = Path(output_dir) + out_path.mkdir(parents=True, exist_ok=True) + + python_script = f""" +import spatialdata as sd +import easy_vitessce as ev +import spatialdata_plot + +sdata = sd.read_zarr('{sdata_path}') + +# easy_vitessce patches sdata.pl.render_images, etc. +# Calling the plot method on sdata (via spatialdata-plot) triggers the Vitessce wrapper +# We use a generic render call that easy_vitessce captures +vc = sdata.pl.render_images().pl.show() + +if hasattr(vc, 'config'): + config = vc.config +else: + config = vc + +config.export(targets=['files'], dest='{output_dir}', base_url='{host_url}') +""" + + try: + result = subprocess.run( + ["python", "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": "easy_vitessce spatialdata conversion", + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": str(out_path.absolute()) + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to generate Vitessce configuration for SpatialData", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def toggle_easy_vitessce( + enable: bool = True, + plot_types: Optional[List[str]] = None, +): + """ + Enable or disable interactive Vitessce plots for specific categories. + Note: In this MCP context, this is primarily used to verify configuration. + + :param enable: Whether to enable (True) or disable (False) interactive plots. + :param plot_types: List of plot types to toggle (e.g., ['embedding', 'violin', 'spatialdata-plot']). + If None, toggles all. + """ + action = "enable_plots" if enable else "disable_plots" + types_arg = f"{plot_types}" if plot_types else "" + + python_script = f""" +import easy_vitessce as ev +ev.{action}({types_arg}) +print(f"Successfully called ev.{action}({types_arg})") +""" + + try: + result = subprocess.run( + ["python", "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "status": "success", + "stdout": result.stdout, + "action_performed": action + } + except subprocess.CalledProcessError as e: + return { + "error": f"Failed to perform {action}", + "stderr": e.stderr + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_easy_vitessce/app/easy_vitessce_shim_server.py b/Biomni/mcp_generated/mcp_easy_vitessce/app/easy_vitessce_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..dce02ed8397f5b9e498415a06622798634274b84 --- /dev/null +++ b/Biomni/mcp_generated/mcp_easy_vitessce/app/easy_vitessce_shim_server.py @@ -0,0 +1,55 @@ +#!/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_easy_vitessce/app/easy_vitessce_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_easy_vitessce' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_easy_vitessce/app/requirements.txt b/Biomni/mcp_generated/mcp_easy_vitessce/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_easy_vitessce/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_easy_vitessce/docker-compose.yml b/Biomni/mcp_generated/mcp_easy_vitessce/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..8e3b9e245a067a7ff8e1fcfcdd97e572d32b1bec --- /dev/null +++ b/Biomni/mcp_generated/mcp_easy_vitessce/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-easy_vitessce: + build: . + image: mcp-easy_vitessce:latest + container_name: mcp-easy_vitessce + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=easy_vitessce + 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/Biomni/mcp_generated/mcp_easy_vitessce/environment.yaml b/Biomni/mcp_generated/mcp_easy_vitessce/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9503765b82532070903068fd3efe9f1bd5103c56 --- /dev/null +++ b/Biomni/mcp_generated/mcp_easy_vitessce/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - easy_vitessce + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_easy_vitessce/requirements.txt b/Biomni/mcp_generated/mcp_easy_vitessce/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_easy_vitessce/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_fastp/Dockerfile b/Biomni/mcp_generated/mcp_fastp/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..cba5e72b61d72265377e0d4735c382eaa85788f7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_fastp/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 fastp via conda (e.g., from bioconda) +RUN conda install -c bioconda fastp -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/fastp_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/fastp_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/fastp_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_fastp/app/fastp_server.py b/Biomni/mcp_generated/mcp_fastp/app/fastp_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3b1d94d48cf65ac3bfdf9325fab95c52ffc1124f --- /dev/null +++ b/Biomni/mcp_generated/mcp_fastp/app/fastp_server.py @@ -0,0 +1,452 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_fastp' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def fastp_tool( + # Input/Output Files + in1: Path, + out1: Optional[Path] = None, + in2: Optional[Path] = None, + out2: Optional[Path] = None, + unpaired1: Optional[Path] = None, + unpaired2: Optional[Path] = None, + overlapped_out: Optional[Path] = None, + failed_out: Optional[Path] = None, + merged_out: Optional[Path] = None, + adapter_fasta: Optional[Path] = None, + filter_by_index1: Optional[Path] = None, + filter_by_index2: Optional[Path] = None, + json_report: Path = Path("fastp.json"), + html_report: Path = Path("fastp.html"), + + # Boolean Flags + dedup: bool = False, + merge: bool = False, + include_unmerged: bool = False, + phred64: bool = False, + stdin: bool = False, + stdout: bool = False, + interleaved_in: bool = False, + dont_overwrite: bool = False, + dont_eval_duplication: bool = False, + fix_mgi_id: bool = False, + verbose: bool = False, + disable_adapter_trimming: bool = False, + detect_adapter_for_pe: bool = False, + trim_poly_g: bool = False, # Force polyG trimming + disable_trim_poly_g: bool = False, # Disable polyG trimming + trim_poly_x: bool = False, + cut_front: bool = False, + cut_tail: bool = False, + cut_right: bool = False, + disable_quality_filtering: bool = False, + disable_length_filtering: bool = False, + low_complexity_filter: bool = False, + correction: bool = False, + umi: bool = False, + overrepresentation_analysis: bool = False, + + # Integer Parameters + compression: int = 4, + reads_to_process: int = 0, + trim_front1: int = 0, + trim_tail1: int = 0, + max_len1: int = 0, + trim_front2: int = 0, + trim_tail2: int = 0, + max_len2: int = 0, + poly_g_min_len: int = 10, + poly_x_min_len: int = 10, + cut_window_size: int = 4, + cut_mean_quality: int = 20, + cut_front_window_size: int = 4, + cut_front_mean_quality: int = 20, + cut_tail_window_size: int = 4, + cut_tail_mean_quality: int = 20, + cut_right_window_size: int = 4, + cut_right_mean_quality: int = 20, + qualified_quality_phred: int = 15, + unqualified_percent_limit: int = 40, + n_base_limit: int = 5, + average_qual: int = 0, + length_required: int = 15, + length_limit: int = 0, + complexity_threshold: int = 30, + filter_by_index_threshold: int = 0, + overlap_len_require: int = 30, + overlap_diff_limit: int = 5, + overlap_diff_percent_limit: int = 20, + umi_len: int = 0, + umi_skip: int = 0, + overrepresentation_sampling: int = 20, + thread: int = 2, + split: int = 0, + split_by_lines: int = 0, + split_prefix_digits: int = 4, + + # String Parameters + adapter_sequence: str = "auto", + adapter_sequence_r2: str = "auto", + umi_loc: Optional[str] = None, + umi_prefix: Optional[str] = None, + report_title: str = "fastp report", +) -> Dict[str, Any]: + """ + fastp is an all-in-one FASTQ preprocessor (QC/filtering/trimming/correction/merging...). + It performs quality control, adapter trimming, filtering, and other preprocessing steps + for FASTQ files, supporting both single-end and paired-end reads. + """ + cmd = ["fastp"] + output_files: List[Path] = [] + + # --- Input Validation and Command Construction --- + + # Required input file + if not stdin: + if not in1: + raise ValueError("Input file 'in1' must be specified unless '--stdin' is used.") + if not in1.exists(): + raise FileNotFoundError(f"Input file not found: {in1}") + cmd.extend(["--in1", str(in1)]) + else: + cmd.append("--stdin") + + # Mutually exclusive stdin/in1 check + if stdin and in1: + raise ValueError("Cannot use both '--stdin' and '--in1' simultaneously.") + + # Paired-end input + if in2: + if not in2.exists(): + raise FileNotFoundError(f"Input file not found: {in2}") + cmd.extend(["--in2", str(in2)]) + + # Output files + if stdout: + if any([out1, out2, unpaired1, unpaired2, overlapped_out, failed_out, merged_out]): + raise ValueError("Cannot use '--stdout' with specific output file paths.") + cmd.append("--stdout") + else: + if out1: + out1.parent.mkdir(parents=True, exist_ok=True) + cmd.extend(["--out1", str(out1)]) + output_files.append(out1) + if out2: + out2.parent.mkdir(parents=True, exist_ok=True) + cmd.extend(["--out2", str(out2)]) + output_files.append(out2) + + # Conditional output files + if unpaired1: + unpaired1.parent.mkdir(parents=True, exist_ok=True) + cmd.extend(["--unpaired1", str(unpaired1)]) + output_files.append(unpaired1) + if unpaired2: + unpaired2.parent.mkdir(parents=True, exist_ok=True) + cmd.extend(["--unpaired2", str(unpaired2)]) + output_files.append(unpaired2) + if overlapped_out: + overlapped_out.parent.mkdir(parents=True, exist_ok=True) + cmd.extend(["--overlapped_out", str(overlapped_out)]) + output_files.append(overlapped_out) + if failed_out: + failed_out.parent.mkdir(parents=True, exist_ok=True) + cmd.extend(["--failed_out", str(failed_out)]) + output_files.append(failed_out) + + # Merge mode + if merge: + cmd.append("--merge") + if not merged_out: + raise ValueError("When '--merge' is enabled, '--merged_out' must be specified.") + merged_out.parent.mkdir(parents=True, exist_ok=True) + cmd.extend(["--merged_out", str(merged_out)]) + output_files.append(merged_out) + if include_unmerged: + cmd.append("--include_unmerged") + + # Report files + if json_report: + json_report.parent.mkdir(parents=True, exist_ok=True) + cmd.extend(["--json", str(json_report)]) + output_files.append(json_report) + if html_report: + html_report.parent.mkdir(parents=True, exist_ok=True) + cmd.extend(["--html", str(html_report)]) + output_files.append(html_report) + + # --- General Options --- + if dedup: + cmd.append("--dedup") + if phred64: + cmd.append("--phred64") + if interleaved_in: + cmd.append("--interleaved_in") + if reads_to_process < 0: + raise ValueError("'reads_to_process' must be non-negative.") + if reads_to_process > 0: + cmd.extend(["--reads_to_process", str(reads_to_process)]) + if dont_overwrite: + cmd.append("--dont_overwrite") + if dont_eval_duplication: + cmd.append("--dont_eval_duplication") + if fix_mgi_id: + cmd.append("--fix_mgi_id") + if verbose: + cmd.append("--verbose") + + # Compression level + if not (1 <= compression <= 9): + raise ValueError("'compression' level must be between 1 and 9.") + if compression != 4: # Default is 4, only add if different + cmd.extend(["--compression", str(compression)]) + + # --- Adapter Trimming --- + if disable_adapter_trimming: + cmd.append("--disable_adapter_trimming") + if adapter_sequence != "auto": + cmd.extend(["--adapter_sequence", adapter_sequence]) + if adapter_sequence_r2 != "auto": + cmd.extend(["--adapter_sequence_r2", adapter_sequence_r2]) + if adapter_fasta: + if not adapter_fasta.exists(): + raise FileNotFoundError(f"Adapter FASTA file not found: {adapter_fasta}") + cmd.extend(["--adapter_fasta", str(adapter_fasta)]) + if detect_adapter_for_pe: + cmd.append("--detect_adapter_for_pe") + + # --- Trimming by Length/Position --- + if trim_front1 < 0: raise ValueError("'trim_front1' must be non-negative.") + if trim_front1 > 0: cmd.extend(["--trim_front1", str(trim_front1)]) + if trim_tail1 < 0: raise ValueError("'trim_tail1' must be non-negative.") + if trim_tail1 > 0: cmd.extend(["--trim_tail1", str(trim_tail1)]) + if max_len1 < 0: raise ValueError("'max_len1' must be non-negative.") + if max_len1 > 0: cmd.extend(["--max_len1", str(max_len1)]) + + if trim_front2 < 0: raise ValueError("'trim_front2' must be non-negative.") + if trim_front2 > 0: cmd.extend(["--trim_front2", str(trim_front2)]) + if trim_tail2 < 0: raise ValueError("'trim_tail2' must be non-negative.") + if trim_tail2 > 0: cmd.extend(["--trim_tail2", str(trim_tail2)]) + if max_len2 < 0: raise ValueError("'max_len2' must be non-negative.") + if max_len2 > 0: cmd.extend(["--max_len2", str(max_len2)]) + + # PolyG/PolyX trimming + if trim_poly_g and disable_trim_poly_g: + raise ValueError("Cannot use both '--trim_poly_g' and '--disable_trim_poly_g' simultaneously.") + if trim_poly_g: + cmd.append("--trim_poly_g") + if disable_trim_poly_g: + cmd.append("--disable_trim_poly_g") + if not (poly_g_min_len >= 1): # Minimum length for polyG detection + raise ValueError("'poly_g_min_len' must be at least 1.") + if poly_g_min_len != 10: # Default is 10 + cmd.extend(["--poly_g_min_len", str(poly_g_min_len)]) + + if trim_poly_x: + cmd.append("--trim_poly_x") + if not (poly_x_min_len >= 1): # Minimum length for polyX detection + raise ValueError("'poly_x_min_len' must be at least 1.") + if poly_x_min_len != 10: # Default is 10 + cmd.extend(["--poly_x_min_len", str(poly_x_min_len)]) + + # --- Sliding Window Trimming --- + if cut_front: + cmd.append("--cut_front") + if cut_tail: + cmd.append("--cut_tail") + if cut_right: + cmd.append("--cut_right") + + if not (1 <= cut_window_size <= 1000): + raise ValueError("'cut_window_size' must be between 1 and 1000.") + if cut_window_size != 4: + cmd.extend(["--cut_window_size", str(cut_window_size)]) + + if not (1 <= cut_mean_quality <= 36): + raise ValueError("'cut_mean_quality' must be between 1 and 36.") + if cut_mean_quality != 20: + cmd.extend(["--cut_mean_quality", str(cut_mean_quality)]) + + # Specific sliding window options (only add if different from shared defaults) + if cut_front_window_size != cut_window_size: + if not (1 <= cut_front_window_size <= 1000): raise ValueError("'cut_front_window_size' must be between 1 and 1000.") + cmd.extend(["--cut_front_window_size", str(cut_front_window_size)]) + if cut_front_mean_quality != cut_mean_quality: + if not (1 <= cut_front_mean_quality <= 36): raise ValueError("'cut_front_mean_quality' must be between 1 and 36.") + cmd.extend(["--cut_front_mean_quality", str(cut_front_mean_quality)]) + + if cut_tail_window_size != cut_window_size: + if not (1 <= cut_tail_window_size <= 1000): raise ValueError("'cut_tail_window_size' must be between 1 and 1000.") + cmd.extend(["--cut_tail_window_size", str(cut_tail_window_size)]) + if cut_tail_mean_quality != cut_mean_quality: + if not (1 <= cut_tail_mean_quality <= 36): raise ValueError("'cut_tail_mean_quality' must be between 1 and 36.") + cmd.extend(["--cut_tail_mean_quality", str(cut_tail_mean_quality)]) + + if cut_right_window_size != cut_window_size: + if not (1 <= cut_right_window_size <= 1000): raise ValueError("'cut_right_window_size' must be between 1 and 1000.") + cmd.extend(["--cut_right_window_size", str(cut_right_window_size)]) + if cut_right_mean_quality != cut_mean_quality: + if not (1 <= cut_right_mean_quality <= 36): raise ValueError("'cut_right_mean_quality' must be between 1 and 36.") + cmd.extend(["--cut_right_mean_quality", str(cut_right_mean_quality)]) + + # --- Quality Filtering --- + if disable_quality_filtering: + cmd.append("--disable_quality_filtering") + if qualified_quality_phred < 0: raise ValueError("'qualified_quality_phred' must be non-negative.") + if qualified_quality_phred != 15: + cmd.extend(["--qualified_quality_phred", str(qualified_quality_phred)]) + if not (0 <= unqualified_percent_limit <= 100): + raise ValueError("'unqualified_percent_limit' must be between 0 and 100.") + if unqualified_percent_limit != 40: + cmd.extend(["--unqualified_percent_limit", str(unqualified_percent_limit)]) + if n_base_limit < 0: raise ValueError("'n_base_limit' must be non-negative.") + if n_base_limit != 5: + cmd.extend(["--n_base_limit", str(n_base_limit)]) + if average_qual < 0: raise ValueError("'average_qual' must be non-negative.") + if average_qual > 0: # Default 0 means no requirement + cmd.extend(["--average_qual", str(average_qual)]) + + # --- Length Filtering --- + if disable_length_filtering: + cmd.append("--disable_length_filtering") + if length_required < 0: raise ValueError("'length_required' must be non-negative.") + if length_required != 15: + cmd.extend(["--length_required", str(length_required)]) + if length_limit < 0: raise ValueError("'length_limit' must be non-negative.") + if length_limit > 0: # Default 0 means no limitation + cmd.extend(["--length_limit", str(length_limit)]) + + # --- Low Complexity Filter --- + if low_complexity_filter: + cmd.append("--low_complexity_filter") + if not (0 <= complexity_threshold <= 100): + raise ValueError("'complexity_threshold' must be between 0 and 100.") + if complexity_threshold != 30: + cmd.extend(["--complexity_threshold", str(complexity_threshold)]) + + # --- Index Filtering --- + if filter_by_index1: + if not filter_by_index1.exists(): + raise FileNotFoundError(f"Index1 filter file not found: {filter_by_index1}") + cmd.extend(["--filter_by_index1", str(filter_by_index1)]) + if filter_by_index2: + if not filter_by_index2.exists(): + raise FileNotFoundError(f"Index2 filter file not found: {filter_by_index2}") + cmd.extend(["--filter_by_index2", str(filter_by_index2)]) + if filter_by_index_threshold < 0: raise ValueError("'filter_by_index_threshold' must be non-negative.") + if filter_by_index_threshold > 0: # Default 0 means identical + cmd.extend(["--filter_by_index_threshold", str(filter_by_index_threshold)]) + + # --- Overlap Analysis & Correction --- + if correction: + cmd.append("--correction") + if overlap_len_require < 0: raise ValueError("'overlap_len_require' must be non-negative.") + if overlap_len_require != 30: + cmd.extend(["--overlap_len_require", str(overlap_len_require)]) + if overlap_diff_limit < 0: raise ValueError("'overlap_diff_limit' must be non-negative.") + if overlap_diff_limit != 5: + cmd.extend(["--overlap_diff_limit", str(overlap_diff_limit)]) + if not (0 <= overlap_diff_percent_limit <= 100): + raise ValueError("'overlap_diff_percent_limit' must be between 0 and 100.") + if overlap_diff_percent_limit != 20: + cmd.extend(["--overlap_diff_percent_limit", str(overlap_diff_percent_limit)]) + + # --- UMI Preprocessing --- + if umi: + cmd.append("--umi") + if umi_loc: + valid_umi_locs = ["index1", "index2", "read1", "read2", "per_index", "per_read"] + if umi_loc not in valid_umi_locs: + raise ValueError(f"'umi_loc' must be one of {valid_umi_locs}, but got '{umi_loc}'.") + cmd.extend(["--umi_loc", umi_loc]) + if umi_loc in ["read1", "read2"] and umi_len <= 0: + raise ValueError("'umi_len' must be provided and positive if 'umi_loc' is 'read1' or 'read2'.") + if umi_len < 0: raise ValueError("'umi_len' must be non-negative.") + if umi_len > 0: # Default 0 means not specified + cmd.extend(["--umi_len", str(umi_len)]) + if umi_prefix: + cmd.extend(["--umi_prefix", umi_prefix]) + if umi_skip < 0: raise ValueError("'umi_skip' must be non-negative.") + if umi_skip > 0: # Default 0 means no skip + cmd.extend(["--umi_skip", str(umi_skip)]) + elif any([umi_loc, umi_len > 0, umi_prefix, umi_skip > 0]): + raise ValueError("UMI options ('umi_loc', 'umi_len', 'umi_prefix', 'umi_skip') require '--umi' to be enabled.") + + # --- Overrepresentation Analysis --- + if overrepresentation_analysis: + cmd.append("--overrepresentation_analysis") + if not (1 <= overrepresentation_sampling <= 10000): + raise ValueError("'overrepresentation_sampling' must be between 1 and 10000.") + if overrepresentation_sampling != 20: + cmd.extend(["--overrepresentation_sampling", str(overrepresentation_sampling)]) + + # --- Report Title --- + if report_title != "fastp report": + cmd.extend(["--report_title", report_title]) + + # --- Threading --- + if thread < 1: + raise ValueError("'thread' number must be at least 1.") + if thread != 2: # Default is 2 + cmd.extend(["--thread", str(thread)]) + + # --- Splitting Output --- + if split > 0 and split_by_lines > 0: + raise ValueError("Cannot use both '--split' and '--split_by_lines' simultaneously.") + if split > 0: + if not (2 <= split <= 999): + raise ValueError("'split' must be between 2 and 999 (or 0 to disable).") + cmd.extend(["--split", str(split)]) + if split_by_lines > 0: + if not (split_by_lines >= 1000): + raise ValueError("'split_by_lines' must be at least 1000 (or 0 to disable).") + cmd.extend(["--split_by_lines", str(split_by_lines)]) + if split > 0 or split_by_lines > 0: # Only add prefix digits if splitting is enabled + if not (0 <= split_prefix_digits <= 10): + raise ValueError("'split_prefix_digits' must be between 0 and 10.") + if split_prefix_digits != 4: + cmd.extend(["--split_prefix_digits", str(split_prefix_digits)]) + + # --- Execute fastp --- + command_executed = " ".join(map(str, cmd)) # Ensure all parts are strings for join + try: + process = subprocess.run( + cmd, + 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": f"fastp command failed with exit code {e.returncode}", + "output_files": [] + } + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: fastp executable not found. Please ensure it is installed and in your PATH.", + "error": "fastp executable not found", + "output_files": [] + } + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": [str(f) for f in output_files if f.exists()] # Only return existing files + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_fastp/app/fastp_shim_server.py b/Biomni/mcp_generated/mcp_fastp/app/fastp_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..34461e9edb8b4e9ba114e6b5a84a03ede6eb4099 --- /dev/null +++ b/Biomni/mcp_generated/mcp_fastp/app/fastp_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_fastp/app/fastp_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_fastp' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_fastp/app/requirements.txt b/Biomni/mcp_generated/mcp_fastp/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_fastp/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_fastp/docker-compose.yml b/Biomni/mcp_generated/mcp_fastp/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..5b31f6868b9f07adb27a8e73bc4f8ce725d2ac47 --- /dev/null +++ b/Biomni/mcp_generated/mcp_fastp/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-fastp: + build: . + image: mcp-fastp:latest + container_name: mcp-fastp + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=fastp + 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/Biomni/mcp_generated/mcp_fastp/environment.yaml b/Biomni/mcp_generated/mcp_fastp/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7fa6507b607e39e2369bcfe0b4d21a384328914d --- /dev/null +++ b/Biomni/mcp_generated/mcp_fastp/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - fastp + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_fastp/requirements.txt b/Biomni/mcp_generated/mcp_fastp/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_fastp/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_gene-trajectory-python/Dockerfile b/Biomni/mcp_generated/mcp_gene-trajectory-python/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..32942a00b2b3eaaff07033c3392ce0e04240dc8f --- /dev/null +++ b/Biomni/mcp_generated/mcp_gene-trajectory-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 gene-trajectory-python via conda (e.g., from bioconda) +RUN conda install -c bioconda gene-trajectory-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/gene-trajectory-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/gene-trajectory-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/gene-trajectory-python_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_gene-trajectory-python/app/gene-trajectory-python_server.py b/Biomni/mcp_generated/mcp_gene-trajectory-python/app/gene-trajectory-python_server.py new file mode 100644 index 0000000000000000000000000000000000000000..941dc9f37a6486a848671487cca7ddcc493344b3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gene-trajectory-python/app/gene-trajectory-python_server.py @@ -0,0 +1,215 @@ +import subprocess +import tempfile +import sys +from pathlib import Path +from typing import Optional, Dict, List + +# MCP-related imports are intentionally omitted as per the instructions. +# from mcp import tool + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_gene_trajectory_python' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_gene_trajectory( + adata_path: Path, + output_adata_path: Path, + obsm_key: str = 'X_pca', + n_variable_genes: int = 2000, + layer: Optional[str] = None, + n_components: int = 50, + n_neighbors: int = 15, + use_highly_variable: bool = True, + resolution: float = 1.0, + n_clusters: Optional[int] = None, + n_groups: int = 100, + n_pcs: int = 50, + n_diff_maps: int = 15, + t: int = 1, + random_state: int = 0, + output_trajectory_plot: Optional[Path] = None, + trajectory_plot_basis: str = 'X_diffmap_eval', + output_heatmap_plot: Optional[Path] = None, + heatmap_plot_n_genes: int = 10, + output_feature_plot: Optional[Path] = None, + feature_plot_basis: str = 'X_umap', +) -> Dict: + """ + Runs the full GeneTrajectory pipeline on a single-cell dataset. + + This tool identifies trajectories of genes rather than cells by calculating + optimal transport distances between gene distributions across the cell-cell graph. + It takes an AnnData object as input, performs the analysis, and saves the + updated AnnData object with trajectory information. Optional plots can also be generated. + + Args: + adata_path: Path to the input AnnData file (`.h5ad`). + output_adata_path: Path to save the output AnnData file (`.h5ad`) with results. + obsm_key: Key in `adata.obsm` for the PCA representation. + n_variable_genes: Number of highly variable genes to use. + layer: The layer in `adata` to use for computation. If None, `adata.X` is used. + n_components: Number of principal components to compute if `obsm_key` is not present. + n_neighbors: Number of neighbors for the cell-cell graph. + use_highly_variable: Whether to use highly variable genes. + resolution: Resolution for Leiden clustering. + n_clusters: Number of clusters for K-means in coarse-graining. If None, it's determined automatically. + n_groups: Number of groups for coarse-graining. + n_pcs: Number of principal components for the diffusion map. + n_diff_maps: Number of diffusion maps to compute. + t: Time step for the diffusion map. + random_state: Random state for reproducibility. + output_trajectory_plot: Optional path to save the gene trajectory plot as a PNG file. + trajectory_plot_basis: Basis to use for the trajectory plot (e.g., 'X_diffmap_eval'). + output_heatmap_plot: Optional path to save the gene program heatmap as a PNG file. + heatmap_plot_n_genes: Number of genes to display in the heatmap for each program. + output_feature_plot: Optional path to save the gene program feature plot as a PNG file. + feature_plot_basis: Basis to use for the feature plot (e.g., 'X_umap'). + + Returns: + A dictionary containing the executed command, stdout, stderr, and a + dictionary of output file paths. + """ + # 1. Input validation + if not adata_path.is_file(): + raise FileNotFoundError(f"Input AnnData file not found: {adata_path}") + if n_variable_genes <= 0: + raise ValueError("n_variable_genes must be a positive integer.") + if n_components <= 0: + raise ValueError("n_components must be a positive integer.") + if n_neighbors <= 0: + raise ValueError("n_neighbors must be a positive integer.") + if resolution <= 0: + raise ValueError("resolution must be a positive float.") + if n_groups <= 0: + raise ValueError("n_groups must be a positive integer.") + if n_pcs <= 0: + raise ValueError("n_pcs must be a positive integer.") + if n_diff_maps <= 0: + raise ValueError("n_diff_maps must be a positive integer.") + if t <= 0: + raise ValueError("t must be a positive integer.") + + with tempfile.TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + script_path = temp_dir / "run_gene_trajectory_script.py" + + # 2. Generate the Python script to execute the library's functions + # This approach isolates the execution and dependencies from the MCP server. + script_content = f""" +import sys +import warnings +import anndata as ad +import gene_trajectory as gt +import matplotlib.pyplot as plt + +# Suppress common warnings from underlying libraries for cleaner output +warnings.filterwarnings("ignore", category=UserWarning, module='scanpy') +warnings.filterwarnings("ignore", category=FutureWarning) + +try: + # Load input data + adata = ad.read_h5ad(r'{adata_path.resolve()}') + + # Instantiate the GeneTrajectory model + model = gt.GeneTrajectory() + + # Run the full pipeline + adata_result = model.run( + adata, + obsm_key='{obsm_key}', + n_variable_genes={n_variable_genes}, + layer={repr(layer)}, + n_components={n_components}, + n_neighbors={n_neighbors}, + use_highly_variable={use_highly_variable}, + resolution={resolution}, + n_clusters={repr(n_clusters)}, + n_groups={n_groups}, + n_pcs={n_pcs}, + n_diff_maps={n_diff_maps}, + t={t}, + random_state={random_state}, + copy=True + ) + + # Save the resulting AnnData object + adata_result.write_h5ad(r'{output_adata_path.resolve()}') + + # Generate and save optional plots + if {output_trajectory_plot is not None}: + output_traj_plot_path = r'{str(output_trajectory_plot.resolve()) if output_trajectory_plot else ""}' + fig, ax = plt.subplots(figsize=(6, 6)) + model.plot_trajectory(basis='{trajectory_plot_basis}', ax=ax) + fig.savefig(output_traj_plot_path, bbox_inches='tight', dpi=300) + plt.close(fig) + + if {output_heatmap_plot is not None}: + output_heatmap_plot_path = r'{str(output_heatmap_plot.resolve()) if output_heatmap_plot else ""}' + # plot_program_heatmap returns a seaborn ClusterGrid object + grid = model.plot_program_heatmap(n_genes={heatmap_plot_n_genes}) + grid.savefig(output_heatmap_plot_path, bbox_inches='tight', dpi=300) + plt.close() + + if {output_feature_plot is not None}: + output_feature_plot_path = r'{str(output_feature_plot.resolve()) if output_feature_plot else ""}' + # Check if the required basis exists in the AnnData object + if '{feature_plot_basis}' not in adata_result.obsm: + print( + f"Warning: Basis '{feature_plot_basis}' not found in adata.obsm. Skipping feature plot.", + file=sys.stderr + ) + else: + fig, ax = plt.subplots(figsize=(6, 6)) + model.plot_program_feature(basis='{feature_plot_basis}', ax=ax) + fig.savefig(output_feature_plot_path, bbox_inches='tight', dpi=300) + plt.close(fig) + +except Exception as e: + print(f"An error occurred during GeneTrajectory execution: {{e}}", file=sys.stderr) + sys.exit(1) + +print("GeneTrajectory analysis completed successfully.") +""" + script_path.write_text(script_content) + + # 3. Execute the script in a subprocess + command = [sys.executable, str(script_path)] + command_str = " ".join(command) + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + cwd=temp_dir, + ) + except subprocess.CalledProcessError as e: + print(f"Error executing GeneTrajectory script.") + print(f"Return Code: {e.returncode}") + print(f"Stdout: {e.stdout}") + print(f"Stderr: {e.stderr}") + raise + + # 4. Collect output file paths for the structured result + output_files = {"processed_adata": str(output_adata_path)} + if output_trajectory_plot: + output_files["trajectory_plot"] = str(output_trajectory_plot) + if output_heatmap_plot: + output_files["heatmap_plot"] = str(output_heatmap_plot) + if output_feature_plot: + # Only report the file if it was actually created + if output_feature_plot.exists(): + output_files["feature_plot"] = str(output_feature_plot) + + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_gene-trajectory-python/app/gene-trajectory-python_shim_server.py b/Biomni/mcp_generated/mcp_gene-trajectory-python/app/gene-trajectory-python_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..45ceeae6208e8cee874acce01f5069374b065cfc --- /dev/null +++ b/Biomni/mcp_generated/mcp_gene-trajectory-python/app/gene-trajectory-python_shim_server.py @@ -0,0 +1,55 @@ +#!/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_gene-trajectory-python/app/gene-trajectory-python_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_gene_trajectory_python' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_gene-trajectory-python/app/requirements.txt b/Biomni/mcp_generated/mcp_gene-trajectory-python/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_gene-trajectory-python/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_gene-trajectory-python/docker-compose.yml b/Biomni/mcp_generated/mcp_gene-trajectory-python/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b68133a781ac89b833e4be98d3c9a9cd38515c96 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gene-trajectory-python/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-gene-trajectory-python: + build: . + image: mcp-gene-trajectory-python:latest + container_name: mcp-gene-trajectory-python + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=gene-trajectory-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/Biomni/mcp_generated/mcp_gene-trajectory-python/environment.yaml b/Biomni/mcp_generated/mcp_gene-trajectory-python/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5cb770853c3173ebb88d12829eac41c5511c2817 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gene-trajectory-python/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - gene-trajectory-python + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_gene-trajectory-python/requirements.txt b/Biomni/mcp_generated/mcp_gene-trajectory-python/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gene-trajectory-python/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_gneiss/Dockerfile b/Biomni/mcp_generated/mcp_gneiss/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..645610c98d6e90ef28cfd2502d1cbffb49f64d27 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gneiss/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 gneiss via conda (e.g., from bioconda) +RUN conda install -c bioconda gneiss -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/gneiss_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/gneiss_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/gneiss_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_gneiss/app/gneiss_server.py b/Biomni/mcp_generated/mcp_gneiss/app/gneiss_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0c2130595e6aa780044d1b54361f57cf016f0e78 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gneiss/app/gneiss_server.py @@ -0,0 +1,335 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List + +# It is assumed that 'mcp' is a pre-defined decorator. +# For standalone testing, you could use a mock decorator: +# +# class mcp: +# @staticmethod +# def tool(): +# def decorator(func): +# return func +# return decorator + +logging.basicConfig(level=logging.INFO) + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_gneiss' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def correlation_clustering( + in_biom: Path, + output_tree: Path, + correlation_method: str = 'spearman', + clustering_method: str = 'ward' +) -> dict: + """ + Performs hierarchical clustering on features using their correlation. + + This is useful for creating a hierarchy of features based on their co-occurrence + patterns across samples. + """ + if not in_biom.exists(): + raise FileNotFoundError(f"Input biom table not found: {in_biom}") + + # Ensure output directory exists + output_tree.parent.mkdir(parents=True, exist_ok=True) + + cmd = [ + "gneiss", "correlation-clustering", + "--in-biom", str(in_biom), + "--output-tree", str(output_tree), + "--correlation-method", correlation_method, + "--clustering-method", clustering_method + ] + + command_str = " ".join(cmd) + logging.info(f"Executing command: {command_str}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_tree)] + } + except subprocess.CalledProcessError as e: + logging.error(f"Gneiss correlation-clustering failed: {e.stderr}") + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +@mcp.tool() +def dendrogram_heatmap( + in_biom: Path, + in_tree: Path, + in_meta: Path, + category: str, + output_viz: Path, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + cmap: str = 'YlGnBu' +) -> dict: + """ + Builds a heatmap visualization of a biom table, ordered by a dendrogram. + + This tool is useful for visualizing the relationship between feature balances + and sample metadata categories. + """ + for input_file in [in_biom, in_tree, in_meta]: + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + if not category: + raise ValueError("Metadata category must be a non-empty string.") + + # Ensure output directory exists + output_viz.parent.mkdir(parents=True, exist_ok=True) + + cmd = [ + "gneiss", "dendrogram-heatmap", + "--in-biom", str(in_biom), + "--in-tree", str(in_tree), + "--in-meta", str(in_meta), + "--category", category, + "--output-viz", str(output_viz), + "--cmap", cmap + ] + + if vmin is not None: + cmd.extend(["--vmin", str(vmin)]) + if vmax is not None: + cmd.extend(["--vmax", str(vmax)]) + + command_str = " ".join(cmd) + logging.info(f"Executing command: {command_str}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_viz)] + } + except subprocess.CalledProcessError as e: + logging.error(f"Gneiss dendrogram-heatmap failed: {e.stderr}") + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +@mcp.tool() +def ilr_hierarchical( + in_biom: Path, + in_tree: Path, + output_biom: Path +) -> dict: + """ + Performs isometric log-ratio (ILR) transformation on a table using a hierarchical tree. + + This transforms compositional data into a real-valued space of balances, + which is suitable for standard statistical analyses. + """ + for input_file in [in_biom, in_tree]: + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + # Ensure output directory exists + output_biom.parent.mkdir(parents=True, exist_ok=True) + + cmd = [ + "gneiss", "ilr-hierarchical", + "--in-biom", str(in_biom), + "--in-tree", str(in_tree), + "--output-biom", str(output_biom) + ] + + command_str = " ".join(cmd) + logging.info(f"Executing command: {command_str}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_biom)] + } + except subprocess.CalledProcessError as e: + logging.error(f"Gneiss ilr-hierarchical failed: {e.stderr}") + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +@mcp.tool() +def ilr_phylogenetic( + in_biom: Path, + in_tree: Path, + output_biom: Path +) -> dict: + """ + Performs isometric log-ratio (ILR) transformation on a table using a phylogenetic tree. + + This is similar to ilr-hierarchical but specifically uses a phylogenetic tree + to define the balances, incorporating evolutionary relationships. + """ + for input_file in [in_biom, in_tree]: + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + # Ensure output directory exists + output_biom.parent.mkdir(parents=True, exist_ok=True) + + cmd = [ + "gneiss", "ilr-phylogenetic", + "--in-biom", str(in_biom), + "--in-tree", str(in_tree), + "--output-biom", str(output_biom) + ] + + command_str = " ".join(cmd) + logging.info(f"Executing command: {command_str}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_biom)] + } + except subprocess.CalledProcessError as e: + logging.error(f"Gneiss ilr-phylogenetic failed: {e.stderr}") + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +@mcp.tool() +def lme_regression( + in_biom: Path, + in_meta: Path, + formula: str, + groups: str, + output_dir: Path +) -> dict: + """ + Performs Linear Mixed Effects (LME) regression on balances. + + This models the relationship between balances and metadata variables while + accounting for random effects, such as repeated measures from subjects. + """ + for input_file in [in_biom, in_meta]: + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + if not formula: + raise ValueError("Formula must be a non-empty string.") + if not groups: + raise ValueError("Groups must be a non-empty string.") + + # Ensure output directory exists + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "gneiss", "lme-regression", + "--in-biom", str(in_biom), + "--in-meta", str(in_meta), + "--formula", formula, + "--groups", groups, + "--output-dir", str(output_dir) + ] + + command_str = " ".join(cmd) + logging.info(f"Executing command: {command_str}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + # Gneiss regression outputs multiple files, so we return the directory + output_files = [str(p) for p in output_dir.glob('*')] + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + except subprocess.CalledProcessError as e: + logging.error(f"Gneiss lme-regression failed: {e.stderr}") + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +@mcp.tool() +def ols_regression( + in_biom: Path, + in_meta: Path, + formula: str, + output_dir: Path +) -> dict: + """ + Performs Ordinary Least Squares (OLS) regression on balances. + + This models the linear relationship between balances (response variables) + and metadata variables (predictors). + """ + for input_file in [in_biom, in_meta]: + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + if not formula: + raise ValueError("Formula must be a non-empty string.") + + # Ensure output directory exists + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "gneiss", "ols-regression", + "--in-biom", str(in_biom), + "--in-meta", str(in_meta), + "--formula", formula, + "--output-dir", str(output_dir) + ] + + command_str = " ".join(cmd) + logging.info(f"Executing command: {command_str}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + # Gneiss regression outputs multiple files, so we return the directory + output_files = [str(p) for p in output_dir.glob('*')] + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + except subprocess.CalledProcessError as e: + logging.error(f"Gneiss ols-regression failed: {e.stderr}") + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_gneiss/app/gneiss_shim_server.py b/Biomni/mcp_generated/mcp_gneiss/app/gneiss_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..879718bc6ab4b36f49d6db85bd936ec6661d8d9b --- /dev/null +++ b/Biomni/mcp_generated/mcp_gneiss/app/gneiss_shim_server.py @@ -0,0 +1,55 @@ +#!/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_gneiss/app/gneiss_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_gneiss' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_gneiss/app/requirements.txt b/Biomni/mcp_generated/mcp_gneiss/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_gneiss/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_gneiss/docker-compose.yml b/Biomni/mcp_generated/mcp_gneiss/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..8188b23a6e770bcf257b16fd7c0ac7d7841a2500 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gneiss/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-gneiss: + build: . + image: mcp-gneiss:latest + container_name: mcp-gneiss + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=gneiss + 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/Biomni/mcp_generated/mcp_gneiss/environment.yaml b/Biomni/mcp_generated/mcp_gneiss/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..61580b9529e067472a95fde20dfcd5e18fbe7fff --- /dev/null +++ b/Biomni/mcp_generated/mcp_gneiss/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - gneiss + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_gneiss/requirements.txt b/Biomni/mcp_generated/mcp_gneiss/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gneiss/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_goleft/Dockerfile b/Biomni/mcp_generated/mcp_goleft/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..654ab93c8fe816b5153cf4d183021d0447c40e95 --- /dev/null +++ b/Biomni/mcp_generated/mcp_goleft/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 goleft via conda (e.g., from bioconda) +RUN conda install -c bioconda goleft -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/goleft_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/goleft_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/goleft_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_goleft/app/goleft_server.py b/Biomni/mcp_generated/mcp_goleft/app/goleft_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e4a64b8fe79983ac513f7e73fd368007612b1666 --- /dev/null +++ b/Biomni/mcp_generated/mcp_goleft/app/goleft_server.py @@ -0,0 +1,435 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# NOTE: The @mcp.tool decorator is a placeholder. +# In a real MCP environment, it would be provided by the MCP framework. +class mcp: + @staticmethod + def tool(func): + return func + +@mcp.tool +def goleft_covstats( + bams: List[Path], + denominator: str = "target-size", + n_reads: int = 25000000, + procs: int = 4, + reference: Optional[Path] = None, + target_bed: Optional[Path] = None, +): + """ + Estimate coverage and insert-size statistics on BAM/CRAM files by sampling. + + Args: + bams: List of input BAM/CRAM files. + denominator: Calculate coverage as reads / denominator. Must be 'target-size' or 'genome-size'. + n_reads: Number of reads to sample from each BAM. + procs: Number of processors to use. + reference: Optional reference FASTA for CRAM files. + target_bed: Optional BED file of target regions. + """ + # Input validation + if not bams: + raise ValueError("At least one BAM/CRAM file must be provided.") + for bam in bams: + if not bam.is_file(): + raise FileNotFoundError(f"Input file not found: {bam}") + if denominator not in ["target-size", "genome-size"]: + raise ValueError("Denominator must be one of 'target-size' or 'genome-size'.") + if n_reads <= 0: + raise ValueError("n_reads must be a positive integer.") + if procs <= 0: + raise ValueError("procs must be a positive integer.") + if reference and not reference.is_file(): + raise FileNotFoundError(f"Reference file not found: {reference}") + if target_bed and not target_bed.is_file(): + raise FileNotFoundError(f"Target BED file not found: {target_bed}") + + cmd = [ + "goleft", "covstats", + "--denominator", denominator, + "--nreads", str(n_reads), + "--procs", str(procs), + ] + + if reference: + cmd.extend(["--reference", str(reference)]) + if target_bed: + cmd.extend(["--targetbed", str(target_bed)]) + + cmd.extend([str(p) for p in bams]) + + 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": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"goleft covstats failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool +def goleft_depth( + bams: List[Path], + fai: Path, + output_dir: Path, + bed: Optional[Path] = None, + gaps: int = 2000, + procs: int = 4, + map_quality: int = 10, + base_quality: int = 10, + reference: Optional[Path] = None, + window_size: int = 5000, + min_cov: int = 1, + s_flags: int = 1796, + f_flags: int = 0, +): + """ + Parallelize calls to samtools depth in user-defined windows. + + Args: + bams: List of input BAM/CRAM files. + fai: FAI file of the reference genome. + output_dir: Directory to write depth files. + bed: Optional BED file of regions to process. + gaps: Number of bases to pad between windows for discordant pairs. + procs: Number of processors to use. + map_quality: Only count reads with mapping quality of at least this value. + base_quality: Only count bases with base quality of at least this value. + reference: Optional reference FASTA for CRAM files. + window_size: Window size for coverage depth reporting. + min_cov: Only report windows with coverage at least this value. + s_flags: Samtools flags to skip reads (e.g., 1796 for QCFAIL, DUP, SECONDARY, SUPPLEMENTARY). + f_flags: Samtools flags to filter reads (e.g., 2 to count only proper pairs). + """ + # Input validation + if not bams: + raise ValueError("At least one BAM/CRAM file must be provided.") + for bam in bams: + if not bam.is_file(): + raise FileNotFoundError(f"Input file not found: {bam}") + if not fai.is_file(): + raise FileNotFoundError(f"FAI file not found: {fai}") + if bed and not bed.is_file(): + raise FileNotFoundError(f"BED file not found: {bed}") + if reference and not reference.is_file(): + raise FileNotFoundError(f"Reference file not found: {reference}") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "goleft", "depth", + "--fai", str(fai), + "--dir", str(output_dir), + "--gaps", str(gaps), + "--procs", str(procs), + "-q", str(map_quality), + "-Q", str(base_quality), + "--windowsize", str(window_size), + "--mincov", str(min_cov), + "--sflags", str(s_flags), + "--fflags", str(f_flags), + ] + + if bed: + cmd.extend(["--bed", str(bed)]) + if reference: + cmd.extend(["--reference", str(reference)]) + + cmd.extend([str(p) for p in bams]) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + output_files = [output_dir / f"{bam.stem}.depth.bed.gz" for bam in bams] + final_output_files = [str(f) for f in output_files if f.exists()] + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": final_output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"goleft depth failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool +def goleft_depthwed( + depth_files: List[Path], + bed: Path, + prefix: str, + output_dir: Optional[Path] = None, +): + """ + Matricize output from 'goleft depth' to n-sites * n-samples. + + Args: + depth_files: List of depth files generated by 'goleft depth'. + bed: BED file of regions used to create the depth files. + prefix: Prefix for output files (e.g., 'my_cohort'). + output_dir: Directory to write output files. Defaults to a temporary directory. + """ + # Input validation + if not depth_files: + raise ValueError("At least one depth file must be provided.") + for df in depth_files: + if not df.is_file(): + raise FileNotFoundError(f"Input depth file not found: {df}") + if not bed.is_file(): + raise FileNotFoundError(f"BED file not found: {bed}") + if not prefix.strip(): + raise ValueError("Prefix cannot be empty.") + + with tempfile.TemporaryDirectory() as temp_dir: + work_dir = output_dir or Path(temp_dir) + work_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "goleft", "depthwed", + "--bed", str(bed), + "--prefix", prefix, + ] + cmd.extend([str(p) for p in depth_files]) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + cwd=work_dir, + ) + + expected_files = [ + work_dir / f"{prefix}.depth.tsv", + work_dir / f"{prefix}.sites.bed.gz", + work_dir / f"{prefix}.samples.tsv" + ] + output_files = [str(f) for f in expected_files if f.exists()] + + 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": f"goleft depthwed failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool +def goleft_indexcov( + bams: List[Path], + fai: Path, + output_dir: Path, + procs: int = 4, + sex_file: Optional[Path] = None, + window_size: str = "16K", +): + """ + Quick coverage estimate using only the BAM/CRAM index. + + Args: + bams: List of input BAM/CRAM files (and their .bai/.crai indexes). + fai: FAI file of the reference genome. + output_dir: Directory to write output files (HTML report, etc.). + procs: Number of processors to use. + sex_file: Optional file containing sample\tsex mapping. + window_size: Window size for coverage depth reporting (e.g., '16K', '1M'). + """ + # Input validation + if not bams: + raise ValueError("At least one BAM/CRAM file must be provided.") + for bam in bams: + if not bam.is_file(): + raise FileNotFoundError(f"Input file not found: {bam}") + index_bai = bam.with_suffix(bam.suffix + ".bai") + index_crai = bam.with_suffix(".crai") + if not (index_bai.exists() or index_crai.exists()): + raise FileNotFoundError(f"Index file (.bai or .crai) not found for {bam}") + if not fai.is_file(): + raise FileNotFoundError(f"FAI file not found: {fai}") + if sex_file and not sex_file.is_file(): + raise FileNotFoundError(f"Sex file not found: {sex_file}") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "goleft", "indexcov", + "--fai", str(fai), + "--dir", str(output_dir), + "--procs", str(procs), + "--windowsize", window_size, + ] + + if sex_file: + cmd.extend(["--sex", str(sex_file)]) + + cmd.extend([str(p) for p in bams]) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + html_report = output_dir / "indexcov.html" + output_files = [str(p) for p in output_dir.glob('**/*') if p.is_file()] + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + "main_report": str(html_report) if html_report.exists() else None + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"goleft indexcov failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool +def goleft_indexsplit( + bams: List[Path], + fai: Path, + n_chunks: int = 100, + output_file: Optional[Path] = None, + procs: int = 4, +): + """ + Generate regions of even data across a cohort for parallelization. + + Args: + bams: List of input BAM/CRAM files. + fai: FAI file of the reference genome. + n_chunks: Number of chunks to create. + output_file: File to write regions to. If None, writes to stdout. + procs: Number of processors to use. + """ + # Input validation + if not bams: + raise ValueError("At least one BAM/CRAM file must be provided.") + for bam in bams: + if not bam.is_file(): + raise FileNotFoundError(f"Input file not found: {bam}") + if not fai.is_file(): + raise FileNotFoundError(f"FAI file not found: {fai}") + if n_chunks <= 0: + raise ValueError("n_chunks must be a positive integer.") + if procs <= 0: + raise ValueError("procs must be a positive integer.") + + cmd = [ + "goleft", "indexsplit", + "--fai", str(fai), + "-N", str(n_chunks), + "--procs", str(procs), + ] + + output_files_list = [] + if output_file: + cmd.extend(["--out", str(output_file)]) + output_files_list.append(str(output_file)) + + cmd.extend([str(p) for p in bams]) + + 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_files_list, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"goleft indexsplit failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool +def goleft_samplename( + bams: List[Path], +): + """ + Report samplename(s) from a BAM/CRAM file's SM tag in the header. + + Args: + bams: List of input BAM/CRAM files. + """ + # Input validation + if not bams: + raise ValueError("At least one BAM/CRAM file must be provided.") + for bam in bams: + if not bam.is_file(): + raise FileNotFoundError(f"Input file not found: {bam}") + + cmd = ["goleft", "samplename"] + cmd.extend([str(p) for p in bams]) + + 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": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"goleft samplename failed with exit code {e.returncode}", + "output_files": [], + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_goleft/app/goleft_shim_server.py b/Biomni/mcp_generated/mcp_goleft/app/goleft_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..bd6e062c19067fff91869e360d26f2c5411f9ba3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_goleft/app/goleft_shim_server.py @@ -0,0 +1,55 @@ +#!/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_goleft/app/goleft_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_goleft' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_goleft/app/requirements.txt b/Biomni/mcp_generated/mcp_goleft/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_goleft/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_goleft/docker-compose.yml b/Biomni/mcp_generated/mcp_goleft/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..be5348e342deeffe41b34a8051fd7f83e9ac76d3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_goleft/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-goleft: + build: . + image: mcp-goleft:latest + container_name: mcp-goleft + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=goleft + 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/Biomni/mcp_generated/mcp_goleft/environment.yaml b/Biomni/mcp_generated/mcp_goleft/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fc842e11ccefeccf5dc031cbffa31a8e6486189e --- /dev/null +++ b/Biomni/mcp_generated/mcp_goleft/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - goleft + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_goleft/requirements.txt b/Biomni/mcp_generated/mcp_goleft/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_goleft/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_gromacs/Dockerfile b/Biomni/mcp_generated/mcp_gromacs/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6dddfd57f06bcbce0153d4eafc7cf08b91ad7817 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gromacs/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 gromacs via conda (e.g., from bioconda) +RUN conda install -c bioconda gromacs -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/gromacs_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/gromacs_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/gromacs_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_gromacs/app/gromacs_server.py b/Biomni/mcp_generated/mcp_gromacs/app/gromacs_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2b697c405df96ee73aea3d259529313e6a06157e --- /dev/null +++ b/Biomni/mcp_generated/mcp_gromacs/app/gromacs_server.py @@ -0,0 +1,511 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_gromacs' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def gmx_pdb2gmx( + f: str, + o: str = "processed.gro", + p: str = "topol.top", + i: str = "posre.itp", + ff: str = "oplsaa", + water: str = "spce", + ignh: bool = False, + missing: bool = False, +): + """ + Converts PDB files to GROMACS coordinate and topology files. + + Args: + f: Input structure file (.pdb, .gro, etc.) + o: Output structure file + p: Output topology file + i: Output position restraint file + ff: Force field to use (e.g., 'oplsaa', 'amber99sb-ildn') + water: Water model (e.g., 'spce', 'tip3p') + ignh: Ignore hydrogen atoms in the PDB file + missing: Allow missing atoms + """ + input_path = Path(f) + if not input_path.exists(): + return {"error": f"Input file {f} not found"} + + cmd = [ + "gmx", "pdb2gmx", + "-f", str(input_path), + "-o", o, + "-p", p, + "-i", i, + "-ff", ff, + "-water", water + ] + + if ignh: + cmd.append("-ignh") + if missing: + cmd.append("-missing") + + 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": [o, p, i] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def gmx_editconf( + f: str, + o: str = "box.gro", + c: bool = True, + d: float = 1.0, + bt: str = "cubic", + box: Optional[str] = None, + princ: bool = False, +): + """ + Edits the simulation box and centers the system. + + Args: + f: Input structure file + o: Output structure file + c: Center molecule in box + d: Distance between the solute and the box edge (nm) + bt: Box type: 'cubic', 'triclinic', 'magical', 'octahedron', 'dodecahedron' + box: Box vector lengths (x, y, z) as a string + princ: Orient molecule along principal axes + """ + input_path = Path(f) + if not input_path.exists(): + return {"error": f"Input file {f} not found"} + + cmd = ["gmx", "editconf", "-f", str(input_path), "-o", o, "-bt", bt] + + if c: + cmd.append("-c") + if d > 0: + cmd.extend(["-d", str(d)]) + if box: + cmd.extend(["-box", box]) + if princ: + cmd.append("-princ") + + 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": [o] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def gmx_solvate( + cp: str, + cs: str = "spc216.gro", + p: str = "topol.top", + o: str = "solvated.gro", + maxsol: int = 0, +): + """ + Solvates a system by adding water molecules. + + Args: + cp: Input structure file (solute) + cs: Solvent structure file + p: Topology file (will be updated) + o: Output structure file + maxsol: Maximum number of solvent molecules to add (0 for all) + """ + cp_path = Path(cp) + p_path = Path(p) + if not cp_path.exists(): + return {"error": f"Solute file {cp} not found"} + if not p_path.exists(): + return {"error": f"Topology file {p} not found"} + + cmd = [ + "gmx", "solvate", + "-cp", str(cp_path), + "-cs", cs, + "-p", str(p_path), + "-o", o + ] + + if maxsol > 0: + cmd.extend(["-maxsol", str(maxsol)]) + + 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": [o, p] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def gmx_grompp( + f: str, + c: str, + p: str, + o: str = "topol.tpr", + r: Optional[str] = None, + n: Optional[str] = None, + maxwarn: int = 0, +): + """ + The GROMACS preprocessor: generates a run input file (.tpr). + + Args: + f: Input MD parameter file (.mdp) + c: Input structure file (.gro) + p: Input topology file (.top) + o: Output run input file (.tpr) + r: Structure file for restraints + n: Index file (.ndx) + maxwarn: Number of allowed warnings + """ + f_path = Path(f) + c_path = Path(c) + p_path = Path(p) + + for path in [f_path, c_path, p_path]: + if not path.exists(): + return {"error": f"Required file {path} not found"} + + cmd = [ + "gmx", "grompp", + "-f", str(f_path), + "-c", str(c_path), + "-p", str(p_path), + "-o", o, + "-maxwarn", str(maxwarn) + ] + + if r: + cmd.extend(["-r", r]) + if n: + cmd.extend(["-n", n]) + + 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": [o] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def gmx_genion( + s: str, + p: str, + o: str = "solvated_ions.gro", + pname: str = "NA", + nname: str = "CL", + neutral: bool = True, + conc: float = 0.0, + selection: str = "SOL", +): + """ + Adds ions to a solvated system to neutralize it or reach a concentration. + + Args: + s: Input run input file (.tpr) + p: Topology file (will be updated) + o: Output structure file + pname: Name of the positive ion + nname: Name of the negative ion + neutral: Add ions to neutralize the system + conc: Specify salt concentration (mol/liter) + selection: Group to replace with ions (usually 'SOL' or '13') + """ + s_path = Path(s) + p_path = Path(p) + if not s_path.exists(): + return {"error": f"TPR file {s} not found"} + + cmd = [ + "gmx", "genion", + "-s", str(s_path), + "-p", str(p_path), + "-o", o, + "-pname", pname, + "-nname", nname + ] + + if neutral: + cmd.append("-neutral") + if conc > 0: + cmd.extend(["-conc", str(conc)]) + + try: + # genion requires interactive selection of the solvent group + result = subprocess.run( + cmd, + input=selection, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [o, p] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def gmx_mdrun( + s: str = "topol.tpr", + deffnm: str = "md", + v: bool = True, + nt: int = 0, + nb: str = "auto", + pme: str = "auto", + gpu_id: Optional[str] = None, +): + """ + Executes the molecular dynamics simulation. + + Args: + s: Input run input file (.tpr) + deffnm: Default filename for all outputs + v: Verbose output + nt: Number of threads to use (0 for auto) + nb: Calculate non-bonded interactions on: 'auto', 'cpu', 'gpu' + pme: Calculate PME on: 'auto', 'cpu', 'gpu' + gpu_id: List of GPU device IDs to use + """ + s_path = Path(s) + if not s_path.exists(): + return {"error": f"TPR file {s} not found"} + + cmd = [ + "gmx", "mdrun", + "-s", str(s_path), + "-deffnm", deffnm, + "-nb", nb, + "-pme", pme + ] + + if v: + cmd.append("-v") + if nt > 0: + cmd.extend(["-nt", str(nt)]) + if gpu_id: + cmd.extend(["-gpu_id", gpu_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": [f"{deffnm}.log", f"{deffnm}.xtc", f"{deffnm}.edr", f"{deffnm}.gro"] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def gmx_energy( + f: str = "ener.edr", + o: str = "energy.xvg", + selection: str = "Potential", +): + """ + Extracts energy terms from an energy file (.edr). + + Args: + f: Input energy file + o: Output graph file (.xvg) + selection: Energy term to extract (e.g., 'Potential', 'Temperature', 'Pressure') + """ + f_path = Path(f) + if not f_path.exists(): + return {"error": f"Energy file {f} not found"} + + cmd = ["gmx", "energy", "-f", str(f_path), "-o", o] + + try: + # selection is piped to stdin (e.g., "Potential 0" to select Potential and end) + input_str = f"{selection}\n0\n" + result = subprocess.run( + cmd, + input=input_str, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [o] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def gmx_rms( + s: str, + f: str, + o: str = "rms.xvg", + selection: str = "Backbone Backbone", +): + """ + Calculates Root Mean Square Deviation (RMSD). + + Args: + s: Reference structure file (.tpr, .gro) + f: Trajectory file (.xtc, .trr) + o: Output graph file (.xvg) + selection: Groups for fitting and calculation (e.g., 'Backbone Backbone') + """ + s_path = Path(s) + f_path = Path(f) + if not s_path.exists() or not f_path.exists(): + return {"error": "Input files not found"} + + cmd = ["gmx", "rms", "-s", str(s_path), "-f", str(f_path), "-o", o] + + try: + # selection usually requires two groups: one for fitting, one for calculation + result = subprocess.run( + cmd, + input=selection, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [o] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def gmx_trjconv( + f: str, + s: str, + o: str = "output.xtc", + pbc: str = "mol", + center: bool = True, + ur: str = "compact", + selection: str = "System", +): + """ + Converts and manipulates trajectory files (PBC treatment, centering). + + Args: + f: Input trajectory file + s: Input structure/tpr file + o: Output trajectory/structure file + pbc: PBC treatment: 'none', 'mol', 'res', 'atom', 'nojump', 'cluster' + center: Center the system + ur: Unit cell representation: 'rect', 'tric', 'compact' + selection: Group to write to output + """ + f_path = Path(f) + s_path = Path(s) + if not f_path.exists() or not s_path.exists(): + return {"error": "Input files not found"} + + cmd = [ + "gmx", "trjconv", + "-f", str(f_path), + "-s", str(s_path), + "-o", o, + "-pbc", pbc, + "-ur", ur + ] + + if center: + cmd.append("-center") + + try: + # trjconv often needs selection for centering and for output + input_str = f"{selection}\n{selection}\n" if center else f"{selection}\n" + result = subprocess.run( + cmd, + input=input_str, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [o] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_gromacs/app/gromacs_shim_server.py b/Biomni/mcp_generated/mcp_gromacs/app/gromacs_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c65fc1afa09348e1cf9caec8d7f4485e0cdab1b1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gromacs/app/gromacs_shim_server.py @@ -0,0 +1,55 @@ +#!/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_gromacs/app/gromacs_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_gromacs' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_gromacs/app/requirements.txt b/Biomni/mcp_generated/mcp_gromacs/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_gromacs/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_gromacs/docker-compose.yml b/Biomni/mcp_generated/mcp_gromacs/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c75b9cbb5170acf7f24af3b2a63598fc518eddce --- /dev/null +++ b/Biomni/mcp_generated/mcp_gromacs/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-gromacs: + build: . + image: mcp-gromacs:latest + container_name: mcp-gromacs + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=gromacs + 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/Biomni/mcp_generated/mcp_gromacs/environment.yaml b/Biomni/mcp_generated/mcp_gromacs/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..12eba8048e391284b3123e62844fe8b04ae88e91 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gromacs/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - gromacs + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_gromacs/requirements.txt b/Biomni/mcp_generated/mcp_gromacs/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gromacs/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_harpy/Dockerfile b/Biomni/mcp_generated/mcp_harpy/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2edda10ca7d458317d95614e371bbcc8d4c139b1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_harpy/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 harpy via conda (e.g., from bioconda) +RUN conda install -c bioconda harpy -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/harpy_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/harpy_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/harpy_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_harpy/app/harpy_server.py b/Biomni/mcp_generated/mcp_harpy/app/harpy_server.py new file mode 100644 index 0000000000000000000000000000000000000000..5852c6584072056c08d0ffea0e03694795ac363c --- /dev/null +++ b/Biomni/mcp_generated/mcp_harpy/app/harpy_server.py @@ -0,0 +1,861 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, List + +# MCP-compliant tool definitions for harpy + +# --- Data Processing Subcommands --- + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_harpy' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def align( + reads_r1: Path, + reads_r2: Path, + reference: Path, + sample: str, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Aligns sample sequences to a reference genome assembly using Harpy. + + This function wraps the 'harpy align' command. It takes paired-end reads, + a reference genome, and a sample name to produce alignment files. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not reads_r1.is_file(): + raise FileNotFoundError(f"Input read file R1 not found: {reads_r1}") + if not reads_r2.is_file(): + raise FileNotFoundError(f"Input read file R2 not found: {reads_r2}") + if not reference.is_file(): + raise FileNotFoundError(f"Reference genome file not found: {reference}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "align", + "--reads1", str(reads_r1), + "--reads2", str(reads_r2), + "--reference", str(reference), + "--sample", sample, + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + # Assuming the primary output is a BAM file named after the sample + output_bam = output_dir / f"{sample}.bam" + output_files = [str(output_bam)] if output_bam.exists() 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": "Harpy align command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def assemble( + reads_r1: Path, + reads_r2: Path, + sample: str, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Creates a genome assembly from linked-reads using Harpy. + + This function wraps the 'harpy assemble' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not reads_r1.is_file(): + raise FileNotFoundError(f"Input read file R1 not found: {reads_r1}") + if not reads_r2.is_file(): + raise FileNotFoundError(f"Input read file R2 not found: {reads_r2}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "assemble", + "--reads1", str(reads_r1), + "--reads2", str(reads_r2), + "--sample", sample, + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + # Assuming the primary output is a FASTA file + output_fasta = output_dir / f"{sample}_assembly.fasta" + output_files = [str(output_fasta)] if output_fasta.exists() 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": "Harpy assemble command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def deconvolve( + bam: Path, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Resolves barcode sharing in unrelated molecules from a BAM file. + + This function wraps the 'harpy deconvolve' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "deconvolve", + "--bam", str(bam), + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + # Assuming output is a new BAM file + output_bam = output_dir / f"{bam.stem}.deconvolved.bam" + output_files = [str(output_bam)] if output_bam.exists() 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": "Harpy deconvolve command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def demultiplex( + reads_r1: Path, + reads_r2: Path, + barcodes: Path, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Demultiplexes haplotagged FASTQ files using Harpy. + + This function wraps the 'harpy demultiplex' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not reads_r1.is_file(): + raise FileNotFoundError(f"Input read file R1 not found: {reads_r1}") + if not reads_r2.is_file(): + raise FileNotFoundError(f"Input read file R2 not found: {reads_r2}") + if not barcodes.is_file(): + raise FileNotFoundError(f"Barcodes file not found: {barcodes}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "demultiplex", + "--reads1", str(reads_r1), + "--reads2", str(reads_r2), + "--barcodes", str(barcodes), + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + # Output is likely a set of demultiplexed FASTQ files + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(p) for p in output_dir.glob("*.fastq.gz")], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Harpy demultiplex command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def impute( + vcf: Path, + bam: Path, + reference: Path, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Imputes variant genotypes from alignments using Harpy. + + This function wraps the 'harpy impute' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not vcf.is_file(): + raise FileNotFoundError(f"Input VCF file not found: {vcf}") + if not bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam}") + if not reference.is_file(): + raise FileNotFoundError(f"Reference genome file not found: {reference}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "impute", + "--vcf", str(vcf), + "--bam", str(bam), + "--reference", str(reference), + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + output_vcf = output_dir / f"{vcf.stem}.imputed.vcf.gz" + output_files = [str(output_vcf)] if output_vcf.exists() 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": "Harpy impute command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def metassembly( + reads_r1: Path, + reads_r2: Path, + sample: str, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Creates a metagenome assembly from linked-reads using Harpy. + + This function wraps the 'harpy metassembly' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not reads_r1.is_file(): + raise FileNotFoundError(f"Input read file R1 not found: {reads_r1}") + if not reads_r2.is_file(): + raise FileNotFoundError(f"Input read file R2 not found: {reads_r2}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "metassembly", + "--reads1", str(reads_r1), + "--reads2", str(reads_r2), + "--sample", sample, + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + output_fasta = output_dir / f"{sample}_metassembly.fasta" + output_files = [str(output_fasta)] if output_fasta.exists() 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": "Harpy metassembly command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def phase( + vcf: Path, + bam: Path, + reference: Path, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Phases SNPs into haplotypes using Harpy. + + This function wraps the 'harpy phase' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not vcf.is_file(): + raise FileNotFoundError(f"Input VCF file not found: {vcf}") + if not bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam}") + if not reference.is_file(): + raise FileNotFoundError(f"Reference genome file not found: {reference}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "phase", + "--vcf", str(vcf), + "--bam", str(bam), + "--reference", str(reference), + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + output_vcf = output_dir / f"{vcf.stem}.phased.vcf.gz" + output_files = [str(output_vcf)] if output_vcf.exists() 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": "Harpy phase command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def qc( + reads_r1: Path, + reads_r2: Path, + sample: str, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Performs adapter removal, quality filtering, etc., on FASTQ files. + + This function wraps the 'harpy qc' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not reads_r1.is_file(): + raise FileNotFoundError(f"Input read file R1 not found: {reads_r1}") + if not reads_r2.is_file(): + raise FileNotFoundError(f"Input read file R2 not found: {reads_r2}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "qc", + "--reads1", str(reads_r1), + "--reads2", str(reads_r2), + "--sample", sample, + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + # Assuming output is a pair of trimmed FASTQ files + out_r1 = output_dir / f"{sample}_R1.trimmed.fastq.gz" + out_r2 = output_dir / f"{sample}_R2.trimmed.fastq.gz" + output_files = [] + if out_r1.exists(): + output_files.append(str(out_r1)) + if out_r2.exists(): + output_files.append(str(out_r2)) + 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": "Harpy qc command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def simulate( + reference: Path, + output_dir: Path, + seed: Optional[int] = None, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Simulates genomic variants or linked reads using Harpy. + + This function wraps the 'harpy simulate' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not reference.is_file(): + raise FileNotFoundError(f"Reference genome file not found: {reference}") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "simulate", + "--reference", str(reference), + "--output", str(output_dir), + ] + if seed is not None: + cmd.extend(["--seed", str(seed)]) + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + 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(p) for p in output_dir.iterdir()], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Harpy simulate command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def snp( + bam: Path, + reference: Path, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Calls SNPs and small indels from alignments using Harpy. + + This function wraps the 'harpy snp' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam}") + if not reference.is_file(): + raise FileNotFoundError(f"Reference genome file not found: {reference}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "snp", + "--bam", str(bam), + "--reference", str(reference), + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + output_vcf = output_dir / f"{bam.stem}.vcf.gz" + output_files = [str(output_vcf)] if output_vcf.exists() 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": "Harpy snp command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def sv( + bam: Path, + reference: Path, + output_dir: Path, + threads: int = 8, + dry_run: bool = False, + setup: bool = False, +) -> dict: + """ + Calls large structural variants (inversions, deletions, duplications). + + This function wraps the 'harpy sv' command. + Note: Parameters are inferred from the tool's high-level documentation. + """ + if not bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam}") + if not reference.is_file(): + raise FileNotFoundError(f"Reference genome file not found: {reference}") + if threads <= 0: + raise ValueError("Threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "harpy", "sv", + "--bam", str(bam), + "--reference", str(reference), + "--output", str(output_dir), + "--threads", str(threads), + ] + if dry_run: + cmd.append("--dry-run") + if setup: + cmd.append("--setup") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + output_vcf = output_dir / f"{bam.stem}.sv.vcf.gz" + output_files = [str(output_vcf)] if output_vcf.exists() 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": "Harpy sv command failed", + "return_code": e.returncode, + } + +# --- Other and Troubleshooting Subcommands --- + +@mcp.tool() +def template( + workflow: str, + output_dir: Path, + hpc_profile: Optional[str] = None, +) -> dict: + """ + Creates template files and HPC configs for Harpy workflows. + + This function wraps the 'harpy template' command. + """ + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = ["harpy", "template", workflow, "--output", str(output_dir)] + if hpc_profile: + cmd.extend(["--hpc-profile", hpc_profile]) + + 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(p) for p in output_dir.iterdir()], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Harpy template command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def deps(force: bool = False) -> dict: + """ + Locally installs workflow dependencies for Harpy. + + This function wraps the 'harpy deps' command. + """ + cmd = ["harpy", "deps"] + if force: + cmd.append("--force") + + 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": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Harpy deps command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def diagnose(workflow_dir: Path) -> dict: + """ + Attempts to resolve errors in a failed Harpy workflow. + + This function wraps the 'harpy diagnose' command. + """ + if not workflow_dir.is_dir(): + raise NotADirectoryError(f"Workflow directory not found: {workflow_dir}") + + cmd = ["harpy", "diagnose", str(workflow_dir)] + + 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": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Harpy diagnose command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def resume(workflow_dir: Path) -> dict: + """ + Continues an incomplete Harpy workflow. + + This function wraps the 'harpy resume' command. + """ + if not workflow_dir.is_dir(): + raise NotADirectoryError(f"Workflow directory not found: {workflow_dir}") + + cmd = ["harpy", "resume", str(workflow_dir)] + + 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": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Harpy resume command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def validate(input_file: Path, file_type: str) -> dict: + """ + Runs various format checks for FASTQ, BAM, and other files. + + This function wraps the 'harpy validate' command. + """ + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + valid_types = ["fastq", "bam", "vcf"] + if file_type.lower() not in valid_types: + raise ValueError(f"file_type must be one of {valid_types}") + + cmd = ["harpy", "validate", file_type, str(input_file)] + + 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": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Harpy validate command failed", + "return_code": e.returncode, + } + +@mcp.tool() +def view(workflow: str, output_file: Optional[Path] = None) -> dict: + """ + Views a workflow's components, such as its DAG. + + This function wraps the 'harpy view' command. + """ + cmd = ["harpy", "view", workflow] + if output_file: + cmd.extend(["--output", str(output_file)]) + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, + ) + output_files = [str(output_file)] if output_file and output_file.exists() 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": "Harpy view command failed", + "return_code": e.returncode, + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_harpy/app/harpy_shim_server.py b/Biomni/mcp_generated/mcp_harpy/app/harpy_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6779332ce7310c34f7f575f71735667a2dde116c --- /dev/null +++ b/Biomni/mcp_generated/mcp_harpy/app/harpy_shim_server.py @@ -0,0 +1,55 @@ +#!/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_harpy/app/harpy_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_harpy' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_harpy/app/requirements.txt b/Biomni/mcp_generated/mcp_harpy/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_harpy/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_harpy/docker-compose.yml b/Biomni/mcp_generated/mcp_harpy/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..ffd8498021f6e49151afb302a037f6f8aa7de3e7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_harpy/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-harpy: + build: . + image: mcp-harpy:latest + container_name: mcp-harpy + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=harpy + 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/Biomni/mcp_generated/mcp_harpy/environment.yaml b/Biomni/mcp_generated/mcp_harpy/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..297e3c0ea533df263cfa11e50be4ef5ab3382e80 --- /dev/null +++ b/Biomni/mcp_generated/mcp_harpy/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - harpy + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_harpy/requirements.txt b/Biomni/mcp_generated/mcp_harpy/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_harpy/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_hhsuite/Dockerfile b/Biomni/mcp_generated/mcp_hhsuite/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f514064f1919cc8cf7fc25cfa511c670c740a946 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hhsuite/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 hhsuite via conda (e.g., from bioconda) +RUN conda install -c bioconda hhsuite -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/hhsuite_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/hhsuite_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/hhsuite_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_hhsuite/app/hhsuite_server.py b/Biomni/mcp_generated/mcp_hhsuite/app/hhsuite_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f45508a2a69b596f80f7cf35da121fa5b84c5e27 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hhsuite/app/hhsuite_server.py @@ -0,0 +1,449 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# @mcp.tool() - This decorator is commented out as per the instructions. +def hhblits( + input_file: Path, + database: Path, + output_file: Optional[Path] = None, + output_a3m: Optional[Path] = None, + output_hhm: Optional[Path] = None, + output_psi: Optional[Path] = None, + n_iterations: int = 2, + e_value_inclusion: float = 0.001, + e_value_report: float = 1.0, + min_query_coverage: int = 20, + min_seq_id_msa: int = 0, + max_seq_id_msa: int = 90, + min_coverage_msa: int = 0, + min_query_id_msa: int = 0, + min_score_per_col: float = -20.0, + neff: Optional[float] = None, + neff_max: float = 20.0, + max_filter: int = 20000, + diff: int = 1, + realign_max: int = 500, + mact: float = 0.35, + max_input_sequences: int = 1000001, + show_all_sequences: bool = False, + show_alt_alignments: int = 1, + show_all_hits: bool = False, + alignment_width: int = 80, + db_load_mode: int = 0, + db_presort_mode: int = 0, + input_format: Optional[str] = None, + tags: bool = False, + uppercase: bool = False, + local_alignment_file: Optional[Path] = None, + ss_scoring_mode: int = 2, + subst_matrix_score: int = 1, + avg_subst_score: int = 1, + correlation_score: int = 1, + ss_score_weight: float = 0.11, + subst_matrix_weight: float = 1.0, + avg_subst_weight: float = 0.6, + correlation_weight: float = 0.81, + gapb: float = 1.0, + gapc: float = 1.0, + gapd: float = 1.0, + gape: float = 1.0, + gapf: float = 1.0, + gapi: float = 1.0, + gapj: float = 1.0, + gapt: float = 1.0, + alignment_mode: int = 0, + global_alignment: bool = False, + local_alignment: bool = False, + calibrated_alignment: bool = False, + score_offset: float = 0.0, + exclude_cols: Optional[str] = None, + pca: float = 1.0, + pcb: float = 1.5, + pcf: float = 1.0, + pseudocount_mode: int = 0, + no_realign: bool = False, + context_file: Optional[Path] = None, + cpu: int = 2, + verbose: int = 2, + max_res: int = 10000, +): + """ + Run HHblits for sensitive sequence searching against a sequence database. + + HHblits is a tool for iterative sequence searching that is much faster than + PSI-BLAST but with comparable sensitivity. It represents both query and + database sequences by profile hidden Markov models (HMMs). + """ + # Input validation + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + if local_alignment_file and not local_alignment_file.is_file(): + raise FileNotFoundError(f"Local alignment file not found: {local_alignment_file}") + if context_file and not context_file.is_file(): + raise FileNotFoundError(f"Context file not found: {context_file}") + + if not (0 <= diff <= 3): + raise ValueError("diff must be between 0 and 3.") + if not (0 <= db_load_mode <= 2): + raise ValueError("db_load_mode must be 0, 1, or 2.") + if not (0 <= db_presort_mode <= 2): + raise ValueError("db_presort_mode must be 0, 1, or 2.") + if not (0 <= ss_scoring_mode <= 2): + raise ValueError("ss_scoring_mode must be 0, 1, or 2.") + if not (0 <= alignment_mode <= 3): + raise ValueError("alignment_mode must be between 0 and 3.") + if not (0 <= pseudocount_mode <= 1): + raise ValueError("pseudocount_mode must be 0 or 1.") + if not (0 <= verbose <= 2): + raise ValueError("verbose must be 0, 1, or 2.") + if sum([global_alignment, local_alignment, calibrated_alignment]) > 1: + raise ValueError("Only one of global_alignment, local_alignment, or calibrated_alignment can be set.") + + # Command construction + cmd = ["hhblits", "-i", str(input_file), "-d", str(database)] + + # Handle default output file naming + if output_file is None: + output_file = input_file.with_suffix(".hhr") + cmd.extend(["-o", str(output_file)]) + + output_files = [str(output_file)] + + if output_a3m: + cmd.extend(["-oa3m", str(output_a3m)]) + output_files.append(str(output_a3m)) + if output_hhm: + cmd.extend(["-ohhm", str(output_hhm)]) + output_files.append(str(output_hhm)) + if output_psi: + cmd.extend(["-opsi", str(output_psi)]) + output_files.append(str(output_psi)) + if max_res != 10000: + cmd.extend(["-maxres", str(max_res)]) + if n_iterations != 2: + cmd.extend(["-n", str(n_iterations)]) + if e_value_inclusion != 0.001: + cmd.extend(["-e", str(e_value_inclusion)]) + if e_value_report != 1.0: + cmd.extend(["-E", str(e_value_report)]) + if min_query_coverage != 20: + cmd.extend(["-p", str(min_query_coverage)]) + if min_seq_id_msa != 0: + cmd.extend(["-s", str(min_seq_id_msa)]) + if max_seq_id_msa != 90: + cmd.extend(["-id", str(max_seq_id_msa)]) + if min_coverage_msa != 0: + cmd.extend(["-cov", str(min_coverage_msa)]) + if min_query_id_msa != 0: + cmd.extend(["-qid", str(min_query_id_msa)]) + if min_score_per_col != -20.0: + cmd.extend(["-qsc", str(min_score_per_col)]) + if neff is not None: + cmd.extend(["-neff", str(neff)]) + if neff_max != 20.0: + cmd.extend(["-neffmax", str(neff_max)]) + if max_filter != 20000: + cmd.extend(["-maxfilt", str(max_filter)]) + if diff != 1: + cmd.extend(["-diff", str(diff)]) + if realign_max != 500: + cmd.extend(["-realign_max", str(realign_max)]) + if mact != 0.35: + cmd.extend(["-mact", str(mact)]) + if max_input_sequences != 1000001: + cmd.extend(["-maxseq", str(max_input_sequences)]) + if show_all_sequences: + cmd.append("-all") + if show_alt_alignments != 1: + cmd.extend(["-alt", str(show_alt_alignments)]) + if show_all_hits: + cmd.append("-show_all") + if alignment_width != 80: + cmd.extend(["-aliw", str(alignment_width)]) + if db_load_mode != 0: + cmd.extend(["-db_load_mode", str(db_load_mode)]) + if db_presort_mode != 0: + cmd.extend(["-db_presort_mode", str(db_presort_mode)]) + if input_format: + cmd.extend(["-M", input_format]) + if tags: + cmd.append("-tags") + if uppercase: + cmd.append("-uc") + if local_alignment_file: + cmd.extend(["-g", str(local_alignment_file)]) + if ss_scoring_mode != 2: + cmd.extend(["-ssm", str(ss_scoring_mode)]) + if subst_matrix_score != 1: + cmd.extend(["-sc", str(subst_matrix_score)]) + if avg_subst_score != 1: + cmd.extend(["-sa", str(avg_subst_score)]) + if correlation_score != 1: + cmd.extend(["-corr", str(correlation_score)]) + if ss_score_weight != 0.11: + cmd.extend(["-ssw", str(ss_score_weight)]) + if subst_matrix_weight != 1.0: + cmd.extend(["-scw", str(subst_matrix_weight)]) + if avg_subst_weight != 0.6: + cmd.extend(["-saw", str(avg_subst_weight)]) + if correlation_weight != 0.81: + cmd.extend(["-corrw", str(correlation_weight)]) + if gapb != 1.0: cmd.extend(["-gapb", str(gapb)]) + if gapc != 1.0: cmd.extend(["-gapc", str(gapc)]) + if gapd != 1.0: cmd.extend(["-gapd", str(gapd)]) + if gape != 1.0: cmd.extend(["-gape", str(gape)]) + if gapf != 1.0: cmd.extend(["-gapf", str(gapf)]) + if gapi != 1.0: cmd.extend(["-gapi", str(gapi)]) + if gapj != 1.0: cmd.extend(["-gapj", str(gapj)]) + if gapt != 1.0: cmd.extend(["-gapt", str(gapt)]) + if alignment_mode != 0: + cmd.extend(["-al", str(alignment_mode)]) + if global_alignment: cmd.append("-glob") + if local_alignment: cmd.append("-loc") + if calibrated_alignment: cmd.append("-cal") + if score_offset != 0.0: + cmd.extend(["-shift", str(score_offset)]) + if exclude_cols: + cmd.extend(["-excl", exclude_cols]) + if pca != 1.0: cmd.extend(["-pca", str(pca)]) + if pcb != 1.5: cmd.extend(["-pcb", str(pcb)]) + if pcf != 1.0: cmd.extend(["-pcf", str(pcf)]) + if pseudocount_mode != 0: + cmd.extend(["-pcm", str(pseudocount_mode)]) + if no_realign: + cmd.append("-norealign") + if context_file: + cmd.extend(["-contxt", str(context_file)]) + if cpu != 2: + cmd.extend(["-cpu", str(cpu)]) + if verbose != 2: + cmd.extend(["-v", str(verbose)]) + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "HHblits failed", + "return_code": e.returncode, + } + +# @mcp.tool() - This decorator is commented out as per the instructions. +def hhsearch( + input_file: Path, + database: Path, + output_file: Optional[Path] = None, + output_a3m: Optional[Path] = None, + output_atab: Optional[Path] = None, + e_value_report: float = 1.0, + min_query_coverage: int = 20, + min_coverage: int = 0, + min_query_id: int = 0, + max_filter: int = 1000, + mact: float = 0.35, + max_input_sequences: int = 1000001, + show_all_sequences: bool = False, + show_alt_alignments: int = 1, + show_all_hits: bool = False, + alignment_width: int = 80, + db_load_mode: int = 0, + db_presort_mode: int = 0, + input_format: Optional[str] = None, + tags: bool = False, + uppercase: bool = False, + local_alignment_file: Optional[Path] = None, + ss_scoring_mode: int = 2, + subst_matrix_score: int = 1, + avg_subst_score: int = 1, + correlation_score: int = 1, + ss_score_weight: float = 0.11, + subst_matrix_weight: float = 1.0, + avg_subst_weight: float = 0.6, + correlation_weight: float = 0.81, + gapb: float = 1.0, + gapc: float = 1.0, + gapd: float = 1.0, + gape: float = 1.0, + gapf: float = 1.0, + gapi: float = 1.0, + gapj: float = 1.0, + gapt: float = 1.0, + alignment_mode: int = 0, + global_alignment: bool = False, + local_alignment: bool = False, + calibrated_alignment: bool = False, + score_offset: float = 0.0, + exclude_cols: Optional[str] = None, + pca: float = 1.0, + pcb: float = 1.5, + pcf: float = 1.0, + pseudocount_mode: int = 0, + no_realign: bool = False, + context_file: Optional[Path] = None, + cpu: int = 2, + verbose: int = 2, + max_res: int = 10000, +): + """ + Search a database of HMMs with a query sequence or HMM. + + HHsearch is a sensitive protein homology detection tool based on the + pairwise comparison of profile hidden Markov models (HMMs). + """ + # Input validation + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + if local_alignment_file and not local_alignment_file.is_file(): + raise FileNotFoundError(f"Local alignment file not found: {local_alignment_file}") + if context_file and not context_file.is_file(): + raise FileNotFoundError(f"Context file not found: {context_file}") + + if not (0 <= db_load_mode <= 2): + raise ValueError("db_load_mode must be 0, 1, or 2.") + if not (0 <= db_presort_mode <= 2): + raise ValueError("db_presort_mode must be 0, 1, or 2.") + if not (0 <= ss_scoring_mode <= 2): + raise ValueError("ss_scoring_mode must be 0, 1, or 2.") + if not (0 <= alignment_mode <= 3): + raise ValueError("alignment_mode must be between 0 and 3.") + if not (0 <= pseudocount_mode <= 1): + raise ValueError("pseudocount_mode must be 0 or 1.") + if not (0 <= verbose <= 2): + raise ValueError("verbose must be 0, 1, or 2.") + if sum([global_alignment, local_alignment, calibrated_alignment]) > 1: + raise ValueError("Only one of global_alignment, local_alignment, or calibrated_alignment can be set.") + + # Command construction + cmd = ["hhsearch", "-i", str(input_file), "-d", str(database)] + + if output_file is None: + output_file = input_file.with_suffix(".hhr") + cmd.extend(["-o", str(output_file)]) + + output_files = [str(output_file)] + + if output_a3m: + cmd.extend(["-oa3m", str(output_a3m)]) + output_files.append(str(output_a3m)) + if output_atab: + cmd.extend(["-atab", str(output_atab)]) + output_files.append(str(output_atab)) + if max_res != 10000: + cmd.extend(["-maxres", str(max_res)]) + if e_value_report != 1.0: + cmd.extend(["-e", str(e_value_report)]) + if min_query_coverage != 20: + cmd.extend(["-p", str(min_query_coverage)]) + if min_coverage != 0: + cmd.extend(["-cov", str(min_coverage)]) + if min_query_id != 0: + cmd.extend(["-qid", str(min_query_id)]) + if max_filter != 1000: + cmd.extend(["-maxfilt", str(max_filter)]) + if mact != 0.35: + cmd.extend(["-mact", str(mact)]) + if max_input_sequences != 1000001: + cmd.extend(["-maxseq", str(max_input_sequences)]) + if show_all_sequences: + cmd.append("-all") + if show_alt_alignments != 1: + cmd.extend(["-alt", str(show_alt_alignments)]) + if show_all_hits: + cmd.append("-show_all") + if alignment_width != 80: + cmd.extend(["-aliw", str(alignment_width)]) + if db_load_mode != 0: + cmd.extend(["-db_load_mode", str(db_load_mode)]) + if db_presort_mode != 0: + cmd.extend(["-db_presort_mode", str(db_presort_mode)]) + if input_format: + cmd.extend(["-M", input_format]) + if tags: + cmd.append("-tags") + if uppercase: + cmd.append("-uc") + if local_alignment_file: + cmd.extend(["-g", str(local_alignment_file)]) + if ss_scoring_mode != 2: + cmd.extend(["-ssm", str(ss_scoring_mode)]) + if subst_matrix_score != 1: + cmd.extend(["-sc", str(subst_matrix_score)]) + if avg_subst_score != 1: + cmd.extend(["-sa", str(avg_subst_score)]) + if correlation_score != 1: + cmd.extend(["-corr", str(correlation_score)]) + if ss_score_weight != 0.11: + cmd.extend(["-ssw", str(ss_score_weight)]) + if subst_matrix_weight != 1.0: + cmd.extend(["-scw", str(subst_matrix_weight)]) + if avg_subst_weight != 0.6: + cmd.extend(["-saw", str(avg_subst_weight)]) + if correlation_weight != 0.81: + cmd.extend(["-corrw", str(correlation_weight)]) + if gapb != 1.0: cmd.extend(["-gapb", str(gapb)]) + if gapc != 1.0: cmd.extend(["-gapc", str(gapc)]) + if gapd != 1.0: cmd.extend(["-gapd", str(gapd)]) + if gape != 1.0: cmd.extend(["-gape", str(gape)]) + if gapf != 1.0: cmd.extend(["-gapf", str(gapf)]) + if gapi != 1.0: cmd.extend(["-gapi", str(gapi)]) + if gapj != 1.0: cmd.extend(["-gapj", str(gapj)]) + if gapt != 1.0: cmd.extend(["-gapt", str(gapt)]) + if alignment_mode != 0: + cmd.extend(["-al", str(alignment_mode)]) + if global_alignment: cmd.append("-glob") + if local_alignment: cmd.append("-loc") + if calibrated_alignment: cmd.append("-cal") + if score_offset != 0.0: + cmd.extend(["-shift", str(score_offset)]) + if exclude_cols: + cmd.extend(["-excl", exclude_cols]) + if pca != 1.0: cmd.extend(["-pca", str(pca)]) + if pcb != 1.5: cmd.extend(["-pcb", str(pcb)]) + if pcf != 1.0: cmd.extend(["-pcf", str(pcf)]) + if pseudocount_mode != 0: + cmd.extend(["-pcm", str(pseudocount_mode)]) + if no_realign: + cmd.append("-norealign") + if context_file: + cmd.extend(["-contxt", str(context_file)]) + if cpu != 2: + cmd.extend(["-cpu", str(cpu)]) + if verbose != 2: + cmd.extend(["-v", str(verbose)]) + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "HHsearch failed", + "return_code": e.returncode, + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_hhsuite/app/hhsuite_shim_server.py b/Biomni/mcp_generated/mcp_hhsuite/app/hhsuite_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..345bad2bb78b70668d506a769ac751152f2e9c81 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hhsuite/app/hhsuite_shim_server.py @@ -0,0 +1,55 @@ +#!/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_hhsuite/app/hhsuite_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_hhsuite' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_hhsuite/app/requirements.txt b/Biomni/mcp_generated/mcp_hhsuite/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_hhsuite/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_hhsuite/docker-compose.yml b/Biomni/mcp_generated/mcp_hhsuite/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..5dd2b663ebe1ba63c13a3490850a063babd44097 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hhsuite/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-hhsuite: + build: . + image: mcp-hhsuite:latest + container_name: mcp-hhsuite + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=hhsuite + 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/Biomni/mcp_generated/mcp_hhsuite/environment.yaml b/Biomni/mcp_generated/mcp_hhsuite/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4bd9c79b390350a47889869622b1bd24b0333799 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hhsuite/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - hhsuite + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_hhsuite/requirements.txt b/Biomni/mcp_generated/mcp_hhsuite/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hhsuite/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_hicexplorer/Dockerfile b/Biomni/mcp_generated/mcp_hicexplorer/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..83409c9f7a291dca9975cd20229c0bdb2c16adc7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hicexplorer/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 hicexplorer via conda (e.g., from bioconda) +RUN conda install -c bioconda hicexplorer -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/hicexplorer_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/hicexplorer_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/hicexplorer_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_hicexplorer/app/hicexplorer_server.py b/Biomni/mcp_generated/mcp_hicexplorer/app/hicexplorer_server.py new file mode 100644 index 0000000000000000000000000000000000000000..fc6a415ef7b272f607b4a564506d1393c47e4f10 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hicexplorer/app/hicexplorer_server.py @@ -0,0 +1,691 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# @mcp.tool() - This decorator is commented out as per the instructions. +# In a real MCP environment, it would be uncommented. + +def hic_build_matrix( + sam_files: List[Path], + out_file_name: Path, + bin_size: int = 10000, + threads: int = 4, + restriction_sequence: Optional[str] = None, + restriction_file: Optional[Path] = None, + dangling_ends: Optional[str] = None, + min_mapping_quality: int = 10, + keep_self_circles: bool = False, + keep_self_ligations: bool = False, + keep_duplicates: bool = False, + min_sequence_length: int = 25, + max_sequence_length: int = 0, + min_insert_size: int = 100, + max_insert_size: int = 0, + min_coverage: int = 0, + max_coverage: int = 1000, + stats_file: Optional[Path] = None, + qc_folder: Optional[Path] = None, + chromosome_sizes: Optional[Path] = None, + region: Optional[str] = None, + matrix_format: str = "cool", + tmp_dir: Optional[Path] = None, + enforce_sorted: bool = False, + short_read_names: bool = False, + center_read: bool = False, + quiet: bool = False, +) -> dict: + """ + Builds a Hi-C contact matrix from aligned sequencing reads. + + This tool processes SAM/BAM files from Hi-C experiments to create a binned contact matrix. + It includes steps for filtering reads based on quality, length, and ligation artifacts. + """ + # Input validation + if not all(f.is_file() for f in sam_files): + raise FileNotFoundError("One or more input SAM/BAM files not found.") + if restriction_file and not restriction_file.is_file(): + raise FileNotFoundError(f"Restriction file not found: {restriction_file}") + if chromosome_sizes and not chromosome_sizes.is_file(): + raise FileNotFoundError(f"Chromosome sizes file not found: {chromosome_sizes}") + if bin_size <= 0: + raise ValueError("bin_size must be a positive integer.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + if matrix_format not in ["cool", "h5"]: + raise ValueError("matrix_format must be either 'cool' or 'h5'.") + + cmd = [ + "hicBuildMatrix", + "--samFiles", *[str(f) for f in sam_files], + "--outFileName", str(out_file_name), + "--binSize", str(bin_size), + "--threads", str(threads), + "--minMappingQuality", str(min_mapping_quality), + "--minSequenceLength", str(min_sequence_length), + "--minInsertSize", str(min_insert_size), + "--minCoverage", str(min_coverage), + "--maxCoverage", str(max_coverage), + ] + + if restriction_sequence: + cmd.extend(["--restrictionSequence", restriction_sequence]) + if restriction_file: + cmd.extend(["--restrictionFile", str(restriction_file)]) + if dangling_ends: + cmd.extend(["--danglingEnds", dangling_ends]) + if keep_self_circles: + cmd.append("--keepSelfCircles") + if keep_self_ligations: + cmd.append("--keepSelfLigations") + if keep_duplicates: + cmd.append("--keepDuplicates") + if max_sequence_length > 0: + cmd.extend(["--maxSequenceLength", str(max_sequence_length)]) + if max_insert_size > 0: + cmd.extend(["--maxInsertSize", str(max_insert_size)]) + if stats_file: + cmd.extend(["--statsFile", str(stats_file)]) + if qc_folder: + cmd.extend(["--qcFolder", str(qc_folder)]) + if chromosome_sizes: + cmd.extend(["--chromosomeSizes", str(chromosome_sizes)]) + if region: + cmd.extend(["--region", region]) + if matrix_format: + cmd.extend(["--matrixFormat", matrix_format]) + if tmp_dir: + cmd.extend(["--tmpDir", str(tmp_dir)]) + if enforce_sorted: + cmd.append("--enforceSorted") + if short_read_names: + cmd.append("--shortReadNames") + if center_read: + cmd.append("--center_read") + if quiet: + cmd.append("--quiet") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = [str(out_file_name)] + if stats_file: + output_files.append(str(stats_file)) + if qc_folder: + # This is a directory, so we might list its contents or just return the path + output_files.append(str(qc_folder)) + + 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": f"hicBuildMatrix failed with exit code {e.returncode}", + "output_files": [], + } + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_hicexplorer' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def hic_correct_matrix( + matrix: Path, + out_file_name: Path, + correction_method: str, + filter_threshold: Optional[List[float]] = None, + chromosome: Optional[str] = None, + iterations: Optional[int] = None, + threads: int = 1, + dump_correction_factors: Optional[Path] = None, + load_correction_factors: Optional[Path] = None, + correction_factor_format: str = "hdf5", + version: bool = False, +) -> dict: + """ + Corrects a Hi-C matrix using methods like ICE or KR. + + This tool applies normalization to a raw Hi-C matrix to remove biases, + such as those from restriction enzyme sites, GC content, and mappability. + """ + # Input validation + if not matrix.is_file(): + raise FileNotFoundError(f"Input matrix file not found: {matrix}") + if correction_method not in ["ICE", "KR"]: + raise ValueError("correction_method must be either 'ICE' or 'KR'.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + if correction_factor_format not in ["hdf5", "tsv"]: + raise ValueError("correction_factor_format must be 'hdf5' or 'tsv'.") + + cmd = [ + "hicCorrectMatrix", + "correct", + "--matrix", str(matrix), + "--correctionMethod", correction_method, + "--outFileName", str(out_file_name), + "--threads", str(threads), + "--correctionFactorFormat", correction_factor_format, + ] + + if filter_threshold: + cmd.extend(["--filterThreshold", *[str(f) for f in filter_threshold]]) + if chromosome: + cmd.extend(["--chromosome", chromosome]) + if iterations is not None: + cmd.extend(["--iterations", str(iterations)]) + if dump_correction_factors: + cmd.extend(["--dumpCorrectionFactors", str(dump_correction_factors)]) + if load_correction_factors: + cmd.extend(["--loadCorrectionFactors", str(load_correction_factors)]) + if version: + cmd.append("--version") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = [str(out_file_name)] + if dump_correction_factors: + output_files.append(str(dump_correction_factors)) + + 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": f"hicCorrectMatrix failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def hic_find_tads( + matrix: Path, + out_prefix: str, + correct_for_depth: bool = False, + min_depth: int = 10000, + max_depth: int = 100000, + step: int = 10000, + threshold_comparisons: float = 0.01, + delta: float = 0.01, + correct_for_multiple_testing: Optional[str] = None, + z_score_threshold: float = -2.0, + threads: int = 4, + tad_separation_threshold: float = 0.0, +) -> dict: + """ + Identifies Topologically Associating Domains (TADs) in a Hi-C matrix. + + This tool uses an insulation score-based method to find TAD boundaries and domains. + """ + # Input validation + if not matrix.is_file(): + raise FileNotFoundError(f"Input matrix file not found: {matrix}") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + if correct_for_multiple_testing and correct_for_multiple_testing not in ["fdr", "bonferroni"]: + raise ValueError("correct_for_multiple_testing must be 'fdr' or 'bonferroni'.") + + cmd = [ + "hicFindTADs", + "--matrix", str(matrix), + "--outPrefix", out_prefix, + "--minDepth", str(min_depth), + "--maxDepth", str(max_depth), + "--step", str(step), + "--thresholdComparisons", str(threshold_comparisons), + "--delta", str(delta), + "--zscore_threshold", str(z_score_threshold), + "--threads", str(threads), + "--TAD_separation_threshold", str(tad_separation_threshold), + ] + + if correct_for_depth: + cmd.append("--correctForDepth") + if correct_for_multiple_testing: + cmd.extend(["--correctForMultipleTesting", correct_for_multiple_testing]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + # Expected output files based on prefix + output_files = [ + f"{out_prefix}_zscore_matrix.h5", + f"{out_prefix}_score.bm", + f"{out_prefix}_boundaries.bed", + f"{out_prefix}_domains.bed", + f"{out_prefix}_TAD_separation_score.bm", + ] + 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": f"hicFindTADs failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def hic_plot_matrix( + matrix: Path, + out_file_name: Path, + region: Optional[str] = None, + region2: Optional[str] = None, + chromosome: Optional[str] = None, + title: Optional[str] = None, + v_min: Optional[float] = None, + v_max: Optional[float] = None, + color_map: str = "RdYlBu_r", + log1p: bool = False, + clear_masked_bins: bool = False, + show_masked_bins: bool = False, + loops: Optional[Path] = None, + dpi: int = 300, + bigwig: Optional[Path] = None, + bigwig_y_min: Optional[float] = None, + bigwig_y_max: Optional[float] = None, + bigwig_color: str = "black", + per_chromosome: bool = False, + pca: Optional[Path] = None, + tads: Optional[Path] = None, + scale_factor: float = 1.0, +) -> dict: + """ + Plots a Hi-C contact matrix for a specified region. + + This tool generates a heatmap visualization of a Hi-C matrix, with options to overlay + TADs, loops, PCA eigenvectors (A/B compartments), and bigWig tracks. + """ + # Input validation + if not matrix.is_file(): + raise FileNotFoundError(f"Input matrix file not found: {matrix}") + if loops and not loops.is_file(): + raise FileNotFoundError(f"Loops file not found: {loops}") + if bigwig and not bigwig.is_file(): + raise FileNotFoundError(f"Bigwig file not found: {bigwig}") + if pca and not pca.is_file(): + raise FileNotFoundError(f"PCA file not found: {pca}") + if tads and not tads.is_file(): + raise FileNotFoundError(f"TADs file not found: {tads}") + if dpi <= 0: + raise ValueError("DPI must be a positive integer.") + + cmd = [ + "hicPlotMatrix", + "--matrix", str(matrix), + "--outFileName", str(out_file_name), + "--colorMap", color_map, + "--dpi", str(dpi), + "--scaleFactor", str(scale_factor), + ] + + if region: + cmd.extend(["--region", region]) + if region2: + cmd.extend(["--region2", region2]) + if chromosome: + cmd.extend(["--chromosome", chromosome]) + if title: + cmd.extend(["--title", title]) + if v_min is not None: + cmd.extend(["--vMin", str(v_min)]) + if v_max is not None: + cmd.extend(["--vMax", str(v_max)]) + if log1p: + cmd.append("--log1p") + if clear_masked_bins: + cmd.append("--clearMaskedBins") + if show_masked_bins: + cmd.append("--show_masked_bins") + if loops: + cmd.extend(["--loops", str(loops)]) + if bigwig: + cmd.extend(["--bigwig", str(bigwig)]) + if bigwig_y_min is not None: + cmd.extend(["--bigwig_y_min", str(bigwig_y_min)]) + if bigwig_y_max is not None: + cmd.extend(["--bigwig_y_max", str(bigwig_y_max)]) + cmd.extend(["--bigwig_color", bigwig_color]) + if per_chromosome: + cmd.append("--perChromosome") + if pca: + cmd.extend(["--pca", str(pca)]) + if tads: + cmd.extend(["--TADs", str(tads)]) + + 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(out_file_name)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"hicPlotMatrix failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def hic_pca( + matrix: Path, + out_file_name_bigwig: Path, + out_file_name_bedgraph: Path, + format: str = "bigwig", + chromosomes: Optional[List[str]] = None, + number_of_eigenvectors: int = 1, + threads: int = 4, +) -> dict: + """ + Computes A/B compartments using Principal Component Analysis (PCA). + + This tool calculates the first few eigenvectors of the observed/expected-transformed + and Pearson-correlation-transformed Hi-C matrix to identify genomic compartments. + """ + # Input validation + if not matrix.is_file(): + raise FileNotFoundError(f"Input matrix file not found: {matrix}") + if format not in ["bigwig", "bedgraph"]: + raise ValueError("Format must be 'bigwig' or 'bedgraph'.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + if number_of_eigenvectors <= 0: + raise ValueError("number_of_eigenvectors must be a positive integer.") + + cmd = [ + "hicPCA", + "--matrix", str(matrix), + "--format", format, + "--numberOfEigenvectors", str(number_of_eigenvectors), + "--threads", str(threads), + ] + + if format == "bigwig": + cmd.extend(["--outputFileName", str(out_file_name_bigwig)]) + else: # bedgraph + cmd.extend(["--outputFileName", str(out_file_name_bedgraph)]) + + if chromosomes: + cmd.extend(["--chromosomes", *chromosomes]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = [] + if format == "bigwig": + output_files.append(str(out_file_name_bigwig)) + else: + output_files.append(str(out_file_name_bedgraph)) + + 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": f"hicPCA failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def hic_compare_matrices( + matrices: List[Path], + out_file_name: Path, + operation: str, + region: Optional[str] = None, + chromosome: Optional[str] = None, + v_min: Optional[float] = None, + v_max: Optional[float] = None, + color_map: str = "jet", + log1p: bool = False, + plot_title: Optional[str] = None, + dpi: int = 300, +) -> dict: + """ + Compares two or more Hi-C matrices using various operations. + + This tool can compute the difference, ratio, or log2ratio between two matrices, + or find the mean of multiple matrices. The result is saved as a new matrix and a plot. + """ + # Input validation + if len(matrices) < 2: + raise ValueError("At least two matrices are required for comparison.") + if not all(f.is_file() for f in matrices): + raise FileNotFoundError("One or more input matrix files not found.") + if operation not in ["difference", "ratio", "log2ratio", "mean"]: + raise ValueError("Operation must be one of 'difference', 'ratio', 'log2ratio', 'mean'.") + if dpi <= 0: + raise ValueError("DPI must be a positive integer.") + + cmd = [ + "hicCompareMatrices", + "--matrices", *[str(m) for m in matrices], + "--outFileName", str(out_file_name), + "--operation", operation, + "--colorMap", color_map, + "--dpi", str(dpi), + ] + + if region: + cmd.extend(["--region", region]) + if chromosome: + cmd.extend(["--chromosome", chromosome]) + if v_min is not None: + cmd.extend(["--vMin", str(v_min)]) + if v_max is not None: + cmd.extend(["--vMax", str(v_max)]) + if log1p: + cmd.append("--log1p") + if plot_title: + cmd.extend(["--plotTitle", plot_title]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + # The tool creates a matrix (.h5 or .cool) and a plot (.png, .pdf, etc.) + # We return the path provided by the user, assuming the extension is correct. + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(out_file_name)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"hicCompareMatrices failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def hic_convert_format( + in_file_name: Path, + out_file_name: Path, + in_format: str, + out_format: str, + threads: int = 1, + load_as_cooler: bool = False, + store_as_cooler: bool = False, +) -> dict: + """ + Converts Hi-C matrices between different formats. + + Supported formats include homer, hic-pro, cool, and h5 (HiCExplorer's format). + """ + # Input validation + if not in_file_name.is_file(): + raise FileNotFoundError(f"Input file not found: {in_file_name}") + supported_formats = ["homer", "hicpro", "cool", "h5"] + if in_format not in supported_formats: + raise ValueError(f"Input format '{in_format}' is not supported.") + if out_format not in supported_formats: + raise ValueError(f"Output format '{out_format}' is not supported.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + + cmd = [ + "hicConvertFormat", + "--inFileName", str(in_file_name), + "--outFileName", str(out_file_name), + "--inputFormat", in_format, + "--outputFormat", out_format, + "--threads", str(threads), + ] + + if load_as_cooler: + cmd.append("--load_as_cooler") + if store_as_cooler: + cmd.append("--store_as_cooler") + + 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(out_file_name)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"hicConvertFormat failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def hic_detect_loops( + matrix: Path, + out_file_name: Path, + window_size: int = 10, + peak_width: int = 6, + p_value_threshold: float = 0.01, + threads: int = 4, + chromosomes: Optional[List[str]] = None, +) -> dict: + """ + Detects chromatin loops in a Hi-C matrix using the HiCCUPS algorithm. + """ + # Input validation + if not matrix.is_file(): + raise FileNotFoundError(f"Input matrix file not found: {matrix}") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + if window_size <= 0: + raise ValueError("window_size must be a positive integer.") + if peak_width <= 0: + raise ValueError("peak_width must be a positive integer.") + if not (0 < p_value_threshold <= 1): + raise ValueError("p_value_threshold must be between 0 and 1.") + + cmd = [ + "hicDetectLoops", + "--matrix", str(matrix), + "--outputFile", str(out_file_name), + "--windowSize", str(window_size), + "--peakWidth", str(peak_width), + "--pValue", str(p_value_threshold), + "--threads", str(threads), + ] + + if chromosomes: + cmd.extend(["--chromosomes", *chromosomes]) + + 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(out_file_name)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"hicDetectLoops failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def hic_quick_qc( + bam_files: List[Path], + out_prefix: str, + threads: int = 4, + bin_size: int = 500000, + tmp_dir: Optional[Path] = None, + config_file: Optional[Path] = None, +) -> dict: + """ + Generates a comprehensive QC report for one or more Hi-C datasets. + """ + # Input validation + if not all(f.is_file() for f in bam_files): + raise FileNotFoundError("One or more input BAM files not found.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + if bin_size <= 0: + raise ValueError("bin_size must be a positive integer.") + if config_file and not config_file.is_file(): + raise FileNotFoundError(f"Config file not found: {config_file}") + + cmd = [ + "hicQuickQC", + "--bamfiles", *[str(f) for f in bam_files], + "--outPrefix", out_prefix, + "--threads", str(threads), + "--binSize", str(bin_size), + ] + + if tmp_dir: + cmd.extend(["--tmpdir", str(tmp_dir)]) + if config_file: + cmd.extend(["--configFile", str(config_file)]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + # The tool creates an HTML report and a folder with data + output_files = [f"{out_prefix}.html", f"{out_prefix}_qc"] + 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": f"hicQuickQC failed with exit code {e.returncode}", + "output_files": [], + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_hicexplorer/app/hicexplorer_shim_server.py b/Biomni/mcp_generated/mcp_hicexplorer/app/hicexplorer_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9b88d9ca46046541af6a5f01f7ff484b36676cfa --- /dev/null +++ b/Biomni/mcp_generated/mcp_hicexplorer/app/hicexplorer_shim_server.py @@ -0,0 +1,55 @@ +#!/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_hicexplorer/app/hicexplorer_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_hicexplorer' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_hicexplorer/app/requirements.txt b/Biomni/mcp_generated/mcp_hicexplorer/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_hicexplorer/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_hicexplorer/docker-compose.yml b/Biomni/mcp_generated/mcp_hicexplorer/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..94500985754dd7519d7f0306df52ae97de438691 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hicexplorer/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-hicexplorer: + build: . + image: mcp-hicexplorer:latest + container_name: mcp-hicexplorer + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=hicexplorer + 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/Biomni/mcp_generated/mcp_hicexplorer/environment.yaml b/Biomni/mcp_generated/mcp_hicexplorer/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..84541a801de77aab09c16dd83dfc8b7cc14dec8d --- /dev/null +++ b/Biomni/mcp_generated/mcp_hicexplorer/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - hicexplorer + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_hicexplorer/requirements.txt b/Biomni/mcp_generated/mcp_hicexplorer/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_hicexplorer/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_igv/Dockerfile b/Biomni/mcp_generated/mcp_igv/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4ebb15fcc60ad53ded034507a9613530b7d6d0a3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_igv/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 igv via conda (e.g., from bioconda) +RUN conda install -c bioconda igv -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/igv_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/igv_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/igv_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_igv/app/igv_server.py b/Biomni/mcp_generated/mcp_igv/app/igv_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f3c6f3b892009c52946b1adc36698d1c59eb87c2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_igv/app/igv_server.py @@ -0,0 +1,317 @@ +import subprocess +from pathlib import Path +from typing import List, Optional + +# MCP-related imports are intentionally omitted as per instructions. +# from mcp import tool as mcp_tool +# +# class mcp: +# @staticmethod +# def tool(func): +# return func + + +@mcp.tool +def to_tdf( + input_file: Path, + output_file: Path, + genome: str, + max_zoom: int = 7, + probe_window: Optional[int] = None, + window_size: Optional[int] = None, + tmp_dir: Optional[Path] = None, + verbose: bool = False, +): + """ + Converts sorted data files to the Tiled Data Format (.tdf) using igvtools. + + Args: + input_file: Path to the input file (e.g., .wig, .cn, .snp, .igv, .gct). + output_file: Path for the output .tdf file. + genome: A genome ID (e.g., 'hg19') or path to a .genome or chromosome sizes file. + max_zoom: Maximum zoom level to pre-compute. + probe_window: The window size for probe-based array data. + window_size: The window size for density computation. + tmp_dir: Path to a temporary directory. + verbose: If True, enables verbose output. + """ + # Input validation + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + if max_zoom < 0: + raise ValueError("max_zoom must be a non-negative integer.") + + cmd = ["igvtools", "toTDF"] + + cmd.extend(["-z", str(max_zoom)]) + + if probe_window is not None: + if probe_window <= 0: + raise ValueError("probe_window must be a positive integer.") + cmd.extend(["-p", str(probe_window)]) + + if window_size is not None: + if window_size <= 0: + raise ValueError("window_size must be a positive integer.") + cmd.extend(["-w", str(window_size)]) + + if tmp_dir: + if not tmp_dir.is_dir(): + raise NotADirectoryError(f"Temporary directory not found: {tmp_dir}") + cmd.extend(["--tmpDir", str(tmp_dir)]) + + if verbose: + cmd.extend(["--verbose", "true"]) + + cmd.extend([str(input_file), str(output_file), genome]) + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, encoding="utf-8" + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_file)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"igvtools toTDF failed with exit code {e.returncode}", + "output_files": [], + } + + +@mcp.tool +def count( + input_file: Path, + output_file: Path, + genome: str, + max_zoom: int = 7, + window_size: int = 25, + ext_factor: Optional[int] = None, + pre_ext_factor: Optional[int] = None, + post_ext_factor: Optional[int] = None, + bases: bool = False, + query: Optional[str] = None, + min_map_quality: int = 0, + include_duplicates: bool = False, + pairs: bool = False, + strands: Optional[str] = None, + tmp_dir: Optional[Path] = None, + verbose: bool = False, +): + """ + Computes coverage from an alignment file using igvtools. + + Args: + input_file: Path to the input alignment file (.bam, .sam, .aligned). + output_file: Path for the output file (.wig or .tdf). + genome: A genome ID (e.g., 'hg19') or path to a .genome or chromosome sizes file. + max_zoom: Maximum zoom level to pre-compute (for .tdf output). + window_size: The window size for coverage calculation. + ext_factor: The read extension factor. + pre_ext_factor: The read pre-extension factor (upstream). + post_ext_factor: The read post-extension factor (downstream). + bases: If True, count bases instead of fragments. + query: Restricts computation to a region, e.g., 'chr1:100-200'. + min_map_quality: Minimum mapping quality for reads to be included. + include_duplicates: If True, include reads marked as duplicates. + pairs: If True, compute coverage from paired reads only. + strands: Specifies which strand to use for paired-end reads ('read', 'first', or 'second'). + tmp_dir: Path to a temporary directory. + verbose: If True, enables verbose output. + """ + # Input validation + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + if max_zoom < 0: + raise ValueError("max_zoom must be a non-negative integer.") + if window_size <= 0: + raise ValueError("window_size must be a positive integer.") + if min_map_quality < 0: + raise ValueError("min_map_quality must be a non-negative integer.") + if strands and strands not in ["read", "first", "second"]: + raise ValueError("strands must be one of 'read', 'first', or 'second'.") + + cmd = ["igvtools", "count"] + + cmd.extend(["-z", str(max_zoom)]) + cmd.extend(["-w", str(window_size)]) + cmd.extend(["--minMapQuality", str(min_map_quality)]) + + if ext_factor is not None: + cmd.extend(["-e", str(ext_factor)]) + if pre_ext_factor is not None: + cmd.extend(["--preExtFactor", str(pre_ext_factor)]) + if post_ext_factor is not None: + cmd.extend(["--postExtFactor", str(post_ext_factor)]) + if bases: + cmd.append("--bases") + if query: + cmd.extend(["--query", query]) + if include_duplicates: + cmd.append("--includeDuplicates") + if pairs: + cmd.append("--pairs") + if strands: + cmd.extend(["--strands", strands]) + if tmp_dir: + if not tmp_dir.is_dir(): + raise NotADirectoryError(f"Temporary directory not found: {tmp_dir}") + cmd.extend(["--tmpDir", str(tmp_dir)]) + if verbose: + cmd.extend(["--verbose", "true"]) + + cmd.extend([str(input_file), str(output_file), genome]) + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, encoding="utf-8" + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_file)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"igvtools count failed with exit code {e.returncode}", + "output_files": [], + } + + +@mcp.tool +def index(input_file: Path): + """ + Creates an index for an alignment file using igvtools. + + Args: + input_file: Path to the alignment file to index (.sam, .bam, .aligned). + """ + # Input validation + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + # Determine expected output index file name + suffix = input_file.suffix.lower() + output_index_file = None + if suffix == ".bam": + output_index_file = input_file.with_suffix(input_file.suffix + ".bai") + elif suffix in [".sam", ".aligned"]: + output_index_file = input_file.with_suffix(input_file.suffix + ".sai") + + cmd = ["igvtools", "index", str(input_file)] + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, encoding="utf-8" + ) + + output_files = [] + if output_index_file and output_index_file.exists(): + output_files.append(str(output_index_file)) + + 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"igvtools index failed with exit code {e.returncode}", + "output_files": [], + } + + +@mcp.tool +def sort( + input_file: Path, + output_file: Path, + max_records: int = 500000, + tmp_dir: Optional[Path] = None, +): + """ + Sorts an alignment file by genomic position using igvtools. + + Args: + input_file: Path to the input alignment file (.sam, .bam, .aligned). + output_file: Path for the sorted output file. + max_records: Maximum number of records to keep in memory. + tmp_dir: Path to a temporary directory. + """ + # Input validation + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + if max_records <= 0: + raise ValueError("max_records must be a positive integer.") + + cmd = ["igvtools", "sort"] + + if tmp_dir: + if not tmp_dir.is_dir(): + raise NotADirectoryError(f"Temporary directory not found: {tmp_dir}") + cmd.extend(["--tmpDir", str(tmp_dir)]) + + cmd.extend(["-m", str(max_records)]) + cmd.extend([str(input_file), str(output_file)]) + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, encoding="utf-8" + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_file)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"igvtools sort failed with exit code {e.returncode}", + "output_files": [], + } + + +@mcp.tool +def version(): + """ + Prints the version number of igvtools. + """ + cmd = ["igvtools", "version"] + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, encoding="utf-8" + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"igvtools version failed with exit code {e.returncode}", + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_igv/app/igv_shim_server.py b/Biomni/mcp_generated/mcp_igv/app/igv_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a204c3664afa2cd0a11a8bac8d75b56df47410a9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_igv/app/igv_shim_server.py @@ -0,0 +1,55 @@ +#!/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_igv/app/igv_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_igv' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_igv/app/requirements.txt b/Biomni/mcp_generated/mcp_igv/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_igv/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_igv/docker-compose.yml b/Biomni/mcp_generated/mcp_igv/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..93f2cf9f9f5c536dbc54732b4bdb1dae6af6b8cb --- /dev/null +++ b/Biomni/mcp_generated/mcp_igv/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-igv: + build: . + image: mcp-igv:latest + container_name: mcp-igv + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=igv + 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/Biomni/mcp_generated/mcp_igv/environment.yaml b/Biomni/mcp_generated/mcp_igv/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3f420d6ff28c3a95cac0a84b5a71f97c23b9393a --- /dev/null +++ b/Biomni/mcp_generated/mcp_igv/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - igv + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_igv/requirements.txt b/Biomni/mcp_generated/mcp_igv/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_igv/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_intarna/docker-compose.yml b/Biomni/mcp_generated/mcp_intarna/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..19d358cc3aca89575326009f64ef9deb40dec94e --- /dev/null +++ b/Biomni/mcp_generated/mcp_intarna/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-intarna: + build: . + image: mcp-intarna:latest + container_name: mcp-intarna + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=intarna + 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/Biomni/mcp_generated/mcp_intarna/environment.yaml b/Biomni/mcp_generated/mcp_intarna/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c555c6076e1b547beff2356ff99d5e5f8c1a7ca8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_intarna/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - intarna + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_intarna/requirements.txt b/Biomni/mcp_generated/mcp_intarna/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_intarna/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_ipyrad/Dockerfile b/Biomni/mcp_generated/mcp_ipyrad/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..5236eebe1d864c12b08ae081475cf4ca6463f409 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ipyrad/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 ipyrad via conda (e.g., from bioconda) +RUN conda install -c bioconda ipyrad -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/ipyrad_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/ipyrad_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/ipyrad_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ipyrad/app/ipyrad_server.py b/Biomni/mcp_generated/mcp_ipyrad/app/ipyrad_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8660813ad3a9eabcc975f658fdbfe88fb10e4815 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ipyrad/app/ipyrad_server.py @@ -0,0 +1,243 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List + +# Set up logging for better debugging in a server environment +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +# MCP decorator is assumed to be provided by the execution environment. +# This is a placeholder for the actual decorator. +class mcp: + @staticmethod + def tool(func): + return func + +@mcp.tool +def ipyrad_new_params( + name: str, + force: bool = False, + output_dir: Optional[Path] = None +) -> dict: + """ + Creates a new ipyrad parameters file (params-.txt). + + This corresponds to the command `ipyrad -n -p new`. + + Args: + name: The project name for the analysis. + force: If True, overwrite an existing params file with the same name (-f). + output_dir: Directory where the params file will be created. Defaults to the current working directory. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the path to the created params file. + """ + # Input validation + if not name or not name.strip(): + raise ValueError("Project name must be a non-empty string.") + + cmd = ["ipyrad", "-n", name, "-p", "new"] + if force: + cmd.append("-f") + + # Determine the working directory for the command + work_dir = output_dir if output_dir else Path.cwd() + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed} in {work_dir}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + cwd=work_dir, + ) + + output_file = work_dir / f"params-{name}.txt" + if not output_file.exists(): + raise FileNotFoundError( + f"ipyrad command ran successfully but the expected output file was not found: {output_file}\n" + f"STDOUT: {result.stdout}\nSTDERR: {result.stderr}" + ) + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_file)] + } + + except FileNotFoundError: + err_msg = "Error: 'ipyrad' command not found. Make sure ipyrad is installed and in your system's PATH." + logging.error(err_msg) + return { + "command_executed": command_executed, + "stdout": "", + "stderr": err_msg, + "output_files": [] + } + except subprocess.CalledProcessError as e: + logging.error(f"ipyrad failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +@mcp.tool +def ipyrad_run_analysis( + name: str, + params: Path, + steps: str, + cores: int = 1, + threads: int = 1, + force: bool = False, + debug: bool = False, + custom_map: Optional[Path] = None, +) -> dict: + """ + Runs the ipyrad analysis pipeline for one or more steps. + + This corresponds to the command `ipyrad -n -p -s -r ...`. + + Args: + name: The project name for the analysis (must match the name in the params file). + params: Path to the ipyrad parameters file (e.g., 'params-myproject.txt'). + steps: A string of digits representing the steps to run (e.g., '123' or '34567'). + cores: Number of cores to use for parallel jobs (-c). + threads: Number of threads per core for parallel jobs (-t). + force: If True, force overwrite of existing data for the specified steps (-f). + debug: If True, print debug messages to the terminal (-d). + custom_map: Path to a file with ipyrad -n and -p info for many jobs (-m). + + Returns: + A dictionary containing the command executed, stdout, stderr, and the path to the main output directory. + """ + # Input validation + if not name or not name.strip(): + raise ValueError("Project name must be a non-empty string.") + if not params.is_file(): + raise FileNotFoundError(f"Parameters file not found at: {params}") + if not steps or not steps.isdigit() or any(c not in "1234567" for c in steps): + raise ValueError("Steps must be a string containing only digits from 1 to 7 (e.g., '123').") + if cores < 1: + raise ValueError("Number of cores must be a positive integer.") + if threads < 1: + raise ValueError("Number of threads must be a positive integer.") + if custom_map and not custom_map.is_file(): + raise FileNotFoundError(f"Custom map file not found at: {custom_map}") + + cmd = [ + "ipyrad", + "-n", name, + "-p", str(params.resolve()), + "-s", steps, + "-c", str(cores), + "-t", str(threads), + "-r" # The -r flag is essential to execute the run + ] + + if force: + cmd.append("-f") + if debug: + cmd.append("-d") + if custom_map: + cmd.extend(["-m", str(custom_map.resolve())]) + + # ipyrad creates its output directory relative to the current working directory. + # It's best practice to run it from the directory containing the params file. + work_dir = params.parent + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed} in {work_dir}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + cwd=work_dir, + ) + + # The primary output is a directory named after the project + output_directory = work_dir / name + output_files = [str(output_directory)] if output_directory.is_dir() else [] + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + + except FileNotFoundError: + err_msg = "Error: 'ipyrad' command not found. Make sure ipyrad is installed and in your system's PATH." + logging.error(err_msg) + return { + "command_executed": command_executed, + "stdout": "", + "stderr": err_msg, + "output_files": [] + } + except subprocess.CalledProcessError as e: + logging.error(f"ipyrad failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +@mcp.tool +def ipyrad_version() -> dict: + """ + Gets the installed version of ipyrad. + + This corresponds to the command `ipyrad -v`. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the parsed version string. + """ + cmd = ["ipyrad", "-v"] + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + # The version is usually the only thing in stdout, e.g., "ipyrad 0.9.87" + version_string = result.stdout.strip().split()[-1] + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "version": version_string + } + except FileNotFoundError: + err_msg = "Error: 'ipyrad' command not found. Make sure ipyrad is installed and in your system's PATH." + logging.error(err_msg) + return { + "command_executed": command_executed, + "stdout": "", + "stderr": err_msg, + "version": None + } + except (subprocess.CalledProcessError, IndexError) as e: + stderr = e.stderr if hasattr(e, 'stderr') else str(e) + stdout = e.stdout if hasattr(e, 'stdout') else "" + logging.error(f"ipyrad -v failed: {stderr}") + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "version": None + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ipyrad/app/ipyrad_shim_server.py b/Biomni/mcp_generated/mcp_ipyrad/app/ipyrad_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..19f6a420287add5b291a876e1c82a2488d4b2bfa --- /dev/null +++ b/Biomni/mcp_generated/mcp_ipyrad/app/ipyrad_shim_server.py @@ -0,0 +1,55 @@ +#!/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_ipyrad/app/ipyrad_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_ipyrad' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_ipyrad/app/requirements.txt b/Biomni/mcp_generated/mcp_ipyrad/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_ipyrad/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_ipyrad/docker-compose.yml b/Biomni/mcp_generated/mcp_ipyrad/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..48be8956727034a48aecd5dd60fa915f08cde466 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ipyrad/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-ipyrad: + build: . + image: mcp-ipyrad:latest + container_name: mcp-ipyrad + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=ipyrad + 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/Biomni/mcp_generated/mcp_ipyrad/environment.yaml b/Biomni/mcp_generated/mcp_ipyrad/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..104c5fae4ae2884880d9269acc6df2dfb115fc74 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ipyrad/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - ipyrad + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ipyrad/requirements.txt b/Biomni/mcp_generated/mcp_ipyrad/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ipyrad/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_kaiju/Dockerfile b/Biomni/mcp_generated/mcp_kaiju/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e78a2d7a1f6cf1e5bbc56c5606c41daa80bbfaf7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kaiju/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 kaiju via conda (e.g., from bioconda) +RUN conda install -c bioconda kaiju -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/kaiju_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/kaiju_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/kaiju_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_kaiju/app/__pycache__/kaiju_server.cpython-311.pyc b/Biomni/mcp_generated/mcp_kaiju/app/__pycache__/kaiju_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea8b64c8ebcb759b10c101695a285dd2648123b3 Binary files /dev/null and b/Biomni/mcp_generated/mcp_kaiju/app/__pycache__/kaiju_server.cpython-311.pyc differ diff --git a/Biomni/mcp_generated/mcp_kaiju/app/__pycache__/kaiju_shim_server.cpython-311.pyc b/Biomni/mcp_generated/mcp_kaiju/app/__pycache__/kaiju_shim_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bfde2843c5de0ed0e50d04dede51bdedc548dac6 Binary files /dev/null and b/Biomni/mcp_generated/mcp_kaiju/app/__pycache__/kaiju_shim_server.cpython-311.pyc differ diff --git a/Biomni/mcp_generated/mcp_kaiju/app/kaiju_server.py b/Biomni/mcp_generated/mcp_kaiju/app/kaiju_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0ab4f4a01aa55b00c215a77322daee8de9607fc3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kaiju/app/kaiju_server.py @@ -0,0 +1,862 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Literal, Union +import os +import tempfile + +# Helper function for running commands +def _run_command(cmd: List[str], cwd: Optional[str] = None) -> dict: + """ + Executes a shell command and returns its stdout, stderr, and exit code. + """ + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + cwd=cwd, + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "returncode": e.returncode, + "output_files": [], + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": f"Error: Command '{cmd[0]}' not found. Please ensure Kaiju and its sub-tools are installed and in your PATH.", + "error": "Command not found", + "returncode": 127, + "output_files": [], + } + +# Helper for path validation +def _validate_input_file(file_path: Path, param_name: str): + if not file_path.exists(): + raise ValueError(f"Input file for '{param_name}' not found: {file_path}") + if not file_path.is_file(): + raise ValueError(f"Input path for '{param_name}' is not a file: {file_path}") + +def _validate_output_file(file_path: Path, param_name: str): + if file_path.exists() and not file_path.is_file(): + raise ValueError(f"Output path for '{param_name}' exists but is not a file: {file_path}") + if not file_path.parent.exists(): + # Attempt to create parent directory if it doesn't exist + try: + file_path.parent.mkdir(parents=True, exist_ok=True) + except OSError as e: + raise ValueError(f"Could not create output directory for '{param_name}': {file_path.parent}. Error: {e}") + if not os.access(file_path.parent, os.W_OK): + raise ValueError(f"Output directory for '{param_name}' is not writable: {file_path.parent}") + +def _validate_directory(dir_path: Path, param_name: str, must_exist: bool = True, must_be_writable: bool = False): + if must_exist and not dir_path.exists(): + raise ValueError(f"Directory for '{param_name}' not found: {dir_path}") + if dir_path.exists() and not dir_path.is_dir(): + raise ValueError(f"Path for '{param_name}' is not a directory: {dir_path}") + if not dir_path.exists(): + try: + dir_path.mkdir(parents=True, exist_ok=True) + except OSError as e: + raise ValueError(f"Could not create directory for '{param_name}': {dir_path}. Error: {e}") + if must_be_writable and not os.access(dir_path, os.W_OK): + raise ValueError(f"Directory for '{param_name}' is not writable: {dir_path}") + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_kaiju' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def kaiju_classify( + nodes_dmp_file: Path, + fmi_file: Path, + input_file: Path, + second_input_file: Optional[Path] = None, + output_file: Optional[Path] = None, + threads: int = 1, + mismatches: int = 3, + e_value: float = 0.01, + min_match_len: int = 11, + min_match_score: int = 65, + run_mode: Literal["greedy", "mem"] = "greedy", + protein_input: bool = False, + enable_seg_filter: bool = True, + verbose_output: bool = False, +) -> dict: + """ + Performs fast and sensitive taxonomic classification of high-throughput sequencing reads. + + Args: + nodes_dmp_file: Path to the NCBI taxonomy nodes.dmp file. + fmi_file: Path to the Kaiju database index (.fmi) file. + input_file: Path to the input FASTQ/FASTA file for single-end reads or first mate of paired-end reads. + second_input_file: Optional path to the second input FASTQ/FASTA file for paired-end reads. + output_file: Optional path to the output file. If not provided, output will be printed to STDOUT. + threads: Number of parallel threads to use. Must be >= 1. + mismatches: Number of allowed mismatches in Greedy mode. Must be >= 0. + e_value: E-value threshold for classification in Greedy mode. Must be > 0. + min_match_len: Minimum required match length in MEM mode. Must be >= 1. + min_match_score: Minimum required match score in Greedy mode. Must be >= 0. + run_mode: The classification run mode, either 'greedy' or 'mem'. + protein_input: If True, input sequences are treated as protein sequences (disables translation). + enable_seg_filter: If True, enables filtering of query sequences containing low-complexity regions using SEG. + Enabled by default. Set to False to disable. + verbose_output: If True, enables verbose output with additional columns. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + """ + # Input validation + _validate_input_file(nodes_dmp_file, "nodes_dmp_file") + _validate_input_file(fmi_file, "fmi_file") + _validate_input_file(input_file, "input_file") + if second_input_file: + _validate_input_file(second_input_file, "second_input_file") + if output_file: + _validate_output_file(output_file, "output_file") + + if threads < 1: + raise ValueError("Number of threads must be at least 1.") + if mismatches < 0: + raise ValueError("Number of mismatches must be non-negative.") + if e_value <= 0: + raise ValueError("E-value must be greater than 0.") + if min_match_len < 1: + raise ValueError("Minimum match length must be at least 1.") + if min_match_score < 0: + raise ValueError("Minimum match score must be non-negative.") + + cmd = [ + "kaiju", + "-t", str(nodes_dmp_file), + "-f", str(fmi_file), + "-i", str(input_file), + "-z", str(threads), + ] + + if second_input_file: + cmd.extend(["-j", str(second_input_file)]) + if output_file: + cmd.extend(["-o", str(output_file)]) + + # Run mode specific options + if run_mode == "greedy": + cmd.extend(["-e", str(mismatches)]) + cmd.extend(["-E", str(e_value)]) + cmd.extend(["-s", str(min_match_score)]) + elif run_mode == "mem": + cmd.extend(["-a", "mem"]) + cmd.extend(["-m", str(min_match_len)]) + + if protein_input: + cmd.append("-p") + + # SEG filter: -x enables, -X disables. It's enabled by default. + # So, only add -X if enable_seg_filter is False. + if not enable_seg_filter: + cmd.append("-X") + + if verbose_output: + cmd.append("-v") + + result = _run_command(cmd) + if output_file and "error" not in result: + result["output_files"].append(str(output_file)) + return result + + +@mcp.tool() +def kaiju_makedb( + source_database: Literal[ + "refseq", "refseq_nr", "refseq_ref", "progenomes", "viruses", + "plasmids", "fungi", "nr", "nr_euk", "rvdb" + ], + output_dir: Path, + threads: int = 5, +) -> dict: + """ + Downloads reference databases and builds Kaiju's database index. + + Args: + source_database: The name of the reference database to download and build. + Available options: 'refseq', 'refseq_nr', 'refseq_ref', + 'progenomes', 'viruses', 'plasmids', 'fungi', 'nr', 'nr_euk', 'rvdb'. + output_dir: Directory where the database files and index will be created. + Requires at least 500 GB of free space for large databases. + threads: Number of parallel threads for index construction. A higher number + increases memory usage during construction. Must be >= 1. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + # Input validation + _validate_directory(output_dir, "output_dir", must_exist=False, must_be_writable=True) + if threads < 1: + raise ValueError("Number of threads must be at least 1.") + + cmd = [ + "kaiju-makedb", + "-s", source_database, + "-t", str(threads), + ] + + # Run the command in the specified output directory + result = _run_command(cmd, cwd=str(output_dir)) + + if "error" not in result: + # Identify expected output files. + # The docs state that for "-s refseq", the index is "refseq/kaiju_db_refseq.fmi". + # This implies a subdirectory named after the source database. + db_subdir = output_dir / source_database + fmi_file = db_subdir / f"kaiju_db_{source_database}.fmi" + nodes_file = output_dir / "nodes.dmp" + names_file = output_dir / "names.dmp" + + # Check if they exist before adding + if fmi_file.exists(): + result["output_files"].append(str(fmi_file)) + if nodes_file.exists(): + result["output_files"].append(str(nodes_file)) + if names_file.exists(): + result["output_files"].append(str(names_file)) + + return result + + +@mcp.tool() +def kaiju_mkbwt( + input_fasta: Path, + output_prefix: str, + threads: int = 5, + alphabet: str = "ACDEFGHIKLMNPQRSTVWY", +) -> dict: + """ + Builds the Burrows-Wheeler Transform (BWT) for a custom protein database. + This is the first step in creating a custom Kaiju index. + + Args: + input_fasta: Path to the input FASTA file containing protein sequences. + Headers must be numeric NCBI taxon identifiers, optionally prefixed by an identifier and underscore. + output_prefix: Prefix for the output BWT and SA files (e.g., 'proteins' will create proteins.bwt, proteins.sa). + threads: Number of parallel threads to use. Must be >= 1. + alphabet: The alphabet of protein sequences. Defaults to standard 20 amino acids. + Only uppercase characters are allowed. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + # Input validation + _validate_input_file(input_fasta, "input_fasta") + if threads < 1: + raise ValueError("Number of threads must be at least 1.") + if not alphabet.isupper() or not alphabet.isalpha(): + raise ValueError("Alphabet must contain only uppercase alphabetic characters.") + if not output_prefix: + raise ValueError("Output prefix cannot be empty.") + + cmd = [ + "kaiju-mkbwt", + "-n", str(threads), + "-a", alphabet, + "-o", output_prefix, + str(input_fasta), + ] + + result = _run_command(cmd) + if "error" not in result: + # kaiju-mkbwt creates .bwt and .sa files + bwt_file = Path(f"{output_prefix}.bwt") + sa_file = Path(f"{output_prefix}.sa") + if bwt_file.exists(): + result["output_files"].append(str(bwt_file)) + if sa_file.exists(): + result["output_files"].append(str(sa_file)) + return result + + +@mcp.tool() +def kaiju_mkfmi( + input_prefix: str, +) -> dict: + """ + Builds the FM-index from BWT and SA files created by kaiju-mkbwt. + This is the second step in creating a custom Kaiju index. + + Args: + input_prefix: The prefix used for the BWT and SA files (e.g., 'proteins' + if kaiju-mkbwt created proteins.bwt and proteins.sa). + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + # Input validation + if not input_prefix: + raise ValueError("Input prefix cannot be empty.") + + # Check if intermediate files exist + bwt_file = Path(f"{input_prefix}.bwt") + sa_file = Path(f"{input_prefix}.sa") + if not bwt_file.exists(): + raise ValueError(f"Required BWT file not found: {bwt_file}. Run kaiju_mkbwt first.") + if not sa_file.exists(): + raise ValueError(f"Required SA file not found: {sa_file}. Run kaiju_mkbwt first.") + + cmd = [ + "kaiju-mkfmi", + input_prefix, + ] + + result = _run_command(cmd) + if "error" not in result: + # kaiju-mkfmi creates the .fmi file + fmi_file = Path(f"{input_prefix}.fmi") + if fmi_file.exists(): + result["output_files"].append(str(fmi_file)) + return result + + +@mcp.tool() +def kaiju_multi_classify( + nodes_dmp_file: Path, + fmi_file: Path, + input_files: List[Path], + second_input_files: Optional[List[Path]] = None, + output_files: Optional[List[Path]] = None, + threads: int = 1, + mismatches: int = 3, + e_value: float = 0.01, + min_match_len: int = 11, + min_match_score: int = 65, + run_mode: Literal["greedy", "mem"] = "greedy", + protein_input: bool = False, + enable_seg_filter: bool = True, + verbose_output: bool = False, +) -> dict: + """ + Performs fast and sensitive taxonomic classification for multiple samples simultaneously. + + Args: + nodes_dmp_file: Path to the NCBI taxonomy nodes.dmp file. + fmi_file: Path to the Kaiju database index (.fmi) file. + input_files: A list of paths to input FASTQ/FASTA files for single-end reads + or first mates of paired-end reads. + second_input_files: Optional list of paths to second input FASTQ/FASTA files + for paired-end reads. If provided, its length must match `input_files`. + output_files: Optional list of paths to output files. If not provided, output will be + printed to STDOUT (merged if multiple inputs). If provided, its length + must match `input_files`. + threads: Number of parallel threads to use. Must be >= 1. + mismatches: Number of allowed mismatches in Greedy mode. Must be >= 0. + e_value: E-value threshold for classification in Greedy mode. Must be > 0. + min_match_len: Minimum required match length in MEM mode. Must be >= 1. + min_match_score: Minimum required match score in Greedy mode. Must be >= 0. + run_mode: The classification run mode, either 'greedy' or 'mem'. + protein_input: If True, input sequences are treated as protein sequences (disables translation). + enable_seg_filter: If True, enables filtering of query sequences containing low-complexity regions using SEG. + Enabled by default. Set to False to disable. + verbose_output: If True, enables verbose output with additional columns. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + """ + # Input validation + _validate_input_file(nodes_dmp_file, "nodes_dmp_file") + _validate_input_file(fmi_file, "fmi_file") + + if not input_files: + raise ValueError("At least one input file must be provided.") + for f in input_files: + _validate_input_file(f, "input_files") + + if second_input_files: + if len(input_files) != len(second_input_files): + raise ValueError("Number of first input files must match number of second input files.") + for f in second_input_files: + _validate_input_file(f, "second_input_files") + + if output_files: + if len(input_files) != len(output_files): + raise ValueError("Number of input files must match number of output files.") + for f in output_files: + _validate_output_file(f, "output_files") + + if threads < 1: + raise ValueError("Number of threads must be at least 1.") + if mismatches < 0: + raise ValueError("Number of mismatches must be non-negative.") + if e_value <= 0: + raise ValueError("E-value must be greater than 0.") + if min_match_len < 1: + raise ValueError("Minimum match length must be at least 1.") + if min_match_score < 0: + raise ValueError("Minimum match score must be non-negative.") + + cmd = [ + "kaiju-multi", + "-t", str(nodes_dmp_file), + "-f", str(fmi_file), + "-i", ",".join(str(p) for p in input_files), + "-z", str(threads), + ] + + if second_input_files: + cmd.extend(["-j", ",".join(str(p) for p in second_input_files)]) + if output_files: + cmd.extend(["-o", ",".join(str(p) for p in output_files)]) + + # Run mode specific options + if run_mode == "greedy": + cmd.extend(["-e", str(mismatches)]) + cmd.extend(["-E", str(e_value)]) + cmd.extend(["-s", str(min_match_score)]) + elif run_mode == "mem": + cmd.extend(["-a", "mem"]) + cmd.extend(["-m", str(min_match_len)]) + + if protein_input: + cmd.append("-p") + + if not enable_seg_filter: + cmd.append("-X") + + if verbose_output: + cmd.append("-v") + + result = _run_command(cmd) + if output_files and "error" not in result: + result["output_files"].extend([str(f) for f in output_files]) + return result + + +@mcp.tool() +def kaiju2krona( + nodes_dmp_file: Path, + names_dmp_file: Path, + input_kaiju_output: Path, + output_krona_file: Path, + print_full_taxon_path: bool = False, +) -> dict: + """ + Converts Kaiju's tab-separated output file into a Krona-compatible text file. + + Args: + nodes_dmp_file: Path to the NCBI taxonomy nodes.dmp file. + names_dmp_file: Path to the NCBI taxonomy names.dmp file. + input_kaiju_output: Path to the Kaiju classification output file. + output_krona_file: Path to the output Krona-compatible text file. + print_full_taxon_path: If True, print the full taxon path instead of just the taxon name. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + # Input validation + _validate_input_file(nodes_dmp_file, "nodes_dmp_file") + _validate_input_file(names_dmp_file, "names_dmp_file") + _validate_input_file(input_kaiju_output, "input_kaiju_output") + _validate_output_file(output_krona_file, "output_krona_file") + + cmd = [ + "kaiju2krona", + "-t", str(nodes_dmp_file), + "-n", str(names_dmp_file), + "-i", str(input_kaiju_output), + "-o", str(output_krona_file), + ] + + if print_full_taxon_path: + cmd.append("-l") # The doc says -l for kaiju2krona prints full path. + + result = _run_command(cmd) + if "error" not in result: + result["output_files"].append(str(output_krona_file)) + return result + + +@mcp.tool() +def kaiju2table( + nodes_dmp_file: Path, + names_dmp_file: Path, + taxonomic_rank: str, + output_summary_file: Path, + input_kaiju_outputs: List[Path], + min_percentage: float = 0.0, + min_count: int = 0, + exclude_unclassified: bool = False, + print_full_taxon_path: bool = False, + specific_ranks_for_path: Optional[List[str]] = None, +) -> dict: + """ + Converts Kaiju's output file(s) into a summary table for a given taxonomic rank. + + Args: + nodes_dmp_file: Path to the NCBI taxonomy nodes.dmp file. + names_dmp_file: Path to the NCBI taxonomy names.dmp file. + taxonomic_rank: The taxonomic rank for which to create the summary table (e.g., 'genus', 'species'). + output_summary_file: Path to the output summary table file (TSV format). + input_kaiju_outputs: A list of paths to one or more Kaiju classification output files. + min_percentage: Minimum percentage of total reads for a taxon to be included in the summary. Must be >= 0.0. + min_count: Minimum absolute read count for a taxon to be included in the summary. Must be >= 0. + exclude_unclassified: If True, unclassified reads are not counted towards the total + when calculating percentages. + print_full_taxon_path: If True, print the full taxon path instead of just the taxon name. + specific_ranks_for_path: Optional list of specific ranks to print in the taxon path + (e.g., ['superkingdom', 'phylum', 'class']). Overrides `print_full_taxon_path` if set. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + # Input validation + _validate_input_file(nodes_dmp_file, "nodes_dmp_file") + _validate_input_file(names_dmp_file, "names_dmp_file") + _validate_output_file(output_summary_file, "output_summary_file") + if not input_kaiju_outputs: + raise ValueError("At least one Kaiju output file must be provided.") + for f in input_kaiju_outputs: + _validate_input_file(f, "input_kaiju_outputs") + + if min_percentage < 0.0: + raise ValueError("Minimum percentage must be non-negative.") + if min_count < 0: + raise ValueError("Minimum count must be non-negative.") + if not taxonomic_rank: + raise ValueError("Taxonomic rank cannot be empty.") + + cmd = [ + "kaiju2table", + "-t", str(nodes_dmp_file), + "-n", str(names_dmp_file), + "-r", taxonomic_rank, + "-o", str(output_summary_file), + ] + cmd.extend([str(f) for f in input_kaiju_outputs]) + + if min_percentage > 0.0: + cmd.extend(["-m", str(min_percentage)]) + if min_count > 0: + cmd.extend(["-c", str(min_count)]) + if exclude_unclassified: + cmd.append("-u") + + if specific_ranks_for_path: + cmd.extend(["-l", ",".join(specific_ranks_for_path)]) + elif print_full_taxon_path: + cmd.append("-p") + + result = _run_command(cmd) + if "error" not in result: + result["output_files"].append(str(output_summary_file)) + return result + + +@mcp.tool() +def kaiju_add_taxon_names( + nodes_dmp_file: Path, + names_dmp_file: Path, + input_kaiju_output: Path, + output_file_with_names: Path, + omit_unclassified_reads: bool = False, + print_full_taxon_path: bool = False, + specific_ranks_for_path: Optional[List[str]] = None, +) -> dict: + """ + Appends taxon names or full taxon paths to Kaiju's output file. + + Args: + nodes_dmp_file: Path to the NCBI taxonomy nodes.dmp file. + names_dmp_file: Path to the NCBI taxonomy names.dmp file. + input_kaiju_output: Path to the Kaiju classification output file. + output_file_with_names: Path to the output file with appended taxon names/paths. + omit_unclassified_reads: If True, unclassified reads will be omitted from the output. + print_full_taxon_path: If True, print the full taxon path instead of just the taxon name. + specific_ranks_for_path: Optional list of specific ranks to print in the path + (e.g., ['phylum', 'genus']). Overrides `print_full_taxon_path` if set. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + # Input validation + _validate_input_file(nodes_dmp_file, "nodes_dmp_file") + _validate_input_file(names_dmp_file, "names_dmp_file") + _validate_input_file(input_kaiju_output, "input_kaiju_output") + _validate_output_file(output_file_with_names, "output_file_with_names") + + cmd = [ + "kaiju-addTaxonNames", + "-t", str(nodes_dmp_file), + "-n", str(names_dmp_file), + "-i", str(input_kaiju_output), + "-o", str(output_file_with_names), + ] + + if omit_unclassified_reads: + cmd.append("-u") + + if specific_ranks_for_path: + cmd.extend(["-r", ",".join(specific_ranks_for_path)]) + elif print_full_taxon_path: + cmd.append("-p") + + result = _run_command(cmd) + if "error" not in result: + result["output_files"].append(str(output_file_with_names)) + return result + + +@mcp.tool() +def kaiju_merge_outputs( + input_file1: Path, + input_file2: Path, + output_merged_file: Path, + verbose_output: bool = False, + conflict_strategy: Literal["1", "2", "lca", "lowest"] = "lca", + nodes_dmp_file: Optional[Path] = None, + precedence_by_score: bool = False, +) -> dict: + """ + Merges two tab-separated output files (Kaiju or Kraken format) based on read names. + Input files must be sorted by read name (e.g., using `sort -k2,2`). + + Args: + input_file1: Path to the first sorted input file. + input_file2: Path to the second sorted input file. + output_merged_file: Path to the output merged file. + verbose_output: If True, prints all columns from input files (otherwise only first three). + conflict_strategy: Strategy to resolve conflicting taxon identifiers for a classified read: + '1': Use taxon identifier from the first input file. + '2': Use taxon identifier from the second input file. + 'lca': Use the least common ancestor of the taxon identifiers (default). + 'lowest': Use the lowest ranking of the two taxon identifiers if they are + within the same lineage, otherwise use LCA. + nodes_dmp_file: Path to the NCBI taxonomy nodes.dmp file. Required if `conflict_strategy` + is 'lca' or 'lowest'. + precedence_by_score: If True, gives precedence to the classification result with the higher score + (requires 4th column in input files). + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + # Input validation + _validate_input_file(input_file1, "input_file1") + _validate_input_file(input_file2, "input_file2") + _validate_output_file(output_merged_file, "output_merged_file") + + if conflict_strategy in ["lca", "lowest"] and not nodes_dmp_file: + raise ValueError(f"nodes_dmp_file is required when conflict_strategy is '{conflict_strategy}'.") + if nodes_dmp_file: + _validate_input_file(nodes_dmp_file, "nodes_dmp_file") + + cmd = [ + "kaiju-mergeOutputs", + "-i", str(input_file1), + "-j", str(input_file2), + "-o", str(output_merged_file), + "-c", conflict_strategy, + ] + + if nodes_dmp_file: + cmd.extend(["-t", str(nodes_dmp_file)]) + if verbose_output: + cmd.append("-v") + if precedence_by_score: + cmd.append("-s") + + result = _run_command(cmd) + if "error" not in result: + result["output_files"].append(str(output_merged_file)) + return result + + +@mcp.tool() +def kaijux_search( + fmi_file: Path, + input_file: Path, + second_input_file: Optional[Path] = None, + output_file: Optional[Path] = None, + threads: int = 1, + mismatches: int = 3, + e_value: float = 0.01, + min_match_len: int = 11, + min_match_score: int = 65, + run_mode: Literal["greedy", "mem"] = "greedy", + enable_seg_filter: bool = True, + verbose_output: bool = False, +) -> dict: + """ + Searches translated nucleotide reads against a protein database without taxonomic classification. + Prints the name of the best matching database sequence. + + Args: + fmi_file: Path to the Kaiju database index (.fmi) file. + input_file: Path to the input FASTQ/FASTA file for single-end reads or first mate of paired-end reads. + (Nucleotide sequences). + second_input_file: Optional path to the second input FASTQ/FASTA file for paired-end reads. + output_file: Optional path to the output file. If not provided, output will be printed to STDOUT. + threads: Number of parallel threads to use. Must be >= 1. + mismatches: Number of allowed mismatches in Greedy mode. Must be >= 0. + e_value: E-value threshold for classification in Greedy mode. Must be > 0. + min_match_len: Minimum required match length in MEM mode. Must be >= 1. + min_match_score: Minimum required match score in Greedy mode. Must be >= 0. + run_mode: The classification run mode, either 'greedy' or 'mem'. + enable_seg_filter: If True, enables filtering of query sequences containing low-complexity regions using SEG. + Enabled by default. Set to False to disable. + verbose_output: If True, enables verbose output with additional columns. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + """ + # Input validation + _validate_input_file(fmi_file, "fmi_file") + _validate_input_file(input_file, "input_file") + if second_input_file: + _validate_input_file(second_input_file, "second_input_file") + if output_file: + _validate_output_file(output_file, "output_file") + + if threads < 1: + raise ValueError("Number of threads must be at least 1.") + if mismatches < 0: + raise ValueError("Number of mismatches must be non-negative.") + if e_value <= 0: + raise ValueError("E-value must be greater than 0.") + if min_match_len < 1: + raise ValueError("Minimum match length must be at least 1.") + if min_match_score < 0: + raise ValueError("Minimum match score must be non-negative.") + + cmd = [ + "kaijux", + "-f", str(fmi_file), + "-i", str(input_file), + "-z", str(threads), + ] + + if second_input_file: + cmd.extend(["-j", str(second_input_file)]) + if output_file: + cmd.extend(["-o", str(output_file)]) + + # Run mode specific options + if run_mode == "greedy": + cmd.extend(["-e", str(mismatches)]) + cmd.extend(["-E", str(e_value)]) + cmd.extend(["-s", str(min_match_score)]) + elif run_mode == "mem": + cmd.extend(["-a", "mem"]) + cmd.extend(["-m", str(min_match_len)]) + + if not enable_seg_filter: + cmd.append("-X") + + if verbose_output: + cmd.append("-v") + + result = _run_command(cmd) + if output_file and "error" not in result: + result["output_files"].append(str(output_file)) + return result + + +@mcp.tool() +def kaijup_search( + fmi_file: Path, + input_file: Path, + second_input_file: Optional[Path] = None, + output_file: Optional[Path] = None, + threads: int = 1, + mismatches: int = 3, + e_value: float = 0.01, + min_match_len: int = 11, + min_match_score: int = 65, + run_mode: Literal["greedy", "mem"] = "greedy", + enable_seg_filter: bool = True, + verbose_output: bool = False, +) -> dict: + """ + Searches protein queries against a protein database without taxonomic classification. + Prints the name of the best matching database sequence. + + Args: + fmi_file: Path to the Kaiju database index (.fmi) file. + input_file: Path to the input FASTA file for single-end protein queries or first mate of paired-end. + second_input_file: Optional path to the second input FASTA file for paired-end protein queries. + output_file: Optional path to the output file. If not provided, output will be printed to STDOUT. + threads: Number of parallel threads to use. Must be >= 1. + mismatches: Number of allowed mismatches in Greedy mode. Must be >= 0. + e_value: E-value threshold for classification in Greedy mode. Must be > 0. + min_match_len: Minimum required match length in MEM mode. Must be >= 1. + min_match_score: Minimum required match score in Greedy mode. Must be >= 0. + run_mode: The classification run mode, either 'greedy' or 'mem'. + enable_seg_filter: If True, enables filtering of query sequences containing low-complexity regions using SEG. + Enabled by default. Set to False to disable. + verbose_output: If True, enables verbose output with additional columns. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + """ + # Input validation + _validate_input_file(fmi_file, "fmi_file") + _validate_input_file(input_file, "input_file") + if second_input_file: + _validate_input_file(second_input_file, "second_input_file") + if output_file: + _validate_output_file(output_file, "output_file") + + if threads < 1: + raise ValueError("Number of threads must be at least 1.") + if mismatches < 0: + raise ValueError("Number of mismatches must be non-negative.") + if e_value <= 0: + raise ValueError("E-value must be greater than 0.") + if min_match_len < 1: + raise ValueError("Minimum match length must be at least 1.") + if min_match_score < 0: + raise ValueError("Minimum match score must be non-negative.") + + cmd = [ + "kaijup", + "-f", str(fmi_file), + "-i", str(input_file), + "-z", str(threads), + ] + + if second_input_file: + cmd.extend(["-j", str(second_input_file)]) + if output_file: + cmd.extend(["-o", str(output_file)]) + + # Run mode specific options + if run_mode == "greedy": + cmd.extend(["-e", str(mismatches)]) + cmd.extend(["-E", str(e_value)]) + cmd.extend(["-s", str(min_match_score)]) + elif run_mode == "mem": + cmd.extend(["-a", "mem"]) + cmd.extend(["-m", str(min_match_len)]) + + if not enable_seg_filter: + cmd.append("-X") + + if verbose_output: + cmd.append("-v") + + result = _run_command(cmd) + if output_file and "error" not in result: + result["output_files"].append(str(output_file)) + return result + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_kaiju/app/kaiju_shim_server.py b/Biomni/mcp_generated/mcp_kaiju/app/kaiju_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..253ae04ee856435f1bb042770ad37e0613d9f859 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kaiju/app/kaiju_shim_server.py @@ -0,0 +1,55 @@ +#!/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_kaiju/app/kaiju_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_kaiju' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_kaiju/app/requirements.txt b/Biomni/mcp_generated/mcp_kaiju/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_kaiju/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_kaiju/docker-compose.yml b/Biomni/mcp_generated/mcp_kaiju/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..a1ad5b20456123671a7101563711cad806372e4c --- /dev/null +++ b/Biomni/mcp_generated/mcp_kaiju/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-kaiju: + build: . + image: mcp-kaiju:latest + container_name: mcp-kaiju + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=kaiju + 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/Biomni/mcp_generated/mcp_kaiju/environment.yaml b/Biomni/mcp_generated/mcp_kaiju/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..68716f85534f2bc339b296d5dad3f29d3b5c87be --- /dev/null +++ b/Biomni/mcp_generated/mcp_kaiju/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - kaiju + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_kaiju/requirements.txt b/Biomni/mcp_generated/mcp_kaiju/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kaiju/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_kalign2/Dockerfile b/Biomni/mcp_generated/mcp_kalign2/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d817ef4d35cdea14a55f427149a015850feb8d09 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kalign2/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 kalign2 via conda (e.g., from bioconda) +RUN conda install -c bioconda kalign2 -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/kalign2_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/kalign2_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/kalign2_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_kalign2/app/kalign2_server.py b/Biomni/mcp_generated/mcp_kalign2/app/kalign2_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3719c36752648af957db19290e1bba6490216be2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kalign2/app/kalign2_server.py @@ -0,0 +1,118 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_kalign2' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def kalign2_align( + input_fasta: Path, + output_fasta: Path, +) -> Dict[str, Any]: + """ + Performs multiple sequence alignment using kalign2. + + This function aligns protein or DNA/RNA sequences from an input FASTA file + and writes the alignment to an output FASTA file. + + NOTE: The provided documentation for kalign2 (web docs extract and conda info) + did not include detailed command-line help or manual pages. Therefore, this + implementation assumes the most basic usage pattern: `kalign2 ` + which typically prints the alignment to standard output. The output is then + redirected to the specified `output_fasta` file. + + No specific parameters for gap penalties, scoring matrices, sequence type, + or output format could be extracted from the provided documentation. + A more comprehensive MCP tool would require the full `--help` output + or a detailed manual page for kalign2 to expose these options. + + Args: + input_fasta: Path to the input FASTA file containing sequences to align. + This file must exist and be readable. + output_fasta: Path to the output FASTA file where the aligned sequences + will be written. This file will be created or overwritten. + The parent directory will be created if it does not exist. + + Returns: + A dictionary containing the command executed, stdout, stderr, and + a list of generated output files. + + Raises: + ValueError: If `input_fasta` does not exist or is not a file. + subprocess.CalledProcessError: If kalign2 execution fails with a non-zero exit code. + FileNotFoundError: If the `kalign2` executable is not found in the system's PATH. + """ + # Input validation + if not isinstance(input_fasta, Path): + raise TypeError(f"input_fasta must be a Path object, got {type(input_fasta)}") + if not input_fasta.is_file(): + raise ValueError(f"Input FASTA file not found or is not a file: {input_fasta}") + + if not isinstance(output_fasta, Path): + raise TypeError(f"output_fasta must be a Path object, got {type(output_fasta)}") + + # Ensure output directory exists + output_fasta.parent.mkdir(parents=True, exist_ok=True) + + command = ["kalign2", str(input_fasta)] + + stdout_content = "" + stderr_content = "" + output_files: List[Path] = [] + + try: + # Execute kalign2. Assuming it prints the alignment to stdout by default. + # Redirect stdout to the specified output_fasta file. + with open(output_fasta, "w") as outfile: + process = subprocess.run( + command, + stdout=outfile, # Redirect stdout to the output file + stderr=subprocess.PIPE, + check=True, + text=True, + encoding="utf-8", + ) + + # Since stdout is redirected, process.stdout will be empty. + # We provide a descriptive message for stdout_content. + stdout_content = f"Alignment successfully written to {output_fasta}" + stderr_content = process.stderr + output_files.append(output_fasta) + + except FileNotFoundError: + return { + "command_executed": " ".join(command), + "stdout": "", + "stderr": "kalign2 executable not found. Please ensure it is installed and in your system's PATH.", + "output_files": [], + "error": "kalign2 executable not found.", + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, # This will be empty if stdout was redirected + "stderr": e.stderr, + "output_files": [], + "error": f"kalign2 execution failed with exit code {e.returncode}. Stderr: {e.stderr}", + } + except Exception as e: + return { + "command_executed": " ".join(command), + "stdout": stdout_content, + "stderr": stderr_content, + "output_files": [], + "error": f"An unexpected error occurred during kalign2 execution: {str(e)}", + } + + return { + "command_executed": " ".join(command), + "stdout": stdout_content, + "stderr": stderr_content, + "output_files": output_files, + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_kalign2/app/kalign2_shim_server.py b/Biomni/mcp_generated/mcp_kalign2/app/kalign2_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9ac3316e31aac800e6eb4ff555b4f3e5dc863d6c --- /dev/null +++ b/Biomni/mcp_generated/mcp_kalign2/app/kalign2_shim_server.py @@ -0,0 +1,55 @@ +#!/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_kalign2/app/kalign2_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_kalign2' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_kalign2/app/requirements.txt b/Biomni/mcp_generated/mcp_kalign2/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_kalign2/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_kalign2/docker-compose.yml b/Biomni/mcp_generated/mcp_kalign2/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d051ea54c55891614f7f2b8586113f748cf13278 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kalign2/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-kalign2: + build: . + image: mcp-kalign2:latest + container_name: mcp-kalign2 + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=kalign2 + 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/Biomni/mcp_generated/mcp_kalign2/environment.yaml b/Biomni/mcp_generated/mcp_kalign2/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8cb94499dd106cee9e6d985c91ed3388171d77ee --- /dev/null +++ b/Biomni/mcp_generated/mcp_kalign2/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - kalign2 + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_kalign2/requirements.txt b/Biomni/mcp_generated/mcp_kalign2/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kalign2/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_khipu-metabolomics/app/khipu-metabolomics_server.py b/Biomni/mcp_generated/mcp_khipu-metabolomics/app/khipu-metabolomics_server.py new file mode 100644 index 0000000000000000000000000000000000000000..499c6b823e84a84ef9ecd8c80cbb47328c418f4e --- /dev/null +++ b/Biomni/mcp_generated/mcp_khipu-metabolomics/app/khipu-metabolomics_server.py @@ -0,0 +1,124 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, List + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_khipu_metabolomics' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def khipu_annotate( + input_file: Path, + output_prefix: str, + mode: Optional[str] = None, + ppm: Optional[float] = None, + rtol: Optional[float] = None, + start_col: Optional[int] = None, + end_col: Optional[int] = None, +): + """ + Annotates metabolomics features into empirical compounds (empCpds). + + This tool takes a feature table from a preprocessing tool as input and + annotates degenerate ions in relation to the original compound to infer + neutral mass. It identifies isotopic and adduct relationships to group features. + The results are saved to a JSON file and a tab-delimited file. + + Args: + input_file: Path to the input feature table file (tab-delimited). + output_prefix: Prefix for the output files. Two files will be generated: + .json and .tsv. + mode: Mode of ionization, 'pos' or 'neg'. + ppm: Mass precision in ppm (parts per million) for m/z tolerance. + rtol: Tolerance of retention time match, in an arbitrary unit dependent + on the preprocessing tool. + start_col: Start column for intensity data in the input table. + end_col: End column for intensity data in the input table. + """ + # 1. Input Validation + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + if mode and mode not in ["pos", "neg"]: + raise ValueError("Mode must be either 'pos' or 'neg'.") + + if ppm is not None and ppm <= 0: + raise ValueError("PPM must be a positive number.") + + if rtol is not None and rtol < 0: + raise ValueError("RTOL (retention time tolerance) cannot be a negative number.") + + if start_col is not None and start_col <= 0: + raise ValueError("Start column must be a positive integer.") + + if end_col is not None and end_col <= 0: + raise ValueError("End column must be a positive integer.") + + if start_col is not None and end_col is not None and end_col < start_col: + raise ValueError("End column must be greater than or equal to the start column.") + + # 2. Command Construction & Output Handling + with tempfile.TemporaryDirectory() as temp_dir: + output_path_prefix = Path(temp_dir) / output_prefix + + cmd = [ + "khipu", + "-i", str(input_file), + "-o", str(output_path_prefix), + ] + + if mode: + cmd.extend(["-m", mode]) + if ppm is not None: + cmd.extend(["--ppm", str(ppm)]) + if rtol is not None: + cmd.extend(["--rtol", str(rtol)]) + if start_col is not None: + cmd.extend(["-s", str(start_col)]) + if end_col is not None: + cmd.extend(["-e", str(end_col)]) + + command_str = " ".join(cmd) + + # 3. Subprocess Execution + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + encoding='utf-8' + ) + except FileNotFoundError: + raise RuntimeError("khipu executable not found. Please ensure khipu-metabolomics is installed and in your PATH.") + except subprocess.CalledProcessError as e: + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "output_files": [] + } + + # 4. Collect Output Files + output_files_generated: List[str] = [] + json_output = output_path_prefix.with_suffix(".json") + tsv_output = output_path_prefix.with_suffix(".tsv") + + if json_output.exists(): + output_files_generated.append(str(json_output)) + if tsv_output.exists(): + output_files_generated.append(str(tsv_output)) + + # 5. Return Structured Output + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files_generated + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_khipu-metabolomics/app/khipu-metabolomics_shim_server.py b/Biomni/mcp_generated/mcp_khipu-metabolomics/app/khipu-metabolomics_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1e23bc8881aee019f6511444991f0bcbd8e3e142 --- /dev/null +++ b/Biomni/mcp_generated/mcp_khipu-metabolomics/app/khipu-metabolomics_shim_server.py @@ -0,0 +1,55 @@ +#!/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_khipu-metabolomics/app/khipu-metabolomics_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_khipu_metabolomics' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_khipu-metabolomics/docker-compose.yml b/Biomni/mcp_generated/mcp_khipu-metabolomics/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..7040be92dee6f2c41b94e107e1a883bf1323d878 --- /dev/null +++ b/Biomni/mcp_generated/mcp_khipu-metabolomics/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-khipu-metabolomics: + build: . + image: mcp-khipu-metabolomics:latest + container_name: mcp-khipu-metabolomics + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=khipu-metabolomics + 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/Biomni/mcp_generated/mcp_khipu-metabolomics/environment.yaml b/Biomni/mcp_generated/mcp_khipu-metabolomics/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..82f3ae22a998036c0e2f660e5682de2a2f16ea3d --- /dev/null +++ b/Biomni/mcp_generated/mcp_khipu-metabolomics/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - khipu-metabolomics + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_khipu-metabolomics/requirements.txt b/Biomni/mcp_generated/mcp_khipu-metabolomics/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_khipu-metabolomics/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_mlst/requirements.txt b/Biomni/mcp_generated/mcp_mlst/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_mlst/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_ncbi-genome-download/Dockerfile b/Biomni/mcp_generated/mcp_ncbi-genome-download/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1fbfe947a264c574723d5e98f1b1e2e3a1ed0ad7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-genome-download/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 ncbi-genome-download via conda (e.g., from bioconda) +RUN conda install -c bioconda ncbi-genome-download -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/ncbi-genome-download_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/ncbi-genome-download_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/ncbi-genome-download_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ncbi-genome-download/app/ncbi-genome-download_server.py b/Biomni/mcp_generated/mcp_ncbi-genome-download/app/ncbi-genome-download_server.py new file mode 100644 index 0000000000000000000000000000000000000000..41fff228a8ec237e62f583c3edb81cc31c81985f --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-genome-download/app/ncbi-genome-download_server.py @@ -0,0 +1,166 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, Dict, Any, List + +# 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})() + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_ncbi_genome_download' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def ncbi_genome_download( + group: str, + section: str = 'refseq', + formats: str = 'genbank', + assembly_levels: Optional[str] = None, + refseq_categories: Optional[str] = None, + genera: Optional[str] = None, + species_taxids: Optional[str] = None, + taxids: Optional[str] = None, + type_materials: Optional[str] = None, + output_folder: Optional[Path] = None, + parallel: int = 1, + fuzzy_genus: bool = False, + human_readable: bool = False, + dry_run: bool = False, + no_cache: bool = False, + verbose: bool = False, +) -> Dict[str, Any]: + """ + Downloads genome files from the NCBI FTP server based on specified criteria. + + Args: + group: Taxonomic group to download (e.g., 'bacteria', 'viral', 'fungi', or a comma-separated list). 'all' can be used to check all groups. + section: Section of the NCBI FTP site to download from (e.g., 'refseq', 'genbank'). + formats: Comma-separated list of file formats to download (e.g., 'fasta', 'genbank', 'assembly-report'). 'all' downloads all available formats. + assembly_levels: Comma-separated list of assembly levels to download (e.g., 'complete', 'chromosome', 'scaffold', 'contig'). + refseq_categories: Comma-separated list of RefSeq categories to download (e.g., 'reference', 'representative'). + genera: Comma-separated list of genera to download, or a path to a file with one genus per line. + species_taxids: Comma-separated list of NCBI species taxonomy IDs to download, or a path to a file with one ID per line. + taxids: Comma-separated list of NCBI taxonomy IDs to download, or a path to a file with one ID per line. + type_materials: Filter by relation to type material. Comma-separated list of values: 'any', 'all', 'type', 'reference', 'synonym', 'proxytype', 'neotype'. + output_folder: The directory where downloaded files will be stored. If not provided, a temporary directory will be created. + parallel: Number of parallel downloads to run. + fuzzy_genus: Use fuzzy matching for the '--genera' option. + human_readable: Create a human-readable directory structure using symlinks. + dry_run: Show which accessions would be downloaded without actually downloading. + no_cache: Disable the use of cached assembly summary files. + verbose: Increase the verbosity of the output. + + Returns: + A dictionary containing the execution details, including the command, stdout, stderr, and the output directory. + """ + # --- Input Validation --- + if parallel <= 0: + raise ValueError("The 'parallel' parameter must be a positive integer.") + + if type_materials: + allowed_materials = {"any", "all", "type", "reference", "synonym", "proxytype", "neotype"} + materials_list = {item.strip() for item in type_materials.split(',')} + if not materials_list.issubset(allowed_materials): + invalid_materials = materials_list - allowed_materials + raise ValueError(f"Invalid values for 'type_materials': {', '.join(invalid_materials)}. " + f"Allowed values are: {', '.join(allowed_materials)}") + + # --- File Handling --- + if output_folder: + output_dir = Path(output_folder) + output_dir.mkdir(parents=True, exist_ok=True) + temp_dir_manager = None + else: + temp_dir_manager = tempfile.TemporaryDirectory() + output_dir = Path(temp_dir_manager.name) + + # --- Command Construction --- + cmd = ["ncbi-genome-download"] + + if verbose: + cmd.append("-v") + if no_cache: + cmd.append("--no-cache") + if dry_run: + cmd.append("--dry-run") + if human_readable: + cmd.append("--human-readable") + if fuzzy_genus: + cmd.append("--fuzzy-genus") + + # Add options with values + cmd.extend(["--section", section]) + cmd.extend(["--formats", formats]) + cmd.extend(["-o", str(output_dir)]) + + if parallel > 1: + cmd.extend(["--parallel", str(parallel)]) + if assembly_levels: + cmd.extend(["--assembly-levels", assembly_levels]) + if refseq_categories: + cmd.extend(["--refseq-categories", refseq_categories]) + if genera: + cmd.extend(["--genera", genera]) + if species_taxids: + cmd.extend(["--species-taxids", species_taxids]) + if taxids: + cmd.extend(["--taxids", taxids]) + if type_materials: + cmd.extend(["--type-materials", type_materials]) + + # Add positional argument + cmd.append(group) + + command_executed = " ".join(cmd) + + # --- Subprocess Execution --- + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + # List the contents of the output directory + # The tool creates subdirectories, so we return the main output directory path. + output_files = [str(p) for p in output_dir.rglob('*')] + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": str(output_dir), + "output_files": output_files, + } + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'ncbi-genome-download' command not found. Please ensure it is installed and in your PATH.", + "error": "Command not found", + "return_code": -1, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Tool execution failed", + "return_code": e.returncode, + } + finally: + # Clean up the temporary directory if one was created + if temp_dir_manager: + temp_dir_manager.cleanup() + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_ncbi-genome-download/app/ncbi-genome-download_shim_server.py b/Biomni/mcp_generated/mcp_ncbi-genome-download/app/ncbi-genome-download_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..aef119f9ece0ee6f840ede71251d53c3e972981f --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-genome-download/app/ncbi-genome-download_shim_server.py @@ -0,0 +1,55 @@ +#!/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_ncbi-genome-download/app/ncbi-genome-download_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_ncbi_genome_download' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_ncbi-genome-download/app/requirements.txt b/Biomni/mcp_generated/mcp_ncbi-genome-download/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-genome-download/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_ncbi-genome-download/docker-compose.yml b/Biomni/mcp_generated/mcp_ncbi-genome-download/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..aef0ed06bd94c51b01f198bfb2be025208bf5754 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-genome-download/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-ncbi-genome-download: + build: . + image: mcp-ncbi-genome-download:latest + container_name: mcp-ncbi-genome-download + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=ncbi-genome-download + 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/Biomni/mcp_generated/mcp_ncbi-genome-download/environment.yaml b/Biomni/mcp_generated/mcp_ncbi-genome-download/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fc4f384fba4b8a032f449169f1f725a87ebd3dfc --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-genome-download/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - ncbi-genome-download + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ncbi-genome-download/requirements.txt b/Biomni/mcp_generated/mcp_ncbi-genome-download/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-genome-download/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/Dockerfile b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6c574c3c17e3fbaa385c6f6d5c3ac7b792378dcf --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/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 ncbi-ngs-sdk via conda (e.g., from bioconda) +RUN conda install -c bioconda ncbi-ngs-sdk -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/ncbi-ngs-sdk_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/ncbi-ngs-sdk_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/ncbi-ngs-sdk_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/app/ncbi-ngs-sdk_server.py b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/app/ncbi-ngs-sdk_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d5db7d14f2a59ecd6871438484a29fe449e5121c --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/app/ncbi-ngs-sdk_server.py @@ -0,0 +1,56 @@ +import logging +from typing import List + +# In a real MCP environment, the mcp module would be available. +# For demonstration purposes, a dummy class is defined to mimic the decorator. +class MCP: + def tool(self): + def decorator(f): + return f + return decorator + +mcp = MCP() + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_ncbi_ngs_sdk' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def ngs_sdk_info(): + """ + Provides information about the ncbi-ngs-sdk package. + + The 'ncbi-ngs-sdk' is a Software Development Kit (SDK), not a standalone + command-line tool. It provides libraries and Application Programming Interfaces (APIs) + for C++, Java, and Python to access Next Generation Sequencing (NGS) data from + the NCBI Sequence Read Archive (SRA). + + As it is not an executable with command-line arguments, this function does not + execute any subprocess. It serves as an informational placeholder, confirming the + nature of the package and noting that it has been superseded by and integrated into + the 'sra-tools' suite. For command-line access to SRA data, tools from the + 'sra-tools' package (like fastq-dump, sam-dump) should be used instead. + """ + logging.warning( + "ncbi-ngs-sdk is a library/SDK and not a command-line tool. " + "No command was executed." + ) + + info_message = ( + "The 'ncbi-ngs-sdk' package is a Software Development Kit (SDK) and does not " + "provide a direct command-line executable. It is a collection of libraries " + "used by other tools to access NGS data. The project has been merged into " + "the 'sra-tools' repository. Please use tools from the 'sra-tools' package " + "for command-line operations." + ) + + return { + "command_executed": "N/A (ncbi-ngs-sdk is a library)", + "stdout": info_message, + "stderr": "", + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/app/ncbi-ngs-sdk_shim_server.py b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/app/ncbi-ngs-sdk_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..077bf6efde1287bd0388167b2a768223edb88c95 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/app/ncbi-ngs-sdk_shim_server.py @@ -0,0 +1,55 @@ +#!/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_ncbi-ngs-sdk/app/ncbi-ngs-sdk_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_ncbi_ngs_sdk' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/app/requirements.txt b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/docker-compose.yml b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..246ec5a490393f3cf50c15cc7dcdc0b25073e44d --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-ncbi-ngs-sdk: + build: . + image: mcp-ncbi-ngs-sdk:latest + container_name: mcp-ncbi-ngs-sdk + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=ncbi-ngs-sdk + 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/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/environment.yaml b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bcb01608dfcc0d0778708e7d07ef3304226407be --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - ncbi-ngs-sdk + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/requirements.txt b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ncbi-ngs-sdk/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_novoalign/Dockerfile b/Biomni/mcp_generated/mcp_novoalign/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..63b3c104360048b9b73d38b4f96f7f9760c6f74a --- /dev/null +++ b/Biomni/mcp_generated/mcp_novoalign/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 novoalign via conda (e.g., from bioconda) +RUN conda install -c bioconda novoalign -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/novoalign_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/novoalign_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/novoalign_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_novoalign/app/novoalign_server.py b/Biomni/mcp_generated/mcp_novoalign/app/novoalign_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d2111eda5a63f85a8d91f4fe548c50fb5c64f993 --- /dev/null +++ b/Biomni/mcp_generated/mcp_novoalign/app/novoalign_server.py @@ -0,0 +1,264 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Literal + +# MCP decorator is omitted as per instructions, but would be present in a real implementation. +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_novoalign' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def novoalign_index( + index_file: Path, + fasta_files: List[Path], + kmer_length: int = 14, + kmer_step_size: int = 1, +): + """ + Creates a novoalign index from one or more reference FASTA files. + + This is a prerequisite for running the novoalign_align tool. + + Args: + index_file: The path for the output index file. + fasta_files: A list of one or more reference genome FASTA files. + kmer_length: The k-mer length for the index (default: 14). + kmer_step_size: The step size for the k-mer (default: 1). + + Returns: + A dictionary containing the command executed, stdout, stderr, and output file path. + """ + # --- Input Validation --- + if not fasta_files: + raise ValueError("At least one FASTA file must be provided in fasta_files.") + for f in fasta_files: + if not Path(f).exists(): + raise FileNotFoundError(f"Input FASTA file not found: {f}") + if kmer_length <= 0: + raise ValueError("kmer_length must be a positive integer.") + if kmer_step_size <= 0: + raise ValueError("kmer_step_size must be a positive integer.") + + # --- Command Construction --- + cmd = [ + "novoindex", + "-k", str(kmer_length), + "-s", str(kmer_step_size), + str(index_file), + ] + cmd.extend([str(f) for f in fasta_files]) + + command_executed = " ".join(cmd) + + # --- Subprocess Execution --- + 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": {"index_file": str(index_file)} + } + except FileNotFoundError: + raise RuntimeError("novoindex command not found. Please ensure the novoalign suite is installed and in your PATH.") + except subprocess.CalledProcessError as e: + error_message = ( + f"novoindex execution failed with exit code {e.returncode}.\n" + f"Command: {command_executed}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}\n" + "Note: The novoalign suite requires a valid license to run. This error may be due to a missing or expired license." + ) + raise RuntimeError(error_message) from e + +@mcp.tool() +def novoalign_align( + database: Path, + read_files: List[Path], + output_file: Optional[Path] = None, + output_format: Literal["SAM", "BAM", "Native"] = "SAM", + cores: int = 1, + read_group_id: Optional[str] = None, + read_group_library: Optional[str] = None, + read_group_sample: Optional[str] = None, + read_group_platform: Optional[str] = None, + read_group_platform_unit: Optional[str] = None, + read_group_center: Optional[str] = None, + insert_size_mean: Optional[int] = None, + insert_size_stdev: Optional[int] = None, + paired_end_orientation: Literal["FR", "RF", "FF", "RR"] = "FR", + alignment_threshold: Optional[int] = None, + gap_open_penalty: int = 40, + gap_extend_penalty: int = 6, + report_mode: Literal["All", "Random", "None"] = "Random", + report_repeats: Optional[int] = None, + soft_clip: bool = False, + adapter1: Optional[str] = None, + adapter2: Optional[str] = None, + headcrop: int = 0, + tailcrop: int = 0, + quality_trim_threshold: int = 20, + bisulfite_mode: Literal["Off", "F", "R", "Both"] = "Off", + mirna_mode: bool = False, + input_format: Optional[Literal["FA", "FQ", "STDFQ", "ILM1.3", "ILM1.8", "BAM"]] = None, +): + """ + Aligns short reads to a reference genome using novoalign. + + Args: + database: Path to the novoalign index file created by novoalign_index. + read_files: A list of one (single-end) or two (paired-end) read files. + output_file: Path to the output alignment file. If not provided, output is captured. + output_format: The format of the output alignment (SAM, BAM, Native). + cores: Number of CPU cores to use. Multi-threading requires a commercial license. + read_group_id: Read Group ID (ID tag). + read_group_library: Read Group Library (LB tag). + read_group_sample: Read Group Sample (SM tag). + read_group_platform: Read Group Platform (PL tag). + read_group_platform_unit: Read Group Platform Unit (PU tag). + read_group_center: Read Group Center (CN tag). + insert_size_mean: Mean insert size for paired-end reads. Required for PE. + insert_size_stdev: Standard deviation of insert size for paired-end reads. Required for PE. + paired_end_orientation: Orientation of paired-end reads. + alignment_threshold: Alignment score threshold. + gap_open_penalty: Penalty for opening a gap. + gap_extend_penalty: Penalty for extending a gap. + report_mode: Mode for reporting alignments ('All', 'Random', 'None'). + report_repeats: Report repeat alignments up to this many times. + soft_clip: Use soft clipping for alignments. + adapter1: Sequence of the 3' adapter to be trimmed for read 1. + adapter2: Sequence of the 3' adapter to be trimmed for read 2 (if paired-end). + headcrop: Number of bases to crop from the 5' end of reads. + tailcrop: Number of bases to crop from the 3' end of reads. + quality_trim_threshold: Quality score threshold for trimming. + bisulfite_mode: Enable bisulfite alignment mode. + mirna_mode: Enable miRNA alignment mode. + input_format: Specify the format of the input read files. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output file path. + """ + # --- Input Validation --- + if not database.exists(): + raise FileNotFoundError(f"Database file not found: {database}") + if not read_files: + raise ValueError("At least one read file must be provided.") + for f in read_files: + if not Path(f).exists(): + raise FileNotFoundError(f"Read file not found: {f}") + if len(read_files) > 2: + raise ValueError("A maximum of two read files (for paired-end) is supported.") + + is_paired_end = len(read_files) == 2 + pe_params_provided = [p is not None for p in [insert_size_mean, insert_size_stdev]] + if any(pe_params_provided) and not is_paired_end: + raise ValueError("Insert size parameters can only be used with paired-end reads (2 read files).") + if is_paired_end and not all(pe_params_provided): + raise ValueError("For paired-end reads, both insert_size_mean and insert_size_stdev must be provided.") + + if cores < 1: + raise ValueError("Number of cores must be at least 1.") + if headcrop < 0 or tailcrop < 0 or quality_trim_threshold < 0: + raise ValueError("Trimming and crop values cannot be negative.") + + # --- Command Construction --- + cmd = [ + "novoalign", + "-d", str(database), + "-f", *[str(f) for f in read_files], + "-o", output_format, + "-c", str(cores), + "-g", str(gap_open_penalty), + "-x", str(gap_extend_penalty), + "-r", report_mode, + "-h", str(headcrop), + "-l", str(tailcrop), + "-q", str(quality_trim_threshold), + ] + + if read_group_id: cmd.extend(["--rgid", read_group_id]) + if read_group_library: cmd.extend(["--rglb", read_group_library]) + if read_group_sample: cmd.extend(["--rgsm", read_group_sample]) + if read_group_platform: cmd.extend(["--rgpl", read_group_platform]) + if read_group_platform_unit: cmd.extend(["--rgpu", read_group_platform_unit]) + if read_group_center: cmd.extend(["--rgcn", read_group_center]) + + if is_paired_end and all(pe_params_provided): + cmd.extend(["-i", f"PE{insert_size_mean},{insert_size_stdev},{paired_end_orientation}"]) + + if alignment_threshold is not None: + cmd.extend(["-t", str(alignment_threshold)]) + if report_repeats is not None: + cmd.extend(["-R", str(report_repeats)]) + if soft_clip: + cmd.extend(["-s", "4"]) + if adapter1: + adapter_args = ["-a", adapter1] + if adapter2: + adapter_args.append(adapter2) + cmd.extend(adapter_args) + if bisulfite_mode != "Off": + cmd.extend(["-b", bisulfite_mode]) + if mirna_mode: + cmd.append("-m") + if input_format: + cmd.extend(["-F", input_format]) + + command_executed = " ".join(cmd) + output_files_dict = {} + + # --- Subprocess Execution --- + try: + if output_file: + output_files_dict["alignment_output"] = str(output_file) + with open(output_file, "w") as f_out: + result = subprocess.run( + cmd, + stdout=f_out, + capture_output=True, + text=True, + check=True + ) + stdout_content = f"Output successfully written to {output_file}" + else: + # Use a temporary file for large outputs if no file is specified + with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=f".{output_format.lower()}") as tmp_out: + result = subprocess.run( + cmd, + stdout=tmp_out, + capture_output=True, + text=True, + check=True + ) + tmp_out.seek(0) + # For simplicity, we don't read the large output back into memory + stdout_content = f"Output written to temporary file: {tmp_out.name}" + output_files_dict["alignment_output"] = tmp_out.name + + return { + "command_executed": command_executed, + "stdout": stdout_content, + "stderr": result.stderr, + "output_files": output_files_dict + } + except FileNotFoundError: + raise RuntimeError("novoalign command not found. Please ensure the novoalign suite is installed and in your PATH.") + except subprocess.CalledProcessError as e: + error_message = ( + f"novoalign execution failed with exit code {e.returncode}.\n" + f"Command: {command_executed}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}\n" + "Note: The novoalign suite requires a valid license to run. This error may be due to a missing or expired license." + ) + raise RuntimeError(error_message) from e + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_novoalign/app/novoalign_shim_server.py b/Biomni/mcp_generated/mcp_novoalign/app/novoalign_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..7a5f583486133c20c4bdb7bd1ef13d3a22d4ca86 --- /dev/null +++ b/Biomni/mcp_generated/mcp_novoalign/app/novoalign_shim_server.py @@ -0,0 +1,55 @@ +#!/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_novoalign/app/novoalign_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_novoalign' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_novoalign/app/requirements.txt b/Biomni/mcp_generated/mcp_novoalign/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_novoalign/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_novoalign/docker-compose.yml b/Biomni/mcp_generated/mcp_novoalign/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..13ad5516e1557a4a01f2606b349282e00c5d0059 --- /dev/null +++ b/Biomni/mcp_generated/mcp_novoalign/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-novoalign: + build: . + image: mcp-novoalign:latest + container_name: mcp-novoalign + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=novoalign + 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/Biomni/mcp_generated/mcp_novoalign/environment.yaml b/Biomni/mcp_generated/mcp_novoalign/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..18056ee95d4f023b9eb1e222f3b28b47c3126f43 --- /dev/null +++ b/Biomni/mcp_generated/mcp_novoalign/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - novoalign + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_novoalign/requirements.txt b/Biomni/mcp_generated/mcp_novoalign/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_novoalign/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_pairix/Dockerfile b/Biomni/mcp_generated/mcp_pairix/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..faff7c7e52b3d033c8717c00bf56f0a912804eeb --- /dev/null +++ b/Biomni/mcp_generated/mcp_pairix/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 pairix via conda (e.g., from bioconda) +RUN conda install -c bioconda pairix -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/pairix_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/pairix_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/pairix_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_pairix/app/pairix_server.py b/Biomni/mcp_generated/mcp_pairix/app/pairix_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a8d2530332f45a88a2a6a2d9cff5cfb6c9e3bbfa --- /dev/null +++ b/Biomni/mcp_generated/mcp_pairix/app/pairix_server.py @@ -0,0 +1,385 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List + +# NOTE: The @mcp.tool decorator is a placeholder for the Model Context Protocol (MCP) framework. +# This code is designed to be used within an MCP server environment. +# To run this code standalone, you would need to define or remove the decorator. + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +class mcp: + """A mock mcp class for standalone validation.""" + @staticmethod + def tool(): + def decorator(func): + return func + return decorator + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_pairix' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def pairix_index( + input_file: Path, + preset: Optional[str] = None, + seq1: Optional[int] = None, + beg1: Optional[int] = None, + end1: Optional[int] = None, + seq2: Optional[int] = None, + beg2: Optional[int] = None, + end2: Optional[int] = None, + comment_char: Optional[str] = None, + delimiter: Optional[str] = None, + force: bool = False, + tabix_mode: bool = False, + uncompressed: bool = False, + auto_offset: bool = False, +): + """ + Creates a .px2 index for a bgzipped pairs file using pairix. + + Args: + input_file: Path to the input pairs file (usually bgzipped). + preset: Preset for column numbers (gff, pairs, sam, vcf, bed, psltbl). + seq1: Sequence name column for the first mate (1-based). + beg1: Start column for the first mate (1-based). + end1: End column for the first mate (1-based). + seq2: Sequence name column for the second mate (1-based). + beg2: Start column for the second mate (1-based). + end2: End column for the second mate (1-based). + comment_char: Skip lines starting with this character. + delimiter: The delimiter character (default is tab). + force: Overwrite the index file if it exists. + tabix_mode: Use tabix mode (no second mate). + uncompressed: Input file is uncompressed. + auto_offset: Automatically detect column numbers for pairs format. + """ + # Input validation + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + VALID_PRESETS = {"gff", "pairs", "sam", "vcf", "bed", "psltbl"} + if preset and preset not in VALID_PRESETS: + raise ValueError(f"Invalid preset '{preset}'. Must be one of {VALID_PRESETS}") + + if comment_char and len(comment_char) != 1: + raise ValueError("comment_char must be a single character.") + + if delimiter and len(delimiter) != 1: + raise ValueError("delimiter must be a single character.") + + cmd = ["pairix"] + + if preset: + cmd.extend(["-p", preset]) + if seq1 is not None: + cmd.extend(["-s", str(seq1)]) + if beg1 is not None: + cmd.extend(["-b", str(beg1)]) + if end1 is not None: + cmd.extend(["-e", str(end1)]) + if seq2 is not None: + cmd.extend(["-S", str(seq2)]) + if beg2 is not None: + cmd.extend(["-B", str(beg2)]) + if end2 is not None: + cmd.extend(["-E", str(end2)]) + if comment_char: + cmd.extend(["-c", comment_char]) + if delimiter: + cmd.extend(["-d", delimiter]) + if force: + cmd.append("-f") + if tabix_mode: + cmd.append("-T") + if uncompressed: + cmd.append("-u") + if auto_offset: + cmd.append("-a") + + cmd.append(str(input_file)) + + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + + output_index_file = Path(f"{input_file}.px2") + if not output_index_file.is_file(): + raise FileNotFoundError(f"Index file was not created: {output_index_file}. Stderr: {result.stderr}") + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_index_file)] + } + except FileNotFoundError: + raise RuntimeError("pairix command not found. Please ensure it is in your system's PATH.") + except subprocess.CalledProcessError as e: + logging.error(f"pairix indexing failed with return code {e.returncode}: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Pairix indexing failed.", + "return_code": e.returncode + } + +@mcp.tool() +def pairix_query( + indexed_file: Path, + region1: Optional[str] = None, + region2: Optional[str] = None, + query_file: Optional[Path] = None, + uncompressed: bool = False, +): + """ + Queries a pairix-indexed file for specific genomic regions. + + Args: + indexed_file: Path to the indexed pairs file. + region1: The first query region (e.g., "chr1:100-200"). + region2: The second query region. + query_file: A file containing query regions, one per line. + uncompressed: Input file is uncompressed. + """ + # Input validation + if not indexed_file.is_file(): + raise FileNotFoundError(f"Indexed file not found: {indexed_file}") + + index_file = Path(f"{indexed_file}.px2") + if not index_file.is_file(): + raise FileNotFoundError(f"Index file not found for {indexed_file}. Expected at {index_file}") + + if not region1 and not query_file: + raise ValueError("Either 'region1' or 'query_file' must be provided for querying.") + + if region1 and query_file: + raise ValueError("'region1' and 'query_file' are mutually exclusive.") + + if query_file and not query_file.is_file(): + raise FileNotFoundError(f"Query file not found: {query_file}") + + cmd = ["pairix"] + if uncompressed: + cmd.append("-u") + + cmd.append(str(indexed_file)) + + if query_file: + cmd.extend(["-q", str(query_file)]) + elif region1: + cmd.append(region1) + if region2: + cmd.append(region2) + + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + 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": [] + } + except FileNotFoundError: + raise RuntimeError("pairix command not found. Please ensure it is in your system's PATH.") + except subprocess.CalledProcessError as e: + logging.error(f"pairix query failed with return code {e.returncode}: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Pairix query failed.", + "return_code": e.returncode + } + +@mcp.tool() +def pairix_merge( + input_files: List[Path], + output_file: Path, + threads: int = 1, + print_merge_log: bool = False, +): + """ + Merges multiple bgzipped and pairix-indexed files. + + Args: + input_files: A list of paths to the input files to merge. + output_file: Path to the merged output file. + threads: Number of threads for merging. + print_merge_log: Print a log of merged lines during merging to stderr. + """ + # Input validation + if not input_files: + raise ValueError("At least one input file must be provided for merging.") + + for f in input_files: + if not f.is_file(): + raise FileNotFoundError(f"Input file not found: {f}") + + if threads < 1: + raise ValueError("Number of threads must be at least 1.") + + output_file.parent.mkdir(parents=True, exist_ok=True) + + cmd = ["pairix"] + + if print_merge_log: + cmd.append("-M") + + if threads > 1: + cmd.extend(["-t", str(threads)]) + + file_list_str = ",".join(map(str, input_files)) + cmd.extend(["-m", file_list_str]) + + command_executed = " ".join(cmd) + f" > {output_file}" + logging.info(f"Executing command: {command_executed}") + + try: + with open(output_file, "w") as f_out: + result = subprocess.run( + cmd, + stdout=f_out, + stderr=subprocess.PIPE, + text=True, + check=True + ) + + return { + "command_executed": command_executed, + "stdout": f"Output successfully written to {output_file}", + "stderr": result.stderr, + "output_files": [str(output_file)] + } + except FileNotFoundError: + raise RuntimeError("pairix command not found. Please ensure it is in your system's PATH.") + except subprocess.CalledProcessError as e: + logging.error(f"pairix merge failed with return code {e.returncode}: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout or "", + "stderr": e.stderr, + "error": "Pairix merge failed.", + "return_code": e.returncode + } + +@mcp.tool() +def pairix_list_chroms( + indexed_file: Path, +): + """ + Lists chromosome names from a pairix index file. + + Args: + indexed_file: Path to the indexed pairs file. + """ + # Input validation + if not indexed_file.is_file(): + raise FileNotFoundError(f"Indexed file not found: {indexed_file}") + + index_file = Path(f"{indexed_file}.px2") + if not index_file.is_file(): + raise FileNotFoundError(f"Index file not found for {indexed_file}. Expected at {index_file}") + + cmd = ["pairix", "-l", str(indexed_file)] + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + 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": [] + } + except FileNotFoundError: + raise RuntimeError("pairix command not found. Please ensure it is in your system's PATH.") + except subprocess.CalledProcessError as e: + logging.error(f"pairix list-chroms failed with return code {e.returncode}: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Pairix list-chroms failed.", + "return_code": e.returncode + } + +@mcp.tool() +def pairix_line_count( + indexed_file: Path, +): + """ + Prints the number of pairs in a pairix-indexed file. + + Args: + indexed_file: Path to the indexed pairs file. + """ + # Input validation + if not indexed_file.is_file(): + raise FileNotFoundError(f"Indexed file not found: {indexed_file}") + + index_file = Path(f"{indexed_file}.px2") + if not index_file.is_file(): + raise FileNotFoundError(f"Index file not found for {indexed_file}. Expected at {index_file}") + + cmd = ["pairix", "-n", str(indexed_file)] + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + 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": [] + } + except FileNotFoundError: + raise RuntimeError("pairix command not found. Please ensure it is in your system's PATH.") + except subprocess.CalledProcessError as e: + logging.error(f"pairix line-count failed with return code {e.returncode}: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Pairix line-count failed.", + "return_code": e.returncode + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_pairix/app/pairix_shim_server.py b/Biomni/mcp_generated/mcp_pairix/app/pairix_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..4286581416660239f20d4f4cd74ea9abe134bc0e --- /dev/null +++ b/Biomni/mcp_generated/mcp_pairix/app/pairix_shim_server.py @@ -0,0 +1,55 @@ +#!/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_pairix/app/pairix_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_pairix' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_pairix/app/requirements.txt b/Biomni/mcp_generated/mcp_pairix/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_pairix/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_pairix/docker-compose.yml b/Biomni/mcp_generated/mcp_pairix/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..7f0a905a9bda6f5e81efd5c35dd3accced41c2db --- /dev/null +++ b/Biomni/mcp_generated/mcp_pairix/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-pairix: + build: . + image: mcp-pairix:latest + container_name: mcp-pairix + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=pairix + 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/Biomni/mcp_generated/mcp_pairix/environment.yaml b/Biomni/mcp_generated/mcp_pairix/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ee5161e1a13a57aff2565e457aa05a89b70ccf2f --- /dev/null +++ b/Biomni/mcp_generated/mcp_pairix/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - pairix + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_pairix/requirements.txt b/Biomni/mcp_generated/mcp_pairix/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pairix/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_pandaseq/requirements.txt b/Biomni/mcp_generated/mcp_pandaseq/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pandaseq/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_pango-designation/Dockerfile b/Biomni/mcp_generated/mcp_pango-designation/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..070a91440afc1b8810501a1fd5169aa01b834dc6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pango-designation/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 pango-designation via conda (e.g., from bioconda) +RUN conda install -c bioconda pango-designation -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/pango-designation_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/pango-designation_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/pango-designation_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_pango-designation/app/pango-designation_server.py b/Biomni/mcp_generated/mcp_pango-designation/app/pango-designation_server.py new file mode 100644 index 0000000000000000000000000000000000000000..094032fe3733c9d1394121edf1e4e58928385dd6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pango-designation/app/pango-designation_server.py @@ -0,0 +1,48 @@ +import subprocess +from pathlib import Path +from typing import Optional, List + +# MCP decorator is omitted as per instructions +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_pango_designation' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def pango_designation_info() -> dict: + """ + Provides information about the pango-designation repository. + + This function serves as a placeholder because 'pango-designation' is a data + repository for Pango lineage designations, not an executable command-line tool. + The package provides essential data files like 'lineages.csv' and 'lineage_notes.txt' + but does not offer any command-line functionality for analysis or processing. + New lineage suggestions are handled manually through the project's GitHub page. + """ + # This is not a real command, so we don't execute anything. + # The return value is informational. + command_executed = "N/A (pango-designation is a data repository, not a CLI tool)" + + stdout_message = ( + "pango-designation is a data repository for Pango lineages and not a " + "command-line tool. It provides data files such as 'lineages.csv', " + "'lineage_notes.txt', and 'alias_key.json'. There are no executable " + "functions or subcommands available for processing data. To suggest a " + "new lineage, please file an issue on the GitHub repository: " + "https://github.com/cov-lineages/pango-designation" + ) + + stderr_message = ( + "Note: The 'pango_designation' command-line entry point is non-functional " + "and raises a ModuleNotFoundError, confirming its status as a non-executable package." + ) + + return { + "command_executed": command_executed, + "stdout": stdout_message, + "stderr": stderr_message, + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_pango-designation/app/pango-designation_shim_server.py b/Biomni/mcp_generated/mcp_pango-designation/app/pango-designation_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c33680f29fadf6059f30abaef2ea34180fdcc8a2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pango-designation/app/pango-designation_shim_server.py @@ -0,0 +1,55 @@ +#!/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_pango-designation/app/pango-designation_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_pango_designation' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_pango-designation/app/requirements.txt b/Biomni/mcp_generated/mcp_pango-designation/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_pango-designation/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_pango-designation/docker-compose.yml b/Biomni/mcp_generated/mcp_pango-designation/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..da7cef6b9c13c820d91a543339c961a0746638a1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pango-designation/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-pango-designation: + build: . + image: mcp-pango-designation:latest + container_name: mcp-pango-designation + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=pango-designation + 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/Biomni/mcp_generated/mcp_pango-designation/environment.yaml b/Biomni/mcp_generated/mcp_pango-designation/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e20aba93ebfd672093c1dc6250c6222c4e1d15ac --- /dev/null +++ b/Biomni/mcp_generated/mcp_pango-designation/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - pango-designation + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_pango-designation/requirements.txt b/Biomni/mcp_generated/mcp_pango-designation/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pango-designation/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_pangolin/requirements.txt b/Biomni/mcp_generated/mcp_pangolin/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pangolin/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-algorithm-diff/Dockerfile b/Biomni/mcp_generated/mcp_perl-algorithm-diff/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..23d2502717f7eb6e84fdd0174bdbdd605fb972f2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-algorithm-diff/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-algorithm-diff via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-algorithm-diff -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-algorithm-diff_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-algorithm-diff_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-algorithm-diff_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-algorithm-diff/app/perl-algorithm-diff_server.py b/Biomni/mcp_generated/mcp_perl-algorithm-diff/app/perl-algorithm-diff_server.py new file mode 100644 index 0000000000000000000000000000000000000000..81541e06083c9ea94ec0f54004bc4d92613f40da --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-algorithm-diff/app/perl-algorithm-diff_server.py @@ -0,0 +1,244 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_algorithm_diff' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def perl_algorithm_diff( + file1: str, + file2: str, +) -> Dict[str, Any]: + """ + Compute 'intelligent' differences between two files using the Perl Algorithm::Diff module. + This tool provides a basic diff output showing additions, deletions, and changes. + + Args: + file1: Path to the first input file. + file2: Path to the second input file. + """ + # Input validation + path1 = Path(file1) + path2 = Path(file2) + + if not path1.is_file(): + return {"error": f"File not found: {file1}"} + if not path2.is_file(): + return {"error": f"File not found: {file2}"} + + # Perl script to use Algorithm::Diff + # Format: [sign, line_index, line_content] + # signs: - (removed from file1), + (added to file2) + perl_script = """ +use Algorithm::Diff qw(diff); +open my $fh1, '<', $ARGV[0] or die "Cannot open $ARGV[0]: $!"; +open my $fh2, '<', $ARGV[1] or die "Cannot open $ARGV[1]: $!"; +chomp(my @seq1 = <$fh1>); +chomp(my @seq2 = <$fh2>); +close $fh1; +close $fh2; + +my $diffs = diff(\\@seq1, \\@seq2); +foreach my $chunk (@$diffs) { + foreach my $line (@$chunk) { + print join("\t", @$line), "\n"; + } + print "----\n"; +} +""" + + cmd = ["perl", "-MAlgorithm::Diff", "-e", perl_script, str(path1), str(path2)] + + 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": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Perl execution failed: {str(e)}" + } + +@mcp.tool() +def perl_algorithm_sdiff( + file1: str, + file2: str, +) -> Dict[str, Any]: + """ + Compute side-by-side differences (sdiff) between two files using Perl Algorithm::Diff. + Returns a list of changes where each line is marked as: + u: unchanged, +: added, -: deleted, c: changed. + + Args: + file1: Path to the first input file. + file2: Path to the second input file. + """ + path1 = Path(file1) + path2 = Path(file2) + + if not path1.is_file(): + return {"error": f"File not found: {file1}"} + if not path2.is_file(): + return {"error": f"File not found: {file2}"} + + # Perl script for sdiff + # Output format: status | left_val | right_val + perl_script = """ +use Algorithm::Diff qw(sdiff); +open my $fh1, '<', $ARGV[0] or die "Cannot open $ARGV[0]: $!"; +open my $fh2, '<', $ARGV[1] or die "Cannot open $ARGV[1]: $!"; +chomp(my @seq1 = <$fh1>); +chomp(my @seq2 = <$fh2>); +close $fh1; +close $fh2; + +my @sdiffs = sdiff(\\@seq1, \\@seq2); +foreach my $row (@sdiffs) { + # $row is [status, left, right] + print join(" | ", map { defined $_ ? $_ : "" } @$row), "\n"; +} +""" + + cmd = ["perl", "-MAlgorithm::Diff", "-e", perl_script, str(path1), str(path2)] + + 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": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Perl execution failed: {str(e)}" + } + +@mcp.tool() +def perl_algorithm_lcs( + file1: str, + file2: str, +) -> Dict[str, Any]: + """ + Find the Longest Common Subsequence (LCS) between two files using Perl Algorithm::Diff. + This returns the lines that are common to both files in the order they appear. + + Args: + file1: Path to the first input file. + file2: Path to the second input file. + """ + path1 = Path(file1) + path2 = Path(file2) + + if not path1.is_file(): + return {"error": f"File not found: {file1}"} + if not path2.is_file(): + return {"error": f"File not found: {file2}"} + + # Perl script for LCS + perl_script = """ +use Algorithm::Diff qw(LCS); +open my $fh1, '<', $ARGV[0] or die "Cannot open $ARGV[0]: $!"; +open my $fh2, '<', $ARGV[1] or die "Cannot open $ARGV[1]: $!"; +chomp(my @seq1 = <$fh1>); +chomp(my @seq2 = <$fh2>); +close $fh1; +close $fh2; + +my @lcs = LCS(\\@seq1, \\@seq2); +foreach my $line (@lcs) { + print $line, "\n"; +} +""" + + cmd = ["perl", "-MAlgorithm::Diff", "-e", perl_script, str(path1), str(path2)] + + 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": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Perl execution failed: {str(e)}" + } + +@mcp.tool() +def perl_algorithm_diff_compact( + file1: str, + file2: str, +) -> Dict[str, Any]: + """ + Compute differences between two files and return a compact representation + of the changes (hunks) using Perl Algorithm::Diff. + + Args: + file1: Path to the first input file. + file2: Path to the second input file. + """ + path1 = Path(file1) + path2 = Path(file2) + + if not path1.is_file(): + return {"error": f"File not found: {file1}"} + if not path2.is_file(): + return {"error": f"File not found: {file2}"} + + # Perl script for compact diff (using the OO interface for more control) + perl_script = """ +use Algorithm::Diff; +open my $fh1, '<', $ARGV[0] or die; +open my $fh2, '<', $ARGV[1] or die; +chomp(my @seq1 = <$fh1>); +chomp(my @seq2 = <$fh2>); +close $fh1; +close $fh2; + +my $diff = Algorithm::Diff->new(\\@seq1, \\@seq2); +while ($diff->Next()) { + next if $diff->Same(); + my $sep = $diff->Items(2) ? "," : ""; + printf "Hunk at lines %d (file1) and %d (file2):\\n", $diff->Get(qw( Min1 Min2 )); + print " - " . join("\\n - ", $diff->Items(1)) . "\\n" if $diff->Items(1); + print " + " . join("\\n + ", $diff->Items(2)) . "\\n" if $diff->Items(2); +} +""" + + cmd = ["perl", "-MAlgorithm::Diff", "-e", perl_script, str(path1), str(path2)] + + 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": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Perl execution failed: {str(e)}" + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-algorithm-diff/app/perl-algorithm-diff_shim_server.py b/Biomni/mcp_generated/mcp_perl-algorithm-diff/app/perl-algorithm-diff_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..7518ac0afa52aa2e7ddd026e6baccdb4f54b57c2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-algorithm-diff/app/perl-algorithm-diff_shim_server.py @@ -0,0 +1,55 @@ +#!/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-algorithm-diff/app/perl-algorithm-diff_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_algorithm_diff' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-algorithm-diff/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-algorithm-diff/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-algorithm-diff/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-algorithm-diff/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-algorithm-diff/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..cdaac2a82a31184f823e7c46d78ce457516155c3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-algorithm-diff/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-algorithm-diff: + build: . + image: mcp-perl-algorithm-diff:latest + container_name: mcp-perl-algorithm-diff + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-algorithm-diff + 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/Biomni/mcp_generated/mcp_perl-algorithm-diff/environment.yaml b/Biomni/mcp_generated/mcp_perl-algorithm-diff/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8670cf074f4d25da5554c595dd0434d4272df75e --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-algorithm-diff/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-algorithm-diff + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-algorithm-diff/requirements.txt b/Biomni/mcp_generated/mcp_perl-algorithm-diff/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-algorithm-diff/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-appconfig/Dockerfile b/Biomni/mcp_generated/mcp_perl-appconfig/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b3df2861ce7c7b85447418fdd7c9fe193b39ec55 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-appconfig/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-appconfig via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-appconfig -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-appconfig_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-appconfig_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-appconfig_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-appconfig/app/perl-appconfig_server.py b/Biomni/mcp_generated/mcp_perl-appconfig/app/perl-appconfig_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1826874387fbbd125ae0b9886cc6eb276d9ef478 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-appconfig/app/perl-appconfig_server.py @@ -0,0 +1,224 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +# @mcp.tool() is a placeholder for the actual decorator in the MCP framework. +# It is used here to indicate that this function is an MCP tool. +# In a real MCP environment, you would import it: `from mcp import mcp` +class mcp: + @staticmethod + def tool(): + def decorator(func): + return func + return decorator + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_appconfig' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_perl( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + execute_program: Optional[List[str]] = None, + execute_program_extended: Optional[List[str]] = None, + record_separator: Optional[str] = None, + autosplit: 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, + in_place_edit: Optional[str] = None, + include_dirs: Optional[List[Path]] = None, + use_modules: Optional[List[str]] = None, + no_modules: Optional[List[str]] = None, + loop_n: bool = False, + loop_p: bool = False, + parse_switches: bool = False, + search_path: bool = False, + taint_warnings: bool = False, + taint_checks: bool = False, + dump_core: bool = False, + allow_unsafe: bool = False, + print_version: bool = False, + print_config: Optional[str] = None, + enable_warnings_w: bool = False, + enable_warnings_W: bool = False, + extract_from_text: Optional[str] = None, + disable_warnings: bool = False, +) -> Dict[str, Any]: + """ + Executes a Perl program with a wide range of command-line switches. + + This tool serves as a wrapper for the Perl interpreter, allowing you to run scripts + from files, execute one-liners, check syntax, and control various runtime behaviors + like warning levels, module loading, and input/output processing. + + Args: + programfile: Path to the Perl program file to execute. + arguments: A list of arguments to pass to the program file. + execute_program: One or more lines of a program to execute directly (-e). + execute_program_extended: Like execute_program, but enables all optional features (-E). + record_separator: Specify the record separator character, often as an octal value (-0). + autosplit: Enable autosplit mode for -n or -p, splitting $_ into @F (-a). + unicode_features: Enable specific Unicode features by number or list (-C). + check_syntax_only: Check the syntax of the program without executing it (-c). + debugger: Run the program under the specified debugger, or the default if no value is given (-d). + debugging_flags: Set debugging flags from a bit mask or list of letters (-D). + no_sitecustomize: Prevents the execution of $sitelib/sitecustomize.pl at startup (-f). + split_pattern: Specify the pattern for splitting in autosplit mode (-F). + in_place_edit: Edit files in place. Provide an extension to create backups (-i). + include_dirs: A list of directories to add to Perl's @INC search path (-I). + use_modules: A list of modules to 'use' before executing the program (-M). + no_modules: A list of modules to 'no' (disable) before executing the program (-M-). + loop_n: Assume a 'while (<>) { ... }' loop around the program (-n). + loop_p: Assume a loop like -n, but also print each line (-p). + parse_switches: Enable rudimentary parsing for switches after the program file name (-s). + search_path: Look for the program file in the system's PATH environment variable (-S). + taint_warnings: Enable taint warnings (-t). + taint_checks: Enable full taint checks, which is more strict than -t (-T). + dump_core: Dump the compiled program core after parsing (-u). + allow_unsafe: Allow unsafe operations (-U). + print_version: Print the Perl version, patchlevel, and license information (-v). + print_config: Print configuration summary, or a single variable if specified (-V). + enable_warnings_w: Enable many useful warnings (-w). + enable_warnings_W: Enable all warnings (-W). + extract_from_text: Ignore text before the #!perl line. Optionally cd to a directory (-x). + disable_warnings: Disable all warnings (-X). + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + program_sources = sum([ + programfile is not None, + execute_program is not None, + execute_program_extended is not None + ]) + if program_sources > 1: + raise ValueError("Only one of 'programfile', 'execute_program', or 'execute_program_extended' can be specified.") + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"Program file not found: {programfile}") + + if loop_n and loop_p: + raise ValueError("The '-n' and '-p' switches are mutually exclusive.") + + if include_dirs: + for directory in include_dirs: + if not directory.is_dir(): + raise NotADirectoryError(f"Include directory not found: {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 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 in_place_edit is not None: + cmd.append(f"-i{in_place_edit}") + if include_dirs: + for directory in include_dirs: + cmd.extend(["-I", str(directory)]) + if use_modules: + for module in use_modules: + cmd.append(f"-M{module}") + if no_modules: + for module in no_modules: + cmd.append(f"-M-{module}") + if loop_n: + cmd.append("-n") + if loop_p: + cmd.append("-p") + if parse_switches: + cmd.append("-s") + if search_path: + cmd.append("-S") + if taint_warnings: + cmd.append("-t") + if taint_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_w: + cmd.append("-w") + if enable_warnings_W: + cmd.append("-W") + if extract_from_text is not None: + cmd.append(f"-x{extract_from_text}") + if disable_warnings: + cmd.append("-X") + + if execute_program: + for line in execute_program: + cmd.extend(["-e", line]) + if execute_program_extended: + for line in execute_program_extended: + cmd.extend(["-E", line]) + + if programfile: + cmd.append(str(programfile)) + + if arguments: + if programfile or execute_program or execute_program_extended: + cmd.append("--") + cmd.extend(arguments) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "error": "Perl execution failed." + } + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "perl command not found. Please ensure Perl is installed and in your PATH.", + "return_code": 1, + "error": "Command not found." + } + + # --- Structured Result Return --- + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-appconfig/app/perl-appconfig_shim_server.py b/Biomni/mcp_generated/mcp_perl-appconfig/app/perl-appconfig_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..97f8312299c09ac616b6c7db194db4a3814e2229 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-appconfig/app/perl-appconfig_shim_server.py @@ -0,0 +1,55 @@ +#!/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-appconfig/app/perl-appconfig_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_appconfig' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-appconfig/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-appconfig/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-appconfig/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-appconfig/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-appconfig/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..12b27d4ecfa4ad89b370e86fe9f1158c21098de2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-appconfig/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-appconfig: + build: . + image: mcp-perl-appconfig:latest + container_name: mcp-perl-appconfig + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-appconfig + 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/Biomni/mcp_generated/mcp_perl-appconfig/environment.yaml b/Biomni/mcp_generated/mcp_perl-appconfig/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0fbce23d4329f7f425a45a6b22f119e94d72df31 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-appconfig/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-appconfig + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-appconfig/requirements.txt b/Biomni/mcp_generated/mcp_perl-appconfig/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-appconfig/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-bio-featureio/Dockerfile b/Biomni/mcp_generated/mcp_perl-bio-featureio/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..aa420ef5f2888e8bd3fef35985bcdf15c7d0c714 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-bio-featureio/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-featureio via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-bio-featureio -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-featureio_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-featureio_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-featureio_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-bio-featureio/app/perl-bio-featureio_server.py b/Biomni/mcp_generated/mcp_perl-bio-featureio/app/perl-bio-featureio_server.py new file mode 100644 index 0000000000000000000000000000000000000000..09a1fd4d719eaaf10801a56f102a95d0767a3362 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-bio-featureio/app/perl-bio-featureio_server.py @@ -0,0 +1,147 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Dict + +# In a real MCP environment, the mcp decorator would be imported. +# For this example, we define a placeholder. +class mcp: + @staticmethod + def tool(): + def decorator(func): + return func + return decorator + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_bio_featureio' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_perl( + programfile: Optional[Path] = None, + program_string: Optional[str] = None, + program_string_extended: Optional[str] = None, + arguments: Optional[List[str]] = None, + check_syntax_only: bool = False, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + disable_all_warnings: bool = False, + enable_tainting_warnings: bool = False, + enable_tainting_checks: bool = False, + include_directories: Optional[List[Path]] = None, + modules: Optional[List[str]] = None, + print_version: bool = False, + print_config_summary: bool = False, + print_config_variable: Optional[str] = None, +) -> Dict: + """ + Executes a script using the Perl interpreter. + + The 'perl-bio-featureio' package is a library, not a standalone command-line tool. + It provides modules for use within Perl scripts. This tool allows you to run such a + script. Ensure that the necessary Perl modules, like those from + 'perl-bio-featureio', are available in the execution environment. + """ + # --- Input Validation --- + modes = [ + programfile is not None, + program_string is not None, + program_string_extended is not None, + print_version, + print_config_summary, + print_config_variable is not None, + ] + if sum(modes) != 1: + raise ValueError( + "Exactly one of 'programfile', 'program_string', 'program_string_extended', " + "'print_version', 'print_config_summary', or 'print_config_variable' must be specified." + ) + + 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 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_tainting_warnings: + cmd.append("-t") + if enable_tainting_checks: + cmd.append("-T") + + if include_directories: + for directory in include_directories: + cmd.extend(["-I", str(directory)]) + + if modules: + for module in modules: + # Handles both 'use module' (-Mmodule) and 'no module' (-M-module) + cmd.append(f"-M{module}") + + # Add the primary mode of operation + if print_version: + cmd.append("-v") + elif print_config_summary: + cmd.append("-V") + elif print_config_variable is not None: + cmd.append(f"-V:{print_config_variable}") + elif program_string is not None: + cmd.extend(["-e", program_string]) + elif program_string_extended is not None: + cmd.extend(["-E", program_string_extended]) + elif programfile is not None: + cmd.append(str(programfile)) + + # Add script arguments + if arguments: + # Separate interpreter switches from script arguments for clarity and safety + if programfile or program_string or program_string_extended: + cmd.append("--") + 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: + # This error is simplified for the example. In a real scenario, + # more context might be useful. + raise RuntimeError("Perl executable not found. Make sure Perl is installed and in your PATH.") + except subprocess.CalledProcessError as e: + return { + "error": "Perl script execution failed.", + "return_code": e.returncode, + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": command_executed, + } + + # --- Structured Result Return --- + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": [] # The perl interpreter itself doesn't create output files. + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-bio-featureio/app/perl-bio-featureio_shim_server.py b/Biomni/mcp_generated/mcp_perl-bio-featureio/app/perl-bio-featureio_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c7bde815745fb1a832311ce1b9cf0b86a45014e3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-bio-featureio/app/perl-bio-featureio_shim_server.py @@ -0,0 +1,55 @@ +#!/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-featureio/app/perl-bio-featureio_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_bio_featureio' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-bio-featureio/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-bio-featureio/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-bio-featureio/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-bio-featureio/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-bio-featureio/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c4a3cfe7b46f89a0c457b4f5dc43a47bb5be2dc7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-bio-featureio/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-bio-featureio: + build: . + image: mcp-perl-bio-featureio:latest + container_name: mcp-perl-bio-featureio + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-bio-featureio + 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/Biomni/mcp_generated/mcp_perl-bio-featureio/environment.yaml b/Biomni/mcp_generated/mcp_perl-bio-featureio/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..15e36807ed70ff8d8f3139fbdeb9917dda43d983 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-bio-featureio/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-bio-featureio + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-bio-featureio/requirements.txt b/Biomni/mcp_generated/mcp_perl-bio-featureio/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-bio-featureio/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-bioperl/requirements.txt b/Biomni/mcp_generated/mcp_perl-bioperl/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-bioperl/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-class-load-xs/requirements.txt b/Biomni/mcp_generated/mcp_perl-class-load-xs/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-class-load-xs/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-convert-binary-c/Dockerfile b/Biomni/mcp_generated/mcp_perl-convert-binary-c/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..18a01e8d5078b2a1edb35db6092e2986d351a73f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-convert-binary-c/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-convert-binary-c via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-convert-binary-c -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-convert-binary-c_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-convert-binary-c_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-convert-binary-c_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-convert-binary-c/app/perl-convert-binary-c_server.py b/Biomni/mcp_generated/mcp_perl-convert-binary-c/app/perl-convert-binary-c_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f8a38ec0e1e670b22ece30905011b54899c6a14e --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-convert-binary-c/app/perl-convert-binary-c_server.py @@ -0,0 +1,219 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +# MCP decorator is omitted as per instructions. +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_convert_binary_c' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_perl( + programfile: Optional[Path] = None, + one_liner: Optional[str] = None, + one_liner_extended: Optional[str] = None, + args: Optional[List[str]] = None, + record_separator: Optional[str] = None, + autosplit: bool = False, + split_pattern: Optional[str] = None, + unicode_features: Optional[str] = None, + syntax_check_only: bool = False, + enable_debugger: bool = False, + debugger_module: Optional[str] = None, + debugging_flags: Optional[str] = None, + no_sitecustomize: bool = False, + in_place_edit_extension: Optional[str] = None, + include_dirs: Optional[List[Path]] = None, + line_ending_processing: Optional[str] = None, + use_modules: Optional[List[str]] = None, + use_modules_no_import: Optional[List[str]] = None, + no_modules: Optional[List[str]] = None, + loop_around_program: bool = False, + loop_and_print: bool = False, + parse_switches: bool = False, + search_path: bool = False, + taint_warnings: bool = False, + taint_checks: bool = False, + dump_core: bool = False, + unsafe_operations: bool = False, + print_version: bool = False, + print_config: bool = False, + print_config_variable: Optional[str] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + disable_all_warnings: bool = False, + extract_script: bool = False, + extract_dir: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Executes a Perl script or one-liner using the Perl interpreter. + + This tool is a wrapper for the `perl` command. The `perl-convert-binary-c` + package provides a Perl module, not a standalone executable. This tool allows + you to run any Perl script, which can in turn use the `Convert::Binary::C` module + if it is available in the execution environment. + """ + # --- Input Validation --- + script_sources = sum(p is not None for p in [programfile, one_liner, one_liner_extended]) + action_flags = sum([print_version, print_config]) + + if action_flags > 0: + if script_sources > 0: + raise ValueError("Cannot provide a script source (programfile, one_liner) when using action flags like --print-version or --print-config.") + elif script_sources == 0: + raise ValueError("You must provide a script source (programfile, one_liner, or one_liner_extended) or an action flag (--print-version, --print-config).") + elif script_sources > 1: + raise ValueError("Only one script source (programfile, one_liner, or one_liner_extended) can be specified at a time.") + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"The specified program file does not exist: {programfile}") + + if split_pattern and not autosplit: + raise ValueError("The 'split_pattern' option requires 'autosplit' to be enabled.") + + if debugger_module and not enable_debugger: + raise ValueError("The 'debugger_module' option requires 'enable_debugger' to be enabled.") + + if loop_around_program and loop_and_print: + raise ValueError("The 'loop_around_program' (-n) and 'loop_and_print' (-p) options are mutually exclusive.") + + if sum([enable_warnings, enable_all_warnings, disable_all_warnings]) > 1: + raise ValueError("Warning flags -w, -W, and -X are mutually exclusive.") + + if print_config_variable and not print_config: + raise ValueError("The 'print_config_variable' option requires 'print_config' to be enabled.") + + if extract_dir and not extract_script: + raise ValueError("The 'extract_dir' option requires 'extract_script' to be enabled.") + + # --- 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 syntax_check_only: + cmd.append("-c") + + if enable_debugger: + if debugger_module: + cmd.append(f"-d:{debugger_module}") + else: + cmd.append("-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 in_place_edit_extension is not None: + cmd.append(f"-i{in_place_edit_extension}") + if include_dirs: + for directory in include_dirs: + cmd.extend(["-I", str(directory)]) + if line_ending_processing is not None: + cmd.append(f"-l{line_ending_processing}") + + if use_modules: + for module in use_modules: + cmd.append(f"-M{module}") + if use_modules_no_import: + for module in use_modules_no_import: + cmd.append(f"-m{module}") + if no_modules: + for module in no_modules: + cmd.append(f"-M-{module}") + + 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 taint_warnings: + cmd.append("-t") + if taint_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: + if print_config_variable: + cmd.append(f"-V:{print_config_variable}") + else: + cmd.append("-V") + + if enable_warnings: + cmd.append("-w") + if enable_all_warnings: + cmd.append("-W") + if disable_all_warnings: + cmd.append("-X") + + if extract_script: + if extract_dir: + cmd.append(f"-x{str(extract_dir)}") + else: + cmd.append("-x") + + # --- Add Script Source and Arguments --- + if one_liner is not None: + cmd.extend(["-e", one_liner]) + elif one_liner_extended is not None: + cmd.extend(["-E", one_liner_extended]) + elif programfile: + cmd.append(str(programfile)) + + if args: + cmd.extend(args) + + # --- 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 subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "error": "Perl execution failed." + } + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "The 'perl' command was not found. Please ensure Perl is installed and in the system's PATH.", + "return_code": 1, + "error": "Command not found." + } + + # --- Return Structured Output --- + # As this is a generic script runner, output files are not predictable. + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-convert-binary-c/app/perl-convert-binary-c_shim_server.py b/Biomni/mcp_generated/mcp_perl-convert-binary-c/app/perl-convert-binary-c_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..de2f13a0d3dc17741faa9ec22ef43b0448046756 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-convert-binary-c/app/perl-convert-binary-c_shim_server.py @@ -0,0 +1,55 @@ +#!/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-convert-binary-c/app/perl-convert-binary-c_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_convert_binary_c' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-convert-binary-c/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-convert-binary-c/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-convert-binary-c/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-convert-binary-c/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-convert-binary-c/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..3c2d2a2cc613b7e8236d1b565e86f6308026ee76 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-convert-binary-c/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-convert-binary-c: + build: . + image: mcp-perl-convert-binary-c:latest + container_name: mcp-perl-convert-binary-c + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-convert-binary-c + 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/Biomni/mcp_generated/mcp_perl-convert-binary-c/environment.yaml b/Biomni/mcp_generated/mcp_perl-convert-binary-c/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7a45189bdc22da82edd16ead87813bf89ffd2410 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-convert-binary-c/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-convert-binary-c + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-convert-binary-c/requirements.txt b/Biomni/mcp_generated/mcp_perl-convert-binary-c/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-convert-binary-c/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-data-optlist/Dockerfile b/Biomni/mcp_generated/mcp_perl-data-optlist/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..863987154dcbc1346ad372e295db1f08ff56abdf --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-data-optlist/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-data-optlist via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-data-optlist -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-data-optlist_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-data-optlist_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-data-optlist_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-data-optlist/app/perl-data-optlist_server.py b/Biomni/mcp_generated/mcp_perl-data-optlist/app/perl-data-optlist_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6196d6388b287f1d3c02ea3beefce0c239375850 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-data-optlist/app/perl-data-optlist_server.py @@ -0,0 +1,232 @@ +import subprocess +from pathlib import Path +from typing import Optional, List +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO) + +# In a real MCP environment, this would be `from mcp import tool` +# but the instructions say to not include the import. +# We will assume the decorator @mcp.tool is available. + +# This is a placeholder for the mcp.tool decorator. +# In a real MCP environment, this would be `from mcp import tool`. +class mcp: + @staticmethod + def tool(*args, **kwargs): + def decorator(func): + return func + return decorator + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_data_optlist' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_perl_script( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + execute_program_line: Optional[List[str]] = None, + execute_program_line_with_features: Optional[List[str]] = None, + record_separator: Optional[str] = None, + autosplit: bool = False, + unicode_features: Optional[str] = None, + check_syntax_only: bool = False, + run_under_debugger: Optional[str] = None, + debugging_flags: Optional[str] = None, + no_sitecustomize: bool = False, + split_pattern: Optional[str] = None, + edit_in_place: Optional[str] = None, + include_directory: Optional[List[Path]] = None, + line_ending_processing: Optional[str] = None, + use_module: Optional[List[str]] = None, + no_module: Optional[List[str]] = None, + use_module_no_import: Optional[List[str]] = None, + no_module_no_import: Optional[List[str]] = None, + loop_around_program: bool = False, + loop_and_print: bool = False, + preprocess_with_cpp: bool = False, + parse_switches: bool = False, + search_path_for_program: bool = False, + tainting_warnings: bool = False, + tainting_checks: bool = False, + dump_core: bool = False, + unsafe_operations: bool = False, + print_version: bool = False, + print_config: Optional[str] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + extract_script_from_text: bool = False, + extract_script_cd: Optional[Path] = None, + disable_all_warnings: bool = False, +): + """ + Executes a Perl script or one-liner using the Perl interpreter. + + This tool provides a comprehensive interface to the `perl` command-line executable. + It is designed to run Perl scripts, evaluate one-liners, check syntax, and manage + various runtime settings like module loading, warning levels, and input processing. + While the original request mentioned 'perl-data-optlist', that is a Perl module + (a library for parsing options within a script) and not a standalone command-line tool. + This MCP tool wraps the `perl` interpreter itself. + + Args: + programfile: Path to the Perl program file to be executed. + arguments: A list of string arguments to pass to the program file. + execute_program_line: A list of Perl code strings to execute. Corresponds to the -e flag. + execute_program_line_with_features: Like execute_program_line, but enables all optional features (-E). + record_separator: Specifies the input record separator (e.g., '0777'). Corresponds to -0[octal]. + autosplit: Enables auto-splitting of input lines into the @F array. For use with -n or -p. Corresponds to -a. + unicode_features: Enables specified Unicode features. Corresponds to -C[number/list]. + check_syntax_only: Checks the syntax of the script and then exits. Corresponds to -c. + run_under_debugger: Runs the program under the debugger. E.g., "" for default, ":MyDebugger". Corresponds to -d[:debugger]. + debugging_flags: Sets debugging flags. Corresponds to -D[number/list]. + no_sitecustomize: Skips the execution of $sitelib/sitecustomize.pl at startup. Corresponds to -f. + split_pattern: Specifies the pattern to split on for -a mode. Corresponds to -F/pattern/. + edit_in_place: Edits files in place. Provide a string for a backup extension (e.g., ".bak"). Corresponds to -i[extension]. + include_directory: A list of directories to add to Perl's @INC search path. Corresponds to -I. + line_ending_processing: Enables automatic line-ending processing. Corresponds to -l[octal]. + use_module: Executes 'use module...' before the script. Corresponds to -Mmodule. + no_module: Executes 'no module...' before the script. Corresponds to -M-module. + use_module_no_import: Executes 'use module...' but doesn't import symbols. Corresponds to -mmodule. + no_module_no_import: Executes 'no module...' Corresponds to -m-module. + loop_around_program: Assumes a 'while (<>) { ... }' loop around the script. Corresponds to -n. + loop_and_print: Like -n, but also prints the line. Corresponds to -p. + preprocess_with_cpp: Runs the C preprocessor on the script before compilation. Corresponds to -P. + parse_switches: Enables rudimentary switch parsing for switches after the program file. Corresponds to -s. + search_path_for_program: Searches for the program file in the PATH environment variable. Corresponds to -S. + tainting_warnings: Enables tainting warnings. Corresponds to -t. + tainting_checks: Enables tainting checks for security. Corresponds to -T. + dump_core: Dumps core after parsing the program. Corresponds to -u. + unsafe_operations: Allows unsafe operations. Corresponds to -U. + print_version: Prints the Perl version and exits. Corresponds to -v. + print_config: Prints configuration summary. Provide a variable name for specific info. Corresponds to -V[:variable]. + enable_warnings: Enables many useful warnings. Corresponds to -w. + enable_all_warnings: Enables all warnings. Corresponds to -W. + extract_script_from_text: Ignores text before a #!perl line. Corresponds to -x. + extract_script_cd: If `extract_script_from_text` is true, changes to this directory before running the script. + disable_all_warnings: Disables all warnings. Corresponds to -X. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + if not any([programfile, execute_program_line, execute_program_line_with_features, print_version, print_config is not None]): + raise ValueError("A program file, program line (-e/-E), version flag (-v), or config flag (-V) must be provided.") + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"Program file not found: {programfile}") + + if include_directory: + for directory in include_directory: + if not directory.is_dir(): + raise NotADirectoryError(f"Include directory not found: {directory}") + + if extract_script_cd and not extract_script_from_text: + raise ValueError("The 'extract_script_cd' option requires 'extract_script_from_text' to be enabled.") + + if extract_script_cd and not extract_script_cd.is_dir(): + raise NotADirectoryError(f"Directory for -x option not found: {extract_script_cd}") + + # --- 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 debugging_flags is not None: cmd.append(f"-D{debugging_flags}") + if no_sitecustomize: cmd.append("-f") + if split_pattern is not None: cmd.extend(["-F", str(split_pattern)]) + if edit_in_place is not None: cmd.append(f"-i{edit_in_place}") + if line_ending_processing is not None: cmd.append(f"-l{line_ending_processing}") + if loop_around_program: cmd.append("-n") + if loop_and_print: cmd.append("-p") + if preprocess_with_cpp: cmd.append("-P") + if parse_switches: cmd.append("-s") + if search_path_for_program: 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 enable_warnings: cmd.append("-w") + if enable_all_warnings: cmd.append("-W") + if disable_all_warnings: cmd.append("-X") + + if include_directory: + for directory in include_directory: + cmd.extend(["-I", str(directory)]) + + if use_module: + for module in use_module: cmd.append(f"-M{module}") + if no_module: + for module in no_module: cmd.append(f"-M-{module}") + if use_module_no_import: + for module in use_module_no_import: cmd.append(f"-m{module}") + if no_module_no_import: + for module in no_module_no_import: cmd.append(f"-m-{module}") + + if execute_program_line: + for line in execute_program_line: cmd.extend(["-e", line]) + if execute_program_line_with_features: + for line in execute_program_line_with_features: cmd.extend(["-E", line]) + + if extract_script_from_text: + cmd.append("-x") + if extract_script_cd: + cmd.append(str(extract_script_cd)) + + if programfile: + cmd.append(str(programfile)) + + if arguments: + cmd.extend(arguments) + + # --- 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 + ) + + output_files_list = [] + if edit_in_place is not None and arguments: + # Assume arguments are file paths when -i is used + for arg in arguments: + # A simple check to see if it looks like a file path and not another switch + if not arg.startswith('-'): + p = Path(arg) + if p.exists(): + output_files_list.append(str(p.resolve())) + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files_list + } + except FileNotFoundError: + logging.error("Perl command not found.") + raise RuntimeError("The 'perl' executable was not found. Please ensure Perl is installed and in your system's PATH.") + except subprocess.CalledProcessError as e: + logging.error(f"Perl execution failed with return code {e.returncode}") + # Re-raise as a more informative exception for the MCP environment + raise RuntimeError( + f"Perl execution failed with return code {e.returncode}.\n" + f"Command: {command_executed}\n" + f"Stderr: {e.stderr}\n" + f"Stdout: {e.stdout}" + ) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-data-optlist/app/perl-data-optlist_shim_server.py b/Biomni/mcp_generated/mcp_perl-data-optlist/app/perl-data-optlist_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f673a30bf456eaa0e77ade907d69ccfd57903767 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-data-optlist/app/perl-data-optlist_shim_server.py @@ -0,0 +1,55 @@ +#!/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-data-optlist/app/perl-data-optlist_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_data_optlist' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-data-optlist/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-data-optlist/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-data-optlist/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-data-optlist/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-data-optlist/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..8af41e4839a6f9fa70ac6c715e7a918694c3dc87 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-data-optlist/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-data-optlist: + build: . + image: mcp-perl-data-optlist:latest + container_name: mcp-perl-data-optlist + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-data-optlist + 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/Biomni/mcp_generated/mcp_perl-data-optlist/environment.yaml b/Biomni/mcp_generated/mcp_perl-data-optlist/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a7ae7104347cad4018c8bc5c0e784c07782941d0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-data-optlist/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-data-optlist + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-data-optlist/requirements.txt b/Biomni/mcp_generated/mcp_perl-data-optlist/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-data-optlist/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-devel-globaldestruction/requirements.txt b/Biomni/mcp_generated/mcp_perl-devel-globaldestruction/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-devel-globaldestruction/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-dynaloader/requirements.txt b/Biomni/mcp_generated/mcp_perl-dynaloader/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-dynaloader/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-eval-closure/requirements.txt b/Biomni/mcp_generated/mcp_perl-eval-closure/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-eval-closure/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-ffi-checklib/Dockerfile b/Biomni/mcp_generated/mcp_perl-ffi-checklib/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0125d3455f512f75cd7293a240bb35ba1c20914d --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-ffi-checklib/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-ffi-checklib via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-ffi-checklib -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-ffi-checklib_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-ffi-checklib_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-ffi-checklib_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-ffi-checklib/app/perl-ffi-checklib_server.py b/Biomni/mcp_generated/mcp_perl-ffi-checklib/app/perl-ffi-checklib_server.py new file mode 100644 index 0000000000000000000000000000000000000000..fff7136b366091edf1952a04b29a7539587c1a8e --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-ffi-checklib/app/perl-ffi-checklib_server.py @@ -0,0 +1,573 @@ +import subprocess +import json +import os +from pathlib import Path +from typing import List, Optional, Union, Dict, Any +import tempfile + +# Helper to build Perl arguments string for FFI::CheckLib functions +def _build_perl_args( + lib: Optional[Union[str, List[str]]] = None, + libpath: Optional[Union[str, List[str]]] = None, + systempath: Optional[Union[str, List[str]]] = None, + symbol: Optional[Union[str, List[str]]] = None, + recursive: bool = False, + try_linker_script: bool = False, + alien: Optional[Union[str, List[str]]] = None, +) -> str: + """ + Constructs a Perl hash argument string from Python parameters. + """ + args = [] + + def format_list(items: List[str]) -> str: + """Formats a Python list into a Perl array string.""" + return f"[{', '.join(f'{repr(item)}' for item in items)}]" + + if lib is not None: + if isinstance(lib, list): + args.append(f"lib => {format_list(lib)}") + else: + args.append(f"lib => {repr(lib)}") + + if libpath is not None: + if isinstance(libpath, list): + args.append(f"libpath => {format_list(libpath)}") + else: + args.append(f"libpath => {repr(libpath)}") + + if systempath is not None: + if isinstance(systempath, list): + args.append(f"systempath => {format_list(systempath)}") + else: + args.append(f"systempath => {repr(systempath)}") + + if symbol is not None: + if isinstance(symbol, list): + args.append(f"symbol => {format_list(symbol)}") + else: + args.append(f"symbol => {repr(symbol)}") + + if recursive: + args.append("recursive => 1") + + if try_linker_script: + args.append("try_linker_script => 1") + + if alien is not None: + if isinstance(alien, list): + args.append(f"alien => {format_list(alien)}") + else: + args.append(f"alien => {repr(alien)}") + + return ', '.join(args) + +# Helper to run Perl script and parse JSON output +def _run_perl_ffi_checklib_script( + perl_code: str, + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Executes a Perl script using FFI::CheckLib and captures its output. + Attempts to parse stdout as JSON. + """ + temp_script_path = None + try: + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pl', encoding='utf-8') as temp_script: + temp_script.write(perl_code) + temp_script_path = Path(temp_script.name) + + command = ["perl", str(temp_script_path)] + env = os.environ.copy() + if ffi_checklib_package: + env["FFI_CHECKLIB_PACKAGE"] = ffi_checklib_package + if ffi_checklib_path: + env["FFI_CHECKLIB_PATH"] = ffi_checklib_path + + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + env=env + ) + stdout = process.stdout.strip() + stderr = process.stderr.strip() + + parsed_output = None + if stdout: + try: + parsed_output = json.loads(stdout) + except json.JSONDecodeError: + # If stdout is not JSON, return it as raw string + parsed_output = stdout + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output": parsed_output, + "output_files": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout.strip(), + "stderr": e.stderr.strip(), + "error": f"Perl script failed with exit code {e.returncode}", + "output_files": [], + } + finally: + if temp_script_path and temp_script_path.exists(): + os.unlink(temp_script_path) + +# Helper for validating FFI_CHECKLIB_PACKAGE +def _validate_ffi_checklib_package(ffi_checklib_package: Optional[str]): + if ffi_checklib_package is not None: + allowed_packages = ["none", "homebrew", "macports"] + packages = [p.strip() for p in ffi_checklib_package.split(',')] + if not all(p in allowed_packages for p in packages): + raise ValueError( + f"Invalid value for ffi_checklib_package. Allowed values: " + f"{', '.join(allowed_packages)}, comma-separated." + ) + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_ffi_checklib' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def find_lib( + lib: Union[str, List[str]], + libpath: Optional[Union[str, List[str]]] = None, + systempath: Optional[Union[str, List[str]]] = None, + symbol: Optional[Union[str, List[str]]] = None, + recursive: bool = False, + try_linker_script: bool = False, + alien: Optional[Union[str, List[str]]] = None, + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Finds dynamic libraries available for FFI. + + Returns a list of full paths to dynamic libraries found, or an empty list if none were found. + Note: The 'verify' argument (a Perl code reference) is not supported in this MCP tool. + """ + if not lib: + raise ValueError("The 'lib' parameter cannot be empty.") + _validate_ffi_checklib_package(ffi_checklib_package) + + perl_args_str = _build_perl_args( + lib=lib, + libpath=libpath, + systempath=systempath, + symbol=symbol, + recursive=recursive, + try_linker_script=try_linker_script, + alien=alien, + ) + + perl_code = f""" +use FFI::CheckLib; +use JSON; +my @found_libs = FFI::CheckLib::find_lib({perl_args_str}); +print to_json(\\@found_libs); +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" in result: + return result + + if isinstance(result.get("output"), list): + result["output"] = [Path(item) for item in result["output"] if isinstance(item, str)] + elif isinstance(result.get("output"), str) and result["output"]: + result["output"] = [Path(result["output"])] + else: + result["output"] = [] + + return result + +@mcp.tool() +def assert_lib( + lib: Union[str, List[str]], + libpath: Optional[Union[str, List[str]]] = None, + systempath: Optional[Union[str, List[str]]] = None, + symbol: Optional[Union[str, List[str]]] = None, + recursive: bool = False, + try_linker_script: bool = False, + alien: Optional[Union[str, List[str]]] = None, + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Asserts that a dynamic library is available for FFI. + + Behaves exactly like `find_lib`, but throws an exception (Perl `die`) if no libraries are found. + Returns a list of full paths to dynamic libraries found on success. + Note: The 'verify' argument (a Perl code reference) is not supported in this MCP tool. + """ + if not lib: + raise ValueError("The 'lib' parameter cannot be empty.") + _validate_ffi_checklib_package(ffi_checklib_package) + + perl_args_str = _build_perl_args( + lib=lib, + libpath=libpath, + systempath=systempath, + symbol=symbol, + recursive=recursive, + try_linker_script=try_linker_script, + alien=alien, + ) + + perl_code = f""" +use FFI::CheckLib; +use JSON; +my @found_libs = FFI::CheckLib::assert_lib({perl_args_str}); +print to_json(\\@found_libs); +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" in result: + return result + + if isinstance(result.get("output"), list): + result["output"] = [Path(item) for item in result["output"] if isinstance(item, str)] + elif isinstance(result.get("output"), str) and result["output"]: + result["output"] = [Path(result["output"])] + else: + result["output"] = [] + + return result + +@mcp.tool() +def check_lib_or_exit( + lib: Union[str, List[str]], + libpath: Optional[Union[str, List[str]]] = None, + systempath: Optional[Union[str, List[str]]] = None, + symbol: Optional[Union[str, List[str]]] = None, + recursive: bool = False, + try_linker_script: bool = False, + alien: Optional[Union[str, List[str]]] = None, + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Checks if a dynamic library is available for FFI and exits if not found. + + Intended for use in build scripts. If the library is not found, it will warn + and exit the Perl script with a non-zero status. On success, it returns a confirmation. + Note: The 'verify' argument (a Perl code reference) is not supported in this MCP tool. + """ + if not lib: + raise ValueError("The 'lib' parameter cannot be empty.") + _validate_ffi_checklib_package(ffi_checklib_package) + + perl_args_str = _build_perl_args( + lib=lib, + libpath=libpath, + systempath=systempath, + symbol=symbol, + recursive=recursive, + try_linker_script=try_linker_script, + alien=alien, + ) + + perl_code = f""" +use FFI::CheckLib; +FFI::CheckLib::check_lib_or_exit({perl_args_str}); +print "Library check successful."; +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" not in result: + result["output"] = "Library check successful." + else: + result["output"] = "Library check failed." + + return result + +@mcp.tool() +def find_lib_or_exit( + lib: Union[str, List[str]], + libpath: Optional[Union[str, List[str]]] = None, + systempath: Optional[Union[str, List[str]]] = None, + symbol: Optional[Union[str, List[str]]] = None, + recursive: bool = False, + try_linker_script: bool = False, + alien: Optional[Union[str, List[str]]] = None, + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Finds dynamic libraries and exits with a diagnostic if not found. + + Behaves exactly like `find_lib`, but if the library is not found, it will call + exit with an appropriate diagnostic. Returns a list of full paths to dynamic libraries found on success. + Note: The 'verify' argument (a Perl code reference) is not supported in this MCP tool. + """ + if not lib: + raise ValueError("The 'lib' parameter cannot be empty.") + _validate_ffi_checklib_package(ffi_checklib_package) + + perl_args_str = _build_perl_args( + lib=lib, + libpath=libpath, + systempath=systempath, + symbol=symbol, + recursive=recursive, + try_linker_script=try_linker_script, + alien=alien, + ) + + perl_code = f""" +use FFI::CheckLib; +use JSON; +my @found_libs = FFI::CheckLib::find_lib_or_exit({perl_args_str}); +print to_json(\\@found_libs); +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" in result: + return result + + if isinstance(result.get("output"), list): + result["output"] = [Path(item) for item in result["output"] if isinstance(item, str)] + elif isinstance(result.get("output"), str) and result["output"]: + result["output"] = [Path(result["output"])] + else: + result["output"] = [] + + return result + +@mcp.tool() +def find_lib_or_die( + lib: Union[str, List[str]], + libpath: Optional[Union[str, List[str]]] = None, + systempath: Optional[Union[str, List[str]]] = None, + symbol: Optional[Union[str, List[str]]] = None, + recursive: bool = False, + try_linker_script: bool = False, + alien: Optional[Union[str, List[str]]] = None, + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Finds dynamic libraries and dies with a diagnostic if not found. + + Behaves exactly like `find_lib`, but if the library is not found, it will die + with an appropriate diagnostic. Returns a list of full paths to dynamic libraries found on success. + Note: The 'verify' argument (a Perl code reference) is not supported in this MCP tool. + """ + if not lib: + raise ValueError("The 'lib' parameter cannot be empty.") + _validate_ffi_checklib_package(ffi_checklib_package) + + perl_args_str = _build_perl_args( + lib=lib, + libpath=libpath, + systempath=systempath, + symbol=symbol, + recursive=recursive, + try_linker_script=try_linker_script, + alien=alien, + ) + + perl_code = f""" +use FFI::CheckLib; +use JSON; +my @found_libs = FFI::CheckLib::find_lib_or_die({perl_args_str}); +print to_json(\\@found_libs); +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" in result: + return result + + if isinstance(result.get("output"), list): + result["output"] = [Path(item) for item in result["output"] if isinstance(item, str)] + elif isinstance(result.get("output"), str) and result["output"]: + result["output"] = [Path(result["output"])] + else: + result["output"] = [] + + return result + +@mcp.tool() +def check_lib( + lib: Union[str, List[str]], + libpath: Optional[Union[str, List[str]]] = None, + systempath: Optional[Union[str, List[str]]] = None, + symbol: Optional[Union[str, List[str]]] = None, + recursive: bool = False, + try_linker_script: bool = False, + alien: Optional[Union[str, List[str]]] = None, + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Checks if a dynamic library is available for FFI. + + Returns true (1) if the appropriate libraries are found, or false (0) otherwise. + Note: The 'verify' argument (a Perl code reference) is not supported in this MCP tool. + """ + if not lib: + raise ValueError("The 'lib' parameter cannot be empty.") + _validate_ffi_checklib_package(ffi_checklib_package) + + perl_args_str = _build_perl_args( + lib=lib, + libpath=libpath, + systempath=systempath, + symbol=symbol, + recursive=recursive, + try_linker_script=try_linker_script, + alien=alien, + ) + + perl_code = f""" +use FFI::CheckLib; +use JSON; +my $bool_result = FFI::CheckLib::check_lib({perl_args_str}); +print to_json($bool_result ? \1 : \0); +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" in result: + return result + + output = result.get("output") + result["output"] = bool(output) + + return result + +@mcp.tool() +def which( + name: str, + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Returns the path to the first dynamic library that matches the given name. + """ + if not name: + raise ValueError("The 'name' parameter cannot be empty.") + _validate_ffi_checklib_package(ffi_checklib_package) + + perl_code = f""" +use FFI::CheckLib; +use JSON; +my $path = FFI::CheckLib::which({repr(name)}); +print to_json($path); +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" in result: + return result + + output = result.get("output") + if isinstance(output, str) and output: + result["output"] = Path(output) + else: + result["output"] = None + + return result + +@mcp.tool() +def where( + name: str, + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Returns the paths to all dynamic libraries that match the given name. + """ + if not name: + raise ValueError("The 'name' parameter cannot be empty.") + _validate_ffi_checklib_package(ffi_checklib_package) + + perl_code = f""" +use FFI::CheckLib; +use JSON; +my @paths = FFI::CheckLib::where({repr(name)}); +print to_json(\\@paths); +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" in result: + return result + + if isinstance(result.get("output"), list): + result["output"] = [Path(item) for item in result["output"] if isinstance(item, str)] + else: + result["output"] = [] + + return result + +@mcp.tool() +def has_symbols( + path: Path, + symbol_names: List[str], + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Checks if all specified symbols can be found in the dynamic library located at the given path. + """ + if not path.is_file(): + raise FileNotFoundError(f"Library file not found at: {path}") + if not symbol_names: + raise ValueError("The 'symbol_names' parameter cannot be empty.") + _validate_ffi_checklib_package(ffi_checklib_package) + + symbol_list_str = ', '.join(f"{repr(s)}" for s in symbol_names) + + perl_code = f""" +use FFI::CheckLib; +use JSON; +my $bool_result = FFI::CheckLib::has_symbols({repr(str(path))}, {symbol_list_str}); +print to_json($bool_result ? \1 : \0); +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" in result: + return result + + output = result.get("output") + result["output"] = bool(output) + + return result + +@mcp.tool() +def system_path( + ffi_checklib_package: Optional[str] = None, + ffi_checklib_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Returns the system path as a list of directories where dynamic libraries are searched. + """ + _validate_ffi_checklib_package(ffi_checklib_package) + + perl_code = f""" +use FFI::CheckLib; +use JSON; +my $path_ref = FFI::CheckLib::system_path(); +print to_json($path_ref); +""" + result = _run_perl_ffi_checklib_script(perl_code, ffi_checklib_package, ffi_checklib_path) + + if "error" in result: + return result + + if isinstance(result.get("output"), list): + result["output"] = [Path(item) for item in result["output"] if isinstance(item, str)] + else: + result["output"] = [] + + return result + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-ffi-checklib/app/perl-ffi-checklib_shim_server.py b/Biomni/mcp_generated/mcp_perl-ffi-checklib/app/perl-ffi-checklib_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1cd1fc768cad97992d72e52132589ee561c03d6f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-ffi-checklib/app/perl-ffi-checklib_shim_server.py @@ -0,0 +1,55 @@ +#!/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-ffi-checklib/app/perl-ffi-checklib_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_ffi_checklib' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-ffi-checklib/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-ffi-checklib/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-ffi-checklib/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-ffi-checklib/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-ffi-checklib/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..85e6af84643eec569195b50775817ff9453b29f2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-ffi-checklib/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-ffi-checklib: + build: . + image: mcp-perl-ffi-checklib:latest + container_name: mcp-perl-ffi-checklib + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-ffi-checklib + 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/Biomni/mcp_generated/mcp_perl-ffi-checklib/environment.yaml b/Biomni/mcp_generated/mcp_perl-ffi-checklib/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..de5049454375ae348ded8fa5140a99475d09f7d4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-ffi-checklib/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-ffi-checklib + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-ffi-checklib/requirements.txt b/Biomni/mcp_generated/mcp_perl-ffi-checklib/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-ffi-checklib/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-image-size/Dockerfile b/Biomni/mcp_generated/mcp_perl-image-size/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e85541f17667c010fbbaada30c170c150858da9f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-size/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-image-size via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-image-size -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-image-size_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-image-size_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-image-size_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-image-size/app/perl-image-size_server.py b/Biomni/mcp_generated/mcp_perl-image-size/app/perl-image-size_server.py new file mode 100644 index 0000000000000000000000000000000000000000..61cbcf78d0bd1ad62bef145cbf5680931096ad0f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-size/app/perl-image-size_server.py @@ -0,0 +1,122 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Dict, Any, Optional + +# from mcp import mcp + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_image_size' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def image_size( + image_file: Path +) -> Dict[str, Any]: + """ + Determines the width and height of an image file using the Perl Image::Size library. + + This tool acts as a wrapper around the Perl 'Image::Size' module, which does not + provide a direct command-line executable. It dynamically creates and executes a + Perl script to extract the dimensions from the specified image file. + + Args: + image_file: The path to the input image file (e.g., JPG, PNG, GIF, etc.). + + Returns: + A dictionary containing the command executed, stdout, stderr, and the + parsed width and height of the image. + """ + # 1. Input validation + if not image_file.is_file(): + raise FileNotFoundError(f"Input file not found: {image_file}") + + # 2. Perl script content + # This script uses the Image::Size module to get dimensions and prints them + # to stdout in a "width,height" format for easy parsing. + perl_script_content = r""" + use strict; + use warnings; + use Image::Size; + + # Get the image file path from the command line arguments + my $file = shift @ARGV; + + # Basic validation within the script + die "Usage: $0 \n" unless defined $file; + die "File not found or is not a regular file: $file\n" unless -f $file; + + # Get the image size + my ($width, $height) = imgsize($file); + + # Print the result in a machine-readable format if successful + if (defined $width && defined $height) { + print "$width,$height\n"; + } else { + # Print a descriptive error message to stderr if size could not be determined + my $error_msg = Image::Size::errormsg(); + print STDERR "Could not determine size for '$file': $error_msg\n"; + exit 1; + } + """ + + command_to_run = [] + try: + # 3. Create a temporary file for the Perl script + with tempfile.NamedTemporaryFile(mode='w', delete=True, suffix=".pl", encoding='utf-8') as tmp_script: + tmp_script.write(perl_script_content) + tmp_script.flush() # Ensure all content is written to disk + + command_to_run = ['perl', tmp_script.name, str(image_file)] + + # 4. Subprocess execution + process = subprocess.run( + command_to_run, + capture_output=True, + text=True, + check=True + ) + + # 5. Parse the output + stdout_clean = process.stdout.strip() + if not stdout_clean: + raise ValueError("Perl script produced no output to stdout.") + + width_str, height_str = stdout_clean.split(',') + width = int(width_str) + height = int(height_str) + + # 6. Return structured result + return { + "command_executed": " ".join(command_to_run), + "stdout": process.stdout, + "stderr": process.stderr, + "width": width, + "height": height, + "output_files": [] + } + + except FileNotFoundError: + raise RuntimeError("The 'perl' command was not found. Is Perl installed and in your system's PATH?") + except subprocess.CalledProcessError as e: + # This catches errors when the Perl script returns a non-zero exit code + return { + "command_executed": " ".join(command_to_run), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Perl script execution failed with a non-zero exit code.", + "output_files": [] + } + except (ValueError, IndexError) as e: + # This catches errors if the stdout format is not "width,height" + return { + "command_executed": " ".join(command_to_run), + "stdout": process.stdout if 'process' in locals() else "", + "stderr": process.stderr if 'process' in locals() else "", + "error": f"Failed to parse Perl script output. Expected 'width,height', got '{stdout_clean}'. Details: {e}", + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-image-size/app/perl-image-size_shim_server.py b/Biomni/mcp_generated/mcp_perl-image-size/app/perl-image-size_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b555e5617221049bd13416a0607928fab36cee37 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-size/app/perl-image-size_shim_server.py @@ -0,0 +1,55 @@ +#!/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-image-size/app/perl-image-size_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_image_size' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-image-size/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-image-size/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-size/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-image-size/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-image-size/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..ea360db949a356123325255905d6a1df2c49c518 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-size/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-image-size: + build: . + image: mcp-perl-image-size:latest + container_name: mcp-perl-image-size + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-image-size + 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/Biomni/mcp_generated/mcp_perl-image-size/environment.yaml b/Biomni/mcp_generated/mcp_perl-image-size/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..957f2faa5f1ec2947f7e6cd91f6db1fb23184444 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-size/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-image-size + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-image-size/requirements.txt b/Biomni/mcp_generated/mcp_perl-image-size/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-size/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-importer/Dockerfile b/Biomni/mcp_generated/mcp_perl-importer/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e21e934802aaef34656fed68839e1e5f53b1ca62 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-importer/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-importer via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-importer -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-importer_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-importer_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-importer_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-importer/app/perl-importer_server.py b/Biomni/mcp_generated/mcp_perl-importer/app/perl-importer_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6e51d506a1614733573678fa4cc6fec1435f67de --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-importer/app/perl-importer_server.py @@ -0,0 +1,245 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_importer' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def install_perl_module( + module_name: str, + notest: bool = True, + force: bool = False, + local_lib: Optional[str] = None, + sudo: bool = False, +): + """ + Installs Perl modules using cpanm (App::cpanminus), a common requirement for + bioinformatics pipelines (e.g., BioPerl, VCFtools). + """ + # Input validation + if not module_name: + return {"error": "Module name must be provided"} + + command = [] + if sudo: + command.append("sudo") + + command.extend(["cpanm", module_name]) + + if notest: + command.append("--notest") + if force: + command.append("--force") + if local_lib: + lib_path = Path(local_lib) + command.extend(["-l", str(lib_path.absolute())]) + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(command), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Installation failed with exit code {e.returncode}" + } + except FileNotFoundError: + return {"error": "cpanm is not installed on the system. Please install App::cpanminus."} + +@mcp.tool() +def execute_perl_script( + script_path: str, + args: Optional[List[str]] = None, + perl5lib: Optional[str] = None, + include_path: Optional[str] = None, + warnings: bool = True, +): + """ + Executes a Perl script with specified arguments and environment variables. + Commonly used for running BioPerl scripts or custom bioinformatics parsers. + """ + script = Path(script_path) + if not script.exists(): + return {"error": f"Script not found at {script_path}"} + + command = ["perl"] + if warnings: + command.append("-w") + if include_path: + command.extend(["-I", include_path]) + + command.append(str(script.absolute())) + + if args: + command.extend(args) + + env = {} + if perl5lib: + env["PERL5LIB"] = perl5lib + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + env=env if env else None + ) + return { + "command_executed": " ".join(command), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] # Perl scripts often output to stdout unless redirected + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Script execution failed with exit code {e.returncode}" + } + +@mcp.tool() +def check_perl_syntax( + script_path: str, + include_path: Optional[str] = None, +): + """ + Performs a syntax check on a Perl script (perl -c). + Useful for validating bioinformatics scripts before long-running pipeline execution. + """ + script = Path(script_path) + if not script.exists(): + return {"error": f"Script not found at {script_path}"} + + command = ["perl", "-c"] + if include_path: + command.extend(["-I", include_path]) + command.append(str(script.absolute())) + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(command), + "stdout": result.stdout, + "stderr": result.stderr, + "valid": True + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "valid": False, + "error": "Syntax check failed" + } + +@mcp.tool() +def list_installed_perl_modules( + search_term: Optional[str] = None, +): + """ + Lists installed Perl modules or searches for a specific module. + Helps verify if bioinformatics dependencies like Bio::Seq or Bio::DB::Fasta are available. + """ + # Using perldoc -l is a reliable way to find module paths + if search_term: + command = ["perldoc", "-l", search_term] + else: + # Fallback to listing all modules via instmodsh (part of ExtUtils::Installed) + # This is a bit complex in shell, so we use a perl one-liner + command = ["perl", "-MExtUtils::Installed", "-e", "print join('\\n', ExtUtils::Installed->new()->modules())"] + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(command), + "installed_modules": result.stdout.splitlines(), + "stdout": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stderr": e.stderr, + "error": "Could not retrieve module list. Ensure ExtUtils::Installed is available." + } + +@mcp.tool() +def bioperl_load_gff( + gff_file: str, + database_name: str, + db_type: str = "sqlite", + create: bool = True, + user: Optional[str] = None, + password: Optional[str] = None, +): + """ + Specialized importer tool for BioPerl's bp_seqfeature_load.pl. + Imports GFF3/GTF files into a SeqFeature database (SQLite, MySQL, or PostgreSQL). + """ + gff_path = Path(gff_file) + if not gff_path.exists(): + return {"error": f"GFF file not found: {gff_file}"} + + # Constructing the bp_seqfeature_load.pl command + command = ["bp_seqfeature_load.pl", "-d", database_name, "-f", db_type] + + if create: + command.append("-c") + if user: + command.extend(["-u", user]) + if password: + command.extend(["-p", password]) + + command.append(str(gff_path.absolute())) + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(command), + "stdout": result.stdout, + "stderr": result.stderr, + "database": database_name, + "status": "Import completed" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"BioPerl GFF import failed: {e.stderr}" + } + except FileNotFoundError: + return {"error": "bp_seqfeature_load.pl not found. Is BioPerl installed?"} + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-importer/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-importer/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-importer/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-importer/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-importer/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c7567e20b08ae22c47460f9f6c83554e2dc65c3f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-importer/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-importer: + build: . + image: mcp-perl-importer:latest + container_name: mcp-perl-importer + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-importer + 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/Biomni/mcp_generated/mcp_perl-importer/environment.yaml b/Biomni/mcp_generated/mcp_perl-importer/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dadb85fc3ebe13a606226fe218ba4cde9de05a44 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-importer/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-importer + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-importer/requirements.txt b/Biomni/mcp_generated/mcp_perl-importer/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-importer/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-io-html/Dockerfile b/Biomni/mcp_generated/mcp_perl-io-html/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..89d3c482c3ba55f7d1334e2163d7c54cea9ad674 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-io-html/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-html via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-io-html -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-html_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-html_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-html_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-io-html/app/perl-io-html_server.py b/Biomni/mcp_generated/mcp_perl-io-html/app/perl-io-html_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2dd196f548530d44f40ce9911791d7e21d4539f0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-io-html/app/perl-io-html_server.py @@ -0,0 +1,173 @@ +import subprocess +from pathlib import Path +from typing import Optional, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_io_html' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def io_html_get_encoding( + file_path: str, +) -> Dict[str, Any]: + """ + Detects the character encoding of an HTML file using the HTML5 sniffing algorithm. + This is equivalent to the IO::HTML::html_file_and_encoding function. + + Args: + file_path: Path to the HTML file to analyze. + """ + path = Path(file_path) + if not path.exists(): + return {"error": f"File not found: {file_path}"} + if not path.is_file(): + return {"error": f"Path is not a file: {file_path}"} + + # Perl script to extract encoding and BOM status + # html_file_and_encoding returns ($filehandle, $encoding, $bom) + perl_cmd = [ + "perl", + "-MIO::HTML", + "-e", + 'my ($fh, $enc, $bom) = IO::HTML::html_file_and_encoding($ARGV[0]); ' + 'print "encoding:$enc\\n"; ' + 'print "bom:" . ($bom ? "true" : "false") . "\\n";', + str(path) + ] + + try: + result = subprocess.run(perl_cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(perl_cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "detected_info": result.stdout.strip().split('\n') + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(perl_cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def io_html_read_content( + file_path: str, +) -> Dict[str, Any]: + """ + Opens an HTML file with the correctly detected encoding and returns its content. + This ensures that the HTML is read using the charset specified in the tags or BOM. + + Args: + file_path: Path to the HTML file to read. + """ + path = Path(file_path) + if not path.exists(): + return {"error": f"File not found: {file_path}"} + + # Perl script to open file with IO::HTML and print content to stdout + # We use binmode STDOUT :utf8 to ensure the output is passed correctly to the shell + perl_cmd = [ + "perl", + "-MIO::HTML", + "-e", + 'my $fh = IO::HTML::html_file($ARGV[0]); ' + 'binmode(STDOUT, ":utf8"); ' + 'while(<$fh>){ print $_; }', + str(path) + ] + + try: + result = subprocess.run(perl_cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(perl_cmd), + "content": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(perl_cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def io_html_sniff_encoding( + html_content: str, +) -> Dict[str, Any]: + """ + Sniffs the encoding from a string of HTML content. + Useful for detecting encoding of HTML data already in memory or from a stream. + + Args: + html_content: The raw HTML string/bytes to analyze for encoding. + """ + # We pass the content via a temporary file or stdin to avoid shell argument length limits + # Here we use a simple perl one-liner that takes the string as an argument + perl_cmd = [ + "perl", + "-MIO::HTML", + "-e", + 'my $encoding = IO::HTML::sniff_encoding($ARGV[0]); ' + 'print $encoding;', + html_content + ] + + try: + result = subprocess.run(perl_cmd, capture_output=True, text=True, check=True) + return { + "command_executed": "perl -MIO::HTML -e '...sniff_encoding...'", + "detected_encoding": result.stdout.strip(), + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": "perl -MIO::HTML -e '...sniff_encoding...'", + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def io_html_check_compatibility( + file_path: str, +) -> Dict[str, Any]: + """ + Checks if the IO::HTML module is correctly installed and can process the target file. + This is a diagnostic tool for the environment. + + Args: + file_path: Path to a sample HTML file. + """ + path = Path(file_path) + + perl_cmd = [ + "perl", + "-MIO::HTML", + "-e", + 'print "IO::HTML version: $IO::HTML::VERSION\\n"; ' + 'eval { IO::HTML::html_file($ARGV[0]) }; ' + 'if ($@) { print "Error: $@"; exit 1; } else { print "Status: OK\\n"; }', + str(path) if path.exists() else "" + ] + + try: + result = subprocess.run(perl_cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(perl_cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(perl_cmd), + "error": "Module IO::HTML might not be installed or file is inaccessible.", + "stdout": e.stdout, + "stderr": e.stderr + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-io-html/app/perl-io-html_shim_server.py b/Biomni/mcp_generated/mcp_perl-io-html/app/perl-io-html_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..72c30f79f43b0f5816b98e67d3ab436abb315cbc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-io-html/app/perl-io-html_shim_server.py @@ -0,0 +1,55 @@ +#!/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-html/app/perl-io-html_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_io_html' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-io-html/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-io-html/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-io-html/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-io-html/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-io-html/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..e8d588e44df8502d977f5fa6820119ac52ee8318 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-io-html/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-io-html: + build: . + image: mcp-perl-io-html:latest + container_name: mcp-perl-io-html + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-io-html + 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/Biomni/mcp_generated/mcp_perl-io-html/environment.yaml b/Biomni/mcp_generated/mcp_perl-io-html/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cac91f069c11d84b42c72b87cfe6d73303945a06 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-io-html/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-io-html + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-io-html/requirements.txt b/Biomni/mcp_generated/mcp_perl-io-html/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-io-html/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-json/Dockerfile b/Biomni/mcp_generated/mcp_perl-json/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4cbbe0ed169a2427c11c1fbda20c83f14baf9dce --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-json/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-json via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-json -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-json_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-json_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-json_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-json/app/perl-json_server.py b/Biomni/mcp_generated/mcp_perl-json/app/perl-json_server.py new file mode 100644 index 0000000000000000000000000000000000000000..37cc314f6499ab8908c9d5abf397a8132b828a7e --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-json/app/perl-json_server.py @@ -0,0 +1,276 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_json' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def perl_json_encode( + perl_code: str, + ascii: bool = False, + utf8: bool = True, + pretty: bool = False, + canonical: bool = False, + allow_nonref: bool = True, + convert_blessed: bool = False, + space_before: bool = False, + space_after: bool = False, +): + """ + Encodes a Perl data structure into a JSON string using the perl-json module. + + Args: + perl_code: A string containing a Perl data structure (e.g., '{a => 1, b => [2, 3]}'). + ascii: If true, encodes non-ASCII characters as \\uXXXX escapes. + utf8: If true, produces UTF-8 encoded binary string. + pretty: Enables indent, space_before, and space_after for readable output. + canonical: Sorts object keys alphabetically. + allow_nonref: Allows encoding of non-reference scalars (strings, numbers, null). + convert_blessed: Allows encoding of blessed objects (requires TO_JSON method or universal conversion). + space_before: Adds a space before the ':' in key-value pairs. + space_after: Adds a space after the ':' and ',' in JSON. + """ + # Construct the Perl one-liner + # We use JSON->new and chain the methods based on parameters + perl_script = "use JSON; " + perl_script += "my $json = JSON->new;" + if ascii: perl_script += "$json->ascii(1);" + if utf8: perl_script += "$json->utf8(1);" + if pretty: perl_script += "$json->pretty(1);" + if canonical: perl_script += "$json->canonical(1);" + if allow_nonref: perl_script += "$json->allow_nonref(1);" + if convert_blessed: perl_script += "$json->convert_blessed(1);" + if space_before: perl_script += "$json->space_before(1);" + if space_after: perl_script += "$json->space_after(1);" + + # The eval(shift) takes the perl_code from the command line arguments + perl_script += "print $json->encode(eval(shift));" + + try: + result = subprocess.run( + ["perl", "-MJSON", "-e", perl_script, "--", perl_code], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"perl -MJSON -e '...' -- '{perl_code}'", + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": e.cmd, + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def perl_json_decode( + json_text: str, + utf8: bool = True, + relaxed: bool = False, + allow_nonref: bool = True, + max_depth: int = 512, +): + """ + Decodes a JSON string into a Perl data structure representation (using Data::Dumper). + + Args: + json_text: The JSON string to decode. + utf8: If true, expects the input to be UTF-8 encoded. + relaxed: Allows shell-style comments and trailing commas in the JSON. + allow_nonref: Allows decoding of non-reference JSON values (scalars). + max_depth: Sets the maximum nesting depth for the JSON structure. + """ + if max_depth < 1: + max_depth = 512 + + perl_script = "use JSON; use Data::Dumper; " + perl_script += "$Data::Dumper::Terse = 1; $Data::Dumper::Indent = 1; " + perl_script += "my $json = JSON->new;" + if utf8: perl_script += "$json->utf8(1);" + if relaxed: perl_script += "$json->relaxed(1);" + if allow_nonref: perl_script += "$json->allow_nonref(1);" + perl_script += f"$json->max_depth({max_depth});" + + perl_script += "print Dumper($json->decode(shift));" + + try: + result = subprocess.run( + ["perl", "-MJSON", "-MData::Dumper", "-e", perl_script, "--", json_text], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"perl -MJSON -MData::Dumper -e '...' -- '{json_text}'", + "perl_structure": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": e.cmd, + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def perl_json_reformat_file( + input_file: str, + output_file: Optional[str] = None, + pretty: bool = True, + canonical: bool = True, + ascii: bool = False, + relaxed: bool = False, +): + """ + Reads a JSON file and reformats it (pretty-print or minify). + + Args: + input_file: Path to the input JSON file. + output_file: Path to save the reformatted JSON. If not provided, returns to stdout. + pretty: If true, produces human-readable indented output. + canonical: If true, sorts object keys. + ascii: If true, escapes non-ASCII characters. + relaxed: If true, allows comments and trailing commas in the input file. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + perl_script = "use JSON; use File::Slurp; " + # Note: Using a simple file read if File::Slurp isn't guaranteed, + # but standard perl-json users often have it. We'll use a core-only way for safety. + perl_script = ( + "use JSON; " + "undef $/; " + "my $content = <>; " + "my $json = JSON->new;" + ) + + if pretty: perl_script += "$json->pretty(1);" + if canonical: perl_script += "$json->canonical(1);" + if ascii: perl_script += "$json->ascii(1);" + if relaxed: perl_script += "$json->relaxed(1);" + + perl_script += "print $json->encode($json->decode($content));" + + cmd = ["perl", "-MJSON", "-e", perl_script, input_file] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + output_files = [] + if output_file: + out_path = Path(output_file) + out_path.write_text(result.stdout) + output_files.append(str(out_path)) + return { + "command_executed": " ".join(cmd), + "message": f"Reformatted JSON saved to {output_file}", + "output_files": output_files, + "status": "success" + } + else: + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": e.cmd, + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "error" + } + +@mcp.tool() +def perl_json_validate_file( + input_file: str, + relaxed: bool = False, +): + """ + Validates the syntax of a JSON file. + + Args: + input_file: Path to the JSON file to validate. + relaxed: If true, allows comments and trailing commas during validation. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + perl_script = ( + "use JSON; " + "undef $/; " + "my $content = <>; " + "my $json = JSON->new;" + ) + if relaxed: perl_script += "$json->relaxed(1);" + perl_script += "eval { $json->decode($content) }; if ($@) { die $@ } else { print 'Valid JSON' }" + + try: + result = subprocess.run( + ["perl", "-MJSON", "-e", perl_script, input_file], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"perl -MJSON -e '...' {input_file}", + "stdout": result.stdout, + "is_valid": True, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": e.cmd, + "stdout": e.stdout, + "stderr": e.stderr, + "is_valid": False, + "error_message": e.stderr.strip(), + "status": "invalid" + } + +@mcp.tool() +def perl_json_get_backend(): + """ + Identifies which backend module (JSON::XS, JSON::PP, or Cpanel::JSON::XS) is currently being used by the JSON module. + """ + perl_script = "use JSON; my $json = JSON->new; print $json->backend;" + + try: + result = subprocess.run( + ["perl", "-MJSON", "-e", perl_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"perl -MJSON -e '{perl_script}'", + "backend": result.stdout, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": e.cmd, + "error": str(e), + "status": "error" + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-json/app/perl-json_shim_server.py b/Biomni/mcp_generated/mcp_perl-json/app/perl-json_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a19147395595fb7545254ffcf75fcad2b8351210 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-json/app/perl-json_shim_server.py @@ -0,0 +1,55 @@ +#!/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-json/app/perl-json_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_json' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-json/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-json/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-json/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-json/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-json/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..87a79e338c5c185eba1959ea07f471d116bc10f6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-json/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-json: + build: . + image: mcp-perl-json:latest + container_name: mcp-perl-json + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-json + 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/Biomni/mcp_generated/mcp_perl-json/environment.yaml b/Biomni/mcp_generated/mcp_perl-json/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..72ba2caaab2c492444ec46c032118d63bf77c146 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-json/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-json + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-json/requirements.txt b/Biomni/mcp_generated/mcp_perl-json/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-json/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-lwp-simple/requirements.txt b/Biomni/mcp_generated/mcp_perl-lwp-simple/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-lwp-simple/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-math-bezier/Dockerfile b/Biomni/mcp_generated/mcp_perl-math-bezier/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8e1c93b29d3e5a1566628158daa83817e3f62e05 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-math-bezier/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-math-bezier via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-math-bezier -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-math-bezier_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-math-bezier_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-math-bezier_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-math-bezier/app/perl-math-bezier_server.py b/Biomni/mcp_generated/mcp_perl-math-bezier/app/perl-math-bezier_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ae03a34bd531a4bf5cdafd745a7d7c3f40f0beb0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-math-bezier/app/perl-math-bezier_server.py @@ -0,0 +1,135 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# MCP decorator is omitted as per instructions, but this is an MCP-ready tool. +# import mcp + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_math_bezier' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_perl_script( + programfile: Optional[Path] = None, + program: Optional[str] = None, + arguments: Optional[List[str]] = None, + include_dirs: Optional[List[Path]] = None, + modules: Optional[List[str]] = None, + check_syntax_only: bool = False, + enable_warnings: bool = False, + disable_warnings: bool = False, + enable_all_warnings: bool = False, + enable_taint_checks: bool = False, + enable_optional_features_for_program: bool = False, +) -> dict: + """ + Executes a Perl script using the Perl interpreter. + + This tool serves as a generic wrapper for the `perl` command-line interpreter. + The `perl-math-bezier` package is a library and does not provide a direct executable. + To use it, you must provide a Perl script or a one-liner that utilizes the library's modules (e.g., via the -M flag or 'use Math::Bezier;' in the script). + + Args: + programfile: Path to the Perl program file to execute. + program: A one-liner string of a Perl program to execute (uses -e or -E). + arguments: A list of arguments to pass to the program file. + include_dirs: A list of directories to add to Perl's @INC search path (uses -I). + modules: A list of Perl modules to load before executing the program (uses -M). + Example: ["Math::Bezier"] + check_syntax_only: If True, checks the syntax of the script without executing it (uses -c). + enable_warnings: If True, enables many useful warnings (uses -w). + disable_warnings: If True, disables all warnings (uses -X). This overrides other warning flags. + enable_all_warnings: If True, enables all warnings (uses -W). This is overridden by disable_warnings. + enable_taint_checks: If True, enables tainting checks for security (uses -T). + enable_optional_features_for_program: If True and `program` is specified, enables all optional features (uses -E instead of -e). + + Returns: + A dictionary containing the executed command, stdout, and stderr. + """ + # --- Input Validation --- + if not programfile and not program: + raise ValueError("Either 'programfile' or 'program' must be provided.") + if programfile and program: + raise ValueError("'programfile' and 'program' are mutually exclusive.") + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"The program file '{programfile}' does not exist.") + + if enable_optional_features_for_program and not program: + raise ValueError("'enable_optional_features_for_program' can only be True when 'program' is specified.") + + if include_dirs: + for directory in include_dirs: + if not directory.is_dir(): + raise FileNotFoundError(f"The include directory '{directory}' does not exist.") + + # --- Command Construction --- + cmd = ["perl"] + + # Handle warning flags (priority: -X > -W > -w) + if disable_warnings: + cmd.append("-X") + elif enable_all_warnings: + cmd.append("-W") + elif enable_warnings: + cmd.append("-w") + + if check_syntax_only: + cmd.append("-c") + + if enable_taint_checks: + cmd.append("-T") + + if include_dirs: + for directory in include_dirs: + cmd.extend(["-I", str(directory)]) + + if modules: + for module in modules: + cmd.extend(["-M", module]) + + # Handle program source + if program: + if enable_optional_features_for_program: + cmd.extend(["-E", program]) + else: + cmd.extend(["-e", program]) + elif programfile: + cmd.append(str(programfile)) + + # Add script arguments + if arguments: + cmd.extend(arguments) + + command_executed = " ".join(cmd) + + # --- Subprocess Execution --- + 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": [] # This tool does not explicitly create output files + } + except FileNotFoundError: + raise RuntimeError("Perl executable not found. Please ensure it is in your system's PATH.") + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Perl script execution failed with return code {e.returncode}", + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-math-bezier/app/perl-math-bezier_shim_server.py b/Biomni/mcp_generated/mcp_perl-math-bezier/app/perl-math-bezier_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..193c1a624d37d9d26085449470c141e8381e94bc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-math-bezier/app/perl-math-bezier_shim_server.py @@ -0,0 +1,55 @@ +#!/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-math-bezier/app/perl-math-bezier_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_math_bezier' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-math-bezier/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-math-bezier/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-math-bezier/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-math-bezier/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-math-bezier/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..bcbe4db6c3bb16a4727957bd9bf6c56430ffdb2f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-math-bezier/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-math-bezier: + build: . + image: mcp-perl-math-bezier:latest + container_name: mcp-perl-math-bezier + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-math-bezier + 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/Biomni/mcp_generated/mcp_perl-math-bezier/environment.yaml b/Biomni/mcp_generated/mcp_perl-math-bezier/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c7fb857c5b0096c00dc149299eb0d2fc44ae908d --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-math-bezier/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-math-bezier + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-math-bezier/requirements.txt b/Biomni/mcp_generated/mcp_perl-math-bezier/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-math-bezier/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/Dockerfile b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ffb5eaac759ff0959075bb715683824bbc7e7dde --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/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-metabolomics-fragment-annotation via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-metabolomics-fragment-annotation -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 perl-metabolomics-fragment-annotation_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-metabolomics-fragment-annotation_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-metabolomics-fragment-annotation_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/app/perl-metabolomics-fragment-annotation_server.py b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/app/perl-metabolomics-fragment-annotation_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e55949336b837f6c8f4f9c80f3ee54f8378589ec --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/app/perl-metabolomics-fragment-annotation_server.py @@ -0,0 +1,148 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, List + +# The mcp.tool decorator is not defined here, as per the problem description. +# It is assumed to be provided by the MCP framework. +# +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_metabolomics_fragment_annotation' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +# def placeholder(): +# pass + + +@mcp.tool() +def fragment_annotation( + in_file: Path, + out_file: Path, + db_file: Optional[Path] = None, + mode: str = "positive", + precursor_mz: Optional[float] = None, + precursor_adduct: Optional[str] = None, + fragment_tol: float = 0.01, + fragment_tol_unit: str = "Da", + min_intensity: float = 1.0, + max_depth: int = 2, + verbose: bool = False, +) -> dict: + """ + Annotates MS/MS fragment ions from metabolomics data using fragment-annotation.pl. + + This tool takes a peak list file (e.g., MGF, MSP) and annotates the fragments + against a compound database. + + Args: + in_file: Input file containing peak list data (e.g., MGF, MSP). + out_file: Output file for annotated results (e.g., CSV, TSV). + db_file: Database file of known compounds (e.g., SDF, MOL). Highly recommended for annotation. + mode: Ionization mode. Must be 'positive' or 'negative'. + precursor_mz: Precursor m/z value. If not provided, it will be read from the input file. + precursor_adduct: Precursor adduct type (e.g., '[M+H]+', '[M-H]-'). + fragment_tol: Fragment tolerance value. Must be a positive number. + fragment_tol_unit: Fragment tolerance unit. Must be 'Da' or 'ppm'. + min_intensity: Minimum relative intensity for fragments to be considered (0-100). + max_depth: Maximum fragmentation depth to consider. Must be a positive integer. + verbose: Enable verbose logging. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # 1. Input Validation + if not in_file.is_file(): + raise FileNotFoundError(f"Input file not found: {in_file}") + + if db_file and not db_file.is_file(): + raise FileNotFoundError(f"Database file not found: {db_file}") + + if out_file.parent and not out_file.parent.exists(): + out_file.parent.mkdir(parents=True, exist_ok=True) + + if mode not in ["positive", "negative"]: + raise ValueError(f"Invalid mode '{mode}'. Must be 'positive' or 'negative'.") + + if fragment_tol <= 0: + raise ValueError(f"Fragment tolerance must be positive, but got {fragment_tol}.") + + if fragment_tol_unit not in ["Da", "ppm"]: + raise ValueError(f"Invalid fragment tolerance unit '{fragment_tol_unit}'. Must be 'Da' or 'ppm'.") + + if not (0.0 <= min_intensity <= 100.0): + raise ValueError(f"Minimum intensity must be between 0 and 100, but got {min_intensity}.") + + if max_depth <= 0: + raise ValueError(f"Maximum depth must be a positive integer, but got {max_depth}.") + + # 2. Command Construction + # Assuming the tool is available in the system's PATH as 'fragment-annotation.pl' + cmd = [ + "fragment-annotation.pl", + "--in", str(in_file), + "--out", str(out_file), + "--mode", mode, + "--fragment_tol", str(fragment_tol), + "--fragment_tol_unit", fragment_tol_unit, + "--min_intensity", str(min_intensity), + "--max_depth", str(max_depth), + ] + + if db_file: + cmd.extend(["--db", str(db_file)]) + if precursor_mz is not None: + cmd.extend(["--precursor_mz", str(precursor_mz)]) + if precursor_adduct: + cmd.extend(["--precursor_adduct", precursor_adduct]) + if verbose: + cmd.append("--verbose") + + # 3. Subprocess Execution and Error Handling + command_executed = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + + # 4. Structured Result Return (Success) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(out_file)] + } + except FileNotFoundError: + # This error occurs if 'fragment-annotation.pl' is not in the PATH + raise RuntimeError("The command 'fragment-annotation.pl' was not found. Please ensure the tool is installed and in your system's PATH.") + except subprocess.CalledProcessError as e: + # This error occurs if the tool returns a non-zero exit code + # The MCP framework will typically handle this exception, but returning a + # structured error provides more context to the user. + error_message = ( + f"Tool execution failed with return code {e.returncode}.\n" + f"Command: {command_executed}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + # Re-raising is often better for MCP to handle, but if we must return: + # For this problem, we will re-raise to let the MCP framework handle it, + # as this is a more robust pattern. If a dictionary must be returned, + # the following code could be used instead of raising the exception. + # + # return { + # "command_executed": command_executed, + # "stdout": e.stdout, + # "stderr": e.stderr, + # "error": f"Tool execution failed with return code {e.returncode}.", + # "output_files": [] + # } + raise RuntimeError(error_message) from e + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/app/perl-metabolomics-fragment-annotation_shim_server.py b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/app/perl-metabolomics-fragment-annotation_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9cfce633eec90492e13797d830b08561422f2bad --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/app/perl-metabolomics-fragment-annotation_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_perl-metabolomics-fragment-annotation/app/perl-metabolomics-fragment-annotation_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_metabolomics_fragment_annotation' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b523ed6b72467dcd5874c2cefd94dcd3ef457613 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-metabolomics-fragment-annotation: + build: . + image: mcp-perl-metabolomics-fragment-annotation:latest + container_name: mcp-perl-metabolomics-fragment-annotation + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-metabolomics-fragment-annotation + 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/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/environment.yaml b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ded292d05dc8663928c1e34800953b626c95932f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-metabolomics-fragment-annotation + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/requirements.txt b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-metabolomics-fragment-annotation/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-moo/Dockerfile b/Biomni/mcp_generated/mcp_perl-moo/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1fe909d9ff33039e1b5d67c1a250d94c66665c94 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moo/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-moo via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-moo -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-moo_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-moo_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-moo_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-moo/app/perl-moo_server.py b/Biomni/mcp_generated/mcp_perl-moo/app/perl-moo_server.py new file mode 100644 index 0000000000000000000000000000000000000000..4644ae69dc2cf558a1e82a919a9ab808219cc3aa --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moo/app/perl-moo_server.py @@ -0,0 +1,224 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Union + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_moo' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_perl_script( + program_file: Optional[Path] = None, + program_string: Optional[List[str]] = None, + program_string_with_features: Optional[List[str]] = None, + program_arguments: Optional[List[str]] = None, + 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: Optional[List[Path]] = None, + line_ending_processing: Optional[str] = None, + modules_to_use: Optional[List[str]] = None, + loop_around_program: bool = False, + loop_and_print: bool = False, + rudimentary_switch_parsing: bool = False, + search_path_for_programfile: bool = False, + tainting_warnings: bool = False, + tainting_checks: bool = False, + dump_core_after_parsing: bool = False, + allow_unsafe_operations: bool = False, + print_version: bool = False, + print_config_summary: Optional[str] = None, # Use "" for -V, None for no -V, "variable" for -V:variable + enable_warnings: bool = False, + enable_all_warnings: bool = False, + ignore_text_before_perl_line: Optional[Path] = None, + disable_all_warnings: bool = False, +) -> dict: + """ + Executes a Perl script or one-liner using the Perl interpreter. + + This tool provides access to the various command-line switches of the Perl + interpreter, allowing for flexible execution of Perl code. The 'perl-moo' + package is a Perl module, not a standalone executable, and is used within + Perl scripts executed by the 'perl' interpreter. + + Args: + program_file: Path to the Perl program file to execute. + program_string: One or more lines of Perl program code to execute. + Can be specified multiple times via a list. + program_string_with_features: One or more lines of Perl program code, + like `program_string`, but enables all + optional features. Can be specified multiple times. + program_arguments: Arguments to pass to the Perl program. + record_separator: Specify record separator (e.g., '\\0' for null, or an octal value). + autosplit_mode: Enable autosplit mode with -n or -p (splits $_ into @F). + unicode_features: Enables the listed Unicode features (number or list). + check_syntax_only: Check syntax only (runs BEGIN and CHECK blocks). + debugger: Run program under debugger (optional debugger name). + 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., '/pattern/'). + edit_in_place_extension: Edit <> files in place (makes backup if extension supplied). + include_directories: Specify @INC/#include directory (can be multiple). + line_ending_processing: Enable line ending processing, specifies line terminator (octal). + modules_to_use: Execute "use/no module..." before executing program. + Each string in the list should be the module name (e.g., "Moo") + or a full flag (e.g., "-Mstrict", "-m-warnings"). + loop_around_program: Assume "while (<>) { ... }" loop around program. + loop_and_print: Assume loop like -n but print line also, like sed. + rudimentary_switch_parsing: Enable rudimentary parsing for switches after programfile. + search_path_for_programfile: Look for programfile using PATH environment variable. + tainting_warnings: Enable tainting warnings. + tainting_checks: Enable tainting checks. + dump_core_after_parsing: Dump core after parsing program. + allow_unsafe_operations: Allow unsafe operations. + print_version: Print Perl version, patchlevel and license. + print_config_summary: Print configuration summary. Pass an empty string `""` for `-V` + or a variable name (e.g., `"archname"`) for `-V:archname`. + enable_warnings: Enable many useful warnings. + enable_all_warnings: Enable all warnings. + ignore_text_before_perl_line: Ignore text before #!perl line (optionally cd to directory). + disable_all_warnings: Disable all warnings. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + """ + command = ["perl"] + output_files: List[str] = [] + + # Input validation + if not (program_file or program_string or program_string_with_features or print_version or print_config_summary is not None): + raise ValueError( + "At least one of 'program_file', 'program_string', 'program_string_with_features', " + "'print_version', or 'print_config_summary' must be provided for a meaningful execution." + ) + + if loop_around_program and loop_and_print: + raise ValueError("Options 'loop_around_program' (-n) and 'loop_and_print' (-p) are mutually exclusive.") + + # Build the command list + if record_separator is not None: + command.append(f"-0{record_separator}") + if autosplit_mode: + command.append("-a") + if unicode_features is not None: + command.append(f"-C{unicode_features}") + if check_syntax_only: + command.append("-c") + if debugger is not None: + command.append(f"-d:{debugger}") + if debugging_flags is not None: + command.append(f"-D{debugging_flags}") + if program_string: + for p_str in program_string: + command.extend(["-e", p_str]) + if program_string_with_features: + for p_str in program_string_with_features: + command.extend(["-E", p_str]) + if no_sitecustomize: + command.append("-f") + if split_pattern is not None: + command.append(f"-F{split_pattern}") + if edit_in_place_extension is not None: + command.append(f"-i{edit_in_place_extension}") + if include_directories: + for inc_dir in include_directories: + if not inc_dir.is_dir(): + raise ValueError(f"Include directory '{inc_dir}' does not exist or is not a directory.") + command.extend(["-I", str(inc_dir)]) + if line_ending_processing is not None: + command.append(f"-l{line_ending_processing}") + if modules_to_use: + for module in modules_to_use: + # If module string doesn't start with -m or -M, prepend -M by default. + if not module.startswith(("-m", "-M")): + command.append(f"-M{module}") + else: + command.append(module) + 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_programfile: + command.append("-S") + if tainting_warnings: + command.append("-t") + if tainting_checks: + command.append("-T") + if dump_core_after_parsing: + 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 "-V") + if enable_warnings: + command.append("-w") + if enable_all_warnings: + command.append("-W") + if ignore_text_before_perl_line is not None: + if not ignore_text_before_perl_line.is_dir(): + raise ValueError(f"Directory for -x '{ignore_text_before_perl_line}' does not exist or is not a directory.") + command.append(f"-x{ignore_text_before_perl_line}") + if disable_all_warnings: + command.append("-X") + + # Add program file and its arguments + if program_file: + if not program_file.is_file(): + raise FileNotFoundError(f"Perl program file '{program_file}' not found.") + command.append(str(program_file)) + + if program_arguments: + command.extend(program_arguments) + + try: + process = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + encoding="utf-8" + ) + stdout = process.stdout + stderr = process.stderr + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } + except FileNotFoundError: + return { + "command_executed": " ".join(command), + "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": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + } + except ValueError as e: + return { + "command_executed": " ".join(command), + "stdout": "", + "stderr": f"Input validation error: {e}", + "output_files": [], + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-moo/app/perl-moo_shim_server.py b/Biomni/mcp_generated/mcp_perl-moo/app/perl-moo_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9ba83cb0cbbb0fc57e1a8a7054799b409de8d9c8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moo/app/perl-moo_shim_server.py @@ -0,0 +1,55 @@ +#!/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-moo/app/perl-moo_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_moo' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-moo/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-moo/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moo/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-moo/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-moo/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..f5eef285b4d280cdfa1ebd00737b062ebd47872b --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moo/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-moo: + build: . + image: mcp-perl-moo:latest + container_name: mcp-perl-moo + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-moo + 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/Biomni/mcp_generated/mcp_perl-moo/environment.yaml b/Biomni/mcp_generated/mcp_perl-moo/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cfde2e7404f7b59df9e9bf83e7ea5b3dcef00f02 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moo/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-moo + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-moo/requirements.txt b/Biomni/mcp_generated/mcp_perl-moo/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moo/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-moose/Dockerfile b/Biomni/mcp_generated/mcp_perl-moose/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..7139289a40a4222e610016961dec2d808edbb423 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moose/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-moose via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-moose -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-moose_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-moose_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-moose_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-moose/app/perl-moose_server.py b/Biomni/mcp_generated/mcp_perl-moose/app/perl-moose_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b3027943ed5f8a9dc3041e1166530b681f742c5d --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moose/app/perl-moose_server.py @@ -0,0 +1,210 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_moose' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def perl_interpreter( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + execute_program: Optional[List[str]] = None, + execute_program_extended: Optional[List[str]] = None, + record_separator: Optional[str] = None, + autosplit: 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, + in_place_edit: Optional[str] = None, + include_dirs: Optional[List[Path]] = None, + line_ending_processing: Optional[str] = None, + modules: Optional[List[str]] = None, + loop_around_program: bool = False, + loop_and_print: bool = False, + enable_switch_parsing: bool = False, + search_path_for_program: bool = False, + taint_warnings: bool = False, + taint_checks: bool = False, + dump_core: bool = False, + allow_unsafe: bool = False, + print_version: bool = False, + print_config: Optional[str] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + extract_from_text: Optional[str] = None, + disable_all_warnings: bool = False, +) -> Dict[str, Any]: + """ + Executes a script using the Perl interpreter. + + This tool is a wrapper for the `perl` command-line executable. It can run any + Perl script, including those that use modules like Moose. + + Args: + programfile: Path to the Perl program file to execute. + arguments: A list of arguments to pass to the program file. + execute_program: One or more lines of a program passed as a string (-e). + execute_program_extended: Like -e, but enables all optional features (-E). + record_separator: Specify record separator, e.g., '777' for octal (-0). + autosplit: Enable autosplit mode with -n or -p (-a). + unicode_features: Enable listed Unicode features, e.g., 'A' or '8' (-C). + check_syntax_only: Check syntax only; does not execute the program (-c). + debugger: Run program under the specified debugger, e.g., 'Deparse' (-d). + debugging_flags: Set debugging flags, e.g., 'p' or '14' (-D). + no_sitecustomize: Don't run $sitelib/sitecustomize.pl at startup (-f). + split_pattern: Specify the split() pattern for the -a switch (-F). + in_place_edit: Edit files in place. Provide an extension for backups, e.g., '.bak' (-i). + include_dirs: List of directories to add to @INC search path (-I). + line_ending_processing: Enable line ending processing; can specify octal terminator (-l). + modules: List of modules to use/no, e.g., ["-Mstrict", "-mwarnings", "-M-CGI"]. + loop_around_program: Assume a "while (<>) { ... }" loop around the program (-n). + loop_and_print: Like -n, but also prints the line, similar to sed (-p). + enable_switch_parsing: Enable rudimentary parsing for switches after programfile (-s). + search_path_for_program: Look for programfile using the PATH environment variable (-S). + taint_warnings: Enable tainting warnings (-t). + taint_checks: Enable tainting checks (-T). + dump_core: Dump core after parsing the program (-u). + allow_unsafe: Allow unsafe operations (-U). + print_version: Print Perl version, patchlevel, and license (-v). + print_config: Print configuration summary or a single variable, e.g., 'archname' (-V). + enable_warnings: Enable many useful warnings (-w). + enable_all_warnings: Enable all warnings (-W). + extract_from_text: Ignore text before #!perl line; optionally cd to a directory (-x). + disable_all_warnings: Disable all warnings (-X). + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + # --- Input Validation --- + source_provided = any([ + programfile, + execute_program, + execute_program_extended, + print_version, + print_config is not None + ]) + if not source_provided: + raise ValueError( + "No program source provided. You must specify 'programfile', " + "'execute_program', 'execute_program_extended', 'print_version', " + "or 'print_config'." + ) + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"The specified program file does not exist: {programfile}") + + if loop_around_program and loop_and_print: + raise ValueError("The flags '-n' (loop_around_program) and '-p' (loop_and_print) are mutually exclusive.") + + warning_flags = [enable_warnings, enable_all_warnings, disable_all_warnings] + if sum(warning_flags) > 1: + raise ValueError("Warning flags '-w' (enable_warnings), '-W' (enable_all_warnings), and '-X' (disable_all_warnings) are mutually exclusive.") + + # --- 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 in_place_edit is not None: + cmd.append(f"-i{in_place_edit}") + if include_dirs: + for directory in include_dirs: + if not directory.is_dir(): + raise NotADirectoryError(f"Include directory does not exist or is not a directory: {directory}") + cmd.extend(["-I", str(directory)]) + if line_ending_processing is not None: + cmd.append(f"-l{line_ending_processing}") + if modules: + for mod in modules: + if not (mod.startswith("-M") or mod.startswith("-m")): + raise ValueError(f"Invalid module string '{mod}'. Must start with '-M' or '-m'.") + cmd.extend(modules) + if loop_around_program: + cmd.append("-n") + if loop_and_print: + cmd.append("-p") + if enable_switch_parsing: + cmd.append("-s") + if search_path_for_program: + cmd.append("-S") + if taint_warnings: + cmd.append("-t") + if taint_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 is not None: + cmd.append(f"-x{extract_from_text}") + if disable_all_warnings: + cmd.append("-X") + + if execute_program: + for prog_line in execute_program: + cmd.extend(["-e", prog_line]) + if execute_program_extended: + for prog_line in execute_program_extended: + cmd.extend(["-E", prog_line]) + + if programfile: + cmd.append(str(programfile)) + + if arguments: + cmd.extend(arguments) + + # --- Subprocess Execution --- + 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": [] + } + except FileNotFoundError: + raise RuntimeError("`perl` command not found. Please ensure Perl is installed and in your PATH.") + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"Perl execution failed with return code {e.returncode}", + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-moose/app/perl-moose_shim_server.py b/Biomni/mcp_generated/mcp_perl-moose/app/perl-moose_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e6c7f21cce09f35e25c9fbb51b2769129d93467e --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moose/app/perl-moose_shim_server.py @@ -0,0 +1,55 @@ +#!/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-moose/app/perl-moose_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_moose' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-moose/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-moose/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moose/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-moose/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-moose/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..ea5ead0b736ccc6d790844e6bef0d388547d315a --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moose/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-moose: + build: . + image: mcp-perl-moose:latest + container_name: mcp-perl-moose + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-moose + 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/Biomni/mcp_generated/mcp_perl-moose/environment.yaml b/Biomni/mcp_generated/mcp_perl-moose/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1c33b8a778306043ae6cc792b3b0c93f53a5fa74 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moose/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-moose + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-moose/requirements.txt b/Biomni/mcp_generated/mcp_perl-moose/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-moose/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-mozilla-ca/requirements.txt b/Biomni/mcp_generated/mcp_perl-mozilla-ca/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-mozilla-ca/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-test-differences/Dockerfile b/Biomni/mcp_generated/mcp_perl-test-differences/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ea1c5d7d00b5ff552e5f01fe3ef90c20341ff063 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-test-differences/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-differences via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-test-differences -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-differences_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-differences_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-differences_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-test-differences/app/perl-test-differences_server.py b/Biomni/mcp_generated/mcp_perl-test-differences/app/perl-test-differences_server.py new file mode 100644 index 0000000000000000000000000000000000000000..543bc108831b8cf31b61ba71b7c9bb998d3115f8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-test-differences/app/perl-test-differences_server.py @@ -0,0 +1,220 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any +import json + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_test_differences' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def perl_test_differences_text( + got: str, + expected: str, + name: str = "text_comparison", + style: str = "table", + context: int = 3, +) -> Dict[str, Any]: + """ + Compare two strings using Perl's Test::Differences and show the differences. + Useful for identifying exact discrepancies between multi-line strings or command outputs. + + Args: + got: The actual string received. + expected: The expected string. + name: A label for the comparison test. + style: The diff style to use ('table', 'unified', 'context', or 'oldstyle'). + context: Number of lines of context to show around differences (for 'unified' or 'context' styles). + """ + # Validate style + valid_styles = ["table", "unified", "context", "oldstyle"] + if style not in valid_styles: + style = "table" + + # Construct Perl one-liner + # We use Test::More to provide the test harness and Test::Differences for the diff logic + perl_script = ( + "use Test::More; " + "use Test::Differences; " + "plan tests => 1; " + f"eq_or_diff($ARGV[0], $ARGV[1], $ARGV[2], {{ style => '{style}', context => {context} }});" + ) + + cmd = ["perl", "-MTest::More", "-MTest::Differences", "-e", perl_script, "--", got, expected, name] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "test_passed": result.returncode == 0, + "output_files": [] + } + except Exception as e: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": str(e), + "test_passed": False, + "output_files": [] + } + +@mcp.tool() +def perl_test_differences_files( + file_path_1: str, + file_path_2: str, + name: str = "file_comparison", + style: str = "table", + context: int = 3, +) -> Dict[str, Any]: + """ + Compare the contents of two files using Perl's Test::Differences. + Commonly used in bioinformatics to verify that a tool's output matches a reference file. + + Args: + file_path_1: Path to the first file (actual). + file_path_2: Path to the second file (expected). + name: A label for the comparison test. + style: The diff style to use ('table', 'unified', 'context', or 'oldstyle'). + context: Number of lines of context to show around differences. + """ + path1 = Path(file_path_1) + path2 = Path(file_path_2) + + if not path1.exists(): + return {"error": f"File not found: {file_path_1}"} + if not path2.exists(): + return {"error": f"File not found: {file_path_2}"} + + # Read file contents + try: + content1 = path1.read_text() + content2 = path2.read_text() + except Exception as e: + return {"error": f"Failed to read files: {str(e)}"} + + # Validate style + valid_styles = ["table", "unified", "context", "oldstyle"] + if style not in valid_styles: + style = "table" + + perl_script = ( + "use Test::More; " + "use Test::Differences; " + "plan tests => 1; " + f"eq_or_diff($ARGV[0], $ARGV[1], $ARGV[2], {{ style => '{style}', context => {context} }});" + ) + + cmd = ["perl", "-MTest::More", "-MTest::Differences", "-e", perl_script, "--", content1, content2, name] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "test_passed": result.returncode == 0, + "output_files": [] + } + except Exception as e: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": str(e), + "test_passed": False, + "output_files": [] + } + +@mcp.tool() +def perl_test_differences_data( + got_json: str, + expected_json: str, + name: str = "data_structure_comparison", + style: str = "table", +) -> Dict[str, Any]: + """ + Compare two complex data structures (provided as JSON strings) using Perl's Test::Differences. + This tool decodes the JSON and compares the underlying structures (hashes, arrays). + + Args: + got_json: The actual data structure in JSON format. + expected_json: The expected data structure in JSON format. + name: A label for the comparison test. + style: The diff style to use ('table', 'unified', 'context', or 'oldstyle'). + """ + # Validate JSON inputs + try: + json.loads(got_json) + json.loads(expected_json) + except json.JSONDecodeError as e: + return {"error": f"Invalid JSON input: {str(e)}"} + + # Validate style + valid_styles = ["table", "unified", "context", "oldstyle"] + if style not in valid_styles: + style = "table" + + # Perl script that uses JSON::PP (core module) to decode strings into Perl structures + # then compares them using eq_or_diff + perl_script = ( + "use Test::More; " + "use Test::Differences; " + "use JSON::PP; " + "my $json = JSON::PP->new; " + "my $data_got = $json->decode($ARGV[0]); " + "my $data_exp = $json->decode($ARGV[1]); " + "plan tests => 1; " + f"eq_or_diff($data_got, $data_exp, $ARGV[2], {{ style => '{style}' }});" + ) + + cmd = ["perl", "-MTest::More", "-MTest::Differences", "-MJSON::PP", "-e", perl_script, "--", got_json, expected_json, name] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "test_passed": result.returncode == 0, + "output_files": [] + } + except Exception as e: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": str(e), + "test_passed": False, + "output_files": [] + } + +@mcp.tool() +def perl_test_differences_version() -> Dict[str, Any]: + """ + Check the installed version of the Test::Differences Perl module. + """ + perl_script = "use Test::Differences; print $Test::Differences::VERSION;" + cmd = ["perl", "-MTest::Differences", "-e", perl_script] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "version": result.stdout.strip(), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Test::Differences module not found or perl execution failed." + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-test-differences/app/perl-test-differences_shim_server.py b/Biomni/mcp_generated/mcp_perl-test-differences/app/perl-test-differences_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..4928a9a21f1e1ac7aba64a8a5dd3ddc87bc6d9d8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-test-differences/app/perl-test-differences_shim_server.py @@ -0,0 +1,55 @@ +#!/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-differences/app/perl-test-differences_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_test_differences' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-test-differences/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-test-differences/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-test-differences/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-test-differences/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-test-differences/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..da2ae77c047f4b0d68f469f632ce6958f71311a8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-test-differences/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-test-differences: + build: . + image: mcp-perl-test-differences:latest + container_name: mcp-perl-test-differences + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-test-differences + 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/Biomni/mcp_generated/mcp_perl-test-differences/environment.yaml b/Biomni/mcp_generated/mcp_perl-test-differences/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a8f7f65557547303244bf741ebf9da6d0df06435 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-test-differences/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-test-differences + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-test-differences/requirements.txt b/Biomni/mcp_generated/mcp_perl-test-differences/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-test-differences/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-text-diff/requirements.txt b/Biomni/mcp_generated/mcp_perl-text-diff/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-text-diff/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-time-hires/Dockerfile b/Biomni/mcp_generated/mcp_perl-time-hires/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d04cdb80a58c5fbdf1dd66eb56b0344c58c6645c --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-time-hires/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-time-hires via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-time-hires -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-time-hires_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-time-hires_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-time-hires_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-time-hires/app/perl-time-hires_server.py b/Biomni/mcp_generated/mcp_perl-time-hires/app/perl-time-hires_server.py new file mode 100644 index 0000000000000000000000000000000000000000..aa71a68f695ec92c9ebb16cad90afa9099b60339 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-time-hires/app/perl-time-hires_server.py @@ -0,0 +1,401 @@ +from typing import Optional, List +import subprocess +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_time_hires' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def usleep(microseconds: int): + """ + Sleeps for the number of microseconds (millionths of a second) specified. + Returns the number of microseconds actually slept. + """ + if microseconds < 0: + return {"error": "microseconds must be a non-negative integer"} + + cmd = ["perl", "-MTime::HiRes=usleep", "-e", "print usleep($ARGV[0])", str(microseconds)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "microseconds_slept": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def nanosleep(nanoseconds: int): + """ + Sleeps for the number of nanoseconds (1e9ths of a second) specified. + Returns the number of nanoseconds actually slept (accurate only to microseconds). + """ + if nanoseconds < 0: + return {"error": "nanoseconds must be a non-negative integer"} + + cmd = ["perl", "-MTime::HiRes=nanosleep", "-e", "print nanosleep($ARGV[0])", str(nanoseconds)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "nanoseconds_slept": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def ualarm(microseconds: int, interval_microseconds: int = 0): + """ + Issues a ualarm call. The interval_microseconds is optional (defaults to 0). + Returns the remaining time in the alarm in microseconds. + """ + if microseconds < 0 or interval_microseconds < 0: + return {"error": "Parameters must be non-negative integers"} + + cmd = ["perl", "-MTime::HiRes=ualarm", "-e", "print ualarm($ARGV[0], $ARGV[1])", str(microseconds), str(interval_microseconds)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "remaining_microseconds": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def gettimeofday(): + """ + Returns the seconds and microseconds since the epoch as a comma-separated string. + """ + cmd = ["perl", "-MTime::HiRes=gettimeofday", "-e", "print join(',', gettimeofday())"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "time_data": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def tv_interval(sec1: int, usec1: int, sec2: Optional[int] = None, usec2: Optional[int] = None): + """ + Returns the floating seconds between two times (sec1, usec1) and (sec2, usec2). + If the second time is omitted, the current time is used. + """ + if sec2 is not None and usec2 is not None: + perl_code = f"print tv_interval([{sec1}, {usec1}], [{sec2}, {usec2}])" + else: + perl_code = f"print tv_interval([{sec1}, {usec1}])" + + cmd = ["perl", "-MTime::HiRes=tv_interval", "-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, + "interval_seconds": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def hires_time(): + """ + Returns floating seconds since the epoch with microsecond resolution. + """ + cmd = ["perl", "-MTime::HiRes=time", "-e", "printf('%.6f', time())"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "current_time": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def hires_sleep(seconds: float): + """ + Sleeps for the specified amount of floating point seconds. + Returns the number of seconds actually slept. + """ + if seconds < 0: + return {"error": "seconds must be a non-negative float"} + + cmd = ["perl", "-MTime::HiRes=sleep", "-e", "print sleep($ARGV[0])", str(seconds)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "seconds_slept": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def hires_alarm(seconds: float, interval: float = 0.0): + """ + Sets an alarm for the specified floating point seconds. + Returns the remaining time in the alarm in seconds. + """ + if seconds < 0 or interval < 0: + return {"error": "Parameters must be non-negative floats"} + + cmd = ["perl", "-MTime::HiRes=alarm", "-e", "print alarm($ARGV[0], $ARGV[1])", str(seconds), str(interval)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "remaining_seconds": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def hires_stat(file_path: str): + """ + Performs a high-resolution stat on a file. + Returns a comma-separated list of stat fields (dev, ino, mode, nlink, uid, gid, rdev, size, atime, mtime, ctime, blksize, blocks). + """ + path = Path(file_path) + if not path.exists(): + return {"error": f"File not found: {file_path}"} + + cmd = ["perl", "-MTime::HiRes=stat", "-e", "print join(',', stat($ARGV[0]))", str(file_path)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "stat_fields": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def hires_lstat(file_path: str): + """ + Performs a high-resolution lstat on a symbolic link or file. + Returns a comma-separated list of stat fields. + """ + path = Path(file_path) + if not path.exists() and not path.is_symlink(): + return {"error": f"File or link not found: {file_path}"} + + cmd = ["perl", "-MTime::HiRes=lstat", "-e", "print join(',', lstat($ARGV[0]))", str(file_path)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "lstat_fields": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def hires_utime(atime: float, mtime: float, files: List[str]): + """ + Sets high-resolution access and modification times for a list of files. + Returns the number of files successfully updated. + """ + for f in files: + if not Path(f).exists(): + return {"error": f"File not found: {f}"} + + cmd = ["perl", "-MTime::HiRes=utime", "-e", "$a=shift; $m=shift; print utime($a, $m, @ARGV)", str(atime), str(mtime)] + files + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "files_updated": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def clock_gettime(clock_id: str = "CLOCK_REALTIME"): + """ + Returns the current time of the specified clock (e.g., CLOCK_REALTIME, CLOCK_MONOTONIC). + """ + cmd = ["perl", "-MTime::HiRes=:all", "-e", "print clock_gettime(eval($ARGV[0]))", clock_id] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "clock_time": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def clock_getres(clock_id: str = "CLOCK_REALTIME"): + """ + Returns the resolution of the specified clock. + """ + cmd = ["perl", "-MTime::HiRes=:all", "-e", "print clock_getres(eval($ARGV[0]))", clock_id] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "clock_resolution": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def clock_nanosleep(nanoseconds: int, clock_id: str = "CLOCK_REALTIME", flags: int = 0): + """ + Sleeps for the specified nanoseconds using the specified clock. + Flags can be TIMER_ABSTIME (1) or 0. + """ + if nanoseconds < 0: + return {"error": "nanoseconds must be non-negative"} + + cmd = ["perl", "-MTime::HiRes=:all", "-e", "print clock_nanosleep(eval($ARGV[1]), $ARGV[0], $ARGV[2])", str(nanoseconds), clock_id, str(flags)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "nanoseconds_slept": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def hires_clock(): + """ + Returns the process time (user + system) in seconds with high resolution. + """ + cmd = ["perl", "-MTime::HiRes=clock", "-e", "print clock()"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "process_time": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-time-hires/app/perl-time-hires_shim_server.py b/Biomni/mcp_generated/mcp_perl-time-hires/app/perl-time-hires_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a99af756450da9232d8bd8863d791389d15132b4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-time-hires/app/perl-time-hires_shim_server.py @@ -0,0 +1,55 @@ +#!/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-time-hires/app/perl-time-hires_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_time_hires' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-time-hires/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-time-hires/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-time-hires/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-time-hires/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-time-hires/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..99e2b434733b7c74e3d8984e4fc7f075151f30d3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-time-hires/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-time-hires: + build: . + image: mcp-perl-time-hires:latest + container_name: mcp-perl-time-hires + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-time-hires + 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/Biomni/mcp_generated/mcp_perl-time-hires/environment.yaml b/Biomni/mcp_generated/mcp_perl-time-hires/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..32c6d05fb886e826b264e96e0ac8aa3a9751a744 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-time-hires/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-time-hires + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-time-hires/requirements.txt b/Biomni/mcp_generated/mcp_perl-time-hires/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-time-hires/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-timedate/requirements.txt b/Biomni/mcp_generated/mcp_perl-timedate/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-timedate/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-xml-libxml/Dockerfile b/Biomni/mcp_generated/mcp_perl-xml-libxml/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0a7aca7ccbcc120265bc3debc46dd9557889966b --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-libxml/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-xml-libxml via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-xml-libxml -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-xml-libxml_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-xml-libxml_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-xml-libxml_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-xml-libxml/app/perl-xml-libxml_server.py b/Biomni/mcp_generated/mcp_perl-xml-libxml/app/perl-xml-libxml_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9c44e426b5383ad8da5800b4d6d0a34ad81f7a77 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-libxml/app/perl-xml-libxml_server.py @@ -0,0 +1,276 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Union + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_xml_libxml' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def perl_xml_libxml_validate( + xml_path: str, + schema_path: Optional[str] = None, + schema_type: str = "xsd", +): + """ + Validates an XML file using perl-xml-libxml. Can check for well-formedness or + validate against an XSD or DTD. + + Args: + xml_path: Path to the XML file to validate. + schema_path: Optional path to a schema file (XSD or DTD). + schema_type: Type of schema provided ('xsd' or 'dtd'). Defaults to 'xsd'. + """ + xml_file = Path(xml_path) + if not xml_file.exists(): + return {"error": f"XML file not found: {xml_path}"} + + # Construct Perl one-liner for validation + perl_script = [ + "use XML::LibXML;", + "my $xml = shift;", + "my $schema_file = shift;", + "my $type = shift;", + "my $parser = XML::LibXML->new();", + "eval {", + " if ($schema_file) {", + " if ($type eq 'xsd') {", + " my $schema = XML::LibXML::Schema->new(location => $schema_file);", + " my $doc = $parser->parse_file($xml);", + " $schema->validate($doc);", + " } elsif ($type eq 'dtd') {", + " $parser->validation(1);", + " $parser->parse_file($xml);", + " }", + " } else {", + " $parser->parse_file($xml);", + " }", + "};", + "if ($@) { die \"Validation failed: $@\"; } else { print \"Validation successful\\n\"; }", + ] + + cmd = ["perl", "-MXML::LibXML", "-e", "".join(perl_script), str(xml_file)] + if schema_path: + s_path = Path(schema_path) + if not s_path.exists(): + return {"error": f"Schema file not found: {schema_path}"} + cmd.append(str(s_path)) + cmd.append(schema_type) + + 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": "Validation failed" + } + +@mcp.tool() +def perl_xml_libxml_xpath( + xml_path: str, + expression: str, + output_format: str = "string", +): + """ + Extracts data from an XML file using an XPath expression via XML::LibXML. + + Args: + xml_path: Path to the XML file. + expression: XPath expression to evaluate. + output_format: How to return results ('string' for text content, 'xml' for raw XML nodes). + """ + xml_file = Path(xml_path) + if not xml_file.exists(): + return {"error": f"XML file not found: {xml_path}"} + + # Construct Perl one-liner for XPath + perl_script = [ + "use XML::LibXML;", + "my $xml = shift;", + "my $xpath = shift;", + "my $fmt = shift;", + "my $dom = XML::LibXML->load_xml(location => $xml);", + "for my $node ($dom->findnodes($xpath)) {", + " if ($fmt eq 'xml') { print $node->toString(), \"\\n\"; }", + " else { print $node->textContent(), \"\\n\"; }", + "}" + ] + + cmd = ["perl", "-MXML::LibXML", "-e", "".join(perl_script), str(xml_file), expression, output_format] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "results": result.stdout.splitlines() + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "XPath query failed" + } + +@mcp.tool() +def perl_xml_libxml_format( + xml_path: str, + indent: int = 1, + no_blanks: bool = True, +): + """ + Pretty-prints/formats an XML file using XML::LibXML. + + Args: + xml_path: Path to the XML file to format. + indent: Indentation level (0, 1, or 2). Defaults to 1. + no_blanks: If True, removes existing whitespace before re-formatting. Defaults to True. + """ + xml_file = Path(xml_path) + if not xml_file.exists(): + return {"error": f"XML file not found: {xml_path}"} + + if indent < 0 or indent > 2: + return {"error": "Indent must be 0, 1, or 2"} + + blanks_flag = 1 if no_blanks else 0 + + perl_script = [ + "use XML::LibXML;", + "my $xml = shift;", + "my $ind = shift;", + "my $nb = shift;", + "my $dom = XML::LibXML->load_xml(location => $xml, no_blanks => $nb);", + "print $dom->toString($ind);" + ] + + cmd = ["perl", "-MXML::LibXML", "-e", "".join(perl_script), str(xml_file), str(indent), str(blanks_flag)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "formatted_xml": result.stdout + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Formatting failed" + } + +@mcp.tool() +def perl_xml_libxml_execute( + script_path: Optional[str] = None, + one_liner: Optional[str] = None, + input_files: Optional[List[str]] = None, + warnings: bool = True, + taint_check: bool = False, + include_dirs: Optional[List[str]] = None, + autosplit: bool = False, + loop_n: bool = False, + loop_p: bool = False, +): + """ + General purpose Perl execution tool with XML::LibXML pre-loaded. + Allows running custom scripts or one-liners that utilize the library. + + Args: + script_path: Path to a .pl script to execute. + one_liner: A string containing Perl code to execute (equivalent to -e). + input_files: List of files to pass as arguments to the script. + warnings: Enable many useful warnings (-w). Defaults to True. + taint_check: Enable tainting checks (-T). Defaults to False. + include_dirs: List of directories to add to @INC (-I). + autosplit: Enable autosplit mode (-a) with -n or -p. + loop_n: Assume "while (<>) { ... }" loop around program (-n). + loop_p: Assume loop like -n but print line also (-p). + """ + cmd = ["perl", "-MXML::LibXML"] + + if warnings: + cmd.append("-w") + if taint_check: + cmd.append("-T") + if autosplit: + cmd.append("-a") + if loop_n: + cmd.append("-n") + if loop_p: + cmd.append("-p") + + if include_dirs: + for d in include_dirs: + cmd.extend(["-I", d]) + + if one_liner: + cmd.extend(["-e", one_liner]) + elif script_path: + s_path = Path(script_path) + if not s_path.exists(): + return {"error": f"Script not found: {script_path}"} + cmd.append(str(s_path)) + else: + return {"error": "Either script_path or one_liner must be provided."} + + if input_files: + for f in input_files: + f_path = Path(f) + if f_path.exists(): + cmd.append(str(f_path)) + + 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, + "exit_code": e.returncode, + "error": "Perl execution failed" + } + +@mcp.tool() +def perl_xml_libxml_info(): + """ + Returns version information for the perl-xml-libxml installation and the underlying libxml2 library. + """ + perl_script = "use XML::LibXML; printf('XML::LibXML Version: %s\\nLibXML2 Version: %s\\n', $XML::LibXML::VERSION, XML::LibXML::LIBXML_DOTTED_VERSION);" + cmd = ["perl", "-e", perl_script] + + 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 version info" + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-xml-libxml/app/perl-xml-libxml_shim_server.py b/Biomni/mcp_generated/mcp_perl-xml-libxml/app/perl-xml-libxml_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e906dd69089785738240768209784bd5bad60d44 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-libxml/app/perl-xml-libxml_shim_server.py @@ -0,0 +1,55 @@ +#!/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-xml-libxml/app/perl-xml-libxml_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_xml_libxml' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-xml-libxml/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-xml-libxml/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-libxml/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-xml-libxml/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-xml-libxml/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..1414450d5cab98c47adcf029be3d8009ab5161cb --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-libxml/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-xml-libxml: + build: . + image: mcp-perl-xml-libxml:latest + container_name: mcp-perl-xml-libxml + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-xml-libxml + 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/Biomni/mcp_generated/mcp_perl-xml-libxml/environment.yaml b/Biomni/mcp_generated/mcp_perl-xml-libxml/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8f9a178635543090a48cafd5b83741825cb5031f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-libxml/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-xml-libxml + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-xml-libxml/requirements.txt b/Biomni/mcp_generated/mcp_perl-xml-libxml/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-libxml/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-xml-sax-base/Dockerfile b/Biomni/mcp_generated/mcp_perl-xml-sax-base/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2d10f06824700eb4504b231de66270b64d60fcad --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-sax-base/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-xml-sax-base via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-xml-sax-base -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-xml-sax-base_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-xml-sax-base_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-xml-sax-base_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-xml-sax-base/app/perl-xml-sax-base_server.py b/Biomni/mcp_generated/mcp_perl-xml-sax-base/app/perl-xml-sax-base_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b79bf77e4b4bc69096b03651fdb114091af894e7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-sax-base/app/perl-xml-sax-base_server.py @@ -0,0 +1,236 @@ +from typing import Optional, List +import subprocess +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_xml_sax_base' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def perl_xml_sax_base_run( + program_file: Optional[str] = None, + one_liner: Optional[str] = None, + arguments: Optional[List[str]] = None, + record_separator: Optional[str] = None, + autosplit: bool = False, + unicode_features: Optional[str] = None, + syntax_check: bool = False, + debugger: Optional[str] = None, + debug_flags: Optional[str] = None, + one_liner_all_features: Optional[str] = None, + no_site_customize: bool = False, + split_pattern: Optional[str] = None, + inplace_extension: Optional[str] = None, + include_dirs: Optional[List[str]] = None, + line_ending_processing: Optional[str] = None, + modules: Optional[List[str]] = None, + loop_n: bool = False, + loop_p: bool = False, + switch_parsing: bool = False, + path_search: bool = False, + taint_warnings: bool = False, + taint_checks: bool = False, + dump_core: bool = False, + unsafe_operations: bool = False, + print_version: bool = False, + config_summary: Optional[str] = None, + warnings: bool = False, + all_warnings: bool = False, + extract_directory: Optional[str] = None, + disable_all_warnings: bool = False, +): + """ + Executes the Perl interpreter with the XML::SAX::Base environment. + This tool provides access to the Perl CLI switches as defined in the perl-xml-sax-base package environment. + + Args: + program_file: Path to the Perl script to execute. + one_liner: One line of program (several -e's allowed, but here as a single string). + arguments: Arguments passed to the programfile. + record_separator: Specify record separator (-0). + autosplit: Autosplit mode with -n or -p (-a). + unicode_features: Enables the listed Unicode features (-C). + syntax_check: Check syntax only (-c). + debugger: Run program under debugger (-d). + debug_flags: Set debugging flags (-D). + one_liner_all_features: Like -e, but enables all optional features (-E). + no_site_customize: Don't do $sitelib/sitecustomize.pl at startup (-f). + split_pattern: split() pattern for -a switch (-F). + inplace_extension: Edit <> files in place, makes backup if extension supplied (-i). + include_dirs: Specify @INC/#include directory (-I). + line_ending_processing: Enable line ending processing, specifies line terminator (-l). + modules: Execute "use module..." before executing program (-M). + loop_n: Assume "while (<>) { ... }" loop around program (-n). + loop_p: Assume loop like -n but print line also (-p). + switch_parsing: Enable rudimentary parsing for switches after programfile (-s). + path_search: Look for programfile using PATH environment variable (-S). + taint_warnings: Enable tainting warnings (-t). + taint_checks: Enable tainting checks (-T). + dump_core: Dump core after parsing program (-u). + unsafe_operations: Allow unsafe operations (-U). + print_version: Print version, patchlevel and license (-v). + config_summary: Print configuration summary or a single Config.pm variable (-V). + warnings: Enable many useful warnings (-w). + all_warnings: Enable all warnings (-W). + extract_directory: Ignore text before #!perl line, optionally cd to directory (-x). + disable_all_warnings: Disable all warnings (-X). + """ + cmd = ["perl"] + + # Handle switches + 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 syntax_check: + cmd.append("-c") + if debugger is not None: + cmd.append(f"-d{debugger}") + if debug_flags is not None: + cmd.append(f"-D{debug_flags}") + if one_liner is not None: + cmd.extend(["-e", one_liner]) + if one_liner_all_features is not None: + cmd.extend(["-E", one_liner_all_features]) + if no_site_customize: + cmd.append("-f") + 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 include_dirs: + for d in include_dirs: + cmd.append(f"-I{d}") + if line_ending_processing is not None: + cmd.append(f"-l{line_ending_processing}") + if modules: + for m in modules: + cmd.append(f"-M{m}") + if loop_n: + cmd.append("-n") + if loop_p: + cmd.append("-p") + if switch_parsing: + cmd.append("-s") + if path_search: + cmd.append("-S") + if taint_warnings: + cmd.append("-t") + if taint_checks: + cmd.append("-T") + if dump_core: + cmd.append("-u") + if unsafe_operations: + cmd.append("-U") + if print_version: + cmd.append("-v") + if config_summary is not None: + cmd.append(f"-V:{config_summary}" if config_summary else "-V") + if warnings: + cmd.append("-w") + if all_warnings: + cmd.append("-W") + if extract_directory is not None: + cmd.append(f"-x{extract_directory}") + if disable_all_warnings: + cmd.append("-X") + + # 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_xml_sax_base_check_version(): + """ + Checks the installed version of the XML::SAX::Base Perl module. + This is useful to verify that the library is correctly installed in the environment. + """ + cmd = ["perl", "-MXML::SAX::Base", "-e", "print $XML::SAX::Base::VERSION"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "module": "XML::SAX::Base", + "version": result.stdout.strip(), + "command_executed": " ".join(cmd), + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "error": "XML::SAX::Base module not found or failed to load.", + "stderr": e.stderr, + "command_executed": " ".join(cmd), + "status": "error" + } + +@mcp.tool() +def perl_xml_sax_base_validate_xml_structure( + xml_file: str, +): + """ + Uses XML::SAX::Base to perform a basic parse of an XML file to check for well-formedness. + Note: XML::SAX::Base is a base class; this tool uses it to verify the SAX parsing chain. + + Args: + xml_file: Path to the XML file to validate. + """ + xml_path = Path(xml_file) + if not xml_path.exists(): + return {"error": f"XML file not found: {xml_file}"} + + # Perl one-liner to attempt parsing using the SAX Base logic + # It uses XML::SAX::ParserFactory to find an available parser that inherits from Base + perl_code = ( + "use XML::SAX::ParserFactory; " + "my $handler = XML::SAX::Base->new(); " + "my $parser = XML::SAX::ParserFactory->parser(Handler => $handler); " + "eval { $parser->parse_uri($ARGV[0]) }; " + "if ($@) { die $@; }" + ) + + cmd = ["perl", "-e", perl_code, str(xml_path)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "message": f"XML file {xml_file} is well-formed according to SAX parser.", + "stdout": result.stdout, + "command_executed": " ".join(cmd), + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "error": f"Validation failed for {xml_file}", + "stderr": e.stderr, + "command_executed": " ".join(cmd), + "status": "error" + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-xml-sax-base/app/perl-xml-sax-base_shim_server.py b/Biomni/mcp_generated/mcp_perl-xml-sax-base/app/perl-xml-sax-base_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..824de42c5ac29cfb1990c1706bd4902a19c3145c --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-sax-base/app/perl-xml-sax-base_shim_server.py @@ -0,0 +1,55 @@ +#!/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-xml-sax-base/app/perl-xml-sax-base_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_xml_sax_base' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_perl-xml-sax-base/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-xml-sax-base/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-sax-base/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-xml-sax-base/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-xml-sax-base/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..1a9a1e4ddd94a241df31aeca32c89f9a39b8d36a --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-sax-base/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-xml-sax-base: + build: . + image: mcp-perl-xml-sax-base:latest + container_name: mcp-perl-xml-sax-base + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-xml-sax-base + 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/Biomni/mcp_generated/mcp_perl-xml-sax-base/environment.yaml b/Biomni/mcp_generated/mcp_perl-xml-sax-base/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d358461587267c837158b1577f9423c6a24b72df --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-sax-base/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-xml-sax-base + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-xml-sax-base/requirements.txt b/Biomni/mcp_generated/mcp_perl-xml-sax-base/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-sax-base/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_prophyle/Dockerfile b/Biomni/mcp_generated/mcp_prophyle/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0adece3de558d759f102c8fc6ba18fd20b635bb6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_prophyle/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 prophyle via conda (e.g., from bioconda) +RUN conda install -c bioconda prophyle -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/prophyle_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/prophyle_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/prophyle_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_prophyle/app/prophyle_server.py b/Biomni/mcp_generated/mcp_prophyle/app/prophyle_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c19aca80722250f1d5bdf8765bd11f27a2aed89d --- /dev/null +++ b/Biomni/mcp_generated/mcp_prophyle/app/prophyle_server.py @@ -0,0 +1,348 @@ +from typing import Optional, List +import subprocess +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_prophyle' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def prophyle_download( + library: str, + dest_dir: str = "prophyle_db", + force: bool = False, +): + """ + Download a genomic library for ProPhyle (e.g., bacteria, viruses, archaea, fungi). + + Args: + library: Name of the library to download (e.g., 'bacteria', 'viruses'). + dest_dir: Destination directory for the library. + force: Force download even if the library already exists. + """ + dest_path = Path(dest_dir) + + cmd = ["prophyle", "download"] + if force: + cmd.append("-f") + cmd.extend(["-d", str(dest_path)]) + cmd.append(library) + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(dest_path)] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def prophyle_index( + tree_nwk: str, + index_dir: str, + k_mer_size: int = 31, + sampling_rate: int = 1, + threads: int = 1, + force: bool = False, +): + """ + Build a ProPhyle index from a phylogenetic tree and associated sequences. + + Args: + tree_nwk: Path to the phylogenetic tree in Newick format (.nwk). + index_dir: Directory where the index will be stored. + k_mer_size: K-mer size for indexing. + sampling_rate: Sampling rate for k-mers. + threads: Number of threads to use. + force: Force overwrite of existing index. + """ + tree_path = Path(tree_nwk) + if not tree_path.exists(): + return {"error": f"Tree file {tree_nwk} not found."} + + if k_mer_size <= 0: + return {"error": "k_mer_size must be a positive integer."} + if threads <= 0: + return {"error": "threads must be a positive integer."} + + cmd = ["prophyle", "index"] + cmd.extend(["-k", str(k_mer_size)]) + cmd.extend(["-s", str(sampling_rate)]) + cmd.extend(["-t", str(threads)]) + if force: + cmd.append("-F") + + cmd.extend([str(tree_path), index_dir]) + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [index_dir] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def prophyle_classify( + index_dir: str, + reads_fwd: str, + reads_rev: Optional[str] = None, + k_mer_size: Optional[int] = None, + threads: int = 1, + output_format: str = "sam", + tie_lighting: bool = False, +): + """ + Classify metagenomic reads against a ProPhyle index. + + Args: + index_dir: Path to the ProPhyle index directory. + reads_fwd: Path to the forward reads (FASTA/FASTQ). + reads_rev: Path to the reverse reads (optional, for paired-end). + k_mer_size: K-mer size (must match index). + threads: Number of threads to use. + output_format: Output format (sam, bam, kraken, or gist). + tie_lighting: Use tie-lighting to reduce memory (for large trees). + """ + idx_path = Path(index_dir) + if not idx_path.exists(): + return {"error": f"Index directory {index_dir} not found."} + + fwd_path = Path(reads_fwd) + if not fwd_path.exists(): + return {"error": f"Reads file {reads_fwd} not found."} + + cmd = ["prophyle", "classify"] + if k_mer_size is not None: + cmd.extend(["-k", str(k_mer_size)]) + cmd.extend(["-t", str(threads)]) + cmd.extend(["-f", output_format]) + if tie_lighting: + cmd.append("-p") + + cmd.append(str(idx_path)) + cmd.append(str(fwd_path)) + if reads_rev: + rev_path = Path(reads_rev) + if not rev_path.exists(): + return {"error": f"Reverse reads file {reads_rev} not found."} + cmd.append(str(rev_path)) + + try: + # Classification usually outputs to stdout + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "info": "Classification results are in stdout." + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def prophyle_analyze( + index_dir: str, + bam_file: str, + output_prefix: str = "prophyle_analysis", +): + """ + Analyze ProPhyle classification results (BAM/SAM) to produce taxonomic profiles. + + Args: + index_dir: Path to the ProPhyle index directory. + bam_file: Path to the classification results in BAM or SAM format. + output_prefix: Prefix for the output files. + """ + idx_path = Path(index_dir) + bam_path = Path(bam_file) + + if not idx_path.exists(): + return {"error": f"Index directory {index_dir} not found."} + if not bam_path.exists(): + return {"error": f"BAM/SAM file {bam_file} not found."} + + cmd = ["prophyle", "analyze"] + cmd.extend(["-o", output_prefix]) + cmd.extend([str(idx_path), str(bam_path)]) + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [f"{output_prefix}.txt", f"{output_prefix}.csv"] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def prophyle_propagate( + tree_nwk: str, + k_mer_size: int = 31, + threads: int = 1, +): + """ + Propagate k-mers up the phylogenetic tree (internal indexing step). + + Args: + tree_nwk: Path to the phylogenetic tree in Newick format. + k_mer_size: K-mer size. + threads: Number of threads. + """ + tree_path = Path(tree_nwk) + if not tree_path.exists(): + return {"error": f"Tree file {tree_nwk} not found."} + + cmd = ["prophyle", "propagate"] + cmd.extend(["-k", str(k_mer_size)]) + cmd.extend(["-t", str(threads)]) + cmd.append(str(tree_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": str(e) + } + +@mcp.tool() +def prophyle_compress( + index_dir: str, +): + """ + Compress a ProPhyle index to save disk space. + + Args: + index_dir: Path to the ProPhyle index directory. + """ + idx_path = Path(index_dir) + if not idx_path.exists(): + return {"error": f"Index directory {index_dir} not found."} + + cmd = ["prophyle", "compress", str(idx_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": str(e) + } + +@mcp.tool() +def prophyle_decompress( + index_dir: str, +): + """ + Decompress a ProPhyle index. + + Args: + index_dir: Path to the ProPhyle index directory. + """ + idx_path = Path(index_dir) + if not idx_path.exists(): + return {"error": f"Index directory {index_dir} not found."} + + cmd = ["prophyle", "decompress", str(idx_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": str(e) + } + +@mcp.tool() +def prophyle_sample( + tree_nwk: str, + index_dir: str, + k_mer_size: int = 31, + sampling_rate: int = 1, +): + """ + Sample k-mers from sequences associated with a tree. + + Args: + tree_nwk: Path to the phylogenetic tree. + index_dir: Directory for the index. + k_mer_size: K-mer size. + sampling_rate: Sampling rate. + """ + tree_path = Path(tree_nwk) + if not tree_path.exists(): + return {"error": f"Tree file {tree_nwk} not found."} + + cmd = ["prophyle", "sample"] + cmd.extend(["-k", str(k_mer_size)]) + cmd.extend(["-s", str(sampling_rate)]) + cmd.extend([str(tree_path), index_dir]) + + 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": str(e) + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_prophyle/app/prophyle_shim_server.py b/Biomni/mcp_generated/mcp_prophyle/app/prophyle_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6fcf3d53a45db518a24587f1727166e1fec4f55f --- /dev/null +++ b/Biomni/mcp_generated/mcp_prophyle/app/prophyle_shim_server.py @@ -0,0 +1,55 @@ +#!/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_prophyle/app/prophyle_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_prophyle' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_prophyle/app/requirements.txt b/Biomni/mcp_generated/mcp_prophyle/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_prophyle/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_prophyle/docker-compose.yml b/Biomni/mcp_generated/mcp_prophyle/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..a1f69c353c93343340e16aae9cfb36095b01c7cb --- /dev/null +++ b/Biomni/mcp_generated/mcp_prophyle/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-prophyle: + build: . + image: mcp-prophyle:latest + container_name: mcp-prophyle + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=prophyle + 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/Biomni/mcp_generated/mcp_prophyle/environment.yaml b/Biomni/mcp_generated/mcp_prophyle/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f81f86acfffe098cd32609a2d96a0e7bb640005a --- /dev/null +++ b/Biomni/mcp_generated/mcp_prophyle/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - prophyle + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_prophyle/requirements.txt b/Biomni/mcp_generated/mcp_prophyle/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_prophyle/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_proteinortho/Dockerfile b/Biomni/mcp_generated/mcp_proteinortho/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..5aed52ffb2fd45384fe91f5b0c566261207dd108 --- /dev/null +++ b/Biomni/mcp_generated/mcp_proteinortho/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 proteinortho via conda (e.g., from bioconda) +RUN conda install -c bioconda proteinortho -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/proteinortho_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/proteinortho_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/proteinortho_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_proteinortho/app/proteinortho_server.py b/Biomni/mcp_generated/mcp_proteinortho/app/proteinortho_server.py new file mode 100644 index 0000000000000000000000000000000000000000..efd8a19e61ac9e0c660206454c53fdbe69a3d62f --- /dev/null +++ b/Biomni/mcp_generated/mcp_proteinortho/app/proteinortho_server.py @@ -0,0 +1,291 @@ +from typing import List, Optional, Union +import subprocess +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_proteinortho' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def proteinortho( + fasta_files: List[str], + project: str = "myproject", + cpus: int = 1, + p: str = "diamond", + e: float = 1e-05, + cov: int = 50, + identity: int = 25, + sim: float = 0.95, + singles: bool = False, + selfblast: bool = False, + clean: bool = True, + step: int = 0, + force: bool = False, + keep: bool = False, + check_dependencies: bool = False, +): + """ + Proteinortho is a tool to detect orthologous genes within different species. + It handles the full pipeline from sequence comparison (BLAST/Diamond) to clustering. + + Args: + fasta_files: List of FASTA files (proteins or DNA) to compare. + project: Prefix for all output files. + cpus: Number of processors to use. + p: Search engine to use (diamond, blastp, blastn, blastp+, blastn+, mmseqs, lastp). + e: E-value threshold for the search engine. + cov: Minimum coverage of the best alignment in % (default: 50). + identity: Minimum identity of the best alignment in % (default: 25). + sim: Minimum bit-score similarity (0..1) (default: 0.95). + singles: Report genes without orthologs (singletons). + selfblast: Perform self-comparisons within species. + clean: Remove temporary files after completion. + step: Start at a specific step (1: blast, 2: clustering, 3: orthology). 0 runs all. + force: Force recalculation even if results exist. + keep: Keep all temporary files. + check_dependencies: Check if all required dependencies are installed. + """ + # Input validation + if not fasta_files: + return {"error": "At least one FASTA file must be provided."} + + valid_fasta = [] + for f in fasta_files: + path = Path(f) + if not path.exists(): + return {"error": f"Input file not found: {f}"} + valid_fasta.append(str(path.absolute())) + + # Build command + cmd = ["proteinortho"] + cmd.append(f"-project={project}") + cmd.append(f"-cpus={cpus}") + cmd.append(f"-p={p}") + cmd.append(f"-e={e}") + cmd.append(f"-cov={cov}") + cmd.append(f"-identity={identity}") + cmd.append(f"-sim={sim}") + + if singles: cmd.append("-singles") + if selfblast: cmd.append("-selfblast") + if clean: cmd.append("-clean") + if force: cmd.append("-force") + if keep: cmd.append("-keep") + if check_dependencies: cmd.append("-check") + if step > 0: cmd.append(f"-step={step}") + + cmd.extend(valid_fasta) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + # Identify expected output files + output_extensions = [".proteinortho", ".proteinortho-graph", ".blast-graph", ".summary"] + found_outputs = [] + for ext in output_extensions: + out_path = Path(f"{project}{ext}") + if out_path.exists(): + found_outputs.append(str(out_path.absolute())) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": found_outputs + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def proteinortho_grab_proteins( + ortho_file: str, + fasta_files: List[str], + group_ids: Optional[List[str]] = None, + species_names: Optional[List[str]] = None, + out_prefix: str = "grabbed_proteins", +): + """ + Extract protein sequences from orthology groups identified by proteinortho. + + Args: + ortho_file: The .proteinortho output file from a proteinortho run. + fasta_files: The original FASTA files used in the proteinortho run. + group_ids: Specific group IDs to extract (e.g., ['0', '1']). + species_names: Specific species names to extract. + out_prefix: Prefix for the output FASTA files. + """ + ortho_path = Path(ortho_file) + if not ortho_path.exists(): + return {"error": f"Orthology file not found: {ortho_file}"} + + valid_fasta = [] + for f in fasta_files: + path = Path(f) + if not path.exists(): + return {"error": f"FASTA file not found: {f}"} + valid_fasta.append(str(path.absolute())) + + # proteinortho_grab_proteins.pl [options] group_ids file.proteinortho file1.fa ... + cmd = ["proteinortho_grab_proteins.pl"] + + if species_names: + cmd.append(f"-species={','.join(species_names)}") + + cmd.append(f"-to={out_prefix}") + + # If no group IDs provided, the tool usually expects a '-' or specific IDs + if group_ids: + cmd.append(",".join(group_ids)) + else: + cmd.append("-") # Extract all + + cmd.append(str(ortho_path.absolute())) + cmd.extend(valid_fasta) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "info": f"Sequences extracted with prefix: {out_prefix}" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def proteinortho_summary( + ortho_file: str, +): + """ + Generate a summary of a proteinortho run. + + Args: + ortho_file: The .proteinortho output file. + """ + ortho_path = Path(ortho_file) + if not ortho_path.exists(): + return {"error": f"Orthology file not found: {ortho_file}"} + + cmd = ["proteinortho_summary.pl", str(ortho_path.absolute())] + + 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), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def proteinortho_clustering( + blast_graph_file: str, + cpus: int = 1, + connectivity: float = 0.1, + sim: float = 0.95, + e_value: float = 1e-05, + verbose: bool = False, +): + """ + The standalone clustering engine of proteinortho. + Useful if you already have a .blast-graph file and want to re-cluster with different parameters. + + Args: + blast_graph_file: The .blast-graph file containing pairwise similarities. + cpus: Number of processors to use. + connectivity: Minimum connectivity (0..1) (default: 0.1). + sim: Minimum bit-score similarity (0..1) (default: 0.95). + e_value: E-value threshold. + verbose: Enable verbose output. + """ + graph_path = Path(blast_graph_file) + if not graph_path.exists(): + return {"error": f"Blast graph file not found: {blast_graph_file}"} + + cmd = ["proteinortho_clustering"] + cmd.append(f"-cpus={cpus}") + cmd.append(f"-p={connectivity}") + cmd.append(f"-m={sim}") + cmd.append(f"-e={e_value}") + if verbose: cmd.append("-v") + + cmd.append(str(graph_path.absolute())) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "info": "Clustering completed. Check stdout for group definitions." + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def proteinortho_format( + blast_files: List[str], + cpus: int = 1, + verbose: bool = False, +): + """ + Formats raw BLAST/Diamond output files into the proteinortho graph format. + + Args: + blast_files: List of raw output files from the search engine. + cpus: Number of processors to use. + verbose: Enable verbose output. + """ + valid_files = [] + for f in blast_files: + path = Path(f) + if not path.exists(): + return {"error": f"Blast file not found: {f}"} + valid_files.append(str(path.absolute())) + + cmd = ["proteinortho_format"] + cmd.append(f"-cpus={cpus}") + if verbose: cmd.append("-v") + cmd.extend(valid_files) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "info": "Formatting completed. Output is usually directed to stdout or a graph file." + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_proteinortho/app/proteinortho_shim_server.py b/Biomni/mcp_generated/mcp_proteinortho/app/proteinortho_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9d83853cfeb9b5e09fd8815961bf74aeaab88d28 --- /dev/null +++ b/Biomni/mcp_generated/mcp_proteinortho/app/proteinortho_shim_server.py @@ -0,0 +1,55 @@ +#!/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_proteinortho/app/proteinortho_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_proteinortho' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_proteinortho/app/requirements.txt b/Biomni/mcp_generated/mcp_proteinortho/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_proteinortho/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_proteinortho/docker-compose.yml b/Biomni/mcp_generated/mcp_proteinortho/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b62e1071ff9d7f5d78acd65780c528790824ad8e --- /dev/null +++ b/Biomni/mcp_generated/mcp_proteinortho/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-proteinortho: + build: . + image: mcp-proteinortho:latest + container_name: mcp-proteinortho + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=proteinortho + 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/Biomni/mcp_generated/mcp_proteinortho/environment.yaml b/Biomni/mcp_generated/mcp_proteinortho/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..55b0be18f2ee33c88b15e417d08c1de3f7377708 --- /dev/null +++ b/Biomni/mcp_generated/mcp_proteinortho/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - proteinortho + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_proteinortho/requirements.txt b/Biomni/mcp_generated/mcp_proteinortho/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_proteinortho/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_py2bit/Dockerfile b/Biomni/mcp_generated/mcp_py2bit/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e5dc111981e016c932c1bfce12fd56d6940ed089 --- /dev/null +++ b/Biomni/mcp_generated/mcp_py2bit/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 py2bit via conda (e.g., from bioconda) +RUN conda install -c bioconda py2bit -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/py2bit_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/py2bit_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/py2bit_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_py2bit/app/py2bit_server.py b/Biomni/mcp_generated/mcp_py2bit/app/py2bit_server.py new file mode 100644 index 0000000000000000000000000000000000000000..87c2cd05b6667872117029ff80819d7c2148ef56 --- /dev/null +++ b/Biomni/mcp_generated/mcp_py2bit/app/py2bit_server.py @@ -0,0 +1,232 @@ +import subprocess +import json +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_py2bit' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def py2bit_get_info( + input_2bit: str, +) -> Dict[str, Any]: + """ + Retrieve general information about a 2bit file, including file size, + number of chromosomes, total sequence length, and masked lengths. + + Args: + input_2bit: Path to the .2bit file. + """ + input_path = Path(input_2bit) + if not input_path.exists(): + return {"error": f"Input file {input_2bit} does not exist."} + + # We use a python one-liner to access the library as it is a Python extension + # Note: storeMasked=True is used to ensure soft-masked length is reported if available + py_cmd = ( + f"import py2bit, json; " + f"tb = py2bit.open('{str(input_path)}', True); " + f"print(json.dumps(tb.info()))" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "info": json.loads(result.stdout.strip()), + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": e.stderr or str(e), + "stdout": e.stdout + } + +@mcp.tool() +def py2bit_get_chroms( + input_2bit: str, + chromosome: Optional[str] = None, +) -> Dict[str, Any]: + """ + Access the list of chromosomes and their lengths from a 2bit file. + + Args: + input_2bit: Path to the .2bit file. + chromosome: Optional specific chromosome name to get the length for. + If omitted, returns a dictionary of all chromosomes and lengths. + """ + input_path = Path(input_2bit) + if not input_path.exists(): + return {"error": f"Input file {input_2bit} does not exist."} + + chrom_arg = f"'{chromosome}'" if chromosome else "" + py_cmd = ( + f"import py2bit, json; " + f"tb = py2bit.open('{str(input_path)}'); " + f"print(json.dumps(tb.chroms({chrom_arg})))" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "chromosomes": json.loads(result.stdout.strip()), + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": e.stderr or str(e) + } + +@mcp.tool() +def py2bit_get_sequence( + input_2bit: str, + chromosome: str, + start: int = 0, + end: int = 0, + store_masked: bool = False, +) -> Dict[str, Any]: + """ + Fetch the sequence of a full or partial chromosome from a 2bit file. + Uses 0-based half-open coordinates. + + Args: + input_2bit: Path to the .2bit file. + chromosome: Name of the chromosome/contig. + start: 0-based start position (default 0). + end: 0-based end position (default 0, which fetches until the end of the chromosome). + store_masked: If True, includes soft-masking information (lower case bases). + """ + input_path = Path(input_2bit) + if not input_path.exists(): + return {"error": f"Input file {input_2bit} does not exist."} + + # Handle default end=0 to mean full chromosome in the python script + end_val = end if end > 0 else "None" + py_cmd = ( + f"import py2bit; " + f"tb = py2bit.open('{str(input_path)}', {store_masked}); " + f"print(tb.sequence('{chromosome}', {start}, {end_val}))" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "sequence": result.stdout.strip(), + "length": len(result.stdout.strip()), + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": e.stderr or str(e) + } + +@mcp.tool() +def py2bit_get_bases( + input_2bit: str, + chromosome: str, + start: int = 0, + end: int = 0, + as_fraction: bool = True, +) -> Dict[str, Any]: + """ + Compute the composition of bases (A, C, T, G) in a chromosome or specific region. + + Args: + input_2bit: Path to the .2bit file. + chromosome: Name of the chromosome/contig. + start: 0-based start position (default 0). + end: 0-based end position (default 0, which fetches until the end of the chromosome). + as_fraction: If True (default), returns fractions (0.0-1.0). If False, returns integer counts. + """ + input_path = Path(input_2bit) + if not input_path.exists(): + return {"error": f"Input file {input_2bit} does not exist."} + + end_val = end if end > 0 else "None" + py_cmd = ( + f"import py2bit, json; " + f"tb = py2bit.open('{str(input_path)}'); " + f"print(json.dumps(tb.bases('{chromosome}', {start}, {end_val}, {as_fraction})))" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "base_stats": json.loads(result.stdout.strip()), + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": e.stderr or str(e) + } + +@mcp.tool() +def py2bit_get_masked_blocks( + input_2bit: str, + chromosome: str, + mask_type: str = "hard", + start: int = 0, + end: int = 0, +) -> Dict[str, Any]: + """ + Fetch masked blocks (hard-masked NNNN or soft-masked lowercase) for a chromosome or region. + + Args: + input_2bit: Path to the .2bit file. + chromosome: Name of the chromosome/contig. + mask_type: Type of masking to query: 'hard' (default) or 'soft'. + start: 0-based start position for overlap query (default 0). + end: 0-based end position for overlap query (default 0, returns all blocks). + """ + input_path = Path(input_2bit) + if not input_path.exists(): + return {"error": f"Input file {input_2bit} does not exist."} + + if mask_type not in ["hard", "soft"]: + return {"error": "mask_type must be either 'hard' or 'soft'."} + + method = "hardMaskedBlocks" if mask_type == "hard" else "softMaskedBlocks" + end_val = end if end > 0 else "None" + + # softMaskedBlocks requires storeMasked=True during open + py_cmd = ( + f"import py2bit, json; " + f"tb = py2bit.open('{str(input_path)}', True); " + f"print(json.dumps(tb.{method}('{chromosome}', {start}, {end_val})))" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "mask_type": mask_type, + "blocks": json.loads(result.stdout.strip()), + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": e.stderr or str(e) + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_py2bit/app/py2bit_shim_server.py b/Biomni/mcp_generated/mcp_py2bit/app/py2bit_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..688813cd2e2b6a7125e5300b9e43b0f0396ad697 --- /dev/null +++ b/Biomni/mcp_generated/mcp_py2bit/app/py2bit_shim_server.py @@ -0,0 +1,55 @@ +#!/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_py2bit/app/py2bit_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_py2bit' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_py2bit/app/requirements.txt b/Biomni/mcp_generated/mcp_py2bit/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_py2bit/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_py2bit/docker-compose.yml b/Biomni/mcp_generated/mcp_py2bit/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..39a9a46e92cb0a139fc04a8d84a31ded94953a23 --- /dev/null +++ b/Biomni/mcp_generated/mcp_py2bit/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-py2bit: + build: . + image: mcp-py2bit:latest + container_name: mcp-py2bit + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=py2bit + 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/Biomni/mcp_generated/mcp_py2bit/environment.yaml b/Biomni/mcp_generated/mcp_py2bit/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b972826e7fc22d637f61eb4f9916f6d4aead5a07 --- /dev/null +++ b/Biomni/mcp_generated/mcp_py2bit/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - py2bit + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_py2bit/requirements.txt b/Biomni/mcp_generated/mcp_py2bit/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_py2bit/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_pyhmmer/Dockerfile b/Biomni/mcp_generated/mcp_pyhmmer/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..80b76ccbf2a98077416e7790faa3f27d530fc3c0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pyhmmer/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 pyhmmer via conda (e.g., from bioconda) +RUN conda install -c bioconda pyhmmer -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/pyhmmer_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/pyhmmer_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/pyhmmer_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_pyhmmer/app/pyhmmer_server.py b/Biomni/mcp_generated/mcp_pyhmmer/app/pyhmmer_server.py new file mode 100644 index 0000000000000000000000000000000000000000..bbcad3e3b3f5529ac4faadeab688367f0808fed4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pyhmmer/app/pyhmmer_server.py @@ -0,0 +1,771 @@ +import pyhmmer +from pathlib import Path +import tempfile +from typing import Optional, List, Dict, Any, Iterable, Literal + +# --- Helper functions for writing pyhmmer results to files --- + +def _write_hits_to_tblout(hits_iterable: Iterable[pyhmmer.plan7.TopHits], output_path: Path): + """ + Writes an iterable of pyhmmer.plan7.TopHits objects to a simplified tabular + output file, similar to HMMER's --tblout format. + """ + with open(output_path, "w") as f: + f.write("# target name\ttarget accession\tquery name\tquery accession\tE-value\tscore\tbias\t# of domains\n") + for hits in hits_iterable: + query_name = hits.query.name.decode() if hits.query.name else "N/A" + query_acc = hits.query.accession.decode() if hits.query.accession else "N/A" + for hit in hits: + target_name = hit.name.decode() if hit.name else "N/A" + target_acc = hit.accession.decode() if hit.accession else "N/A" + f.write( + f"{target_name}\t{target_acc}\t" + f"{query_name}\t{query_acc}\t" + f"{hit.evalue:.2e}\t{hit.score:.1f}\t{hit.bias:.1f}\t" + f"{len(hit.domains)}\n" + ) + +def _write_hits_to_domtblout(hits_iterable: Iterable[pyhmmer.plan7.TopHits], output_path: Path): + """ + Writes an iterable of pyhmmer.plan7.TopHits objects to a simplified domain + tabular output file, similar to HMMER's --domtblout format. + """ + with open(output_path, "w") as f: + f.write("# target name\ttarget accession\tquery name\tquery accession\t" + "domain E-value\tdomain score\tdomain bias\t" + "hmm_from\thmm_to\tali_from\tali_to\tenv_from\tenv_to\n") + for hits in hits_iterable: + query_name = hits.query.name.decode() if hits.query.name else "N/A" + query_acc = hits.query.accession.decode() if hits.query.accession else "N/A" + for hit in hits: + target_name = hit.name.decode() if hit.name else "N/A" + target_acc = hit.accession.decode() if hit.accession else "N/A" + for domain in hit.domains: + f.write( + f"{target_name}\t{target_acc}\t" + f"{query_name}\t{query_acc}\t" + f"{domain.evalue:.2e}\t{domain.score:.1f}\t{domain.bias:.1f}\t" + f"{domain.hmm_from}\t{domain.hmm_to}\t" + f"{domain.ali_from}\t{domain.ali_to}\t" + f"{domain.env_from}\t{domain.env_to}\n" + ) + +def _write_alignment_to_file(alignment: pyhmmer.easel.Alignment, output_path: Path, output_format: str): + """ + Writes a pyhmmer.easel.Alignment object to a file in the specified format. + """ + # pyhmmer.easel.Alignment.write expects a file-like object opened in binary mode ('wb') + with open(output_path, "wb") as f: + alignment.write(f, format=output_format) + +# --- MCP Tool Definitions --- + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_pyhmmer' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def hmmsearch( + hmm_file: Path, + sequence_file: Path, + cpus: int = 1, + e_value_cutoff: Optional[float] = None, + dom_e_value_cutoff: Optional[float] = None, + bit_score_cutoff: Optional[float] = None, + max_target_seqs: Optional[int] = None, + cut_ga: bool = False, + cut_nc: bool = False, + cut_tc: bool = False, + bias_filter: bool = False, + output_tblout: Optional[Path] = None, + output_domtblout: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Searches a sequence database against one or more profile HMMs using pyhmmer.hmmsearch. + + This function wraps the pyhmmer.hmmsearch API, which provides Python bindings + to HMMER3's hmmsearch functionality. It searches protein sequences against + a database of profile HMMs. + + Args: + hmm_file: Path to the HMM database file (e.g., in HMMER3 text format). + sequence_file: Path to the target sequence database file (e.g., FASTA format). + cpus: Number of CPU threads to use for parallel processing. Must be a positive integer. + e_value_cutoff: E-value threshold for reporting hits. Hits with E-value > this are not reported. + dom_e_value_cutoff: E-value threshold for reporting domains. Domains with E-value > this are not reported. + bit_score_cutoff: Bit score threshold for reporting hits. Hits with score < this are not reported. + max_target_seqs: Maximum number of target sequences to report per query HMM. + cut_ga: Use the HMM's trusted gathering cutoff (GA) to set score thresholds. + cut_nc: Use the HMM's noise cutoff (NC) to set score thresholds. + cut_tc: Use the HMM's trusted cutoff (TC) to set score thresholds. + bias_filter: Turn on the bias filter (default is off). + output_tblout: Optional path to write the tabular summary of hits. If not provided, a temporary file will be created. + output_domtblout: Optional path to write the tabular summary of domain hits. If not provided, a temporary file will be created. + + Returns: + A dictionary containing: + - "command_executed": A string representing the pyhmmer API call. + - "stdout": Standard output (empty for pyhmmer API calls unless explicitly captured). + - "stderr": Standard error (empty for pyhmmer API calls unless explicitly captured). + - "output_files": A list of paths to generated output files (e.g., tabular hit files). + """ + # Input validation + if not hmm_file.is_file(): + raise FileNotFoundError(f"HMM file not found: {hmm_file}") + if not sequence_file.is_file(): + raise FileNotFoundError(f"Sequence file not found: {sequence_file}") + if cpus < 1: + raise ValueError("Number of CPUs must be a positive integer.") + if e_value_cutoff is not None and e_value_cutoff <= 0: + raise ValueError("E-value cutoff must be positive.") + if dom_e_value_cutoff is not None and dom_e_value_cutoff <= 0: + raise ValueError("Domain E-value cutoff must be positive.") + if bit_score_cutoff is not None and bit_score_cutoff < 0: + raise ValueError("Bit score cutoff must be non-negative.") + if max_target_seqs is not None and max_target_seqs < 1: + raise ValueError("Max target sequences must be a positive integer.") + + # Prepare output files + temp_output_tblout = False + if output_tblout is None: + output_tblout = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".tblout").name) + temp_output_tblout = True + + temp_output_domtblout = False + if output_domtblout is None: + output_domtblout = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".domtblout").name) + temp_output_domtblout = True + + output_files: List[Path] = [] + stdout_buffer = [] + stderr_buffer = [] + + try: + # Load HMMs and sequences + with pyhmmer.plan7.HMMFile(hmm_file) as hmms_reader: + hmms = list(hmms_reader) # Load all HMMs into memory + + with pyhmmer.easel.SequenceFile(sequence_file, digital=True) as seqs_reader: + sequences = seqs_reader.read_block() # Load sequences into memory + + # Construct pyhmmer.hmmsearch arguments + search_kwargs = { + "cpus": cpus, + "cut_ga": cut_ga, + "cut_nc": cut_nc, + "cut_tc": cut_tc, + "bias_filter": bias_filter, + } + if e_value_cutoff is not None: + search_kwargs["E"] = e_value_cutoff + if dom_e_value_cutoff is not None: + search_kwargs["domE"] = dom_e_value_cutoff + if bit_score_cutoff is not None: + search_kwargs["T"] = bit_score_cutoff + if max_target_seqs is not None: + search_kwargs["max_target_seqs"] = max_target_seqs + + # Execute hmmsearch and collect results (pyhmmer.hmmsearch returns a generator) + all_hits = list(pyhmmer.hmmsearch(hmms, sequences, **search_kwargs)) + + # Write results to output files + _write_hits_to_tblout(all_hits, output_tblout) + output_files.append(output_tblout) + + _write_hits_to_domtblout(all_hits, output_domtblout) + output_files.append(output_domtblout) + + except Exception as e: + stderr_buffer.append(f"Error during pyhmmer.hmmsearch execution: {e}") + # Clean up temporary files if an error occurred + if temp_output_tblout and output_tblout.exists(): + output_tblout.unlink() + if temp_output_domtblout and output_domtblout.exists(): + output_domtblout.unlink() + return { + "command_executed": f"pyhmmer.hmmsearch(hmms={hmm_file.name}, sequences={sequence_file.name}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": [], + } + + return { + "command_executed": f"pyhmmer.hmmsearch(hmms={hmm_file.name}, sequences={sequence_file.name}, cpus={cpus}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": output_files, + } + +@mcp.tool() +def hmmscan( + sequence_file: Path, + hmm_file: Path, + cpus: int = 1, + e_value_cutoff: Optional[float] = None, + dom_e_value_cutoff: Optional[float] = None, + bit_score_cutoff: Optional[float] = None, + max_target_seqs: Optional[int] = None, + cut_ga: bool = False, + cut_nc: bool = False, + cut_tc: bool = False, + bias_filter: bool = False, + output_tblout: Optional[Path] = None, + output_domtblout: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Searches a sequence (or sequences) against a profile HMM database using pyhmmer.hmmscan. + + This function wraps the pyhmmer.hmmscan API, which provides Python bindings + to HMMER3's hmmscan functionality. It searches protein sequences against + a database of profile HMMs. + + Args: + sequence_file: Path to the query sequence file (e.g., FASTA format). + hmm_file: Path to the HMM database file (e.g., in HMMER3 text format). + cpus: Number of CPU threads to use for parallel processing. Must be a positive integer. + e_value_cutoff: E-value threshold for reporting hits. Hits with E-value > this are not reported. + dom_e_value_cutoff: E-value threshold for reporting domains. Domains with E-value > this are not reported. + bit_score_cutoff: Bit score threshold for reporting hits. Hits with score < this are not reported. + max_target_seqs: Maximum number of target sequences to report per query sequence. + cut_ga: Use the HMM's trusted gathering cutoff (GA) to set score thresholds. + cut_nc: Use the HMM's noise cutoff (NC) to set score thresholds. + cut_tc: Use the HMM's trusted cutoff (TC) to set score thresholds. + bias_filter: Turn on the bias filter (default is off). + output_tblout: Optional path to write the tabular summary of hits. If not provided, a temporary file will be created. + output_domtblout: Optional path to write the tabular summary of domain hits. If not provided, a temporary file will be created. + + Returns: + A dictionary containing: + - "command_executed": A string representing the pyhmmer API call. + - "stdout": Standard output (empty for pyhmmer API calls unless explicitly captured). + - "stderr": Standard error (empty for pyhmmer API calls unless explicitly captured). + - "output_files": A list of paths to generated output files (e.g., tabular hit files). + """ + # Input validation + if not hmm_file.is_file(): + raise FileNotFoundError(f"HMM file not found: {hmm_file}") + if not sequence_file.is_file(): + raise FileNotFoundError(f"Sequence file not found: {sequence_file}") + if cpus < 1: + raise ValueError("Number of CPUs must be a positive integer.") + if e_value_cutoff is not None and e_value_cutoff <= 0: + raise ValueError("E-value cutoff must be positive.") + if dom_e_value_cutoff is not None and dom_e_value_cutoff <= 0: + raise ValueError("Domain E-value cutoff must be positive.") + if bit_score_cutoff is not None and bit_score_cutoff < 0: + raise ValueError("Bit score cutoff must be non-negative.") + if max_target_seqs is not None and max_target_seqs < 1: + raise ValueError("Max target sequences must be a positive integer.") + + # Prepare output files + temp_output_tblout = False + if output_tblout is None: + output_tblout = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".tblout").name) + temp_output_tblout = True + + temp_output_domtblout = False + if output_domtblout is None: + output_domtblout = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".domtblout").name) + temp_output_domtblout = True + + output_files: List[Path] = [] + stdout_buffer = [] + stderr_buffer = [] + + try: + # Load HMMs and sequences + with pyhmmer.plan7.HMMFile(hmm_file) as hmms_reader: + hmms = list(hmms_reader) # Load all HMMs into memory + + with pyhmmer.easel.SequenceFile(sequence_file, digital=True) as seqs_reader: + sequences = seqs_reader.read_block() # Load sequences into memory + + # Construct pyhmmer.hmmscan arguments + search_kwargs = { + "cpus": cpus, + "cut_ga": cut_ga, + "cut_nc": cut_nc, + "cut_tc": cut_tc, + "bias_filter": bias_filter, + } + if e_value_cutoff is not None: + search_kwargs["E"] = e_value_cutoff + if dom_e_value_cutoff is not None: + search_kwargs["domE"] = dom_e_value_cutoff + if bit_score_cutoff is not None: + search_kwargs["T"] = bit_score_cutoff + if max_target_seqs is not None: + search_kwargs["max_target_seqs"] = max_target_seqs + + # Execute hmmscan and collect results (pyhmmer.hmmscan returns a generator) + all_hits = list(pyhmmer.hmmscan(sequences, hmms, **search_kwargs)) + + # Write results to output files + _write_hits_to_tblout(all_hits, output_tblout) + output_files.append(output_tblout) + + _write_hits_to_domtblout(all_hits, output_domtblout) + output_files.append(output_domtblout) + + except Exception as e: + stderr_buffer.append(f"Error during pyhmmer.hmmscan execution: {e}") + # Clean up temporary files if an error occurred + if temp_output_tblout and output_tblout.exists(): + output_tblout.unlink() + if temp_output_domtblout and output_domtblout.exists(): + output_domtblout.unlink() + return { + "command_executed": f"pyhmmer.hmmscan(sequences={sequence_file.name}, hmms={hmm_file.name}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": [], + } + + return { + "command_executed": f"pyhmmer.hmmscan(sequences={sequence_file.name}, hmms={hmm_file.name}, cpus={cpus}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": output_files, + } + +@mcp.tool() +def nhmmer( + hmm_file: Path, + sequence_file: Path, + cpus: int = 1, + e_value_cutoff: Optional[float] = None, + dom_e_value_cutoff: Optional[float] = None, + bit_score_cutoff: Optional[float] = None, + max_target_seqs: Optional[int] = None, + bias_filter: bool = False, + output_tblout: Optional[Path] = None, + output_domtblout: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Searches a DNA/RNA sequence database against one or more profile HMMs using pyhmmer.nhmmer. + + This function wraps the pyhmmer.nhmmer API, which provides Python bindings + to HMMER3's nhmmer functionality. It searches nucleic acid sequences against + a database of profile HMMs. + + Args: + hmm_file: Path to the HMM database file (e.g., in HMMER3 text format). This HMM should be nucleic. + sequence_file: Path to the target nucleic acid sequence database file (e.g., FASTA format). + cpus: Number of CPU threads to use for parallel processing. Must be a positive integer. + e_value_cutoff: E-value threshold for reporting hits. Hits with E-value > this are not reported. + dom_e_value_cutoff: E-value threshold for reporting domains. Domains with E-value > this are not reported. + bit_score_cutoff: Bit score threshold for reporting hits. Hits with score < this are not reported. + max_target_seqs: Maximum number of target sequences to report per query HMM. + bias_filter: Turn on the bias filter (default is off). + output_tblout: Optional path to write the tabular summary of hits. If not provided, a temporary file will be created. + output_domtblout: Optional path to write the tabular summary of domain hits. If not provided, a temporary file will be created. + + Returns: + A dictionary containing: + - "command_executed": A string representing the pyhmmer API call. + - "stdout": Standard output (empty for pyhmmer API calls unless explicitly captured). + - "stderr": Standard error (empty for pyhmmer API calls unless explicitly captured). + - "output_files": A list of paths to generated output files (e.g., tabular hit files). + """ + # Input validation + if not hmm_file.is_file(): + raise FileNotFoundError(f"HMM file not found: {hmm_file}") + if not sequence_file.is_file(): + raise FileNotFoundError(f"Sequence file not found: {sequence_file}") + if cpus < 1: + raise ValueError("Number of CPUs must be a positive integer.") + if e_value_cutoff is not None and e_value_cutoff <= 0: + raise ValueError("E-value cutoff must be positive.") + if dom_e_value_cutoff is not None and dom_e_value_cutoff <= 0: + raise ValueError("Domain E-value cutoff must be positive.") + if bit_score_cutoff is not None and bit_score_cutoff < 0: + raise ValueError("Bit score cutoff must be non-negative.") + if max_target_seqs is not None and max_target_seqs < 1: + raise ValueError("Max target sequences must be a positive integer.") + + # Prepare output files + temp_output_tblout = False + if output_tblout is None: + output_tblout = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".tblout").name) + temp_output_tblout = True + + temp_output_domtblout = False + if output_domtblout is None: + output_domtblout = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".domtblout").name) + temp_output_domtblout = True + + output_files: List[Path] = [] + stdout_buffer = [] + stderr_buffer = [] + + try: + # Load HMMs (nhmmer expects nucleic alphabet HMMs) + with pyhmmer.plan7.HMMFile(hmm_file) as hmms_reader: + hmms = list(hmms_reader) + + # For nhmmer, sequences should be nucleic. Explicitly use DNA alphabet. + # pyhmmer.easel.Alphabet.dna() can be used for both DNA and RNA, + # as HMMER's nhmmer typically handles both. + nucleic_alphabet = pyhmmer.easel.Alphabet.dna() + with pyhmmer.easel.SequenceFile(sequence_file, digital=True, alphabet=nucleic_alphabet) as seqs_reader: + sequences = seqs_reader.read_block() + + # Construct pyhmmer.nhmmer arguments + search_kwargs = { + "cpus": cpus, + "bias_filter": bias_filter, + } + if e_value_cutoff is not None: + search_kwargs["E"] = e_value_cutoff + if dom_e_value_cutoff is not None: + search_kwargs["domE"] = dom_e_value_cutoff + if bit_score_cutoff is not None: + search_kwargs["T"] = bit_score_cutoff + if max_target_seqs is not None: + search_kwargs["max_target_seqs"] = max_target_seqs + + # Execute nhmmer and collect results (pyhmmer.nhmmer returns a generator) + all_hits = list(pyhmmer.nhmmer(hmms, sequences, **search_kwargs)) + + # Write results to output files + _write_hits_to_tblout(all_hits, output_tblout) + output_files.append(output_tblout) + + _write_hits_to_domtblout(all_hits, output_domtblout) + output_files.append(output_domtblout) + + except Exception as e: + stderr_buffer.append(f"Error during pyhmmer.nhmmer execution: {e}") + # Clean up temporary files if an error occurred + if temp_output_tblout and output_tblout.exists(): + output_tblout.unlink() + if temp_output_domtblout and output_domtblout.exists(): + output_domtblout.unlink() + return { + "command_executed": f"pyhmmer.nhmmer(hmms={hmm_file.name}, sequences={sequence_file.name}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": [], + } + + return { + "command_executed": f"pyhmmer.nhmmer(hmms={hmm_file.name}, sequences={sequence_file.name}, cpus={cpus}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": output_files, + } + +@mcp.tool() +def phmmer( + query_sequence_file: Path, + target_sequence_file: Path, + cpus: int = 1, + e_value_cutoff: Optional[float] = None, + bit_score_cutoff: Optional[float] = None, + max_target_seqs: Optional[int] = None, + bias_filter: bool = False, + output_tblout: Optional[Path] = None, + output_domtblout: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Searches a protein sequence against a protein sequence database using pyhmmer.phmmer. + + This function wraps the pyhmmer.phmmer API, which provides Python bindings + to HMMER3's phmmer functionality. It performs sequence-vs-sequence searches + using HMMER's acceleration heuristics. + + Args: + query_sequence_file: Path to the query protein sequence file (e.g., FASTA format). + target_sequence_file: Path to the target protein sequence database file (e.g., FASTA format). + cpus: Number of CPU threads to use for parallel processing. Must be a positive integer. + e_value_cutoff: E-value threshold for reporting hits. Hits with E-value > this are not reported. + bit_score_cutoff: Bit score threshold for reporting hits. Hits with score < this are not reported. + max_target_seqs: Maximum number of target sequences to report per query sequence. + bias_filter: Turn on the bias filter (default is off). + output_tblout: Optional path to write the tabular summary of hits. If not provided, a temporary file will be created. + output_domtblout: Optional path to write the tabular summary of domain hits. If not provided, a temporary file will be created. + + Returns: + A dictionary containing: + - "command_executed": A string representing the pyhmmer API call. + - "stdout": Standard output (empty for pyhmmer API calls unless explicitly captured). + - "stderr": Standard error (empty for pyhmmer API calls unless explicitly captured). + - "output_files": A list of paths to generated output files (e.g., tabular hit files). + """ + # Input validation + if not query_sequence_file.is_file(): + raise FileNotFoundError(f"Query sequence file not found: {query_sequence_file}") + if not target_sequence_file.is_file(): + raise FileNotFoundError(f"Target sequence file not found: {target_sequence_file}") + if cpus < 1: + raise ValueError("Number of CPUs must be a positive integer.") + if e_value_cutoff is not None and e_value_cutoff <= 0: + raise ValueError("E-value cutoff must be positive.") + if bit_score_cutoff is not None and bit_score_cutoff < 0: + raise ValueError("Bit score cutoff must be non-negative.") + if max_target_seqs is not None and max_target_seqs < 1: + raise ValueError("Max target sequences must be a positive integer.") + + # Prepare output files + temp_output_tblout = False + if output_tblout is None: + output_tblout = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".tblout").name) + temp_output_tblout = True + + temp_output_domtblout = False + if output_domtblout is None: + output_domtblout = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".domtblout").name) + temp_output_domtblout = True + + output_files: List[Path] = [] + stdout_buffer = [] + stderr_buffer = [] + + try: + # Load query and target sequences (phmmer expects protein alphabet) + protein_alphabet = pyhmmer.easel.Alphabet.amino() + with pyhmmer.easel.SequenceFile(query_sequence_file, digital=True, alphabet=protein_alphabet) as query_seqs_reader: + query_sequences = query_seqs_reader.read_block() + + with pyhmmer.easel.SequenceFile(target_sequence_file, digital=True, alphabet=protein_alphabet) as target_seqs_reader: + target_sequences = target_seqs_reader.read_block() + + # Construct pyhmmer.phmmer arguments + search_kwargs = { + "cpus": cpus, + "bias_filter": bias_filter, + } + if e_value_cutoff is not None: + search_kwargs["E"] = e_value_cutoff + if bit_score_cutoff is not None: + search_kwargs["T"] = bit_score_cutoff + if max_target_seqs is not None: + search_kwargs["max_target_seqs"] = max_target_seqs + + # Execute phmmer and collect results (pyhmmer.phmmer returns a generator) + all_hits = list(pyhmmer.phmmer(query_sequences, target_sequences, **search_kwargs)) + + # Write results to output files + _write_hits_to_tblout(all_hits, output_tblout) + output_files.append(output_tblout) + + _write_hits_to_domtblout(all_hits, output_domtblout) + output_files.append(output_domtblout) + + except Exception as e: + stderr_buffer.append(f"Error during pyhmmer.phmmer execution: {e}") + # Clean up temporary files if an error occurred + if temp_output_tblout and output_tblout.exists(): + output_tblout.unlink() + if temp_output_domtblout and output_domtblout.exists(): + output_domtblout.unlink() + return { + "command_executed": f"pyhmmer.phmmer(query_sequences={query_sequence_file.name}, target_sequences={target_sequence_file.name}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": [], + } + + return { + "command_executed": f"pyhmmer.phmmer(query_sequences={query_sequence_file.name}, target_sequences={target_sequence_file.name}, cpus={cpus}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": output_files, + } + +@mcp.tool() +def hmmbuild( + alignment_file: Path, + output_hmm_file: Path, + alphabet_type: Literal["amino", "dna", "rna"], + name: Optional[str] = None, + accession: Optional[str] = None, + hand_model: bool = True, # Corresponds to HMMER's default 'hand' model construction + symfrac: float = 0.5, + fragsc: float = 0.0, + fast: bool = False, +) -> Dict[str, Any]: + """ + Builds a profile HMM from a multiple sequence alignment using pyhmmer.hmmbuild. + + This function wraps the pyhmmer.hmmbuild API, which provides Python bindings + to HMMER3's hmmbuild functionality. + + Args: + alignment_file: Path to the multiple sequence alignment file (e.g., Stockholm, FASTA, Clustal). + output_hmm_file: Path to write the output HMM file. + alphabet_type: The type of alphabet for the sequences in the alignment ("amino", "dna", or "rna"). + name: Optional name for the HMM. + accession: Optional accession for the HMM. + hand_model: If True, use the 'hand' model construction algorithm (default). If False, use 'wbl' (WBL model). + Note: pyhmmer.hmmbuild takes `builder` argument, which defaults to `pyhmmer.plan7.HMM.hand()`. + For simplicity, this parameter exposes a boolean for the common 'hand' vs 'wbl' choice. + symfrac: Minimum fraction of residues required to be non-gap in a column for it to be considered a match state. + (0.0 to 1.0, default 0.5). + fragsc: Fragment score. (default 0.0). + fast: Use a faster, less accurate algorithm for model construction. + + Returns: + A dictionary containing: + - "command_executed": A string representing the pyhmmer API call. + - "stdout": Standard output (empty for pyhmmer API calls unless explicitly captured). + - "stderr": Standard error (empty for pyhmmer API calls unless explicitly captured). + - "output_files": A list of paths to generated output files (e.g., the HMM file). + """ + # Input validation + if not alignment_file.is_file(): + raise FileNotFoundError(f"Alignment file not found: {alignment_file}") + if not (0.0 <= symfrac <= 1.0): + raise ValueError("symfrac must be between 0.0 and 1.0.") + + output_files: List[Path] = [] + stdout_buffer = [] + stderr_buffer = [] + + try: + # Determine alphabet + if alphabet_type == "amino": + alphabet = pyhmmer.easel.Alphabet.amino() + elif alphabet_type == "dna": + alphabet = pyhmmer.easel.Alphabet.dna() + elif alphabet_type == "rna": + alphabet = pyhmmer.easel.Alphabet.rna() + else: + raise ValueError(f"Invalid alphabet type: {alphabet_type}") + + # Load alignment + with pyhmmer.easel.AlignmentFile(alignment_file, alphabet=alphabet) as alignment_reader: + alignment = alignment_reader.read() + + # Construct HMM builder + builder = pyhmmer.plan7.HMM.hand() if hand_model else pyhmmer.plan7.HMM.wbl() + + # Construct pyhmmer.hmmbuild arguments + build_kwargs = { + "builder": builder, + "symfrac": symfrac, + "fragsc": fragsc, + "fast": fast, + } + if name: + build_kwargs["name"] = name.encode() # pyhmmer expects bytes + if accession: + build_kwargs["accession"] = accession.encode() # pyhmmer expects bytes + + # Execute hmmbuild + hmm = pyhmmer.hmmbuild(alignment, **build_kwargs) + + # Write HMM to file + with open(output_hmm_file, "wb") as f: + hmm.write(f) + output_files.append(output_hmm_file) + + except Exception as e: + stderr_buffer.append(f"Error during pyhmmer.hmmbuild execution: {e}") + if output_hmm_file.exists(): + output_hmm_file.unlink() + return { + "command_executed": f"pyhmmer.hmmbuild(alignment={alignment_file.name}, alphabet_type='{alphabet_type}', ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": [], + } + + return { + "command_executed": f"pyhmmer.hmmbuild(alignment={alignment_file.name}, alphabet_type='{alphabet_type}', name='{name or 'N/A'}', ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": output_files, + } + +@mcp.tool() +def hmmalign( + hmm_file: Path, + sequence_file: Path, + output_alignment_file: Path, + alphabet_type: Literal["amino", "dna", "rna"], + output_format: Literal["stockholm", "fasta", "clustal", "a2m"] = "stockholm", + cpus: int = 1, +) -> Dict[str, Any]: + """ + Aligns sequences to a profile HMM using pyhmmer.hmmalign. + + This function wraps the pyhmmer.hmmalign API, which provides Python bindings + to HMMER3's hmmalign functionality. + + Args: + hmm_file: Path to the profile HMM file. + sequence_file: Path to the sequences to align (e.g., FASTA format). + output_alignment_file: Path to write the output multiple sequence alignment file. + alphabet_type: The type of alphabet for the sequences and HMM ("amino", "dna", or "rna"). + output_format: Format for the output alignment ("stockholm", "fasta", "clustal", or "a2m"). + cpus: Number of CPU threads to use for parallel processing. Must be a positive integer. + + Returns: + A dictionary containing: + - "command_executed": A string representing the pyhmmer API call. + - "stdout": Standard output (empty for pyhmmer API calls unless explicitly captured). + - "stderr": Standard error (empty for pyhmmer API calls unless explicitly captured). + - "output_files": A list of paths to generated output files (e.g., the alignment file). + """ + # Input validation + if not hmm_file.is_file(): + raise FileNotFoundError(f"HMM file not found: {hmm_file}") + if not sequence_file.is_file(): + raise FileNotFoundError(f"Sequence file not found: {sequence_file}") + if cpus < 1: + raise ValueError("Number of CPUs must be a positive integer.") + if output_format not in ["stockholm", "fasta", "clustal", "a2m"]: + raise ValueError(f"Invalid output format: {output_format}. Must be one of 'stockholm', 'fasta', 'clustal', 'a2m'.") + + output_files: List[Path] = [] + stdout_buffer = [] + stderr_buffer = [] + + try: + # Determine alphabet + if alphabet_type == "amino": + alphabet = pyhmmer.easel.Alphabet.amino() + elif alphabet_type == "dna": + alphabet = pyhmmer.easel.Alphabet.dna() + elif alphabet_type == "rna": + alphabet = pyhmmer.easel.Alphabet.rna() + else: + raise ValueError(f"Invalid alphabet type: {alphabet_type}") + + # Load HMM (hmmalign expects a single HMM) + with pyhmmer.plan7.HMMFile(hmm_file) as hmm_reader: + hmm = hmm_reader.read() + + # Load sequences + with pyhmmer.easel.SequenceFile(sequence_file, digital=True, alphabet=alphabet) as seqs_reader: + sequences = seqs_reader.read_block() + + # Execute hmmalign + alignment = pyhmmer.hmmalign(hmm, sequences, cpus=cpus) + + # Write alignment to file + _write_alignment_to_file(alignment, output_alignment_file, output_format) + output_files.append(output_alignment_file) + + except Exception as e: + stderr_buffer.append(f"Error during pyhmmer.hmmalign execution: {e}") + if output_alignment_file.exists(): + output_alignment_file.unlink() + return { + "command_executed": f"pyhmmer.hmmalign(hmm={hmm_file.name}, sequences={sequence_file.name}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": [], + } + + return { + "command_executed": f"pyhmmer.hmmalign(hmm={hmm_file.name}, sequences={sequence_file.name}, cpus={cpus}, ...)", + "stdout": "\n".join(stdout_buffer), + "stderr": "\n".join(stderr_buffer), + "output_files": output_files, + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_pyhmmer/app/pyhmmer_shim_server.py b/Biomni/mcp_generated/mcp_pyhmmer/app/pyhmmer_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8eb58775a8c2131d7b1acaeaccb232f1fcb57011 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pyhmmer/app/pyhmmer_shim_server.py @@ -0,0 +1,55 @@ +#!/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_pyhmmer/app/pyhmmer_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_pyhmmer' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_pyhmmer/app/requirements.txt b/Biomni/mcp_generated/mcp_pyhmmer/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f380eeefe98e7ee5cedaa02ab7925e0042d17a28 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pyhmmer/app/requirements.txt @@ -0,0 +1 @@ +pyhmmer==0.12.0 diff --git a/Biomni/mcp_generated/mcp_pyhmmer/docker-compose.yml b/Biomni/mcp_generated/mcp_pyhmmer/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..a5c383474f9b7ae4ddef5a395114f2e691db2980 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pyhmmer/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-pyhmmer: + build: . + image: mcp-pyhmmer:latest + container_name: mcp-pyhmmer + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=pyhmmer + 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/Biomni/mcp_generated/mcp_pyhmmer/environment.yaml b/Biomni/mcp_generated/mcp_pyhmmer/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f9aca9bdb48120a18fc8569c01d892235212bed2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pyhmmer/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - pyhmmer + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_pyhmmer/requirements.txt b/Biomni/mcp_generated/mcp_pyhmmer/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..d1de2c99250251e1750b7ddd161435e73286276a --- /dev/null +++ b/Biomni/mcp_generated/mcp_pyhmmer/requirements.txt @@ -0,0 +1,3 @@ +pyhmmer==0.12.0 +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_r-azimuth/Dockerfile b/Biomni/mcp_generated/mcp_r-azimuth/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..7e5cf8cc7373e337a2f75c39afeafa4af426a4f0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-azimuth/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-azimuth via conda (e.g., from bioconda) +RUN conda install -c bioconda r-azimuth -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-azimuth_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-azimuth_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-azimuth_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-azimuth/app/r-azimuth_server.py b/Biomni/mcp_generated/mcp_r-azimuth/app/r-azimuth_server.py new file mode 100644 index 0000000000000000000000000000000000000000..35570abe2fa5f19b65033d388b9237379f535555 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-azimuth/app/r-azimuth_server.py @@ -0,0 +1,135 @@ +import subprocess +import json +from pathlib import Path +from typing import Optional, Dict, Any +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Mock mcp.tool decorator for standalone execution +class mcp: + def tool(func): + return func + +@mcp.tool +def azimuth_app( + reference: Optional[Path] = None, + config: Optional[Path] = None, + do_bridge: bool = False, + max_cells: Optional[int] = None, + de_digits: Optional[int] = None, + map_pbcorthresh: Optional[float] = None, +) -> Dict[str, Any]: + """ + Launches the Azimuth Shiny app for single-cell data mapping. + + Azimuth is a Shiny app for mapping query single-cell datasets to a reference. + This function constructs and executes the R command to launch the app with + specified configurations. Note that as a Shiny app, this tool is interactive + and will launch a web server process that may run indefinitely until terminated. + + Args: + reference: Path to a directory containing reference files (e.g., ref.Rds, idx.annoy). + If not provided, Azimuth attempts to load a default reference from the web. + config: Path to a JSON-formatted configuration file for Azimuth options. + do_bridge: If True, runs the Azimuth ATAC workflow with a sc/snATAC-seq query. + Corresponds to the R option 'Azimuth.app.do_bridge' = TRUE. + max_cells: An integer specifying the maximum number of cells to process. + Corresponds to the R option 'max_cells'. + de_digits: An integer for the number of digits to display for differential expression results. + Corresponds to the R option 'Azimuth.de.digits'. + map_pbcorthresh: A float for the poor batch correction threshold. + Corresponds to the R option 'Azimuth.map.pbcorthresh'. + + Returns: + A dictionary containing the execution details, including the command executed, + stdout, stderr, and a list of output files (which is expected to be empty). + """ + # --- Input Validation --- + if reference: + if not reference.is_dir(): + raise ValueError(f"Reference path '{reference}' must be a directory.") + if not (reference / "ref.Rds").exists() or not (reference / "idx.annoy").exists(): + logger.warning( + f"Reference directory '{reference}' is missing 'ref.Rds' or 'idx.annoy'. " + "Azimuth may fail to load the reference." + ) + + if config: + if not config.is_file(): + raise ValueError(f"Config path '{config}' must be a valid file.") + + if max_cells is not None and max_cells <= 0: + raise ValueError("max_cells must be a positive integer.") + + if de_digits is not None and de_digits < 0: + raise ValueError("de_digits must be a non-negative integer.") + + if map_pbcorthresh is not None and not (0.0 <= map_pbcorthresh <= 1.0): + raise ValueError("map_pbcorthresh must be a float between 0.0 and 1.0.") + + # --- Command Construction --- + r_args = [] + if reference: + r_args.append(f"reference = '{reference.resolve()}'") + if config: + r_args.append(f"config = '{config.resolve()}'") + if do_bridge: + # In R, boolean TRUE is not quoted + r_args.append("'Azimuth.app.do_bridge' = TRUE") + if max_cells is not None: + r_args.append(f"max_cells = {max_cells}") + if de_digits is not None: + # This option requires its full name with quotes + r_args.append(f"'Azimuth.de.digits' = {de_digits}") + if map_pbcorthresh is not None: + # This option requires its full name with quotes + r_args.append(f"'Azimuth.map.pbcorthresh' = {map_pbcorthresh}") + + args_string = ", ".join(r_args) + r_command = f"Azimuth::AzimuthApp({args_string})" + + # The final command to be executed by the shell + cmd = ["R", "-e", r_command] + command_executed = " ".join(map(str, cmd)) + logger.info(f"Executing command: {command_executed}") + + # --- Subprocess Execution --- + try: + # Note: AzimuthApp launches a web server, which is a long-running process. + # The execution environment (e.g., MCP) should handle potential timeouts + # if this process is not intended to run indefinitely. + 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: 'R' executable not found. Please ensure R is installed and in your PATH.", + "output_files": [] + } + except subprocess.CalledProcessError as e: + logger.error(f"Azimuth execution failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + + # --- Structured Result Return --- + # AzimuthApp is interactive and does not produce output files in a non-interactive run. + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": [] + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-azimuth/app/r-azimuth_shim_server.py b/Biomni/mcp_generated/mcp_r-azimuth/app/r-azimuth_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f95df63d7f7d142f0dffdc5c52ec8e1bbd0a2bbd --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-azimuth/app/r-azimuth_shim_server.py @@ -0,0 +1,55 @@ +#!/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-azimuth/app/r-azimuth_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_r_azimuth' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_r-azimuth/app/requirements.txt b/Biomni/mcp_generated/mcp_r-azimuth/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-azimuth/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_r-azimuth/docker-compose.yml b/Biomni/mcp_generated/mcp_r-azimuth/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..48bdd59e0d93835da5d4a05534cda00604a47bb0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-azimuth/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-r-azimuth: + build: . + image: mcp-r-azimuth:latest + container_name: mcp-r-azimuth + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=r-azimuth + 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/Biomni/mcp_generated/mcp_r-azimuth/environment.yaml b/Biomni/mcp_generated/mcp_r-azimuth/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d3ebab03465dccdf3b59f39075c4d750a62ba193 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-azimuth/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - r-azimuth + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-azimuth/requirements.txt b/Biomni/mcp_generated/mcp_r-azimuth/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-azimuth/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_r-flanders/Dockerfile b/Biomni/mcp_generated/mcp_r-flanders/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1e6058c7194b4d1ae80eba7c56f67638ea04448b --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-flanders/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-flanders via conda (e.g., from bioconda) +RUN conda install -c bioconda r-flanders -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-flanders_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-flanders_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-flanders_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-flanders/app/r-flanders_server.py b/Biomni/mcp_generated/mcp_r-flanders/app/r-flanders_server.py new file mode 100644 index 0000000000000000000000000000000000000000..18416b02c1217fde92f4d9b8eb298f34bfb33b85 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-flanders/app/r-flanders_server.py @@ -0,0 +1,281 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Dict, Any + +# Helper function to run R scripts +def _run_r_script(r_script_content: str, output_files: Optional[List[Path]] = None) -> Dict[str, Any]: + """ + Executes an R script and captures its output. + + Args: + r_script_content: The R script code as a string. + output_files: An optional list of expected output file paths to check for existence. + + Returns: + A dictionary containing command execution details, stdout, stderr, and output files. + """ + with tempfile.NamedTemporaryFile(mode="w", suffix=".R", delete=False) as temp_r_script: + temp_r_script.write(r_script_content) + temp_r_script_path = Path(temp_r_script.name) + + command = ["Rscript", str(temp_r_script_path)] + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + stdout = process.stdout + stderr = process.stderr + + final_output_files = [] + if output_files: + for f in output_files: + if f.exists(): + final_output_files.append(str(f)) + else: + # If an expected output file is missing, it indicates a potential issue + # but we don't want to fail the entire tool if R didn't explicitly error. + # This could be logged or handled based on specific tool requirements. + pass + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": final_output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"R script execution failed with exit code {e.returncode}", + "output_files": [], + } + finally: + # Clean up the temporary R script + if temp_r_script_path.exists(): + temp_r_script_path.unlink() + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_r_flanders' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def finemap2anndata( + finemap_files: List[Path], + phenotype_ids: List[str], + study_ids: List[str], + output_anndata_path: Path, +) -> Dict[str, Any]: + """ + Converts a set of finemapping .rds files into a single AnnData object with credible set metadata. + + This function takes multiple finemapping output .rds files, along with corresponding + phenotype and study IDs, and merges them into a unified AnnData object. The resulting + AnnData object can then be used for downstream colocalization analysis. + + Args: + finemap_files: A list of paths to finemapping .rds files. Each file should be an + output from a finemapping pipeline (e.g., nf-flanders). + phenotype_ids: A list of phenotype identifiers, where each element corresponds + to a file in `finemap_files`. The length of this list must match + `finemap_files`. + study_ids: A list of study identifiers, where each element corresponds to a file + in `finemap_files`. The length of this list must match `finemap_files`. + output_anndata_path: The desired path to write the resulting AnnData object + in H5AD format (.h5ad extension is required). + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of + generated output files. + + Raises: + ValueError: If input validation fails (e.g., empty file list, mismatched lengths, + incorrect file extensions, or non-existent files). + """ + # Input validation + if not finemap_files: + raise ValueError("finemap_files cannot be empty.") + for f in finemap_files: + if not f.is_file(): + raise ValueError(f"Finemap file '{f}' does not exist.") + if f.suffix != ".rds": + raise ValueError(f"Finemap file '{f}' must have a .rds extension.") + + if not (len(finemap_files) == len(phenotype_ids) == len(study_ids)): + raise ValueError( + "finemap_files, phenotype_ids, and study_ids must have the same length. " + f"Lengths: {len(finemap_files)}, {len(phenotype_ids)}, {len(study_ids)}" + ) + if output_anndata_path.suffix != ".h5ad": + raise ValueError(f"output_anndata_path '{output_anndata_path}' must have a .h5ad extension.") + + # Prepare R vectors from Python lists + r_finemap_files = 'c(' + ', '.join([f'"{str(f)}"' for f in finemap_files]) + ')' + r_phenotype_ids = 'c(' + ', '.join([f'"{pid}"' for pid in phenotype_ids]) + ')' + r_study_ids = 'c(' + ', '.join([f'"{sid}"' for sid in study_ids]) + ')' + + r_script = f""" + library(flanders) + library(data.table) # Dependency for flanders, often used for data manipulation + library(dplyr) # Dependency for flanders, often used for data manipulation + library(anndata) # Required for writing AnnData objects to h5ad + + finemap_files <- {r_finemap_files} + phenotype_id <- {r_phenotype_ids} + study_id <- {r_study_ids} + + # Ensure the output directory exists + output_dir <- dirname("{str(output_anndata_path)}") + if (!dir.exists(output_dir)) {{ + dir.create(output_dir, recursive = TRUE) + }} + + ad <- finemap2anndata( + finemap_files = finemap_files, + phenotype_id = phenotype_id, + study_id = study_id + ) + + # Write the AnnData object to disk + ad$write_h5ad("{str(output_anndata_path)}") + """ + + return _run_r_script(r_script, output_files=[output_anndata_path]) + + +@mcp.tool() +def anndata2coloc_input( + input_anndata_path: Path, + output_coloc_input_path: Path, +) -> Dict[str, Any]: + """ + Generates a data frame specifying pairs of credible sets for colocalization tests. + + This function takes an existing AnnData object (typically generated by `finemap2anndata`) + and processes it to create an input table suitable for colocalization testing. This + table defines the pairs of credible sets that will be tested for colocalization. + + Args: + input_anndata_path: Path to an existing AnnData object (.h5ad) containing + credible set metadata. + output_coloc_input_path: The desired path to write the resulting colocalization + input table in CSV format (.csv extension is required). + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of + generated output files. + + Raises: + ValueError: If input validation fails (e.g., non-existent input file, + incorrect file extensions). + """ + # Input validation + if not input_anndata_path.is_file(): + raise ValueError(f"Input AnnData file '{input_anndata_path}' does not exist.") + if input_anndata_path.suffix != ".h5ad": + raise ValueError(f"input_anndata_path '{input_anndata_path}' must be an .h5ad file.") + if output_coloc_input_path.suffix != ".csv": + raise ValueError(f"output_coloc_input_path '{output_coloc_input_path}' must have a .csv extension.") + + r_script = f""" + library(zellkonverter) + library(SingleCellExperiment) + library(scRNAseq) # Often loaded in contexts using zellkonverter/SingleCellExperiment + library(flanders) + library(data.table) # Required for fwrite + + # Ensure the output directory exists + output_dir <- dirname("{str(output_coloc_input_path)}") + if (!dir.exists(output_dir)) {{ + dir.create(output_dir, recursive = TRUE) + }} + + sce <- readH5AD("{str(input_anndata_path)}", reader = "R") + coloc_input <- anndata2coloc_input(sce) + fwrite(coloc_input, file = "{str(output_coloc_input_path)}") + """ + + return _run_r_script(r_script, output_files=[output_coloc_input_path]) + + +@mcp.tool() +def anndata2coloc( + input_anndata_path: Path, + input_coloc_input_path: Path, + output_coloc_results_path: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Performs colocalization tests using the provided AnnData object and trait-pair table. + + This function executes pairwise colocalization tests based on an AnnData object + (containing finemapping results) and a pre-generated colocalization input table. + The results can either be printed to standard output or saved to a specified CSV file. + + Args: + input_anndata_path: Path to an existing AnnData object (.h5ad) containing + credible set metadata. + input_coloc_input_path: Path to the colocalization input table (.csv) + generated by `anndata2coloc_input`. This table specifies + which credible set pairs to test. + output_coloc_results_path: Optional path to write the colocalization results + table in CSV format (.csv extension is required if provided). + If not provided, the results will be printed to stdout. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of + generated output files. + + Raises: + ValueError: If input validation fails (e.g., non-existent input files, + incorrect file extensions). + """ + # Input validation + if not input_anndata_path.is_file(): + raise ValueError(f"Input AnnData file '{input_anndata_path}' does not exist.") + if input_anndata_path.suffix != ".h5ad": + raise ValueError(f"input_anndata_path '{input_anndata_path}' must be an .h5ad file.") + + if not input_coloc_input_path.is_file(): + raise ValueError(f"Input colocalization input file '{input_coloc_input_path}' does not exist.") + if input_coloc_input_path.suffix != ".csv": + raise ValueError(f"input_coloc_input_path '{input_coloc_input_path}' must be a .csv file.") + + if output_coloc_results_path and output_coloc_results_path.suffix != ".csv": + raise ValueError(f"output_coloc_results_path '{output_coloc_results_path}' must have a .csv extension if provided.") + + r_script_lines = [ + "library(zellkonverter)", + "library(SingleCellExperiment)", + "library(scRNAseq)", # Often loaded in contexts using zellkonverter/SingleCellExperiment + "library(flanders)", + "library(data.table)", # Required for fread and fwrite + "", + f'sce <- readH5AD("{str(input_anndata_path)}", reader = "R")', + f'coloc_input <- fread("{str(input_coloc_input_path)}")', + "coloc_results <- anndata2coloc(sce, coloc_input)", + ] + + output_files_to_check = [] + if output_coloc_results_path: + # Ensure the output directory exists + r_script_lines.append(f'output_dir <- dirname("{str(output_coloc_results_path)}")') + r_script_lines.append('if (!dir.exists(output_dir)) { dir.create(output_dir, recursive = TRUE) }') + r_script_lines.append(f'fwrite(coloc_results, file = "{str(output_coloc_results_path)}")') + output_files_to_check.append(output_coloc_results_path) + else: + r_script_lines.append("print(coloc_results)") + + r_script = "\n".join(r_script_lines) + + return _run_r_script(r_script, output_files=output_files_to_check) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_r-flanders/app/r-flanders_shim_server.py b/Biomni/mcp_generated/mcp_r-flanders/app/r-flanders_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3073fbda100bcb6d192b478eeca790b0446aaf34 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-flanders/app/r-flanders_shim_server.py @@ -0,0 +1,55 @@ +#!/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-flanders/app/r-flanders_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_r_flanders' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_r-flanders/app/requirements.txt b/Biomni/mcp_generated/mcp_r-flanders/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-flanders/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_r-flanders/docker-compose.yml b/Biomni/mcp_generated/mcp_r-flanders/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..a175064158b5acf2bdc30af56b54ae776471343d --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-flanders/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-r-flanders: + build: . + image: mcp-r-flanders:latest + container_name: mcp-r-flanders + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=r-flanders + 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/Biomni/mcp_generated/mcp_r-flanders/environment.yaml b/Biomni/mcp_generated/mcp_r-flanders/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7b27db9c59039267ea0618da7f321cde3f19d266 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-flanders/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - r-flanders + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-flanders/requirements.txt b/Biomni/mcp_generated/mcp_r-flanders/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-flanders/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_r-mams/Dockerfile b/Biomni/mcp_generated/mcp_r-mams/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6978c0fced048acb8a49efa97ed9ff1a8f0858e3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-mams/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-mams via conda (e.g., from bioconda) +RUN conda install -c bioconda r-mams -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-mams_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-mams_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-mams_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-mams/app/r-mams_server.py b/Biomni/mcp_generated/mcp_r-mams/app/r-mams_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f2d9932e3217b0a5276e2f788a5a7294f65b7919 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-mams/app/r-mams_server.py @@ -0,0 +1,144 @@ +import subprocess +from pathlib import Path +from typing import Optional, List +import os + +# This is a placeholder for the MCP decorator. +# In a real MCP environment, this would be provided by the MCP framework. +class mcp: + @staticmethod + def tool(func): + return func + +@mcp.tool +def r_mams( + input_file: Path, + output_dir: Path, + param_file: Optional[Path] = None, + ppm: float = 5.0, + rt_tol: float = 30.0, + min_peak_height: float = 1000.0, + min_peak_width: float = 5.0, + max_peak_width: float = 60.0, + sn_thresh: float = 10.0, + prefilter_intensity: float = 100.0, + noise_intensity: float = 100.0, + threads: int = 1, + verbose: bool = False, +) -> dict: + """ + Executes the r-mams tool for metabolomics data processing. + + This tool performs peak picking and feature detection on mass spectrometry data. + It requires an input file with at least 'mz' and 'rt' columns and generates + output files in the specified directory. + + Args: + input_file: Path to the input file. A tab-separated file with at least 'mz' and 'rt' columns. + output_dir: Path to the output directory where results will be stored. + param_file: Optional path to a JSON file with MAMS parameters. + ppm: Mass tolerance in parts-per-million (ppm). Must be positive. + rt_tol: Retention time tolerance in seconds. Must be non-negative. + min_peak_height: Minimum peak height. Must be non-negative. + min_peak_width: Minimum peak width in seconds. Must be positive. + max_peak_width: Maximum peak width in seconds. Must be greater than min_peak_width. + sn_thresh: Signal-to-noise ratio threshold. Must be non-negative. + prefilter_intensity: Prefilter intensity threshold. Must be non-negative. + noise_intensity: Noise intensity threshold. Must be non-negative. + threads: Number of threads to use for processing. Must be at least 1. + verbose: Enable verbose output. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + if param_file and not param_file.is_file(): + raise FileNotFoundError(f"Parameter file not found: {param_file}") + + if ppm <= 0: + raise ValueError("ppm must be a positive number.") + if rt_tol < 0: + raise ValueError("rt_tol cannot be negative.") + if min_peak_height < 0: + raise ValueError("min_peak_height cannot be negative.") + if min_peak_width <= 0: + raise ValueError("min_peak_width must be a positive number.") + if max_peak_width <= min_peak_width: + raise ValueError("max_peak_width must be greater than min_peak_width.") + if sn_thresh < 0: + raise ValueError("sn_thresh cannot be negative.") + if prefilter_intensity < 0: + raise ValueError("prefilter_intensity cannot be negative.") + if noise_intensity < 0: + raise ValueError("noise_intensity cannot be negative.") + if threads < 1: + raise ValueError("threads must be at least 1.") + + # --- File Path Handling --- + try: + output_dir.mkdir(parents=True, exist_ok=True) + except OSError as e: + raise OSError(f"Could not create output directory {output_dir}: {e}") + + # --- Command Construction --- + cmd = [ + "r-mams", + "--input_file", str(input_file), + "--output_dir", str(output_dir), + "--ppm", str(ppm), + "--rt_tol", str(rt_tol), + "--min_peak_height", str(min_peak_height), + "--min_peak_width", str(min_peak_width), + "--max_peak_width", str(max_peak_width), + "--sn_thresh", str(sn_thresh), + "--prefilter_intensity", str(prefilter_intensity), + "--noise_intensity", str(noise_intensity), + "--threads", str(threads), + ] + + if param_file: + cmd.extend(["--param_file", str(param_file)]) + + if verbose: + cmd.append("--verbose") + + command_executed = " ".join(cmd) + + # --- Subprocess Execution & Error Handling --- + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + stdout = result.stdout + stderr = result.stderr + except FileNotFoundError: + raise RuntimeError("r-mams executable not found. Please ensure it is in your system's PATH.") + except subprocess.CalledProcessError as e: + # In case of an error, return a structured error message + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"r-mams failed with exit code {e.returncode}", + "output_files": [] + } + + # --- Structured Result Return --- + try: + output_files = [str(f) for f in output_dir.glob("**/*") if f.is_file()] + except Exception as e: + output_files = [] + stderr += f"\nWarning: Could not list output files in {output_dir}. Error: {e}" + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-mams/app/r-mams_shim_server.py b/Biomni/mcp_generated/mcp_r-mams/app/r-mams_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..03a5227f97bae3b87a948c872c0af6bb3ff97232 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-mams/app/r-mams_shim_server.py @@ -0,0 +1,55 @@ +#!/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-mams/app/r-mams_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_r_mams' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_r-mams/app/requirements.txt b/Biomni/mcp_generated/mcp_r-mams/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-mams/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_r-mams/docker-compose.yml b/Biomni/mcp_generated/mcp_r-mams/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..0215d12b56a637868ef49a1c1e760504cdb80050 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-mams/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-r-mams: + build: . + image: mcp-r-mams:latest + container_name: mcp-r-mams + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=r-mams + 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/Biomni/mcp_generated/mcp_r-mams/environment.yaml b/Biomni/mcp_generated/mcp_r-mams/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7ee49bff10b0e03fc56f62fe58b6c7f5f1d0b89a --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-mams/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - r-mams + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-mams/requirements.txt b/Biomni/mcp_generated/mcp_r-mams/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-mams/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_r-redeemr/Dockerfile b/Biomni/mcp_generated/mcp_r-redeemr/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3c1f7338c4e8de5ce0eb67cbeb826d86db0d78db --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-redeemr/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-redeemr via conda (e.g., from bioconda) +RUN conda install -c bioconda r-redeemr -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-redeemr_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-redeemr_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-redeemr_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-redeemr/app/r-redeemr_server.py b/Biomni/mcp_generated/mcp_r-redeemr/app/r-redeemr_server.py new file mode 100644 index 0000000000000000000000000000000000000000..14bbf735219bc9ad10f1bd1fd8b1415623344fe1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-redeemr/app/r-redeemr_server.py @@ -0,0 +1,145 @@ +import subprocess +import tempfile +import shlex +from pathlib import Path +from typing import Optional, List, Dict, Any + +# Assuming mcp is available in the environment as per the instructions. +# from mcp import tool as mcp.tool + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_r_redeemr' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def redeemr( + genome: Path, + species: str, + output_dir: Optional[Path] = None, + nproc: int = 1, + repeatmasker_dir: Optional[Path] = None, + repeatmodeler_dir: Optional[Path] = None, + trf_dir: Optional[Path] = None, + rmblast_dir: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Runs the redeemr pipeline for REpeats DE-novo Estimation and Masking in R. + + This tool serves as a wrapper for the `redeemr::redeem()` R function, which + automates the process of de-novo repeat identification and masking using + RepeatModeler and RepeatMasker. + + Args: + genome: Path to the input genome file in FASTA format. + species: The species name to be used by RepeatMasker (e.g., "homo_sapiens"). + output_dir: Directory to save the output files. If not provided, a temporary directory will be created. + nproc: Number of processors to use for the analysis. + repeatmasker_dir: Optional path to the RepeatMasker installation directory. + repeatmodeler_dir: Optional path to the RepeatModeler installation directory. + trf_dir: Optional path to the Tandem Repeats Finder (TRF) installation directory. + rmblast_dir: Optional path to the RMBlast (a modified version of NCBI BLAST) installation directory. + + Returns: + A dictionary containing the execution details, including the command, + stdout, stderr, and paths to the output directory and files. + """ + # 1. Input Validation + if not genome.is_file(): + raise FileNotFoundError(f"Input genome file not found at: {genome}") + if not species.strip(): + raise ValueError("Species name cannot be empty.") + if nproc <= 0: + raise ValueError(f"Number of processors (nproc) must be a positive integer, but got {nproc}.") + + for optional_path in [repeatmasker_dir, repeatmodeler_dir, trf_dir, rmblast_dir]: + if optional_path and not optional_path.is_dir(): + raise NotADirectoryError(f"Provided path is not a valid directory: {optional_path}") + + # 2. Output Directory Handling + # If an output directory is not specified, create a temporary one. + # This is a safe practice for server environments. + temp_dir_obj = None + if output_dir: + final_output_dir = output_dir + else: + temp_dir_obj = tempfile.TemporaryDirectory(prefix="redeemr_output_") + final_output_dir = Path(temp_dir_obj.name) + + final_output_dir.mkdir(parents=True, exist_ok=True) + + # 3. R Script Generation + # Building an R script dynamically is safer than complex command-line strings. + r_args = [ + f'genome = "{genome.resolve()}"', + f'species = "{species}"', + f'outputDir = "{final_output_dir.resolve()}"', + f'nproc = {nproc}' + ] + + def format_r_path(name: str, path: Optional[Path]) -> str: + """Helper to format optional paths for the R script (path or NULL).""" + if path: + return f'{name} = "{path.resolve()}"' + return f'{name} = NULL' + + r_args.append(format_r_path('RepeatMasker_dir', repeatmasker_dir)) + r_args.append(format_r_path('RepeatModeler_dir', repeatmodeler_dir)) + r_args.append(format_r_path('TRF_dir', trf_dir)) + r_args.append(format_r_path('RMBlast_dir', rmblast_dir)) + + r_code = f"library(redeemr)\nredeemr::redeem({', '.join(r_args)})" + + # 4. Subprocess Execution + script_file = None + command_executed = "" + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False, dir=final_output_dir) as fp: + script_file = Path(fp.name) + fp.write(r_code) + + cmd = ["Rscript", str(script_file)] + command_executed = " ".join(shlex.quote(c) for c in cmd) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + cwd=final_output_dir # Execute from the output directory + ) + + # 5. Collect Output Files + output_files = [str(p) for p in final_output_dir.rglob('*') if p.is_file()] + + return { + "command_executed": command_executed, + "r_script_content": r_code, + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": str(final_output_dir), + "output_files": output_files + } + + except FileNotFoundError: + raise RuntimeError("`Rscript` command not found. Please ensure R is installed and in the system's PATH.") + except subprocess.CalledProcessError as e: + # Structured error return + return { + "command_executed": command_executed, + "r_script_content": r_code, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "r-redeemr execution failed with a non-zero exit code.", + "return_code": e.returncode + } + finally: + # Cleanup the temporary R script + if script_file and script_file.exists(): + script_file.unlink() + # Cleanup the temporary directory object if it was created + if temp_dir_obj: + temp_dir_obj.cleanup() + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_r-redeemr/app/r-redeemr_shim_server.py b/Biomni/mcp_generated/mcp_r-redeemr/app/r-redeemr_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..dbd50945780f19f25a781c9030ec556174986676 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-redeemr/app/r-redeemr_shim_server.py @@ -0,0 +1,55 @@ +#!/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-redeemr/app/r-redeemr_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_r_redeemr' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_r-redeemr/app/requirements.txt b/Biomni/mcp_generated/mcp_r-redeemr/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-redeemr/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_r-redeemr/docker-compose.yml b/Biomni/mcp_generated/mcp_r-redeemr/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..da7308bfd1fd8ecef5ac7487ca4610a6351c8ec5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-redeemr/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-r-redeemr: + build: . + image: mcp-r-redeemr:latest + container_name: mcp-r-redeemr + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=r-redeemr + 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/Biomni/mcp_generated/mcp_r-redeemr/environment.yaml b/Biomni/mcp_generated/mcp_r-redeemr/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3797808281b0dabf1ec3fff525d74d1a722cbe01 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-redeemr/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - r-redeemr + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_r-redeemr/requirements.txt b/Biomni/mcp_generated/mcp_r-redeemr/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_r-redeemr/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_recognizer/requirements.txt b/Biomni/mcp_generated/mcp_recognizer/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_recognizer/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_rnftools/Dockerfile b/Biomni/mcp_generated/mcp_rnftools/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e940b3ea9cbe7abf8e2d595fd0802b3001474012 --- /dev/null +++ b/Biomni/mcp_generated/mcp_rnftools/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 rnftools via conda (e.g., from bioconda) +RUN conda install -c bioconda rnftools -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/rnftools_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/rnftools_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/rnftools_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_rnftools/app/requirements.txt b/Biomni/mcp_generated/mcp_rnftools/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_rnftools/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_rnftools/app/rnftools_server.py b/Biomni/mcp_generated/mcp_rnftools/app/rnftools_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a7044d050bd9a13f8dc12ee09a9e06592dfc3229 --- /dev/null +++ b/Biomni/mcp_generated/mcp_rnftools/app/rnftools_server.py @@ -0,0 +1,162 @@ +import subprocess +import shlex +from pathlib import Path +from typing import Optional, List, Dict, Any + +# Note: 'mcp' is not imported as per instructions. + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_rnftools' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def rnftools_run_generic( + command_args: List[str], + working_dir: Optional[Path] = None, + log_file: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Executes a generic rnftools command. + + This function serves as a placeholder because the specific subcommands and + their parameters for rnftools could not be extracted from the provided + documentation. To create a fully functional MCP tool, detailed command-line + help output (e.g., `rnftools --help`) is required to identify all + subcommands, their arguments, types, and default values. + + Args: + command_args: A list of strings representing the rnftools subcommand + and its arguments. For example: ["simulate", "--genome", "ref.fa"]. + This parameter is required to specify what rnftools should do. + working_dir: Optional path to the directory where the command should be executed. + If None, the current working directory is used. + log_file: Optional path to a file where stdout and stderr will be redirected. + If None, stdout and stderr are captured and returned. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any + output files. + """ + if not command_args: + raise ValueError("command_args cannot be empty. Please specify an rnftools subcommand and its arguments.") + + cmd = ["rnftools"] + command_args + + # Validate working_dir if provided + if working_dir: + if not working_dir.is_dir(): + raise ValueError(f"Working directory '{working_dir}' does not exist or is not a directory.") + working_dir_str = str(working_dir) + else: + working_dir_str = None + + # Prepare for output redirection if log_file is provided + stdout_capture = subprocess.PIPE + stderr_capture = subprocess.PIPE + log_file_handle = None + + if log_file: + try: + log_file.parent.mkdir(parents=True, exist_ok=True) + log_file_handle = open(log_file, 'w') + stdout_capture = log_file_handle + stderr_capture = log_file_handle + except IOError as e: + raise IOError(f"Could not open log file '{log_file}' for writing: {e}") + + command_line = shlex.join(cmd) + stdout_output = "" + stderr_output = "" + output_files: List[Path] = [] # This would be populated if specific subcommands were known + + try: + process = subprocess.run( + cmd, + cwd=working_dir_str, + stdout=stdout_capture, + stderr=stderr_capture, + check=True, + text=True, + encoding='utf-8' + ) + if log_file is None: + stdout_output = process.stdout + stderr_output = process.stderr + + except subprocess.CalledProcessError as e: + if log_file_handle: + log_file_handle.close() + return { + "command_executed": command_line, + "stdout": e.stdout if log_file is None else f"See log file: {log_file}", + "stderr": e.stderr if log_file is None else f"See log file: {log_file}", + "returncode": e.returncode, + "error": f"rnftools command failed with exit code {e.returncode}", + "output_files": output_files, + } + except FileNotFoundError: + if log_file_handle: + log_file_handle.close() + return { + "command_executed": command_line, + "stdout": "", + "stderr": "Error: rnftools executable not found. Please ensure it is installed and in your PATH.", + "returncode": 127, + "error": "rnftools executable not found", + "output_files": output_files, + } + except Exception as e: + if log_file_handle: + log_file_handle.close() + return { + "command_executed": command_line, + "stdout": stdout_output, + "stderr": str(e), + "returncode": 1, + "error": f"An unexpected error occurred: {e}", + "output_files": output_files, + } + finally: + if log_file_handle: + log_file_handle.close() + + return { + "command_executed": command_line, + "stdout": stdout_output if log_file is None else f"See log file: {log_file}", + "stderr": stderr_output if log_file is None else f"See log file: {log_file}", + "output_files": output_files, + } + +# Example of how specific subcommands would be implemented if documentation were available: +# +@mcp.tool() +# def rnftools_simulate( +# genome_fasta: Path, +# output_prefix: Path, +# read_length: int = 100, +# num_reads: int = 1000000, +# sequencing_model: str = "illumina", # Example, based on common simulators +# # ... other parameters specific to 'simulate' subcommand +# ) -> Dict[str, Any]: +# """ +# Simulates NGS reads using rnftools. +# """ +# # ... implementation for 'rnftools simulate' +# pass +# +@mcp.tool() +# def rnftools_evaluate( +# rnf_file: Path, +# mapper_output_bam: Path, +# report_dir: Path, +# # ... other parameters specific to 'evaluate' subcommand +# ) -> Dict[str, Any]: +# """ +# Evaluates mapper performance using RNF-compliant data. +# """ +# # ... implementation for 'rnftools evaluate' +# pass + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_rnftools/app/rnftools_shim_server.py b/Biomni/mcp_generated/mcp_rnftools/app/rnftools_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c05a7263105bb0cc00a70bfbec4f1be0056b8277 --- /dev/null +++ b/Biomni/mcp_generated/mcp_rnftools/app/rnftools_shim_server.py @@ -0,0 +1,55 @@ +#!/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_rnftools/app/rnftools_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_rnftools' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_rnftools/docker-compose.yml b/Biomni/mcp_generated/mcp_rnftools/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..f9397dcd5f01221073132142e8153d727c5530b5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_rnftools/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-rnftools: + build: . + image: mcp-rnftools:latest + container_name: mcp-rnftools + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=rnftools + 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/Biomni/mcp_generated/mcp_rnftools/environment.yaml b/Biomni/mcp_generated/mcp_rnftools/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4ce6b970982e7d9c7394dd670c86faacf8de4fb4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_rnftools/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - rnftools + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_rnftools/requirements.txt b/Biomni/mcp_generated/mcp_rnftools/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_rnftools/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_samtools/Dockerfile b/Biomni/mcp_generated/mcp_samtools/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c3466c3e1924a54976d282de13c5f020d8406c5b --- /dev/null +++ b/Biomni/mcp_generated/mcp_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 samtools via conda (e.g., from bioconda) +RUN conda install -c bioconda 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/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/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/samtools_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_samtools/app/__pycache__/samtools_server.cpython-311.pyc b/Biomni/mcp_generated/mcp_samtools/app/__pycache__/samtools_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f28817de16f19b99be4b4e28c6fd6f0bc02bab32 Binary files /dev/null and b/Biomni/mcp_generated/mcp_samtools/app/__pycache__/samtools_server.cpython-311.pyc differ diff --git a/Biomni/mcp_generated/mcp_samtools/app/__pycache__/samtools_shim_server.cpython-311.pyc b/Biomni/mcp_generated/mcp_samtools/app/__pycache__/samtools_shim_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04369d8b678b63982d14b3e52fdf9605487ebc5e Binary files /dev/null and b/Biomni/mcp_generated/mcp_samtools/app/__pycache__/samtools_shim_server.cpython-311.pyc differ diff --git a/Biomni/mcp_generated/mcp_samtools/app/requirements.txt b/Biomni/mcp_generated/mcp_samtools/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_samtools/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_samtools/app/samtools_server.py b/Biomni/mcp_generated/mcp_samtools/app/samtools_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e1c17ae7020a89991d8e4ca5f3079dbd3964ee8d --- /dev/null +++ b/Biomni/mcp_generated/mcp_samtools/app/samtools_server.py @@ -0,0 +1,1337 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Union + +# Helper function to run samtools commands +def _run_samtools_command( + command_args: List[str], + input_files: Optional[List[Path]] = None, + output_files: Optional[List[Path]] = None, + check_existence: bool = True, +) -> dict: + """ + Executes a samtools command and handles output/errors. + + Args: + command_args: List of command-line arguments for samtools. + input_files: List of input file paths to check for existence. + output_files: List of expected output file paths. + check_existence: If True, checks if input_files exist before running. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + full_command = ["samtools"] + command_args + + if check_existence and input_files: + for f in input_files: + if not f.exists(): + raise FileNotFoundError(f"Input file not found: {f}") + + 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"Samtools command failed with exit code {e.returncode}", + "output_files": [], + } + except FileNotFoundError: + return { + "command_executed": " ".join([str(arg) for arg in full_command]), + "stdout": "", + "stderr": "Error: samtools command not found. Is it installed and in PATH?", + "error": "Samtools executable not found.", + "output_files": [], + } + + +# --- Indexing Commands --- + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_dict( + fasta_file: Path, + output_dict_file: Optional[Path] = None, + sequence_name_style: Optional[str] = None, +) -> dict: + """ + Creates a sequence dictionary file (.dict) from a FASTA reference. + + Args: + fasta_file: Path to the input FASTA reference file. + output_dict_file: Optional path for the output dictionary file. If not + provided, it defaults to .dict. + sequence_name_style: Optional style for sequence names in the dictionary. + (e.g., 'ucsc', 'ensembl', 'genbank'). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not fasta_file.exists(): + raise FileNotFoundError(f"Input FASTA file not found: {fasta_file}") + + cmd = ["dict", fasta_file] + output_file = output_dict_file if output_dict_file else fasta_file.with_suffix(".dict") + cmd.extend(["-o", output_file]) + + if sequence_name_style: + cmd.extend(["-s", sequence_name_style]) + + return _run_samtools_command(cmd, input_files=[fasta_file], output_files=[output_file]) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_faidx( + fasta_file: Path, + regions: Optional[List[str]] = None, + output_file: Optional[Path] = None, + regions_file: Optional[Path] = None, + line_length: Optional[int] = None, + chrom_only: bool = False, +) -> dict: + """ + Indexes a FASTA file or extracts sequences from an indexed FASTA. + + Args: + fasta_file: Path to the input FASTA file. + regions: Optional list of regions (e.g., "chr1:100-200") to extract. + If provided, sequences are extracted. + output_file: Optional path for the output FASTA file when extracting regions. + If not provided, extracted sequences go to stdout. + regions_file: Optional path to a file containing regions to extract. + line_length: Optional line length for output sequences (default is 60). + chrom_only: If True, only output chromosome names, not full headers. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not fasta_file.exists(): + raise FileNotFoundError(f"Input FASTA file not found: {fasta_file}") + + cmd = ["faidx", fasta_file] + + if output_file: + cmd.extend(["-o", output_file]) + if regions_file: + if not regions_file.exists(): + raise FileNotFoundError(f"Regions file not found: {regions_file}") + cmd.extend(["-r", regions_file]) + if line_length is not None: + if line_length < 1: + raise ValueError("Line length must be a positive integer.") + cmd.extend(["-n", str(line_length)]) + if chrom_only: + cmd.append("-c") + if regions: + cmd.extend(regions) + + output_files_list = [output_file] if output_file else [] + # faidx also creates a .fai file, but it's implicit and often not returned explicitly + # unless it's the primary output. For extraction, the output_file is primary. + return _run_samtools_command(cmd, input_files=[fasta_file], output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_fqidx( + fastq_file: Path, + regions: Optional[List[str]] = None, + output_file: Optional[Path] = None, + regions_file: Optional[Path] = None, + line_length: Optional[int] = None, + chrom_only: bool = False, +) -> dict: + """ + Indexes a FASTQ file or extracts sequences from an indexed FASTQ. + + Args: + fastq_file: Path to the input FASTQ file. + regions: Optional list of regions (e.g., "chr1:100-200") to extract. + If provided, sequences are extracted. + output_file: Optional path for the output FASTQ file when extracting regions. + If not provided, extracted sequences go to stdout. + regions_file: Optional path to a file containing regions to extract. + line_length: Optional line length for output sequences (default is 60). + chrom_only: If True, only output chromosome names, not full headers. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not fastq_file.exists(): + raise FileNotFoundError(f"Input FASTQ file not found: {fastq_file}") + + cmd = ["fqidx", fastq_file] + + if output_file: + cmd.extend(["-o", output_file]) + if regions_file: + if not regions_file.exists(): + raise FileNotFoundError(f"Regions file not found: {regions_file}") + cmd.extend(["-r", regions_file]) + if line_length is not None: + if line_length < 1: + raise ValueError("Line length must be a positive integer.") + cmd.extend(["-n", str(line_length)]) + if chrom_only: + cmd.append("-c") + if regions: + cmd.extend(regions) + + output_files_list = [output_file] if output_file else [] + # fqidx also creates a .fqi file, but it's implicit. + return _run_samtools_command(cmd, input_files=[fastq_file], output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_index( + alignment_file: Path, + use_csi: bool = False, + min_shift: Optional[int] = None, +) -> dict: + """ + Indexes a BAM or CRAM alignment file. + + Args: + alignment_file: Path to the input BAM or CRAM file. + use_csi: If True, create a CSI index (.csi) instead of a BAI index (.bai). + min_shift: Minimum shift for CSI indexing (default 14 for BAI, 13 for CSI). + Only applicable if `use_csi` is True. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not alignment_file.exists(): + raise FileNotFoundError(f"Input alignment file not found: {alignment_file}") + + cmd = ["index", alignment_file] + output_suffix = ".csi" if use_csi else ".bai" + output_index_file = alignment_file.with_suffix(alignment_file.suffix + output_suffix) + + if use_csi: + cmd.append("-c") + if min_shift is not None: + if min_shift < 1: + raise ValueError("Minimum shift must be a positive integer.") + cmd.extend(["-m", str(min_shift)]) + elif min_shift is not None: + # min_shift is only relevant for CSI, warn if used with BAI + print("Warning: --min-shift is only applicable for CSI indexing (-c). Ignoring for BAI.") + + return _run_samtools_command(cmd, input_files=[alignment_file], output_files=[output_index_file]) + + +# --- Editing Commands --- + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_calmd( + input_bam: Path, + reference_fasta: Path, + output_bam: Optional[Path] = None, + recalculate_tags: bool = False, + uncompressed_output: bool = False, + coefficient: Optional[int] = None, + threads: int = 0, +) -> dict: + """ + Recalculates MD/NM tags and '=' bases in an alignment file. + + Args: + input_bam: Path to the input BAM file. + reference_fasta: Path to the reference FASTA file. + output_bam: Optional path for the output BAM file. If not provided, output goes to stdout. + recalculate_tags: If True, recalculate NM and MD tags. + uncompressed_output: If True, output uncompressed BAM. + coefficient: Coefficient for calculating BQ. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if not reference_fasta.exists(): + raise FileNotFoundError(f"Reference FASTA file not found: {reference_fasta}") + + cmd = ["calmd", input_bam, reference_fasta] + + if output_bam: + cmd.extend(["-o", output_bam]) + if recalculate_tags: + cmd.append("-e") + if uncompressed_output: + cmd.append("-u") + if coefficient is not None: + cmd.extend(["-C", str(coefficient)]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + + output_files_list = [output_bam] if output_bam else [] + return _run_samtools_command( + cmd, input_files=[input_bam, reference_fasta], output_files=output_files_list + ) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_fixmate( + input_bam: Path, + output_bam: Path, + remove_secondary_supplementary: bool = False, + add_pg_tag: bool = False, + add_mate_score: bool = False, + threads: int = 0, +) -> dict: + """ + Fixes mate information for paired-end reads in a BAM file. + Input BAM must be sorted by read name. + + Args: + input_bam: Path to the input BAM file (must be sorted by read name). + output_bam: Path for the output BAM file. + remove_secondary_supplementary: If True, remove secondary and supplementary alignments. + add_pg_tag: If True, add PG tag to output. + add_mate_score: If True, add mate score (ms tag). + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + + cmd = ["fixmate", input_bam, output_bam] + + if remove_secondary_supplementary: + cmd.append("-r") + if add_pg_tag: + cmd.append("-p") + if add_mate_score: + cmd.append("-m") + if threads > 0: + cmd.extend(["-@", str(threads)]) + + return _run_samtools_command(cmd, input_files=[input_bam], output_files=[output_bam]) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_reheader( + new_header_file: Path, + input_bam: Path, + output_bam: Optional[Path] = None, +) -> dict: + """ + Replaces the header of a BAM file with a new header from a SAM file. + + Args: + new_header_file: Path to the SAM file containing the new header. + input_bam: Path to the input BAM file. + output_bam: Optional path for the output BAM file. If not provided, output goes to stdout. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not new_header_file.exists(): + raise FileNotFoundError(f"New header file not found: {new_header_file}") + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + + cmd = ["reheader", new_header_file, input_bam] + + if output_bam: + cmd.extend(["-o", output_bam]) + + output_files_list = [output_bam] if output_bam else [] + return _run_samtools_command( + cmd, input_files=[new_header_file, input_bam], output_files=output_files_list + ) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_targetcut( + input_bam: Path, + output_bam: Optional[Path] = None, + reference_fasta: Optional[Path] = None, +) -> dict: + """ + Cuts fosmid regions (for fosmid pool only). This is a specialized tool. + + Args: + input_bam: Path to the input BAM file. + output_bam: Optional path for the output BAM file. If not provided, output goes to stdout. + reference_fasta: Optional path to the reference FASTA file. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if reference_fasta and not reference_fasta.exists(): + raise FileNotFoundError(f"Reference FASTA file not found: {reference_fasta}") + + cmd = ["targetcut", input_bam] + + if output_bam: + cmd.extend(["-o", output_bam]) + if reference_fasta: + cmd.extend(["-f", reference_fasta]) + + output_files_list = [output_bam] if output_bam else [] + input_files_list = [input_bam] + if reference_fasta: + input_files_list.append(reference_fasta) + + return _run_samtools_command(cmd, input_files=input_files_list, output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_addreplacerg( + input_bam: Path, + rg_line: str, + output_bam: Optional[Path] = None, + overwrite_existing_rg: bool = False, + threads: int = 0, +) -> dict: + """ + Adds or replaces RG (Read Group) tags in an alignment file. + + Args: + input_bam: Path to the input BAM file. + rg_line: The full RG line to add or replace (e.g., '@RG\\tID:group1\\tSM:sample1'). + output_bam: Optional path for the output BAM file. If not provided, output goes to stdout. + overwrite_existing_rg: If True, overwrite existing RG tags. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + + cmd = ["addreplacerg", "-r", rg_line, input_bam] + + if output_bam: + cmd.extend(["-o", output_bam]) + if overwrite_existing_rg: + cmd.append("-w") + if threads > 0: + cmd.extend(["-@", str(threads)]) + + output_files_list = [output_bam] if output_bam else [] + return _run_samtools_command(cmd, input_files=[input_bam], output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_markdup( + input_bam: Path, + output_bam: Path, + remove_duplicates: bool = False, + mark_supplementary: bool = False, + fill_tags: bool = False, + max_isize: Optional[int] = None, + threads: int = 0, +) -> dict: + """ + Marks duplicates in a BAM file. Input BAM must be sorted by coordinate. + + Args: + input_bam: Path to the input BAM file (must be sorted by coordinate). + output_bam: Path for the output BAM file. + remove_duplicates: If True, remove duplicate reads instead of just marking them. + mark_supplementary: If True, mark supplementary alignments as duplicates. + fill_tags: If True, fill in the MC and ML tags. + max_isize: Maximum insert size to consider for duplicates. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + + cmd = ["markdup", input_bam, output_bam] + + if remove_duplicates: + cmd.append("-r") + if mark_supplementary: + cmd.append("-s") + if fill_tags: + cmd.append("-f") + if max_isize is not None: + if max_isize < 0: + raise ValueError("Maximum insert size must be a non-negative integer.") + cmd.extend(["-l", str(max_isize)]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + + return _run_samtools_command(cmd, input_files=[input_bam], output_files=[output_bam]) + + +# --- File Operations Commands --- + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_collate( + input_bam: Path, + output_prefix: Path, + uncompressed_output: bool = False, + threads: int = 0, +) -> dict: + """ + Shuffles and groups alignments by name. + + Args: + input_bam: Path to the input BAM file. + output_prefix: Prefix for the output BAM file(s). + E.g., if 'output_prefix' is 'out', output will be 'out.bam'. + uncompressed_output: If True, output uncompressed BAM. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + + cmd = ["collate", input_bam, output_prefix] + + if uncompressed_output: + cmd.append("-u") + if threads > 0: + cmd.extend(["-@", str(threads)]) + + # samtools collate outputs to .bam + output_file = output_prefix.with_suffix(".bam") + return _run_samtools_command(cmd, input_files=[input_bam], output_files=[output_file]) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_cat( + input_bams: List[Path], + output_bam: Optional[Path] = None, + threads: int = 0, +) -> dict: + """ + Concatenates BAM files. All input files must have identical headers. + + Args: + input_bams: List of paths to input BAM files. + output_bam: Optional path for the output concatenated BAM file. + If not provided, output goes to stdout. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bams: + raise ValueError("At least one input BAM file is required.") + for f in input_bams: + if not f.exists(): + raise FileNotFoundError(f"Input BAM file not found: {f}") + + cmd = ["cat"] + if output_bam: + cmd.extend(["-o", output_bam]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + cmd.extend(input_bams) + + output_files_list = [output_bam] if output_bam else [] + return _run_samtools_command(cmd, input_files=input_bams, output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_merge( + output_bam: Path, + input_bams: List[Path], + add_rg_tags: bool = False, + header_file: Optional[Path] = None, + uncompressed_output: bool = False, + threads: int = 0, +) -> dict: + """ + Merges multiple sorted alignment files into a single sorted file. + + Args: + output_bam: Path for the output merged BAM file. + input_bams: List of paths to input BAM files (must be sorted). + add_rg_tags: If True, add RG tags to output reads. + header_file: Optional path to a SAM header file to use. + uncompressed_output: If True, output uncompressed BAM. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bams: + raise ValueError("At least one input BAM file is required.") + for f in input_bams: + if not f.exists(): + raise FileNotFoundError(f"Input BAM file not found: {f}") + if header_file and not header_file.exists(): + raise FileNotFoundError(f"Header file not found: {header_file}") + + cmd = ["merge", output_bam] + + if add_rg_tags: + cmd.append("-r") + if header_file: + cmd.extend(["-h", header_file]) + if uncompressed_output: + cmd.append("-u") + if threads > 0: + cmd.extend(["-@", str(threads)]) + cmd.extend(input_bams) + + input_files_list = list(input_bams) + if header_file: + input_files_list.append(header_file) + + return _run_samtools_command(cmd, input_files=input_files_list, output_files=[output_bam]) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_mpileup( + reference_fasta: Path, + input_bams: List[Path], + output_file: Optional[Path] = None, + region: Optional[str] = None, + regions_file: Optional[Path] = None, + min_base_quality: int = 0, + min_mapping_quality: int = 0, + uncompressed_bcf_output: bool = False, + threads: int = 0, +) -> dict: + """ + Generates a multi-way pileup from multiple alignment files. + + Args: + reference_fasta: Path to the reference FASTA file. + input_bams: List of paths to input BAM/CRAM files. + output_file: Optional path for the output pileup file. If not provided, output goes to stdout. + region: Optional genomic region to pileup (e.g., "chr1:100-200"). + regions_file: Optional path to a BED file containing regions to pileup. + min_base_quality: Minimum base quality to consider (default 0). + min_mapping_quality: Minimum mapping quality to consider (default 0). + uncompressed_bcf_output: If True, output uncompressed BCF (implies -g). + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not reference_fasta.exists(): + raise FileNotFoundError(f"Reference FASTA file not found: {reference_fasta}") + if not input_bams: + raise ValueError("At least one input BAM file is required.") + for f in input_bams: + if not f.exists(): + raise FileNotFoundError(f"Input BAM file not found: {f}") + if regions_file and not regions_file.exists(): + raise FileNotFoundError(f"Regions file not found: {regions_file}") + if min_base_quality < 0 or min_mapping_quality < 0: + raise ValueError("Quality scores must be non-negative.") + + cmd = ["mpileup", "-f", reference_fasta] + + if output_file: + cmd.extend(["-o", output_file]) + if region: + cmd.extend(["-r", region]) + if regions_file: + cmd.extend(["-l", regions_file]) + if min_base_quality > 0: + cmd.extend(["-q", str(min_base_quality)]) + if min_mapping_quality > 0: + cmd.extend(["-Q", str(min_mapping_quality)]) + if uncompressed_bcf_output: + cmd.append("-u") # -u implies -g for BCF output + if threads > 0: + cmd.extend(["-@", str(threads)]) + cmd.extend(input_bams) + + input_files_list = [reference_fasta] + list(input_bams) + if regions_file: + input_files_list.append(regions_file) + output_files_list = [output_file] if output_file else [] + + return _run_samtools_command(cmd, input_files=input_files_list, output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_sort( + input_alignment_file: Path, + output_alignment_file: Path, + sort_by_name: bool = False, + sort_by_tag: Optional[str] = None, + max_memory_per_thread: Optional[str] = None, + threads: int = 0, + output_format: Optional[str] = None, # 'bam', 'sam', 'cram' +) -> dict: + """ + Sorts an alignment file. + + Args: + input_alignment_file: Path to the input alignment file (BAM, SAM, or CRAM). + output_alignment_file: Path for the output sorted alignment file. + sort_by_name: If True, sort by read name instead of coordinate. + sort_by_tag: Optional tag to sort by (e.g., "RG"). + max_memory_per_thread: Maximum memory per thread (e.g., "750M", "4G"). + threads: Number of additional threads to use (0 for single thread). + output_format: Optional output format ('bam', 'sam', 'cram'). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_alignment_file.exists(): + raise FileNotFoundError(f"Input alignment file not found: {input_alignment_file}") + + cmd = ["sort", input_alignment_file, "-o", output_alignment_file] + + if sort_by_name: + cmd.append("-n") + if sort_by_tag: + cmd.extend(["-t", sort_by_tag]) + if max_memory_per_thread: + cmd.extend(["-m", max_memory_per_thread]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + if output_format: + if output_format not in ["bam", "sam", "cram"]: + raise ValueError(f"Invalid output format: {output_format}. Must be 'bam', 'sam', or 'cram'.") + cmd.extend(["-O", output_format]) + + return _run_samtools_command( + cmd, input_files=[input_alignment_file], output_files=[output_alignment_file] + ) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_split( + input_bam: Path, + output_prefix: Optional[Path] = None, + unmapped_output: Optional[Path] = None, + delete_rg_tag: bool = False, +) -> dict: + """ + Splits a BAM file by read group. + + Args: + input_bam: Path to the input BAM file. + output_prefix: Optional prefix for output files. If not provided, files are named + after read group IDs in the current directory. + unmapped_output: Optional path for a file to store unmapped reads. + delete_rg_tag: If True, delete the RG tag from output reads. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + + cmd = ["split", input_bam] + + if output_prefix: + cmd.extend(["-o", output_prefix]) + if unmapped_output: + cmd.extend(["-u", unmapped_output]) + if delete_rg_tag: + cmd.append("-d") + + # The output files are dynamic based on RG tags. We can't predict them precisely. + # If output_prefix is given, we can assume files like ..bam + # For now, we'll just return the prefix if provided, or an empty list. + output_files_list = [] + if output_prefix: + output_files_list.append(output_prefix) # This is a prefix, not a final file + if unmapped_output: + output_files_list.append(unmapped_output) + + return _run_samtools_command(cmd, input_files=[input_bam], output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_quickcheck( + input_files: List[Path], +) -> dict: + """ + Quickly checks if SAM/BAM/CRAM files appear intact. + + Args: + input_files: List of paths to SAM/BAM/CRAM files to check. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_files: + raise ValueError("At least one input file is required.") + for f in input_files: + if not f.exists(): + raise FileNotFoundError(f"Input file not found: {f}") + + cmd = ["quickcheck"] + input_files + + return _run_samtools_command(cmd, input_files=input_files, output_files=[]) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_fastq( + input_alignment_file: Path, + output_fastq_file: Optional[Path] = None, + output_fastq_r1: Optional[Path] = None, + output_fastq_r2: Optional[Path] = None, + output_fastq_single_end: Optional[Path] = None, + no_comment: bool = False, + threads: int = 0, +) -> dict: + """ + Converts a BAM/CRAM file to FASTQ format. + + Args: + input_alignment_file: Path to the input BAM/CRAM file. + output_fastq_file: Optional path for a single output FASTQ file (for interleaved or single-end). + If not provided, output goes to stdout. + output_fastq_r1: Optional path for R1 output FASTQ file (for paired-end). + output_fastq_r2: Optional path for R2 output FASTQ file (for paired-end). + output_fastq_single_end: Optional path for single-end reads output FASTQ file. + no_comment: If True, do not output comments in FASTQ header. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_alignment_file.exists(): + raise FileNotFoundError(f"Input alignment file not found: {input_alignment_file}") + + cmd = ["fastq", input_alignment_file] + output_files_list = [] + + if output_fastq_file: + cmd.extend(["-o", output_fastq_file]) + output_files_list.append(output_fastq_file) + if output_fastq_r1: + cmd.extend(["-1", output_fastq_r1]) + output_files_list.append(output_fastq_r1) + if output_fastq_r2: + cmd.extend(["-2", output_fastq_r2]) + output_files_list.append(output_fastq_r2) + if output_fastq_single_end: + cmd.extend(["-s", output_fastq_single_end]) + output_files_list.append(output_fastq_single_end) + + if no_comment: + cmd.append("-N") + if threads > 0: + cmd.extend(["-@", str(threads)]) + + # Validate output file combinations + if sum([1 for x in [output_fastq_file, output_fastq_r1, output_fastq_r2] if x is not None]) > 1: + raise ValueError("Cannot specify -o, -1, or -2 simultaneously. Choose one output mode.") + if (output_fastq_r1 and not output_fastq_r2) or (output_fastq_r2 and not output_fastq_r1): + raise ValueError("Both -1 and -2 must be specified for paired-end output.") + + return _run_samtools_command( + cmd, input_files=[input_alignment_file], output_files=output_files_list + ) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_fasta( + input_alignment_file: Path, + output_fasta_file: Optional[Path] = None, + no_comment: bool = False, + region: Optional[str] = None, + regions_file: Optional[Path] = None, + threads: int = 0, +) -> dict: + """ + Converts a BAM/CRAM file to FASTA format. + + Args: + input_alignment_file: Path to the input BAM/CRAM file. + output_fasta_file: Optional path for the output FASTA file. If not provided, output goes to stdout. + no_comment: If True, do not output comments in FASTA header. + region: Optional genomic region to extract (e.g., "chr1:100-200"). + regions_file: Optional path to a BED file containing regions to extract. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_alignment_file.exists(): + raise FileNotFoundError(f"Input alignment file not found: {input_alignment_file}") + if regions_file and not regions_file.exists(): + raise FileNotFoundError(f"Regions file not found: {regions_file}") + + cmd = ["fasta", input_alignment_file] + output_files_list = [] + + if output_fasta_file: + cmd.extend(["-o", output_fasta_file]) + output_files_list.append(output_fasta_file) + if no_comment: + cmd.append("-N") + if region: + cmd.extend(["-r", region]) + if regions_file: + cmd.extend(["-L", regions_file]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + + input_files_list = [input_alignment_file] + if regions_file: + input_files_list.append(regions_file) + + return _run_samtools_command(cmd, input_files=input_files_list, output_files=output_files_list) + + +# --- Statistics Commands --- + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_bedcov( + bed_file: Path, + input_bams: List[Path], + min_base_quality: int = 0, + min_mapping_quality: int = 0, +) -> dict: + """ + Computes read depth per BED region. + + Args: + bed_file: Path to the input BED file. + input_bams: List of paths to input BAM/CRAM files. + min_base_quality: Minimum base quality to consider (default 0). + min_mapping_quality: Minimum mapping quality to consider (default 0). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not bed_file.exists(): + raise FileNotFoundError(f"Input BED file not found: {bed_file}") + if not input_bams: + raise ValueError("At least one input BAM file is required.") + for f in input_bams: + if not f.exists(): + raise FileNotFoundError(f"Input BAM file not found: {f}") + if min_base_quality < 0 or min_mapping_quality < 0: + raise ValueError("Quality scores must be non-negative.") + + cmd = ["bedcov"] + + if min_base_quality > 0: + cmd.extend(["-q", str(min_base_quality)]) + if min_mapping_quality > 0: + cmd.extend(["-Q", str(min_mapping_quality)]) + + cmd.extend([bed_file] + input_bams) + + input_files_list = [bed_file] + list(input_bams) + return _run_samtools_command(cmd, input_files=input_files_list, output_files=[]) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_depth( + input_bams: List[Path], + output_file: Optional[Path] = None, + all_positions: bool = False, + bed_file: Optional[Path] = None, + region: Optional[str] = None, + min_base_quality: int = 0, + min_mapping_quality: int = 0, + threads: int = 0, +) -> dict: + """ + Computes the depth of coverage at each position or within specified regions. + + Args: + input_bams: List of paths to input BAM/CRAM files. + output_file: Optional path for the output depth file. If not provided, output goes to stdout. + all_positions: If True, output all positions, even if depth is 0. + bed_file: Optional path to a BED file to restrict depth calculation to specific regions. + region: Optional genomic region to calculate depth for (e.g., "chr1:100-200"). + min_base_quality: Minimum base quality to consider (default 0). + min_mapping_quality: Minimum mapping quality to consider (default 0). + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bams: + raise ValueError("At least one input BAM file is required.") + for f in input_bams: + if not f.exists(): + raise FileNotFoundError(f"Input BAM file not found: {f}") + if bed_file and not bed_file.exists(): + raise FileNotFoundError(f"BED file not found: {bed_file}") + if min_base_quality < 0 or min_mapping_quality < 0: + raise ValueError("Quality scores must be non-negative.") + + cmd = ["depth"] + + if output_file: + cmd.extend(["-o", output_file]) + if all_positions: + cmd.append("-a") + if bed_file: + cmd.extend(["-b", bed_file]) + if region: + cmd.extend(["-r", region]) + if min_base_quality > 0: + cmd.extend(["-q", str(min_base_quality)]) + if min_mapping_quality > 0: + cmd.extend(["-Q", str(min_mapping_quality)]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + + cmd.extend(input_bams) + + input_files_list = list(input_bams) + if bed_file: + input_files_list.append(bed_file) + output_files_list = [output_file] if output_file else [] + + return _run_samtools_command(cmd, input_files=input_files_list, output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_flagstat( + input_alignment_file: Path, + output_file: Optional[Path] = None, + threads: int = 0, +) -> dict: + """ + Generates simple statistics on alignment flags. + + Args: + input_alignment_file: Path to the input BAM/SAM/CRAM file. + output_file: Optional path for the output statistics file. If not provided, output goes to stdout. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_alignment_file.exists(): + raise FileNotFoundError(f"Input alignment file not found: {input_alignment_file}") + + cmd = ["flagstat", input_alignment_file] + + if output_file: + cmd.extend(["-o", output_file]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + + output_files_list = [output_file] if output_file else [] + return _run_samtools_command( + cmd, input_files=[input_alignment_file], output_files=output_files_list + ) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_idxstats( + input_alignment_file: Path, +) -> dict: + """ + Retrieves and prints statistics from the BAM index file. + + Args: + input_alignment_file: Path to the input BAM/CRAM file. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_alignment_file.exists(): + raise FileNotFoundError(f"Input alignment file not found: {input_alignment_file}") + + cmd = ["idxstats", input_alignment_file] + + return _run_samtools_command(cmd, input_files=[input_alignment_file], output_files=[]) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_phase( + input_bam: Path, + output_file: Optional[Path] = None, + min_base_quality: int = 0, + min_mapping_quality: int = 0, + min_coverage: int = 0, +) -> dict: + """ + Phases heterozygotes in a BAM file. + + Args: + input_bam: Path to the input BAM file. + output_file: Optional path for the output phased variants file. If not provided, output goes to stdout. + min_base_quality: Minimum base quality to consider (default 0). + min_mapping_quality: Minimum mapping quality to consider (default 0). + min_coverage: Minimum coverage to consider a site (default 0). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if min_base_quality < 0 or min_mapping_quality < 0 or min_coverage < 0: + raise ValueError("Quality scores and coverage must be non-negative.") + + cmd = ["phase", input_bam] + + if output_file: + cmd.extend(["-o", output_file]) + if min_base_quality > 0: + cmd.extend(["-q", str(min_base_quality)]) + if min_mapping_quality > 0: + cmd.extend(["-Q", str(min_mapping_quality)]) + if min_coverage > 0: + cmd.extend(["-k", str(min_coverage)]) + + output_files_list = [output_file] if output_file else [] + return _run_samtools_command(cmd, input_files=[input_bam], output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_stats( + input_alignment_file: Path, + output_file: Optional[Path] = None, + reference_fasta: Optional[Path] = None, + coverage_cap: Optional[int] = None, + threads: int = 0, +) -> dict: + """ + Generates detailed statistics from an alignment file. + + Args: + input_alignment_file: Path to the input BAM/SAM/CRAM file. + output_file: Optional path for the output statistics file. If not provided, output goes to stdout. + reference_fasta: Optional path to the reference FASTA file. + coverage_cap: Optional maximum coverage to count (default 250). + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_alignment_file.exists(): + raise FileNotFoundError(f"Input alignment file not found: {input_alignment_file}") + if reference_fasta and not reference_fasta.exists(): + raise FileNotFoundError(f"Reference FASTA file not found: {reference_fasta}") + if coverage_cap is not None and coverage_cap < 0: + raise ValueError("Coverage cap must be a non-negative integer.") + + cmd = ["stats", input_alignment_file] + + if output_file: + cmd.extend(["-o", output_file]) + if reference_fasta: + cmd.extend(["-r", reference_fasta]) + if coverage_cap is not None: + cmd.extend(["-c", str(coverage_cap)]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + + input_files_list = [input_alignment_file] + if reference_fasta: + input_files_list.append(reference_fasta) + output_files_list = [output_file] if output_file else [] + + return _run_samtools_command(cmd, input_files=input_files_list, output_files=output_files_list) + + +# --- Viewing Commands --- + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_flags( + flag_value: Union[int, str], +) -> dict: + """ + Explains BAM flags (integer value or symbolic string). + + Args: + flag_value: An integer representing the flag value or a symbolic string + (e.g., "0x4", "PAIRED", "UNMAP"). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + cmd = ["flags", str(flag_value)] + + return _run_samtools_command(cmd, check_existence=False) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_tview( + input_alignment_file: Path, + reference_fasta: Optional[Path] = None, + region: Optional[str] = None, + display_deletions: bool = False, + position: Optional[str] = None, + sequence: Optional[str] = None, +) -> dict: + """ + Provides a text alignment viewer. Note: This is an interactive tool and its + output is typically not suitable for automated parsing. This wrapper will + capture the initial screen output if not interactive. + + Args: + input_alignment_file: Path to the input BAM/SAM/CRAM file. + reference_fasta: Optional path to the reference FASTA file. + region: Optional genomic region to view (e.g., "chr1:100-200"). + display_deletions: If True, display deletions as '-'. + position: Optional starting position (e.g., "chr1:100"). + sequence: Optional sequence to highlight. + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_alignment_file.exists(): + raise FileNotFoundError(f"Input alignment file not found: {input_alignment_file}") + if reference_fasta and not reference_fasta.exists(): + raise FileNotFoundError(f"Reference FASTA file not found: {reference_fasta}") + + cmd = ["tview", input_alignment_file] + + if reference_fasta: + cmd.extend(["-f", reference_fasta]) + if region: + cmd.extend(["-r", region]) + if display_deletions: + cmd.append("-d") + if position: + cmd.extend(["-p", position]) + if sequence: + cmd.extend(["-s", sequence]) + + input_files_list = [input_alignment_file] + if reference_fasta: + input_files_list.append(reference_fasta) + + # tview is interactive, so capturing stdout might not be useful for all cases. + # It will capture the initial screen dump. + return _run_samtools_command(cmd, input_files=input_files_list, output_files=[]) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_view( + input_alignment_file: Path, + output_file: Optional[Path] = None, + output_format: Optional[str] = None, # 'sam', 'bam', 'cram' + input_is_sam: bool = False, + include_header: bool = False, + regions: Optional[List[str]] = None, + regions_file: Optional[Path] = None, + min_mapping_quality: int = 0, + filter_flags: Optional[int] = None, + require_flags: Optional[int] = None, + threads: int = 0, +) -> dict: + """ + Views, filters, and converts SAM/BAM/CRAM files. + + Args: + input_alignment_file: Path to the input SAM/BAM/CRAM file. + output_file: Optional path for the output alignment file. If not provided, output goes to stdout. + output_format: Optional output format ('sam', 'bam', 'cram'). + input_is_sam: If True, assume input is SAM format. + include_header: If True, include header in output. + regions: Optional list of genomic regions to extract (e.g., "chr1:100-200"). + regions_file: Optional path to a BED file containing regions to extract. + min_mapping_quality: Minimum mapping quality to include reads (default 0). + filter_flags: Exclude reads with any of these flags set (integer value). + require_flags: Only include reads with all of these flags set (integer value). + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_alignment_file.exists(): + raise FileNotFoundError(f"Input alignment file not found: {input_alignment_file}") + if regions_file and not regions_file.exists(): + raise FileNotFoundError(f"Regions file not found: {regions_file}") + if min_mapping_quality < 0: + raise ValueError("Minimum mapping quality must be non-negative.") + if filter_flags is not None and filter_flags < 0: + raise ValueError("Filter flags must be a non-negative integer.") + if require_flags is not None and require_flags < 0: + raise ValueError("Require flags must be a non-negative integer.") + if output_format and output_format not in ["sam", "bam", "cram"]: + raise ValueError(f"Invalid output format: {output_format}. Must be 'sam', 'bam', or 'cram'.") + + cmd = ["view", input_alignment_file] + output_files_list = [] + + if output_file: + cmd.extend(["-o", output_file]) + output_files_list.append(output_file) + if output_format == "sam": + cmd.append("-S") # This is usually for input, but samtools view -O SAM is also possible + # For output, -O is preferred. Let's use -O. + cmd.extend(["-O", "SAM"]) + elif output_format == "bam": + cmd.extend(["-O", "BAM"]) + elif output_format == "cram": + cmd.extend(["-O", "CRAM"]) + + if input_is_sam: + cmd.append("-S") + if include_header: + cmd.append("-h") + if regions_file: + cmd.extend(["-L", regions_file]) + if min_mapping_quality > 0: + cmd.extend(["-q", str(min_mapping_quality)]) + if filter_flags is not None: + cmd.extend(["-F", str(filter_flags)]) + if require_flags is not None: + cmd.extend(["-f", str(require_flags)]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + if regions: + cmd.extend(regions) + + input_files_list = [input_alignment_file] + if regions_file: + input_files_list.append(regions_file) + + return _run_samtools_command(cmd, input_files=input_files_list, output_files=output_files_list) + + +# @mcp.tool() # Uncomment this line when integrating with MCP +def samtools_depad( + input_bam: Path, + output_bam: Optional[Path] = None, + threads: int = 0, +) -> dict: + """ + Converts a padded BAM to an unpadded BAM. + + Args: + input_bam: Path to the input padded BAM file. + output_bam: Optional path for the output unpadded BAM file. If not provided, output goes to stdout. + threads: Number of additional threads to use (0 for single thread). + + Returns: + A dictionary containing command_executed, stdout, stderr, and output_files. + """ + if not input_bam.exists(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + + cmd = ["depad", input_bam] + + if output_bam: + cmd.extend(["-o", output_bam]) + if threads > 0: + cmd.extend(["-@", str(threads)]) + + output_files_list = [output_bam] if output_bam else [] + return _run_samtools_command(cmd, input_files=[input_bam], output_files=output_files_list) \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_samtools/app/samtools_shim_server.py b/Biomni/mcp_generated/mcp_samtools/app/samtools_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..768d2736455ab3e71c84d0f409327f4361ac1d24 --- /dev/null +++ b/Biomni/mcp_generated/mcp_samtools/app/samtools_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_samtools/app/samtools_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_samtools' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_samtools/docker-compose.yml b/Biomni/mcp_generated/mcp_samtools/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..f9f04a0472441a8d98331fc909defa24d3a7bb82 --- /dev/null +++ b/Biomni/mcp_generated/mcp_samtools/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-samtools: + build: . + image: mcp-samtools:latest + container_name: mcp-samtools + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=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/Biomni/mcp_generated/mcp_samtools/environment.yaml b/Biomni/mcp_generated/mcp_samtools/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..863d9f9be5842148dc7d8b005e221928749232c5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_samtools/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - samtools + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_samtools/requirements.txt b/Biomni/mcp_generated/mcp_samtools/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_samtools/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_scarches/Dockerfile b/Biomni/mcp_generated/mcp_scarches/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d67440809be85f6d1dfadf357bbed328e43b193d --- /dev/null +++ b/Biomni/mcp_generated/mcp_scarches/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 scarches via conda (e.g., from bioconda) +RUN conda install -c bioconda scarches -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 scarches_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/scarches_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/scarches_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_scarches/app/scarches_server.py b/Biomni/mcp_generated/mcp_scarches/app/scarches_server.py new file mode 100644 index 0000000000000000000000000000000000000000..eb4c0455aa0bebb00fbcc90751f2653e6a0dd699 --- /dev/null +++ b/Biomni/mcp_generated/mcp_scarches/app/scarches_server.py @@ -0,0 +1,361 @@ +import subprocess +import logging +from pathlib import Path +from typing import List, Dict, Any, Optional + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# MCP decorator is not defined here, but the functions are structured +# as if it were present (e.g., @mcp.tool()). + +def _run_scarches_command(cmd: List[str], output_path: Path) -> Dict[str, Any]: + """ + A helper function to execute scarches commands, handle errors, and + return a structured output. + """ + command_str = " ".join(cmd) + logger.info(f"Executing command: {command_str}") + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_path)] if output_path.exists() else [], + } + except FileNotFoundError: + logger.error("Error: 'python' executable not found. Make sure Python is in your PATH.") + raise + except subprocess.CalledProcessError as e: + logger.error(f"Command failed with exit code {e.returncode}") + logger.error(f"Stdout: {e.stdout}") + logger.error(f"Stderr: {e.stderr}") + raise e + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_scarches' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def train_scvi( + adata_path: Path, + model_path: Path, + gpus: str = "0", + n_epochs: int = 400, + learning_rate: float = 1e-3, + batch_size: int = 128, + early_stopping: bool = False, +) -> Dict[str, Any]: + """ + Trains a scVI model using the scarches command-line interface. + + Args: + adata_path: Path to anndata object with raw counts. + model_path: Path where the trained model will be saved. + gpus: String specifying which GPUs to use (e.g., '0,1'). + n_epochs: Number of epochs for training. + learning_rate: Learning rate for training. + batch_size: Batch size for training. + early_stopping: If True, enables early stopping. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output file path. + """ + if not adata_path.is_file(): + raise FileNotFoundError(f"Input anndata file not found: {adata_path}") + if not model_path.parent.exists(): + model_path.parent.mkdir(parents=True, exist_ok=True) + logger.info(f"Created output directory: {model_path.parent}") + if n_epochs <= 0: + raise ValueError("n_epochs must be a positive integer.") + if batch_size <= 0: + raise ValueError("batch_size must be a positive integer.") + + cmd = [ + "python", "-m", "scarches", "train-scvi", + str(adata_path), + str(model_path), + "--gpus", gpus, + "--n_epochs", str(n_epochs), + "--learning_rate", str(learning_rate), + "--batch_size", str(batch_size), + ] + if early_stopping: + cmd.append("--early_stopping") + + return _run_scarches_command(cmd, model_path) + +@mcp.tool() +def train_scanvi( + adata_path: Path, + model_path: Path, + labels_key: str, + unlabeled_category: str, + gpus: str = "0", + n_epochs_unsupervised: int = 400, + n_epochs_semisupervised: int = 400, + lr_unsupervised: float = 1e-3, + lr_semisupervised: float = 1e-3, + batch_size: int = 128, + early_stopping: bool = False, +) -> Dict[str, Any]: + """ + Trains a scANVI model using the scarches command-line interface. + + Args: + adata_path: Path to anndata object with raw counts. + model_path: Path where the trained model will be saved. + labels_key: Key in adata.obs for cell type labels. + unlabeled_category: Value in adata.obs[labels_key] for unlabeled cells. + gpus: String specifying which GPUs to use (e.g., '0,1'). + n_epochs_unsupervised: Number of epochs for unsupervised training. + n_epochs_semisupervised: Number of epochs for semi-supervised training. + lr_unsupervised: Learning rate for unsupervised training. + lr_semisupervised: Learning rate for semi-supervised training. + batch_size: Batch size for training. + early_stopping: If True, enables early stopping. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output file path. + """ + if not adata_path.is_file(): + raise FileNotFoundError(f"Input anndata file not found: {adata_path}") + if not model_path.parent.exists(): + model_path.parent.mkdir(parents=True, exist_ok=True) + logger.info(f"Created output directory: {model_path.parent}") + + cmd = [ + "python", "-m", "scarches", "train-scanvi", + str(adata_path), + str(model_path), + "--labels_key", labels_key, + "--unlabeled_category", unlabeled_category, + "--gpus", gpus, + "--n_epochs_unsupervised", str(n_epochs_unsupervised), + "--n_epochs_semisupervised", str(n_epochs_semisupervised), + "--lr_unsupervised", str(lr_unsupervised), + "--lr_semisupervised", str(lr_semisupervised), + "--batch_size", str(batch_size), + ] + if early_stopping: + cmd.append("--early_stopping") + + return _run_scarches_command(cmd, model_path) + +@mcp.tool() +def train_trvae( + adata_path: Path, + model_path: Path, + condition_key: str, + gpus: str = "0", + n_epochs: int = 400, + learning_rate: float = 1e-3, + batch_size: int = 128, + early_stopping: bool = False, +) -> Dict[str, Any]: + """ + Trains a trVAE model using the scarches command-line interface. + + Args: + adata_path: Path to anndata object with raw counts. + model_path: Path where the trained model will be saved. + condition_key: Key in adata.obs for batch information. + gpus: String specifying which GPUs to use (e.g., '0,1'). + n_epochs: Number of epochs for training. + learning_rate: Learning rate for training. + batch_size: Batch size for training. + early_stopping: If True, enables early stopping. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output file path. + """ + if not adata_path.is_file(): + raise FileNotFoundError(f"Input anndata file not found: {adata_path}") + if not model_path.parent.exists(): + model_path.parent.mkdir(parents=True, exist_ok=True) + logger.info(f"Created output directory: {model_path.parent}") + + cmd = [ + "python", "-m", "scarches", "train-trvae", + str(adata_path), + str(model_path), + "--condition_key", condition_key, + "--gpus", gpus, + "--n_epochs", str(n_epochs), + "--learning_rate", str(learning_rate), + "--batch_size", str(batch_size), + ] + if early_stopping: + cmd.append("--early_stopping") + + return _run_scarches_command(cmd, model_path) + +@mcp.tool() +def query_scvi( + adata_path: Path, + ref_model_path: Path, + new_model_path: Path, + gpus: str = "0", + n_epochs: int = 200, + learning_rate: float = 1e-3, + batch_size: int = 128, + early_stopping: bool = False, +) -> Dict[str, Any]: + """ + Maps a query dataset to a reference scVI model. + + Args: + adata_path: Path to query anndata object with raw counts. + ref_model_path: Path to the reference model. + new_model_path: Path where the updated query model will be saved. + gpus: String specifying which GPUs to use (e.g., '0,1'). + n_epochs: Number of epochs for training. + learning_rate: Learning rate for training. + batch_size: Batch size for training. + early_stopping: If True, enables early stopping. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output file path. + """ + if not adata_path.is_file(): + raise FileNotFoundError(f"Input query anndata file not found: {adata_path}") + if not ref_model_path.is_dir(): + raise FileNotFoundError(f"Reference model directory not found: {ref_model_path}") + if not new_model_path.parent.exists(): + new_model_path.parent.mkdir(parents=True, exist_ok=True) + logger.info(f"Created output directory: {new_model_path.parent}") + + cmd = [ + "python", "-m", "scarches", "query-scvi", + str(adata_path), + str(ref_model_path), + str(new_model_path), + "--gpus", gpus, + "--n_epochs", str(n_epochs), + "--learning_rate", str(learning_rate), + "--batch_size", str(batch_size), + ] + if early_stopping: + cmd.append("--early_stopping") + + return _run_scarches_command(cmd, new_model_path) + +@mcp.tool() +def query_scanvi( + adata_path: Path, + ref_model_path: Path, + new_model_path: Path, + gpus: str = "0", + n_epochs_unsupervised: int = 200, + n_epochs_semisupervised: int = 100, + lr_unsupervised: float = 1e-3, + lr_semisupervised: float = 1e-3, + batch_size: int = 128, + early_stopping: bool = False, +) -> Dict[str, Any]: + """ + Maps a query dataset to a reference scANVI model. + + Args: + adata_path: Path to query anndata object with raw counts. + ref_model_path: Path to the reference model. + new_model_path: Path where the updated query model will be saved. + gpus: String specifying which GPUs to use (e.g., '0,1'). + n_epochs_unsupervised: Number of epochs for unsupervised training. + n_epochs_semisupervised: Number of epochs for semi-supervised training. + lr_unsupervised: Learning rate for unsupervised training. + lr_semisupervised: Learning rate for semi-supervised training. + batch_size: Batch size for training. + early_stopping: If True, enables early stopping. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output file path. + """ + if not adata_path.is_file(): + raise FileNotFoundError(f"Input query anndata file not found: {adata_path}") + if not ref_model_path.is_dir(): + raise FileNotFoundError(f"Reference model directory not found: {ref_model_path}") + if not new_model_path.parent.exists(): + new_model_path.parent.mkdir(parents=True, exist_ok=True) + logger.info(f"Created output directory: {new_model_path.parent}") + + cmd = [ + "python", "-m", "scarches", "query-scanvi", + str(adata_path), + str(ref_model_path), + str(new_model_path), + "--gpus", gpus, + "--n_epochs_unsupervised", str(n_epochs_unsupervised), + "--n_epochs_semisupervised", str(n_epochs_semisupervised), + "--lr_unsupervised", str(lr_unsupervised), + "--lr_semisupervised", str(lr_semisupervised), + "--batch_size", str(batch_size), + ] + if early_stopping: + cmd.append("--early_stopping") + + return _run_scarches_command(cmd, new_model_path) + +@mcp.tool() +def query_trvae( + adata_path: Path, + ref_model_path: Path, + new_model_path: Path, + condition_key: str, + gpus: str = "0", + n_epochs: int = 200, + learning_rate: float = 1e-3, + batch_size: int = 128, + early_stopping: bool = False, +) -> Dict[str, Any]: + """ + Maps a query dataset to a reference trVAE model. + + Args: + adata_path: Path to query anndata object with raw counts. + ref_model_path: Path to the reference model. + new_model_path: Path where the updated query model will be saved. + condition_key: Key in adata.obs for batch information. + gpus: String specifying which GPUs to use (e.g., '0,1'). + n_epochs: Number of epochs for training. + learning_rate: Learning rate for training. + batch_size: Batch size for training. + early_stopping: If True, enables early stopping. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output file path. + """ + if not adata_path.is_file(): + raise FileNotFoundError(f"Input query anndata file not found: {adata_path}") + if not ref_model_path.is_dir(): + raise FileNotFoundError(f"Reference model directory not found: {ref_model_path}") + if not new_model_path.parent.exists(): + new_model_path.parent.mkdir(parents=True, exist_ok=True) + logger.info(f"Created output directory: {new_model_path.parent}") + + cmd = [ + "python", "-m", "scarches", "query-trvae", + str(adata_path), + str(ref_model_path), + str(new_model_path), + "--condition_key", condition_key, + "--gpus", gpus, + "--n_epochs", str(n_epochs), + "--learning_rate", str(learning_rate), + "--batch_size", str(batch_size), + ] + if early_stopping: + cmd.append("--early_stopping") + + return _run_scarches_command(cmd, new_model_path) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_scarches/app/scarches_shim_server.py b/Biomni/mcp_generated/mcp_scarches/app/scarches_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d0fbf26f97e61de9b637b4b49aefde6216861f53 --- /dev/null +++ b/Biomni/mcp_generated/mcp_scarches/app/scarches_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_scarches/app/scarches_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_scarches' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_scarches/docker-compose.yml b/Biomni/mcp_generated/mcp_scarches/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..36515829bd5c93102752c1a043532a508bcab763 --- /dev/null +++ b/Biomni/mcp_generated/mcp_scarches/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-scarches: + build: . + image: mcp-scarches:latest + container_name: mcp-scarches + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=scarches + 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/Biomni/mcp_generated/mcp_scarches/environment.yaml b/Biomni/mcp_generated/mcp_scarches/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7ae141f0d6db963f6b50ae1249b206c468ca6d98 --- /dev/null +++ b/Biomni/mcp_generated/mcp_scarches/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - scarches + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_scarches/requirements.txt b/Biomni/mcp_generated/mcp_scarches/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_scarches/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_sccaf/Dockerfile b/Biomni/mcp_generated/mcp_sccaf/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..951b1c9c12b9a51375afc80056f842c8211e1a93 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sccaf/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 sccaf via conda (e.g., from bioconda) +RUN conda install -c bioconda sccaf -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 sccaf_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/sccaf_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/sccaf_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sccaf/app/sccaf_server.py b/Biomni/mcp_generated/mcp_sccaf/app/sccaf_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2f94928d2f15dd4fd616951b38021a43b761ab0d --- /dev/null +++ b/Biomni/mcp_generated/mcp_sccaf/app/sccaf_server.py @@ -0,0 +1,180 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +# MCP decorator placeholder. In a real MCP environment, this would be imported. +class mcp: + @staticmethod + def tool(func): + return func + +@mcp.tool +def sccaf( + input_adata: Path, + output_adata: Path, + self_project: bool = False, + min_acc: float = 0.9, + prefix: str = 'sccaf', + n_jobs: int = 1, + use_rep: str = 'X_pca', + n_features: int = 2000, + n_components: int = 50, + resolution: float = 0.2, + k: int = 15, + reference: Optional[Path] = None, + reference_leiden: str = 'leiden', + reference_cell_type: str = 'cell_type', + reference_embedding: str = 'X_umap', + reference_n_jobs: int = 1, + reference_n_features: int = 2000, + reference_n_components: int = 50, + reference_resolution: float = 0.2, + reference_k: int = 15, + log_file: Optional[Path] = None, + verbose: int = 1, +) -> Dict[str, Any]: + """ + Runs SCCAF (Single-Cell Clustering Assessment Framework) for clustering and cell type identification. + + SCCAF is a tool for automated cell type identification and clustering refinement based on the + self-projection of a dataset. It can also project a query dataset onto a reference. + + Args: + input_adata: Path to the input AnnData object (.h5ad). + output_adata: Path where the output AnnData object (.h5ad) will be saved. + self_project: If True, run in self-projection mode. + min_acc: Minimum accuracy for self-projection. + prefix: Prefix for the column names for the results in the output AnnData. + n_jobs: Number of jobs to run in parallel. + use_rep: The key in `.obsm` for the embedding to use (e.g., 'X_pca'). + n_features: Number of features to use. + n_components: Number of components to use. + resolution: Resolution for Louvain clustering. + k: Number of nearest neighbors. + reference: Path to the reference AnnData object for projection. + reference_leiden: The key in `.obs` for the reference leiden clusters. + reference_cell_type: The key in `.obs` for the reference cell type annotations. + reference_embedding: The key in `.obsm` for the reference embedding. + reference_n_jobs: Number of jobs to run in parallel for the reference analysis. + reference_n_features: Number of features to use for the reference. + reference_n_components: Number of components to use for the reference. + reference_resolution: Resolution for Louvain for the reference. + reference_k: Number of nearest neighbors for the reference. + log_file: Optional path to a file for logging output. + verbose: Verbosity level (e.g., 0 for silent, 1 for info). + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + if not input_adata.is_file(): + raise FileNotFoundError(f"Input file not found: {input_adata}") + if reference and not reference.is_file(): + raise FileNotFoundError(f"Reference file not found: {reference}") + + # --- Output Directory Handling --- + output_adata.parent.mkdir(parents=True, exist_ok=True) + if log_file: + log_file.parent.mkdir(parents=True, exist_ok=True) + + # --- Parameter Value Validation --- + if not (0.0 <= min_acc <= 1.0): + raise ValueError("min_acc must be between 0.0 and 1.0") + if n_jobs <= 0: + raise ValueError("n_jobs must be a positive integer") + if n_features <= 0: + raise ValueError("n_features must be a positive integer") + if n_components <= 0: + raise ValueError("n_components must be a positive integer") + if resolution <= 0: + raise ValueError("resolution must be a positive float") + if k <= 0: + raise ValueError("k must be a positive integer") + if verbose < 0: + raise ValueError("verbose must be a non-negative integer") + + # --- Command Construction --- + cmd = [ + "sccaf", + "-i", str(input_adata), + "-o", str(output_adata), + "--min-acc", str(min_acc), + "--prefix", prefix, + "--n-jobs", str(n_jobs), + "--use-rep", use_rep, + "--n-features", str(n_features), + "--n-components", str(n_components), + "--resolution", str(resolution), + "--k", str(k), + "--verbose", str(verbose), + ] + + if self_project: + cmd.append("--self-project") + + if reference: + if reference_n_jobs <= 0: + raise ValueError("reference_n_jobs must be a positive integer") + if reference_n_features <= 0: + raise ValueError("reference_n_features must be a positive integer") + if reference_n_components <= 0: + raise ValueError("reference_n_components must be a positive integer") + if reference_resolution <= 0: + raise ValueError("reference_resolution must be a positive float") + if reference_k <= 0: + raise ValueError("reference_k must be a positive integer") + + cmd.extend([ + "--reference", str(reference), + "--reference-leiden", reference_leiden, + "--reference-cell-type", reference_cell_type, + "--reference-embedding", reference_embedding, + "--reference-n-jobs", str(reference_n_jobs), + "--reference-n-features", str(reference_n_features), + "--reference-n-components", str(reference_n_components), + "--reference-resolution", str(reference_resolution), + "--reference-k", str(reference_k), + ]) + + if log_file: + cmd.extend(["--log-file", str(log_file)]) + + command_executed = " ".join(cmd) + + # --- Subprocess Execution --- + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + stdout = result.stdout + stderr = result.stderr + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'sccaf' command not found. Make sure the tool is installed and in your system's 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: List[str] = [str(output_adata)] + if log_file and log_file.exists(): + output_files.append(str(log_file)) + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sccaf/app/sccaf_shim_server.py b/Biomni/mcp_generated/mcp_sccaf/app/sccaf_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..5f09b1779fcffbd485cefab31fb42c47a8275e39 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sccaf/app/sccaf_shim_server.py @@ -0,0 +1,55 @@ +#!/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_help_txt/mcp_sccaf/app/sccaf_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_sccaf' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_sccaf/docker-compose.yml b/Biomni/mcp_generated/mcp_sccaf/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..6068a48b1fd1b217fd92ff7a691d6ed4a3c02d61 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sccaf/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-sccaf: + build: . + image: mcp-sccaf:latest + container_name: mcp-sccaf + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=sccaf + 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/Biomni/mcp_generated/mcp_sccaf/environment.yaml b/Biomni/mcp_generated/mcp_sccaf/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..629ed66308b69d792273a7db85a5e52aac46a7a6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sccaf/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - sccaf + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sccaf/requirements.txt b/Biomni/mcp_generated/mcp_sccaf/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sccaf/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_sdeper/Dockerfile b/Biomni/mcp_generated/mcp_sdeper/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..39b79be57e49f5946d1ea9f415b04c331a44314c --- /dev/null +++ b/Biomni/mcp_generated/mcp_sdeper/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 sdeper via conda (e.g., from bioconda) +RUN conda install -c bioconda sdeper -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/sdeper_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/sdeper_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/sdeper_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sdeper/app/requirements.txt b/Biomni/mcp_generated/mcp_sdeper/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_sdeper/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_sdeper/app/sdeper_server.py b/Biomni/mcp_generated/mcp_sdeper/app/sdeper_server.py new file mode 100644 index 0000000000000000000000000000000000000000..adbea73681af622dd9f1bea0fd6ebc5c9b78224b --- /dev/null +++ b/Biomni/mcp_generated/mcp_sdeper/app/sdeper_server.py @@ -0,0 +1,214 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_sdeper' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_deconvolution( + spatial_file: str, + ref_file: str, + anno_file: str, + adj_file: Optional[str] = None, + output_dir: str = "./sdeper_results", + n_marker: int = 15, + n_hvgs: int = 2000, + use_all_genes: bool = False, + platform_effect: bool = True, + spatial_loss: bool = True, + sparsity: bool = True, + batch_size: int = 500, + learning_rate: float = 0.001, + epochs: int = 400, + seed: int = 42 +) -> Dict[str, Any]: + """ + Perform cell type deconvolution of spatial transcriptomics data using SDePER. + + SDePER (Spatial Deconvolution method with Platform Effect Removal) deconvolves + spatial barcoding-based transcriptomic data using reference scRNA-seq data. + + Args: + spatial_file: Path to raw nUMI counts of spatial transcriptomics data (spots x genes, CSV). + ref_file: Path to raw nUMI counts of reference scRNA-seq data (cells x genes, CSV). + anno_file: Path to cell type annotations for all cells in scRNA-seq data (cells x 1, CSV). + adj_file: Optional path to adjacency matrix of spots (spots x spots, CSV). + output_dir: Directory to save output files. + n_marker: Number of marker genes for each cell type. + n_hvgs: Number of highly variable genes to use. + use_all_genes: Whether to use all genes for deconvolution instead of markers/HVGs. + platform_effect: Whether to remove platform effects between scRNA-seq and spatial data. + spatial_loss: Whether to consider across-spots spatial correlation. + sparsity: Whether to consider sparsity of cell types per capture spot. + batch_size: Batch size for the CVAE model training. + learning_rate: Learning rate for the CVAE model training. + epochs: Number of epochs for the CVAE model training. + seed: Random seed for reproducibility. + """ + # Input validation + spatial_path = Path(spatial_file) + ref_path = Path(ref_file) + anno_path = Path(anno_file) + + if not spatial_path.exists(): + return {"error": f"Spatial file not found: {spatial_file}"} + if not ref_path.exists(): + return {"error": f"Reference scRNA-seq file not found: {ref_file}"} + if not anno_path.exists(): + return {"error": f"Annotation file not found: {anno_file}"} + + # Create output directory + out_path = Path(output_dir) + out_path.mkdir(parents=True, exist_ok=True) + + # Build command + cmd = [ + "runDeconvolution", + "-q", str(spatial_path), + "-r", str(ref_path), + "-c", str(anno_path), + "-o", str(out_path), + "-n", str(n_marker), + "-m", str(n_hvgs), + "--batch_size", str(batch_size), + "--learning_rate", str(learning_rate), + "--epochs", str(epochs), + "--seed", str(seed) + ] + + if adj_file: + adj_path = Path(adj_file) + if adj_path.exists(): + cmd.extend(["-a", str(adj_path)]) + else: + return {"error": f"Adjacency file not found: {adj_file}"} + + if use_all_genes: + cmd.append("--use_all_genes") + + # SDePER uses flags to DISABLE these if they are False, as they are True by default + if not platform_effect: + cmd.append("--no_platform") + if not spatial_loss: + cmd.append("--no_spatial") + if not sparsity: + cmd.append("--no_sparsity") + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + + # Identify expected output files + output_files = [str(f) for f in out_path.glob("*") if f.is_file()] + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + "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 run_imputation( + spatial_file: str, + ref_file: str, + anno_file: str, + deconv_file: str, + adj_file: Optional[str] = None, + output_dir: str = "./sdeper_imputation", + n_marker: int = 15, + n_hvgs: int = 2000, + seed: int = 42 +) -> Dict[str, Any]: + """ + Perform cell type composition and gene expression imputation at unmeasured locations. + + This tool uses the results from a previous deconvolution run to enhance resolution + and impute values for missing or unmeasured spots in the tissue map. + + Args: + spatial_file: Path to raw nUMI counts of spatial transcriptomics data (CSV). + ref_file: Path to raw nUMI counts of reference scRNA-seq data (CSV). + anno_file: Path to cell type annotations for scRNA-seq data (CSV). + deconv_file: Path to the cell type proportion file generated by runDeconvolution (CSV). + adj_file: Optional path to adjacency matrix of spots (CSV). + output_dir: Directory to save imputation results. + n_marker: Number of marker genes (should match deconvolution settings). + n_hvgs: Number of highly variable genes (should match deconvolution settings). + seed: Random seed for reproducibility. + """ + # Input validation + spatial_path = Path(spatial_file) + ref_path = Path(ref_file) + anno_path = Path(anno_file) + deconv_path = Path(deconv_file) + + paths_to_check = { + "Spatial": spatial_path, + "Reference": ref_path, + "Annotation": anno_path, + "Deconvolution result": deconv_path + } + + for name, path in paths_to_check.items(): + if not path.exists(): + return {"error": f"{name} file not found: {path}"} + + # Create output directory + out_path = Path(output_dir) + out_path.mkdir(parents=True, exist_ok=True) + + # Build command + cmd = [ + "runImputation", + "-q", str(spatial_path), + "-r", str(ref_path), + "-c", str(anno_path), + "-d", str(deconv_path), + "-o", str(out_path), + "-n", str(n_marker), + "-m", str(n_hvgs), + "--seed", str(seed) + ] + + if adj_file: + adj_path = Path(adj_file) + if adj_path.exists(): + cmd.extend(["-a", str(adj_path)]) + else: + return {"error": f"Adjacency file not found: {adj_file}"} + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + + output_files = [str(f) for f in out_path.glob("*") if f.is_file()] + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e), + "status": "failed" + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_sdeper/app/sdeper_shim_server.py b/Biomni/mcp_generated/mcp_sdeper/app/sdeper_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e8c2afc55f521bae3a2461c151a72e6aa019419c --- /dev/null +++ b/Biomni/mcp_generated/mcp_sdeper/app/sdeper_shim_server.py @@ -0,0 +1,55 @@ +#!/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_sdeper/app/sdeper_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_sdeper' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_sdeper/docker-compose.yml b/Biomni/mcp_generated/mcp_sdeper/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..cbc2bb8dc71bbe0481aa5ac6b826b311a571adab --- /dev/null +++ b/Biomni/mcp_generated/mcp_sdeper/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-sdeper: + build: . + image: mcp-sdeper:latest + container_name: mcp-sdeper + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=sdeper + 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/Biomni/mcp_generated/mcp_sdeper/environment.yaml b/Biomni/mcp_generated/mcp_sdeper/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..774263079558f03835fa36089c8ddc8262d764c9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sdeper/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - sdeper + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sdeper/requirements.txt b/Biomni/mcp_generated/mcp_sdeper/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sdeper/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_seqkit/Dockerfile b/Biomni/mcp_generated/mcp_seqkit/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..82aaaf0b0b7550d0b7665ef4caf05988dfd4522a --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqkit/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 seqkit via conda (e.g., from bioconda) +RUN conda install -c bioconda seqkit -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/seqkit_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/seqkit_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/seqkit_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_seqkit/app/requirements.txt b/Biomni/mcp_generated/mcp_seqkit/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqkit/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_seqkit/app/seqkit_server.py b/Biomni/mcp_generated/mcp_seqkit/app/seqkit_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c496cf3ed5342f1994aa189dd29441a381d52e2a --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqkit/app/seqkit_server.py @@ -0,0 +1,693 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Union +import tempfile + +def _run_seqkit(args: List[str]): + """Helper function to execute seqkit commands and handle errors.""" + try: + result = subprocess.run( + ["seqkit"] + args, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"seqkit {' '.join(args)}", + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": f"seqkit {' '.join(args)}", + "stdout": e.stdout, + "stderr": e.stderr, + "status": "error", + "error_message": str(e) + } + +def _validate_files(files: List[str]): + """Validates that input files exist.""" + for f in files: + if not Path(f).exists(): + raise FileNotFoundError(f"Input file not found: {f}") + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_seqkit' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def seqkit_stats( + input_files: List[str], + all_stats: bool = False, + tabular: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Simple statistics of FASTA/Q files. + + :param input_files: List of FASTA/Q files to analyze. + :param all_stats: Show all statistics including quartiles and N50. + :param tabular: Output in machine-friendly tabular format. + :param threads: Number of CPUs to use. + :param out_file: Output file path. + """ + _validate_files(input_files) + args = ["stats"] + if all_stats: args.append("-a") + if tabular: args.append("-T") + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_seq( + input_files: List[str], + name: bool = False, + seq: bool = False, + reverse: bool = False, + complement: bool = False, + min_len: int = -1, + max_len: int = -1, + remove_gaps: bool = False, + lower_case: bool = False, + upper_case: bool = False, + dna2rna: bool = False, + rna2dna: bool = False, + validate_seq: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Transform sequences (extract ID, filter by length, remove gaps, reverse complement...). + + :param input_files: List of FASTA/Q files. + :param name: Only print names. + :param seq: Only print sequences. + :param reverse: Reverse sequence. + :param complement: Complement sequence. + :param min_len: Minimum sequence length to keep (-1 to disable). + :param max_len: Maximum sequence length to keep (-1 to disable). + :param remove_gaps: Remove gaps. + :param lower_case: Print sequences in lower case. + :param upper_case: Print sequences in upper case. + :param dna2rna: DNA to RNA. + :param rna2dna: RNA to DNA. + :param validate_seq: Validate sequences. + """ + _validate_files(input_files) + args = ["seq"] + if name: args.append("-n") + if seq: args.append("-s") + if reverse: args.append("-r") + if complement: args.append("-p") + if min_len >= 0: args.extend(["-m", str(min_len)]) + if max_len >= 0: args.extend(["-M", str(max_len)]) + if remove_gaps: args.append("-g") + if lower_case: args.append("-l") + if upper_case: args.append("-u") + if dna2rna: args.append("--dna2rna") + if rna2dna: args.append("--rna2dna") + if validate_seq: args.append("-v") + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_grep( + input_files: List[str], + pattern: Optional[str] = None, + pattern_file: Optional[str] = None, + use_regexp: bool = False, + ignore_case: bool = False, + invert_match: bool = False, + by_name: bool = False, + by_seq: bool = False, + degenerate: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Search sequences by ID/name/sequence/sequence motifs. + + :param pattern: Search pattern (regular expression if use_regexp is True). + :param pattern_file: File containing patterns (one per line). + :param use_regexp: Use regular expression. + :param ignore_case: Ignore case. + :param invert_match: Invert match (grep -v). + :param by_name: Match by full name instead of just ID. + :param by_seq: Search in sequences instead of IDs. + :param degenerate: Pattern contains degenerate bases. + """ + _validate_files(input_files) + args = ["grep"] + if pattern: args.extend(["-p", pattern]) + if pattern_file: + if not Path(pattern_file).exists(): + raise FileNotFoundError(f"Pattern file not found: {pattern_file}") + args.extend(["-f", pattern_file]) + if use_regexp: args.append("-r") + if ignore_case: args.append("-i") + if invert_match: args.append("-v") + if by_name: args.append("-n") + if by_seq: args.append("-s") + if degenerate: args.append("-d") + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_subseq( + input_files: List[str], + region: Optional[str] = None, + gtf: Optional[str] = None, + bed: Optional[str] = None, + feature: Optional[str] = None, + flank: int = 0, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Get subsequences by region/gtf/bed. + + :param region: Region (e.g. 1:10, -100:-1). + :param gtf: GTF file. + :param bed: BED file. + :param feature: Feature type in GTF to extract (e.g. CDS). + :param flank: Flanking sequence length. + """ + _validate_files(input_files) + args = ["subseq"] + if region: args.extend(["-r", region]) + if gtf: args.extend(["--gtf", gtf]) + if bed: args.extend(["--bed", bed]) + if feature: args.extend(["--feature", feature]) + if flank > 0: args.extend(["-f", str(flank)]) + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_fq2fa( + input_files: List[str], + threads: int = 4, + out_file: Optional[str] = None +): + """Convert FASTQ to FASTA.""" + _validate_files(input_files) + args = ["fq2fa"] + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_fx2tab( + input_files: List[str], + length: bool = False, + gc: bool = False, + name: bool = False, + only_id: bool = False, + base_content: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Convert FASTA/Q to tabular format. + + :param length: Print sequence length. + :param gc: Print GC content. + :param name: Print full name instead of ID. + :param only_id: Only print ID. + :param base_content: Print base content (A, C, G, T, N). + """ + _validate_files(input_files) + args = ["fx2tab"] + if length: args.append("-l") + if gc: args.append("-g") + if name: args.append("-n") + if only_id: args.append("-i") + if base_content: args.append("-B") + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_tab2fx( + input_files: List[str], + threads: int = 4, + out_file: Optional[str] = None +): + """Convert tabular format to FASTA/Q format.""" + _validate_files(input_files) + args = ["tab2fx"] + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_translate( + input_files: List[str], + frame: int = 1, + table: int = 1, + trim: bool = False, + clean: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Translate DNA/RNA to protein sequence. + + :param frame: Frame(s) to translate (1, 2, 3, 4, 5, 6 or 0 for all 6 frames). + :param table: Genetic code table ID. + :param trim: Trim stop codon '*' at the end. + :param clean: Transform all non-AA characters to 'X'. + """ + _validate_files(input_files) + args = ["translate"] + args.extend(["-f", str(frame)]) + args.extend(["-l", str(table)]) + if trim: args.append("-T") + if clean: args.append("-c") + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_rmdup( + input_files: List[str], + by_id: bool = False, + by_name: bool = False, + by_seq: bool = False, + ignore_case: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Remove duplicated sequences by ID/name/sequence. + + :param by_id: Remove by ID. + :param by_name: Remove by full name. + :param by_seq: Remove by sequence. + :param ignore_case: Ignore case. + """ + _validate_files(input_files) + args = ["rmdup"] + if by_id: args.append("-n") + if by_name: args.append("-N") + if by_seq: args.append("-s") + if ignore_case: args.append("-i") + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_sample( + input_files: List[str], + number: int = -1, + proportion: float = -1.0, + seed: int = 11, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Sample sequences by number or proportion. + + :param number: Sample by number. + :param proportion: Sample by proportion (0-1). + :param seed: Random seed. + """ + _validate_files(input_files) + args = ["sample"] + if number > 0: args.extend(["-n", str(number)]) + if proportion > 0: args.extend(["-p", str(proportion)]) + args.extend(["-s", str(seed)]) + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_sort( + input_files: List[str], + by_id: bool = False, + by_name: bool = False, + by_seq: bool = False, + by_length: bool = False, + reverse: bool = False, + ignore_case: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Sort sequences by id/name/sequence/length. + + :param by_id: Sort by ID. + :param by_name: Sort by full name. + :param by_seq: Sort by sequence. + :param by_length: Sort by length. + :param reverse: Reverse order. + :param ignore_case: Ignore case. + """ + _validate_files(input_files) + args = ["sort"] + if by_id: args.append("-n") + if by_name: args.append("-N") + if by_seq: args.append("-s") + if by_length: args.append("-l") + if reverse: args.append("-r") + if ignore_case: args.append("-i") + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_split2( + input_files: List[str], + size: int = -1, + parts: int = -1, + out_dir: str = "split_out", + threads: int = 4 +): + """ + Split sequences into files by size or parts (FASTA, PE/SE FASTQ). + + :param size: Split by number of records per file. + :param parts: Split into N parts. + :param out_dir: Output directory. + """ + _validate_files(input_files) + args = ["split2"] + if size > 0: args.extend(["-s", str(size)]) + if parts > 0: args.extend(["-p", str(parts)]) + args.extend(["-O", out_dir]) + + args.extend(["-j", str(threads)]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_locate( + input_files: List[str], + pattern: Optional[str] = None, + pattern_file: Optional[str] = None, + max_mismatch: int = 0, + ignore_case: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Locate subsequences/motifs, mismatch allowed. + + :param pattern: Search pattern. + :param pattern_file: File containing patterns. + :param max_mismatch: Max mismatch. + :param ignore_case: Ignore case. + """ + _validate_files(input_files) + args = ["locate"] + if pattern: args.extend(["-p", pattern]) + if pattern_file: args.extend(["-f", pattern_file]) + args.extend(["-m", str(max_mismatch)]) + if ignore_case: args.append("-i") + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_replace( + input_files: List[str], + pattern: str, + replacement: str, + by_name: bool = False, + by_seq: bool = False, + ignore_case: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Replace name/sequence by regular expression. + + :param pattern: Regular expression pattern. + :param replacement: Replacement string (supports capture groups like $1). + :param by_name: Replace in full name. + :param by_seq: Replace in sequence. + :param ignore_case: Ignore case. + """ + _validate_files(input_files) + args = ["replace", "-p", pattern, "-r", replacement] + if by_name: args.append("-n") + if by_seq: args.append("-s") + if ignore_case: args.append("-i") + + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_faidx( + input_files: List[str], + subsequences: Optional[List[str]] = None, + bed: Optional[str] = None, + out_file: Optional[str] = None +): + """ + Create the FASTA index file and extract subsequences. + + :param input_files: FASTA files. + :param subsequences: List of sequence IDs or regions (e.g. "chr1:1-10"). + :param bed: BED file for extraction. + """ + _validate_files(input_files) + args = ["faidx"] + if bed: args.extend(["--bed", bed]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + if subsequences: args.extend(subsequences) + + return _run_seqkit(args) + +@mcp.tool() +def seqkit_sliding( + input_files: List[str], + step: int = 1, + window: int = 100, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Extract subsequences in sliding windows. + + :param step: Step size. + :param window: Window size. + """ + _validate_files(input_files) + args = ["sliding", "-s", str(step), "-W", str(window)] + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_common( + input_files: List[str], + by_id: bool = True, + by_name: bool = False, + by_seq: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Find common/shared sequences of multiple files by id/name/sequence. + """ + _validate_files(input_files) + args = ["common"] + if by_id: args.append("-n") + if by_name: args.append("-N") + if by_seq: args.append("-s") + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_duplicate( + input_files: List[str], + times: int = 2, + threads: int = 4, + out_file: Optional[str] = None +): + """Duplicate sequences N times.""" + _validate_files(input_files) + args = ["duplicate", "-n", str(times)] + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_head( + input_files: List[str], + number: int = 10, + threads: int = 4, + out_file: Optional[str] = None +): + """Print the first N FASTA/Q records.""" + _validate_files(input_files) + args = ["head", "-n", str(number)] + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_pair( + input_file1: str, + input_file2: str, + out_dir: str = "paired_out", + threads: int = 4 +): + """Match up paired-end reads from two fastq files.""" + _validate_files([input_file1, input_file2]) + args = ["pair", "-1", input_file1, "-2", input_file2, "-O", out_dir] + args.extend(["-j", str(threads)]) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_mutate( + input_files: List[str], + point_mutation: Optional[str] = None, + insertion: Optional[str] = None, + deletion: Optional[str] = None, + threads: int = 4, + out_file: Optional[str] = None +): + """ + Edit sequence (point mutation, insertion, deletion). + Example point_mutation: "1:A" (position 1 to A). + """ + _validate_files(input_files) + args = ["mutate"] + if point_mutation: args.extend(["-p", point_mutation]) + if insertion: args.extend(["-i", insertion]) + if deletion: args.extend(["-d", deletion]) + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_sum( + input_files: List[str], + threads: int = 4 +): + """Compute message digest (MD5/SHA1/SHA256) for all sequences.""" + _validate_files(input_files) + args = ["sum"] + args.extend(["-j", str(threads)]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_bam( + input_files: List[str], + threads: int = 4 +): + """Monitoring and online histograms of BAM record features.""" + _validate_files(input_files) + args = ["bam"] + args.extend(["-j", str(threads)]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_sana( + input_files: List[str], + threads: int = 4, + out_file: Optional[str] = None +): + """Sanitize broken single line FASTQ files.""" + _validate_files(input_files) + args = ["sana"] + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_shuffle( + input_files: List[str], + seed: int = 11, + threads: int = 4, + out_file: Optional[str] = None +): + """Shuffle sequences.""" + _validate_files(input_files) + args = ["shuffle", "-s", str(seed)] + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_convert( + input_files: List[str], + to_sanger: bool = False, + to_illumina: bool = False, + to_solexa: bool = False, + threads: int = 4, + out_file: Optional[str] = None +): + """Convert FASTQ quality encoding between Sanger, Solexa and Illumina.""" + _validate_files(input_files) + args = ["convert"] + if to_sanger: args.append("--to-sanger") + if to_illumina: args.append("--to-illumina") + if to_solexa: args.append("--to-solexa") + args.extend(["-j", str(threads)]) + if out_file: args.extend(["-o", out_file]) + args.extend(input_files) + return _run_seqkit(args) + +@mcp.tool() +def seqkit_version(): + """Print version information.""" + return _run_seqkit(["version"]) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_seqkit/app/seqkit_shim_server.py b/Biomni/mcp_generated/mcp_seqkit/app/seqkit_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e50e295b0858d46c2b9d4038e04272117d0f359d --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqkit/app/seqkit_shim_server.py @@ -0,0 +1,55 @@ +#!/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_seqkit/app/seqkit_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_seqkit' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_seqkit/docker-compose.yml b/Biomni/mcp_generated/mcp_seqkit/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..6599e563d7803861b18844e03a80e89420333996 --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqkit/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-seqkit: + build: . + image: mcp-seqkit:latest + container_name: mcp-seqkit + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=seqkit + 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/Biomni/mcp_generated/mcp_seqkit/environment.yaml b/Biomni/mcp_generated/mcp_seqkit/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..54a8d4cbeddc452a88fcbb9268b06226008f6a67 --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqkit/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - seqkit + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_seqkit/requirements.txt b/Biomni/mcp_generated/mcp_seqkit/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqkit/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_simo-omics/Dockerfile b/Biomni/mcp_generated/mcp_simo-omics/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f4e4b4a46547cee15060efbc75086b222635927d --- /dev/null +++ b/Biomni/mcp_generated/mcp_simo-omics/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 simo-omics via conda (e.g., from bioconda) +RUN conda install -c bioconda simo-omics -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/simo-omics_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/simo-omics_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/simo-omics_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_simo-omics/app/requirements.txt b/Biomni/mcp_generated/mcp_simo-omics/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_simo-omics/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_simo-omics/app/simo-omics_server.py b/Biomni/mcp_generated/mcp_simo-omics/app/simo-omics_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6fe2f64e60fb467ff3ebc68a74a97e28edb3efe9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_simo-omics/app/simo-omics_server.py @@ -0,0 +1,204 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# The mcp.tool decorator is assumed to be available in the environment. +# To make this code runnable, you would need to install the MCP framework. +# For demonstration, we assume a decorator `@mcp.tool()` exists. +class mcp: + def tool(func): + return func + +@mcp.tool +def run( + spatial_adata_path: Path, + scrna_adata_path: Path, + output_prefix: str, + n_top_genes: int = 3000, + n_pcs: int = 50, + n_neighbors: int = 30, + T: float = 0.01, + beta: float = 1.0, + beta2: float = 1.0, + lambda_g: float = 1.0, + lambda_d: float = 1.0, + lambda_ot: float = 1.0, + epochs: int = 500, + lr: float = 0.001, + w_pc: float = 1.0, + w_ge: float = 1.0, + w_da: float = 1.0, + w_ot: float = 1.0, +) -> dict: + """ + Integrates spatial and scRNA-seq datasets using the SIMO (Spatial Integration of Multi-Omics) library. + + SIMO is a computational method that performs spatial integration of multi-omics single-cell + datasets through probabilistic alignment. This tool wraps the simo-omics Python library. + + Args: + spatial_adata_path: Path to the input spatial AnnData file (.h5ad). + scrna_adata_path: Path to the input single-cell RNA-seq AnnData file (.h5ad). + output_prefix: Prefix for the output files. The main output will be named '{output_prefix}_integrated.h5ad'. + n_top_genes: Number of highly variable genes to use. + n_pcs: Number of principal components to compute. + n_neighbors: Number of neighbors to use for graph construction. + T: Temperature parameter for the model. + beta: Weight for graph regularization. + beta2: Weight for domain adversarial regularization. + lambda_g: Weight for gene expression reconstruction loss. + lambda_d: Weight for domain adversarial loss. + lambda_ot: Weight for optimal transport loss. + epochs: Number of training epochs. + lr: Learning rate for the optimizer. + w_pc: Weight for PC alignment. + w_ge: Weight for gene expression alignment. + w_da: Weight for domain adversarial alignment. + w_ot: Weight for optimal transport alignment. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + if not spatial_adata_path.is_file(): + raise FileNotFoundError(f"Input spatial AnnData file not found: {spatial_adata_path}") + if not scrna_adata_path.is_file(): + raise FileNotFoundError(f"Input scRNA-seq AnnData file not found: {scrna_adata_path}") + if not output_prefix: + raise ValueError("output_prefix cannot be empty.") + + # Validate numerical parameters + if n_top_genes <= 0: + raise ValueError("n_top_genes must be a positive integer.") + if n_pcs <= 0: + raise ValueError("n_pcs must be a positive integer.") + if n_neighbors <= 0: + raise ValueError("n_neighbors must be a positive integer.") + if epochs <= 0: + raise ValueError("epochs must be a positive integer.") + if lr <= 0: + raise ValueError("lr (learning rate) must be positive.") + if T <= 0: + raise ValueError("T (temperature) must be positive.") + + # Validate non-negative weight parameters + for param_name, param_value in { + "beta": beta, "beta2": beta2, "lambda_g": lambda_g, "lambda_d": lambda_d, + "lambda_ot": lambda_ot, "w_pc": w_pc, "w_ge": w_ge, "w_da": w_da, "w_ot": w_ot + }.items(): + if param_value < 0: + raise ValueError(f"{param_name} must be non-negative.") + + with tempfile.TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + script_path = temp_dir / "run_simo_script.py" + # Define output path in the current working directory + output_adata_path = Path.cwd() / f"{output_prefix}_integrated.h5ad" + + # --- Script Generation --- + # This script will be executed in a separate Python process to isolate dependencies. + # It loads the data, runs the SIMO workflow, and saves the result. + script_content = f""" +import sys +import warnings +import scanpy as sc +import simo + +# Suppress warnings from the library for cleaner output +warnings.filterwarnings('ignore') + +try: + # Load data + spatial_data = sc.read_h5ad(r'{spatial_adata_path.resolve()}') + scrna_data = sc.read_h5ad(r'{scrna_adata_path.resolve()}') + + # Initialize SIMO object + simo_model = simo.SIMO(spatial_data, scrna_data) + + # Preprocess data + simo_model.preprocess( + n_top_genes={n_top_genes}, + n_pcs={n_pcs}, + n_neighbors={n_neighbors} + ) + + # Train the model + simo_model.train( + T={T}, + beta={beta}, + beta2={beta2}, + lambda_g={lambda_g}, + lambda_d={lambda_d}, + lambda_ot={lambda_ot}, + epochs={epochs}, + lr={lr}, + w_pc={w_pc}, + w_ge={w_ge}, + w_da={w_da}, + w_ot={w_ot} + ) + + # Get results and save + integrated_adata = simo_model.get_results() + integrated_adata.write_h5ad(r'{output_adata_path.resolve()}') + + print(f"Successfully integrated data and saved to {{str(output_adata_path.resolve())}}") + +except Exception as e: + print(f"An error occurred during SIMO execution: {{e}}", file=sys.stderr) + sys.exit(1) +""" + with open(script_path, "w") as f: + f.write(script_content) + + # --- Subprocess Execution --- + command = ["python", str(script_path)] + command_str = " ".join(command) + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + cwd=Path.cwd(), + ) + + if not output_adata_path.is_file(): + # This case is important if the script runs without error but fails to produce the file. + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "error": "SIMO script completed, but the output file was not created.", + "return_code": 1, + "output_files": [] + } + + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_adata_path)], + } + except subprocess.CalledProcessError as e: + # This catches errors from within the executed script (e.g., Python exceptions that cause a non-zero exit code) + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "SIMO execution failed with a non-zero exit code.", + "return_code": e.returncode, + "output_files": [] + } + except FileNotFoundError: + # This catches an error if 'python' is not in the system's PATH + return { + "command_executed": command_str, + "stdout": "", + "stderr": "Error: The 'python' interpreter was not found in the system's PATH.", + "error": "Python interpreter not found.", + "return_code": 1, + "output_files": [] + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_simo-omics/app/simo-omics_shim_server.py b/Biomni/mcp_generated/mcp_simo-omics/app/simo-omics_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..5fa6b131490d023cf4da4cad7e71e97e142a7352 --- /dev/null +++ b/Biomni/mcp_generated/mcp_simo-omics/app/simo-omics_shim_server.py @@ -0,0 +1,55 @@ +#!/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_simo-omics/app/simo-omics_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_simo_omics' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_simo-omics/docker-compose.yml b/Biomni/mcp_generated/mcp_simo-omics/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..dcdd4f34ad0e589c6b405cb49928e5660b93a398 --- /dev/null +++ b/Biomni/mcp_generated/mcp_simo-omics/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-simo-omics: + build: . + image: mcp-simo-omics:latest + container_name: mcp-simo-omics + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=simo-omics + 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/Biomni/mcp_generated/mcp_simo-omics/environment.yaml b/Biomni/mcp_generated/mcp_simo-omics/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..11c88f273b92a5d3816c0683a0489d2d024ca0db --- /dev/null +++ b/Biomni/mcp_generated/mcp_simo-omics/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - simo-omics + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_simo-omics/requirements.txt b/Biomni/mcp_generated/mcp_simo-omics/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_simo-omics/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/Dockerfile b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1b554d5deaad6a6133327ae66001b66b8ac2940b --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/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 snakemake-interface-storage-plugins via conda (e.g., from bioconda) +RUN conda install -c bioconda snakemake-interface-storage-plugins -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/snakemake-interface-storage-plugins_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/snakemake-interface-storage-plugins_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/snakemake-interface-storage-plugins_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/app/requirements.txt b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/app/snakemake-interface-storage-plugins_server.py b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/app/snakemake-interface-storage-plugins_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c9de207ea93d6da94dd6e4bbd387d95a520b2d5c --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/app/snakemake-interface-storage-plugins_server.py @@ -0,0 +1,262 @@ +import subprocess +import os +from pathlib import Path +from typing import Optional, List, Dict, Any +import tempfile + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_snakemake_interface_storage_plugins' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def list_snakemake_storage_plugins() -> Dict[str, Any]: + """ + Lists all currently installed Snakemake storage plugins available in the environment. + This uses the Snakemake CLI which utilizes the storage-plugins interface for discovery. + """ + cmd = ["snakemake", "--list-storage-plugins"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "plugins": result.stdout.strip().split('\n') if result.stdout.strip() else [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Failed to list storage plugins. Ensure Snakemake is installed." + } + +@mcp.tool() +def get_storage_plugin_help(plugin_name: str) -> Dict[str, Any]: + """ + Retrieves detailed help and available settings for a specific Snakemake storage plugin. + + Args: + plugin_name: The name of the storage plugin (e.g., 's3', 'gcs', 'http'). + """ + if not plugin_name: + return {"error": "plugin_name is required"} + + cmd = ["snakemake", "--storage-help", plugin_name] + + 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": f"Failed to get help for plugin '{plugin_name}'." + } + +@mcp.tool() +def generate_storage_plugin_skeleton( + output_directory: str, + plugin_name: str, + include_read_write: bool = True, + include_glob: bool = True, + include_touch: bool = True +) -> Dict[str, Any]: + """ + Generates a Python code skeleton for a new Snakemake storage plugin based on the + snakemake-interface-storage-plugins specification. + + Args: + output_directory: Directory where the skeleton file (__init__.py) will be created. + plugin_name: Name of the plugin (used for class naming). + include_read_write: Whether to include Read/Write capability templates. + include_glob: Whether to include Glob capability templates. + include_touch: Whether to include Touch capability templates. + """ + out_path = Path(output_directory) + if not out_path.exists(): + return {"error": f"Output directory {output_directory} does not exist."} + + plugin_class_name = plugin_name.capitalize() + + # Build inheritance list + base_classes = [] + if include_read_write: + base_classes.extend(["StorageObjectRead", "StorageObjectWrite"]) + if include_glob: + base_classes.append("StorageObjectGlob") + if include_touch: + base_classes.append("StorageObjectTouch") + + inheritance = ", ".join(base_classes) if base_classes else "StorageObjectBase" + + skeleton_code = f"""from dataclasses import dataclass, field +from typing import Any, Iterable, Optional, List +from snakemake_interface_storage_plugins.settings import StorageProviderSettingsBase +from snakemake_interface_storage_plugins.storage_provider import ( + StorageProviderBase, + StorageQueryValidationResult, + ExampleQuery, + Operation, +) +from snakemake_interface_storage_plugins.storage_object import ( + {", ".join(base_classes)}, + retry_decorator, +) +from snakemake_interface_storage_plugins.io import IOCacheStorageInterface + +@dataclass +class StorageProviderSettings(StorageProviderSettingsBase): + # Define settings (e.g. host url, credentials) + # Accessible via --storage-{plugin_name}- + myparam: Optional[int] = field(default=None, metadata={{"help": "Example parameter"}}) + +class StorageProvider(StorageProviderBase): + def __post_init__(self): + # Initialize connection pools or backend clients here + pass + + @classmethod + def example_queries(cls) -> List[ExampleQuery]: + return [ExampleQuery(query="example://path/to/data", description="Example query")] + + def rate_limiter_key(self, query: str, operation: Operation) -> Any: + return "global" + + def default_max_requests_per_second(self) -> float: + return 10.0 + + def use_rate_limiter(self) -> bool: + return True + + @classmethod + def is_valid_query(cls, query: str) -> StorageQueryValidationResult: + if query.startswith("{plugin_name}://"): + return StorageQueryValidationResult(valid=True) + return StorageQueryValidationResult(valid=False, reason="Must start with {plugin_name}://") + +class StorageObject({inheritance}): + def __post_init__(self): + pass + + def local_suffix(self) -> str: + return self.query.replace("://", "_") + + @retry_decorator + def exists(self) -> bool: + # Implementation for existence check + return True + + @retry_decorator + def mtime(self) -> float: + # Implementation for modification time + return 0.0 + + @retry_decorator + def size(self) -> int: + # Implementation for size in bytes + return 0 + + @retry_decorator + def retrieve_object(self): + # Logic to download/access object locally at self.local_path() + pass +""" + + file_path = out_path / "__init__.py" + try: + with open(file_path, "w") as f: + f.write(skeleton_code) + return { + "status": "success", + "file_created": str(file_path), + "plugin_name": plugin_name, + "capabilities": base_classes + } + except Exception as e: + return {"error": f"Failed to write skeleton: {str(e)}"} + +@mcp.tool() +def validate_storage_query( + plugin_module: str, + query: str +) -> Dict[str, Any]: + """ + Validates a storage query string against a specific installed storage plugin + using the interface's validation logic. + + Args: + plugin_module: The python module name of the plugin (e.g., 'snakemake_storage_plugin_s3'). + query: The query string to validate (e.g., 's3://my-bucket/file.txt'). + """ + # We use a python subprocess to avoid polluting the current process and to + # handle cases where the plugin might not be installed. + python_script = f""" +try: + import importlib + plugin = importlib.import_module('{plugin_module}') + # The interface requires a StorageProvider class + provider_cls = plugin.StorageProvider + res = provider_cls.is_valid_query('{query}') + print(f"VALID:{{res.valid}}") + if not res.valid: + print(f"REASON:{{res.reason}}") +except ImportError: + print("ERROR:Plugin module not found") +except AttributeError: + print("ERROR:Module is not a valid Snakemake storage plugin") +except Exception as e: + print(f"ERROR:{{str(e)}}") +""" + + cmd = ["python3", "-c", python_script] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": "python3 -c '...'", + "stdout": result.stdout, + "stderr": result.stderr, + "validation_result": result.stdout.strip() + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "Execution failed during query validation." + } + +@mcp.tool() +def check_storage_interface_version() -> Dict[str, Any]: + """ + Checks the version of the snakemake-interface-storage-plugins package + installed in the current environment. + """ + cmd = ["python3", "-c", "import snakemake_interface_storage_plugins; print(snakemake_interface_storage_plugins.__version__)"] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "package": "snakemake-interface-storage-plugins", + "version": result.stdout.strip(), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + # Fallback to pip show if __version__ is not available + try: + res = subprocess.run(["pip", "show", "snakemake-interface-storage-plugins"], capture_output=True, text=True, check=True) + return {"info": res.stdout} + except: + return {"error": "Could not determine version."} + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/app/snakemake-interface-storage-plugins_shim_server.py b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/app/snakemake-interface-storage-plugins_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b8be7bccaa052831c76e3fc0a04eb4ef35627d22 --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/app/snakemake-interface-storage-plugins_shim_server.py @@ -0,0 +1,55 @@ +#!/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_snakemake-interface-storage-plugins/app/snakemake-interface-storage-plugins_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_snakemake_interface_storage_plugins' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/docker-compose.yml b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..ac5ca419fd5da5277ca2cb3bda86dc09c07ddfd4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-snakemake-interface-storage-plugins: + build: . + image: mcp-snakemake-interface-storage-plugins:latest + container_name: mcp-snakemake-interface-storage-plugins + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=snakemake-interface-storage-plugins + 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/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/environment.yaml b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b675ca805182f8a5affc65d0b5acfd05e270941b --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - snakemake-interface-storage-plugins + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/requirements.txt b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake-interface-storage-plugins/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_snakemake/Dockerfile b/Biomni/mcp_generated/mcp_snakemake/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e079e3f88fbaad22b22c41b0d553ac76a0b1fc04 --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake/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 snakemake via conda (e.g., from bioconda) +RUN conda install -c bioconda snakemake -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/snakemake_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/snakemake_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/snakemake_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_snakemake/app/requirements.txt b/Biomni/mcp_generated/mcp_snakemake/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_snakemake/app/snakemake_server.py b/Biomni/mcp_generated/mcp_snakemake/app/snakemake_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9c6c817bdb59d870e869107b32192fb1ac9852b7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake/app/snakemake_server.py @@ -0,0 +1,314 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict +import os + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_snakemake' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def snakemake_run( + snakefile: Optional[str] = None, + cores: int = 1, + targets: Optional[List[str]] = None, + dryrun: bool = False, + use_conda: bool = False, + use_singularity: bool = False, + config: Optional[List[str]] = None, + configfiles: Optional[List[str]] = None, + workdir: Optional[str] = None, + forceall: bool = False, + keep_going: bool = False, + printshellcmds: bool = True, + reason: bool = False, + latency_wait: int = 5, + rerun_incomplete: bool = True, +): + """ + Execute a Snakemake workflow. + + Args: + snakefile: Path to the Snakemake file (default: Snakefile). + cores: Number of CPU cores to use (required for execution). + targets: Specific files or rules to build. + dryrun: If True, only show what would be done. + use_conda: If True, use Conda to deploy software dependencies. + use_singularity: If True, use Singularity containers. + config: Set or overwrite configuration variables (list of "key=value"). + configfiles: List of configuration files to use. + workdir: Set the working directory. + forceall: Force the execution of all rules. + keep_going: Go on with independent jobs if a job fails. + printshellcmds: Print out the shell commands that will be executed. + reason: Print the reason for each executed rule. + latency_wait: Wait given seconds if an output file of a job is not present after the job finished. + rerun_incomplete: Re-run jobs that have been interrupted. + """ + cmd = ["snakemake", "--cores", str(cores)] + + if snakefile: + path_sf = Path(snakefile) + if not path_sf.exists(): + return {"error": f"Snakefile not found: {snakefile}"} + cmd.extend(["--snakefile", str(path_sf)]) + + if dryrun: + cmd.append("--dry-run") + if use_conda: + cmd.append("--use-conda") + if use_singularity: + cmd.append("--use-singularity") + if forceall: + cmd.append("--forceall") + if keep_going: + cmd.append("--keep-going") + if printshellcmds: + cmd.append("--printshellcmds") + if reason: + cmd.append("--reason") + if rerun_incomplete: + cmd.append("--rerun-incomplete") + + cmd.extend(["--latency-wait", str(latency_wait)]) + + if workdir: + cmd.extend(["--directory", workdir]) + + if config: + cmd.append("--config") + cmd.extend(config) + + if configfiles: + cmd.append("--configfiles") + cmd.extend(configfiles) + + if targets: + cmd.extend(targets) + + 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 snakemake_info( + snakefile: Optional[str] = None, + mode: str = "list", +): + """ + Get information about the Snakemake workflow without executing it. + + Args: + snakefile: Path to the Snakemake file. + mode: Information mode: 'list' (list rules), 'summary' (table of rules/files), 'lint' (check code quality). + """ + cmd = ["snakemake"] + + if snakefile: + cmd.extend(["--snakefile", snakefile]) + + if mode == "list": + cmd.append("--list") + elif mode == "summary": + cmd.append("--summary") + elif mode == "lint": + cmd.append("--lint") + else: + return {"error": f"Invalid mode: {mode}. Choose from 'list', 'summary', 'lint'."} + + 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), + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def snakemake_visualize( + snakefile: Optional[str] = None, + graph_type: str = "dag", + output_file: str = "workflow.dot", + targets: Optional[List[str]] = None, +): + """ + Generate a visualization of the workflow (DAG or Rulegraph) in DOT format. + + Args: + snakefile: Path to the Snakemake file. + graph_type: Type of graph: 'dag' (Directed Acyclic Graph of jobs) or 'rulegraph' (Graph of rules). + output_file: Path to save the .dot file. + targets: Specific files or rules to include in the graph. + """ + cmd = ["snakemake"] + + if snakefile: + cmd.extend(["--snakefile", snakefile]) + + if graph_type == "dag": + cmd.append("--dag") + elif graph_type == "rulegraph": + cmd.append("--rulegraph") + else: + return {"error": "graph_type must be 'dag' or 'rulegraph'"} + + if targets: + cmd.extend(targets) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + output_path = Path(output_file) + output_path.write_text(result.stdout) + + return { + "command_executed": " ".join(cmd), + "output_file": str(output_path.absolute()), + "message": f"Graph saved to {output_file}. Use 'dot -Tpdf {output_file} > graph.pdf' to render." + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def snakemake_report( + snakefile: Optional[str] = None, + report_path: str = "report.html", + config: Optional[List[str]] = None, +): + """ + Create an HTML report containing results and statistics. + + Args: + snakefile: Path to the Snakemake file. + report_path: Path where the HTML report should be saved. + config: Set or overwrite configuration variables (list of "key=value"). + """ + cmd = ["snakemake", "--report", report_path] + + if snakefile: + cmd.extend(["--snakefile", snakefile]) + + if config: + cmd.append("--config") + cmd.extend(config) + + 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(Path(report_path).absolute())] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def snakemake_management( + snakefile: Optional[str] = None, + unlock: bool = False, + cleanup_metadata: Optional[List[str]] = None, + cleanup_shadow: bool = False, +): + """ + Perform maintenance tasks on the Snakemake working directory. + + Args: + snakefile: Path to the Snakemake file. + unlock: Remove a lock on the working directory. + cleanup_metadata: Cleanup the metadata of given files. + cleanup_shadow: Cleanup old shadow directories. + """ + cmd = ["snakemake"] + + if snakefile: + cmd.extend(["--snakefile", snakefile]) + + if unlock: + cmd.append("--unlock") + elif cleanup_metadata: + cmd.append("--cleanup-metadata") + cmd.extend(cleanup_metadata) + elif cleanup_shadow: + cmd.append("--cleanup-shadow") + else: + return {"error": "No management action specified (unlock, cleanup_metadata, or cleanup_shadow)."} + + 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), + "stderr": e.stderr, + "error": str(e), + "status": "failed" + } + +@mcp.tool() +def snakemake_archive( + output_file: str, + snakefile: Optional[str] = None, +): + """ + Archive the workflow into a tarball, including all input files and software environment. + + Args: + output_file: Path to the output archive (e.g., workflow.tar.gz). + snakefile: Path to the Snakemake file. + """ + cmd = ["snakemake", "--archive", output_file] + + if snakefile: + cmd.extend(["--snakefile", snakefile]) + + 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(Path(output_file).absolute())] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stderr": e.stderr, + "error": str(e) + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_snakemake/app/snakemake_shim_server.py b/Biomni/mcp_generated/mcp_snakemake/app/snakemake_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c06c90a6efcc35cf479acc1dd02920d9c0bd6bd2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake/app/snakemake_shim_server.py @@ -0,0 +1,55 @@ +#!/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_snakemake/app/snakemake_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_snakemake' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_snakemake/docker-compose.yml b/Biomni/mcp_generated/mcp_snakemake/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..06457ecd8749d433ab983eff3cc1dbed6121b260 --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-snakemake: + build: . + image: mcp-snakemake:latest + container_name: mcp-snakemake + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=snakemake + 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/Biomni/mcp_generated/mcp_snakemake/environment.yaml b/Biomni/mcp_generated/mcp_snakemake/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8b397091e44e26bf726ec2926ccb3db895f108a4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - snakemake + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_snakemake/requirements.txt b/Biomni/mcp_generated/mcp_snakemake/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_snakemake/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_sourmash/Dockerfile b/Biomni/mcp_generated/mcp_sourmash/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..517328aacda6a9b32488e23d0efe1cbc8d36a446 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sourmash/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 sourmash via conda (e.g., from bioconda) +RUN conda install -c bioconda sourmash -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/sourmash_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/sourmash_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/sourmash_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sourmash/app/requirements.txt b/Biomni/mcp_generated/mcp_sourmash/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_sourmash/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_sourmash/app/sourmash_server.py b/Biomni/mcp_generated/mcp_sourmash/app/sourmash_server.py new file mode 100644 index 0000000000000000000000000000000000000000..82ec5a419af8b9200046bcd579ea93d63a5dd68d --- /dev/null +++ b/Biomni/mcp_generated/mcp_sourmash/app/sourmash_server.py @@ -0,0 +1,384 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Union +import os + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_sourmash' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def sourmash_sketch_dna( + input_files: List[str], + output: str, + ksize: int = 31, + scaled: int = 1000, + name: Optional[str] = None, + singleton: bool = False, + merge: Optional[str] = None, + param_string: Optional[str] = None, +): + """ + Create DNA signatures (sketches) from genomic or metagenomic sequences. + + Args: + input_files: List of paths to FASTA/FASTQ files (can be gzipped). + output: Path to save the resulting .sig or .zip signature file. + ksize: K-mer size to use (default 31). + scaled: Compression ratio (default 1000). + name: Name to give to the signature(s). + singleton: Create a separate signature for each sequence in the input files. + merge: Merge all input files into a single signature with this name. + param_string: Advanced parameter string (e.g., 'k=21,k=31,scaled=1000'). + """ + # Input validation + for f in input_files: + if not Path(f).exists(): + return {"error": f"Input file not found: {f}"} + + cmd = ["sourmash", "sketch", "dna"] + + for f in input_files: + cmd.append(f) + + cmd.extend(["-o", output]) + cmd.extend(["--param-string", f"k={ksize},scaled={scaled}"]) + + if name: + cmd.extend(["--name", name]) + if singleton: + cmd.append("--singleton") + if merge: + cmd.extend(["--merge", merge]) + if param_string: + # Overwrites the default k/scaled if provided + cmd.extend(["--param-string", param_string]) + + 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] if Path(output).exists() else [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": e.stderr, + "stdout": e.stdout + } + +@mcp.tool() +def sourmash_compare( + signatures: List[str], + output: str, + ksize: int = 31, + ignore_abundance: bool = False, + containment: bool = False, + processes: int = 1, +): + """ + Compare multiple signatures to create a similarity or containment matrix. + + Args: + signatures: List of paths to signature files (.sig, .zip, or directories). + output: Path to save the comparison matrix. + ksize: K-mer size to use for comparison (default 31). + ignore_abundance: Do not use k-mer abundances if present. + containment: Calculate containment instead of Jaccard similarity. + processes: Number of processes to use for parallelization. + """ + for s in signatures: + if not Path(s).exists(): + return {"error": f"Signature file not found: {s}"} + + cmd = ["sourmash", "compare"] + cmd.extend(signatures) + cmd.extend(["-o", output, "-k", str(ksize)]) + + if ignore_abundance: + cmd.append("--ignore-abundance") + if containment: + cmd.append("--containment") + if processes > 1: + cmd.extend(["--processes", str(processes)]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + # sourmash compare creates a binary matrix file and often a .labels.txt file + output_files = [output] + labels_file = f"{output}.labels.txt" + if Path(labels_file).exists(): + output_files.append(labels_file) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + except subprocess.CalledProcessError as e: + return {"error": e.stderr, "command_executed": " ".join(cmd)} + +@mcp.tool() +def sourmash_gather( + query: str, + databases: List[str], + output: Optional[str] = None, + threshold_bp: int = 50000, + ksize: int = 31, + save_unassigned: Optional[str] = None, + num_results: Optional[int] = None, +): + """ + Search a metagenome signature against a database to find the minimum set of genomes that cover it. + + Args: + query: Path to the query signature file. + databases: List of paths to database signatures or SBT/LCA indexes. + output: Path to save the results in CSV format. + threshold_bp: Minimum number of base pairs in common to report a match (default 50,000). + ksize: K-mer size to use (default 31). + save_unassigned: Path to save unassigned k-mers as a signature. + num_results: Stop after finding this many matches. + """ + if not Path(query).exists(): + return {"error": f"Query file not found: {query}"} + + cmd = ["sourmash", "gather", query] + cmd.extend(databases) + cmd.extend(["-k", str(ksize), "--threshold-bp", str(threshold_bp)]) + + if output: + cmd.extend(["-o", output]) + if save_unassigned: + cmd.extend(["--save-unassigned", save_unassigned]) + if num_results: + cmd.extend(["--num-results", str(num_results)]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = [] + if output and Path(output).exists(): + output_files.append(output) + if save_unassigned and Path(save_unassigned).exists(): + output_files.append(save_unassigned) + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + except subprocess.CalledProcessError as e: + return {"error": e.stderr, "command_executed": " ".join(cmd)} + +@mcp.tool() +def sourmash_search( + query: str, + databases: List[str], + output: Optional[str] = None, + threshold: float = 0.08, + ksize: int = 31, + containment: bool = False, + num_results: int = 3, +): + """ + Search a query signature against a database for similar signatures. + + Args: + query: Path to the query signature file. + databases: List of paths to database signatures or indexes. + output: Path to save results in CSV format. + threshold: Minimum similarity/containment to report (default 0.08). + ksize: K-mer size to use (default 31). + containment: Search for containment instead of Jaccard similarity. + num_results: Number of results to return (default 3). + """ + if not Path(query).exists(): + return {"error": f"Query file not found: {query}"} + + cmd = ["sourmash", "search", query] + cmd.extend(databases) + cmd.extend(["-k", str(ksize), "--threshold", str(threshold), "-n", str(num_results)]) + + if output: + cmd.extend(["-o", output]) + if containment: + cmd.append("--containment") + + 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] if output and Path(output).exists() else [] + } + except subprocess.CalledProcessError as e: + return {"error": e.stderr, "command_executed": " ".join(cmd)} + +@mcp.tool() +def sourmash_plot( + matrix: str, + output_prefix: str, + labels: bool = False, + indices: bool = False, + vmax: float = 1.0, + vmin: float = 0.0, +): + """ + Visualize a comparison matrix created by 'sourmash compare'. + + Args: + matrix: Path to the matrix file created by 'sourmash compare'. + output_prefix: Prefix for the output plot files (.png, .pdf). + labels: Show signature names on the plot (requires .labels.txt file). + indices: Show matrix indices on the plot. + vmax: Maximum value for the color scale (default 1.0). + vmin: Minimum value for the color scale (default 0.0). + """ + if not Path(matrix).exists(): + return {"error": f"Matrix file not found: {matrix}"} + + cmd = ["sourmash", "plot", matrix, "--output-dir", os.path.dirname(output_prefix) or "."] + + # sourmash plot uses the matrix filename to find labels, but we can specify prefix + # Note: sourmash plot usually outputs {matrix}.matrix.png + + if labels: + cmd.append("--labels") + if indices: + cmd.append("--indices") + cmd.extend(["--vmax", str(vmax), "--vmin", str(vmin)]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + # Identify generated files + base = Path(matrix).name + output_files = [str(f) for f in Path(".").glob(f"{base}*") if f.suffix in ['.png', '.pdf', '.matrix.png']] + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + except subprocess.CalledProcessError as e: + return {"error": e.stderr, "command_executed": " ".join(cmd)} + +@mcp.tool() +def sourmash_tax_metagenome( + gather_csv: List[str], + taxonomy_csv: List[str], + output_prefix: str, + rank: str = "species", + force: bool = False, +): + """ + Summarize 'sourmash gather' results using taxonomic information. + + Args: + gather_csv: List of CSV files from 'sourmash gather'. + taxonomy_csv: List of CSV files containing taxonomic information (e.g., GTDB or NCBI). + output_prefix: Prefix for the output summary files. + rank: Taxonomic rank to summarize at (default 'species'). + force: Force overwrite of existing files. + """ + for f in gather_csv + taxonomy_csv: + if not Path(f).exists(): + return {"error": f"Input file not found: {f}"} + + cmd = ["sourmash", "tax", "metagenome"] + cmd.extend(["--gather-results"]) + cmd.extend(gather_csv) + cmd.extend(["--taxonomy"]) + cmd.extend(taxonomy_csv) + cmd.extend(["--output-prefix", output_prefix, "--rank", rank]) + + if force: + cmd.append("--force") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = [f"{output_prefix}.summarized.csv"] + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [f for f in output_files if Path(f).exists()] + } + except subprocess.CalledProcessError as e: + return {"error": e.stderr, "command_executed": " ".join(cmd)} + +@mcp.tool() +def sourmash_sig_merge( + signatures: List[str], + output: str, + ksize: Optional[int] = None, + flatten: bool = False, +): + """ + Merge multiple signatures into a single signature file. + + Args: + signatures: List of signature files to merge. + output: Path to the output signature file. + ksize: Filter by k-mer size before merging. + flatten: Remove abundance information (set all counts to 1). + """ + for s in signatures: + if not Path(s).exists(): + return {"error": f"Signature file not found: {s}"} + + cmd = ["sourmash", "sig", "merge"] + cmd.extend(signatures) + cmd.extend(["-o", output]) + + if ksize: + cmd.extend(["-k", str(ksize)]) + if flatten: + cmd.append("--flatten") + + 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] if Path(output).exists() else [] + } + except subprocess.CalledProcessError as e: + return {"error": e.stderr, "command_executed": " ".join(cmd)} + +@mcp.tool() +def sourmash_sig_describe( + signatures: List[str], +): + """ + Provide a human-readable summary of the contents of signature files. + + Args: + signatures: List of signature files to describe. + """ + for s in signatures: + if not Path(s).exists(): + return {"error": f"Signature file not found: {s}"} + + cmd = ["sourmash", "sig", "describe"] + cmd.extend(signatures) + + 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": e.stderr, "command_executed": " ".join(cmd)} + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_sourmash/app/sourmash_shim_server.py b/Biomni/mcp_generated/mcp_sourmash/app/sourmash_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..886c36f1c1df90dc9568620f80c9ffbc1212cfdf --- /dev/null +++ b/Biomni/mcp_generated/mcp_sourmash/app/sourmash_shim_server.py @@ -0,0 +1,55 @@ +#!/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_sourmash/app/sourmash_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_sourmash' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_sourmash/docker-compose.yml b/Biomni/mcp_generated/mcp_sourmash/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b163b34254d72f5261702b8546bd29767ef1b46d --- /dev/null +++ b/Biomni/mcp_generated/mcp_sourmash/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-sourmash: + build: . + image: mcp-sourmash:latest + container_name: mcp-sourmash + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=sourmash + 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/Biomni/mcp_generated/mcp_sourmash/environment.yaml b/Biomni/mcp_generated/mcp_sourmash/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..749ba7b7fd416595e392aef0a7b338bd6b9e63fc --- /dev/null +++ b/Biomni/mcp_generated/mcp_sourmash/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - sourmash + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sourmash/requirements.txt b/Biomni/mcp_generated/mcp_sourmash/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sourmash/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_stream/Dockerfile b/Biomni/mcp_generated/mcp_stream/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..752138d85aefd1a07d0946ef5a2a16fd29f129b2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_stream/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 stream via conda (e.g., from bioconda) +RUN conda install -c bioconda stream -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/stream_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/stream_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/stream_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_stream/app/requirements.txt b/Biomni/mcp_generated/mcp_stream/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_stream/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_stream/app/stream_server.py b/Biomni/mcp_generated/mcp_stream/app/stream_server.py new file mode 100644 index 0000000000000000000000000000000000000000..cac73c9baa78f17afe51e5fb6565ba9eda76bf46 --- /dev/null +++ b/Biomni/mcp_generated/mcp_stream/app/stream_server.py @@ -0,0 +1,336 @@ +from typing import Optional, List, Literal +import subprocess +from pathlib import Path +import tempfile + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_stream' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def stream_read_data( + input_file: str, + file_format: Literal["tsv", "csv", "loom", "h5ad"] = "tsv", + output_h5ad: str = "data.h5ad", +) -> dict: + """ + Read single-cell data into STREAM's AnnData format. + + :param input_file: Path to the input data file. + :param file_format: Format of the input file (tsv, csv, loom, h5ad). + :param output_h5ad: Path to save the resulting AnnData object. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} not found."} + + # Construct python command to use the stream library + py_cmd = ( + f"import stream as st; " + f"adata = st.read_{file_format}('{input_file}'); " + f"adata.write('{output_h5ad}')" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_h5ad] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def stream_select_variable_genes( + input_h5ad: str, + output_h5ad: str, + loess_frac: float = 0.1, + percentile: int = 95, + save_fig: bool = False, + fig_name: str = "variable_genes.pdf" +) -> dict: + """ + Select variable genes for downstream analysis. + + :param input_h5ad: Path to the input AnnData file. + :param output_h5ad: Path to save the updated AnnData file. + :param loess_frac: The fraction of the data used when estimating the LOESS curve. + :param percentile: The percentile used to select variable genes. + :param save_fig: Whether to save the variable gene selection plot. + :param fig_name: Filename for the saved plot. + """ + if not Path(input_h5ad).exists(): + return {"error": f"Input file {input_h5ad} not found."} + + py_cmd = ( + f"import stream as st; " + f"adata = st.read_h5ad('{input_h5ad}'); " + f"st.select_variable_genes(adata, loess_frac={loess_frac}, percentile={percentile}, save_fig={save_fig}, fig_name='{fig_name}'); " + f"adata.write('{output_h5ad}')" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_h5ad] + ([fig_name] if save_fig else []) + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +@mcp.tool() +def stream_dimension_reduction( + input_h5ad: str, + output_h5ad: str, + method: Literal["pca", "se", "mlle", "umap", "tsne"] = "se", + feature: Literal["genes", "var_genes", "peaks"] = "var_genes", + n_components: int = 3, + n_neighbors: int = 50, +) -> dict: + """ + Perform dimensionality reduction on the single-cell data. + + :param input_h5ad: Path to the input AnnData file. + :param output_h5ad: Path to save the updated AnnData file. + :param method: Dimensionality reduction method (pca, se, mlle, umap, tsne). + :param feature: Features to use (genes, var_genes, peaks). + :param n_components: Number of components to keep. + :param n_neighbors: Number of neighbors for methods like SE or UMAP. + """ + if not Path(input_h5ad).exists(): + return {"error": f"Input file {input_h5ad} not found."} + + py_cmd = ( + f"import stream as st; " + f"adata = st.read_h5ad('{input_h5ad}'); " + f"st.dimension_reduction(adata, method='{method}', feature='{feature}', n_components={n_components}, n_neighbors={n_neighbors}); " + f"adata.write('{output_h5ad}')" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_h5ad] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +@mcp.tool() +def stream_seed_elastic_principal_graph( + input_h5ad: str, + output_h5ad: str, + n_clusters: int = 10, + method: Literal["kmeans", "scg"] = "kmeans", +) -> dict: + """ + Initialize the elastic principal graph using clustering. + + :param input_h5ad: Path to the input AnnData file. + :param output_h5ad: Path to save the updated AnnData file. + :param n_clusters: Number of clusters (seeds) for the graph. + :param method: Clustering method (kmeans or scg). + """ + if not Path(input_h5ad).exists(): + return {"error": f"Input file {input_h5ad} not found."} + + py_cmd = ( + f"import stream as st; " + f"adata = st.read_h5ad('{input_h5ad}'); " + f"st.seed_elastic_principal_graph(adata, n_clusters={n_clusters}, method='{method}'); " + f"adata.write('{output_h5ad}')" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_h5ad] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +@mcp.tool() +def stream_elastic_principal_graph( + input_h5ad: str, + output_h5ad: str, + epg_lambda: float = 0.02, + epg_mu: float = 0.1, + epg_alpha: float = 0.02, + max_iter: int = 10, +) -> dict: + """ + Refine the elastic principal graph. + + :param input_h5ad: Path to the input AnnData file. + :param output_h5ad: Path to save the updated AnnData file. + :param epg_lambda: Lambda parameter for ElPiGraph (controls edge stretching). + :param epg_mu: Mu parameter for ElPiGraph (controls graph curvature). + :param epg_alpha: Alpha parameter for ElPiGraph. + :param max_iter: Maximum number of iterations. + """ + if not Path(input_h5ad).exists(): + return {"error": f"Input file {input_h5ad} not found."} + + py_cmd = ( + f"import stream as st; " + f"adata = st.read_h5ad('{input_h5ad}'); " + f"st.elastic_principal_graph(adata, epg_lambda={epg_lambda}, epg_mu={epg_mu}, epg_alpha={epg_alpha}, max_iter={max_iter}); " + f"adata.write('{output_h5ad}')" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_h5ad] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +@mcp.tool() +def stream_plot_subwaymap( + input_h5ad: str, + fig_name: str = "subway_map.pdf", + root: str = "S0", + feature: Optional[str] = None, + percentile_limit: float = 100.0, +) -> dict: + """ + Generate a subway map plot of the reconstructed trajectories. + + :param input_h5ad: Path to the input AnnData file. + :param fig_name: Filename for the output plot. + :param root: The root node of the trajectory. + :param feature: Feature (gene/peak) to visualize on the subway map. + :param percentile_limit: Percentile limit for color scaling if feature is provided. + """ + if not Path(input_h5ad).exists(): + return {"error": f"Input file {input_h5ad} not found."} + + feature_arg = f"'{feature}'" if feature else "None" + py_cmd = ( + f"import stream as st; " + f"adata = st.read_h5ad('{input_h5ad}'); " + f"st.subwaymap_plot(adata, root='{root}', feature={feature_arg}, percentile_limit={percentile_limit}, save_fig=True, fig_name='{fig_name}')" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [fig_name] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +@mcp.tool() +def stream_plot_streamplot( + input_h5ad: str, + fig_name: str = "stream_plot.pdf", + root: str = "S0", + feature: Optional[str] = None, + factor: float = 1.0, +) -> dict: + """ + Generate a STREAM plot (density-based trajectory visualization). + + :param input_h5ad: Path to the input AnnData file. + :param fig_name: Filename for the output plot. + :param root: The root node of the trajectory. + :param feature: Feature (gene/peak) to visualize on the stream plot. + :param factor: Scaling factor for the width of the stream. + """ + if not Path(input_h5ad).exists(): + return {"error": f"Input file {input_h5ad} not found."} + + feature_arg = f"'{feature}'" if feature else "None" + py_cmd = ( + f"import stream as st; " + f"adata = st.read_h5ad('{input_h5ad}'); " + f"st.stream_plot(adata, root='{root}', feature={feature_arg}, factor={factor}, save_fig=True, fig_name='{fig_name}')" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [fig_name] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +@mcp.tool() +def stream_plot_leafplot( + input_h5ad: str, + fig_name: str = "leaf_plot.pdf", + root: str = "S0", + leaf_genes: Optional[List[str]] = None, +) -> dict: + """ + Generate a leaf plot to visualize gene expression along branches. + + :param input_h5ad: Path to the input AnnData file. + :param fig_name: Filename for the output plot. + :param root: The root node of the trajectory. + :param leaf_genes: List of genes to visualize. + """ + if not Path(input_h5ad).exists(): + return {"error": f"Input file {input_h5ad} not found."} + + genes_arg = str(leaf_genes) if leaf_genes else "None" + py_cmd = ( + f"import stream as st; " + f"adata = st.read_h5ad('{input_h5ad}'); " + f"st.leaf_plot(adata, root='{root}', leaf_genes={genes_arg}, save_fig=True, fig_name='{fig_name}')" + ) + + cmd = ["python", "-c", py_cmd] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [fig_name] + } + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_stream/app/stream_shim_server.py b/Biomni/mcp_generated/mcp_stream/app/stream_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..80e60570576b6b23275d580907fb9cc6d85c6e0e --- /dev/null +++ b/Biomni/mcp_generated/mcp_stream/app/stream_shim_server.py @@ -0,0 +1,55 @@ +#!/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_stream/app/stream_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_stream' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_stream/docker-compose.yml b/Biomni/mcp_generated/mcp_stream/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..12bffaa5d1593c4e654fae7a4a29fb8d784095fb --- /dev/null +++ b/Biomni/mcp_generated/mcp_stream/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-stream: + build: . + image: mcp-stream:latest + container_name: mcp-stream + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=stream + 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/Biomni/mcp_generated/mcp_stream/environment.yaml b/Biomni/mcp_generated/mcp_stream/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..581db92130af990e1bf5a567e4a9120f05701fe5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_stream/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - stream + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_stream/requirements.txt b/Biomni/mcp_generated/mcp_stream/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_stream/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_switchtfi/Dockerfile b/Biomni/mcp_generated/mcp_switchtfi/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..03712376d884e40e2d789940320927103dacc29c --- /dev/null +++ b/Biomni/mcp_generated/mcp_switchtfi/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 switchtfi via conda (e.g., from bioconda) +RUN conda install -c bioconda switchtfi -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/switchtfi_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/switchtfi_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/switchtfi_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_switchtfi/app/requirements.txt b/Biomni/mcp_generated/mcp_switchtfi/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_switchtfi/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_switchtfi/app/switchtfi_server.py b/Biomni/mcp_generated/mcp_switchtfi/app/switchtfi_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8df769c4adf310faeeb6cabb510b7496ec4fe764 --- /dev/null +++ b/Biomni/mcp_generated/mcp_switchtfi/app/switchtfi_server.py @@ -0,0 +1,87 @@ +import subprocess +import tempfile +import os +from pathlib import Path +from typing import List, Dict, Union + +# MCP decorator is assumed to be available in the execution environment. +# No import is included as per the instructions. + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_switchtfi' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_example_analysis( + dataset: str, +) -> Dict[str, Union[str, List[str], int]]: + """ + Runs the SwitchTFI example analysis on a preprocessed scRNA-seq dataset. + + This function executes the 'example.py' script provided in the SwitchTFI + repository to identify transcription factors driving cell differentiation + for one of the built-in example datasets. + + Args: + dataset: The example dataset to use. Must be one of 'ery', 'beta', or 'alpha'. + + Returns: + A dictionary containing the execution details, including the command, + stdout, stderr, and a list of output files generated. + """ + # 1. Input Validation + allowed_datasets = ['ery', 'beta', 'alpha'] + if dataset not in allowed_datasets: + raise ValueError(f"Dataset must be one of {allowed_datasets}, but got '{dataset}'.") + + # The command assumes 'example.py' is in the current working directory or PATH. + # This is based on the usage instructions: `python example.py -d ery` + cmd = ["python", "example.py", "-d", dataset] + command_str = " ".join(cmd) + + # 2. Subprocess Execution in a temporary directory to isolate the run + try: + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir_path = Path(tmpdir) + + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + cwd=tmpdir_path, # Run inside the temp directory to capture all outputs + encoding='utf-8' + ) + + # 3. Collect any files generated in the temporary directory + output_files = [str(tmpdir_path / f) for f in os.listdir(tmpdir_path)] + + # 4. Return structured output for success + return { + "command_executed": command_str, + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": output_files, + } + + except FileNotFoundError: + # This error occurs if 'python' or 'example.py' is not found. + return { + "error": "Execution failed: 'python' or 'example.py' not found. Please ensure Python is installed and the SwitchTFI 'example.py' script is in the execution path.", + "command_executed": command_str, + "stdout": "", + "stderr": "FileNotFoundError: The command could not be located.", + } + except subprocess.CalledProcessError as e: + # This error occurs if the script runs but returns a non-zero exit code. + return { + "error": "SwitchTFI example script failed with a non-zero exit code.", + "command_executed": command_str, + "return_code": e.returncode, + "stdout": e.stdout, + "stderr": e.stderr, + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_switchtfi/app/switchtfi_shim_server.py b/Biomni/mcp_generated/mcp_switchtfi/app/switchtfi_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e5a9a6ab327a5ab1f26a00238ad1f2811daeff17 --- /dev/null +++ b/Biomni/mcp_generated/mcp_switchtfi/app/switchtfi_shim_server.py @@ -0,0 +1,55 @@ +#!/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_switchtfi/app/switchtfi_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_switchtfi' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_switchtfi/docker-compose.yml b/Biomni/mcp_generated/mcp_switchtfi/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..12093a3a645e749d4d580dcb5107cd46e470c904 --- /dev/null +++ b/Biomni/mcp_generated/mcp_switchtfi/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-switchtfi: + build: . + image: mcp-switchtfi:latest + container_name: mcp-switchtfi + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=switchtfi + 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/Biomni/mcp_generated/mcp_switchtfi/environment.yaml b/Biomni/mcp_generated/mcp_switchtfi/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..970b9f9178c7f08ec02252bd51e85f91e08dbfcc --- /dev/null +++ b/Biomni/mcp_generated/mcp_switchtfi/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - switchtfi + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_switchtfi/requirements.txt b/Biomni/mcp_generated/mcp_switchtfi/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_switchtfi/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_trimadap/Dockerfile b/Biomni/mcp_generated/mcp_trimadap/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d5add988a337bffd9172c19c3687c5bb4f1a7b93 --- /dev/null +++ b/Biomni/mcp_generated/mcp_trimadap/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 trimadap via conda (e.g., from bioconda) +RUN conda install -c bioconda trimadap -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/trimadap_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/trimadap_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/trimadap_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_trimadap/app/requirements.txt b/Biomni/mcp_generated/mcp_trimadap/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_trimadap/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_trimadap/app/trimadap_server.py b/Biomni/mcp_generated/mcp_trimadap/app/trimadap_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b0e34c5dc71a8bf9864700baeafbd8e90e33503e --- /dev/null +++ b/Biomni/mcp_generated/mcp_trimadap/app/trimadap_server.py @@ -0,0 +1,108 @@ +from pathlib import Path +from typing import Optional +import subprocess + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_trimadap' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def trimadap( + input_fastq: str, + output_fastq: str, + adapter_5: Optional[str] = None, + adapter_3: Optional[str] = None, + min_match_len: int = 15, + max_error_rate: float = 0.10, + min_read_len: int = 20, + threads: int = 1, +): + """ + Fast but inaccurate adapter trimmer for Illumina reads. + + Args: + input_fastq: Path to the input FASTQ file (can be gzipped). + output_fastq: Path where the trimmed FASTQ output will be saved. + adapter_5: 5'-end adapter sequence to trim. + adapter_3: 3'-end adapter sequence to trim. + min_match_len: Minimum match length for an adapter to be trimmed (default: 15). + max_error_rate: Maximum allowed error rate for adapter matching (default: 0.10). + min_read_len: Minimum length of reads to keep after trimming (default: 20). + threads: Number of threads to use (default: 1). + """ + # Input validation + input_path = Path(input_fastq) + if not input_path.exists(): + return { + "command_executed": "None", + "stdout": "", + "stderr": f"Error: Input file '{input_fastq}' does not exist.", + "output_files": [] + } + + output_path = Path(output_fastq) + # Ensure output directory exists + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Parameter constraints validation + if min_match_len < 1: + return {"error": "min_match_len must be a positive integer."} + if not (0.0 <= max_error_rate <= 1.0): + return {"error": "max_error_rate must be between 0.0 and 1.0."} + if min_read_len < 0: + return {"error": "min_read_len cannot be negative."} + if threads < 1: + return {"error": "threads must be at least 1."} + + # Construct command + # Usage: trimadap [options] + cmd = ["trimadap"] + + if adapter_5: + cmd.extend(["-5", adapter_5]) + if adapter_3: + cmd.extend(["-3", adapter_3]) + + cmd.extend(["-l", str(min_match_len)]) + cmd.extend(["-e", str(max_error_rate)]) + cmd.extend(["-s", str(min_read_len)]) + cmd.extend(["-p", str(threads)]) + + cmd.append(str(input_path)) + + try: + # trimadap outputs the trimmed reads to stdout + with open(output_path, "w") as f_out: + process = subprocess.run( + cmd, + stdout=f_out, + stderr=subprocess.PIPE, + text=True, + check=True + ) + + return { + "command_executed": " ".join(cmd), + "stdout": f"Trimmed reads successfully written to {output_fastq}", + "stderr": process.stderr, + "output_files": [str(output_path)] + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": f"Execution failed with return code {e.returncode}: {e.stderr}", + "output_files": [] + } + except Exception as e: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": f"An unexpected error occurred: {str(e)}", + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_trimadap/app/trimadap_shim_server.py b/Biomni/mcp_generated/mcp_trimadap/app/trimadap_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..81d3f8a45304aac7238762a405e737062c75158c --- /dev/null +++ b/Biomni/mcp_generated/mcp_trimadap/app/trimadap_shim_server.py @@ -0,0 +1,55 @@ +#!/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_trimadap/app/trimadap_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_trimadap' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_trimadap/docker-compose.yml b/Biomni/mcp_generated/mcp_trimadap/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..567bf0f89982aa6353607e7c7f15739b4a27bfb3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_trimadap/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-trimadap: + build: . + image: mcp-trimadap:latest + container_name: mcp-trimadap + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=trimadap + 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/Biomni/mcp_generated/mcp_trimadap/environment.yaml b/Biomni/mcp_generated/mcp_trimadap/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c62333ba72bddb2b8bfe7610e48e3ffe281ee74a --- /dev/null +++ b/Biomni/mcp_generated/mcp_trimadap/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - trimadap + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/Dockerfile b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..5363d49cc8c3c7a11b284a31b651a49b4dc90353 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/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-bedtobigbed via conda (e.g., from bioconda) +RUN conda install -c bioconda ucsc-bedtobigbed -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-bedtobigbed_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-bedtobigbed_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-bedtobigbed_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/app/requirements.txt b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/app/ucsc-bedtobigbed_server.py b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/app/ucsc-bedtobigbed_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e3b0c44de6bce3b6cd96948e6bf300e36ed6448c --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/app/ucsc-bedtobigbed_server.py @@ -0,0 +1,170 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Dict, Union + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_ucsc_bedtobigbed' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def bed_to_bigbed( + bed_file: Path, + chrom_sizes_file: Path, + output_bigbed_file: Path, + auto_sql_file: Optional[Path] = None, + bed_type: Optional[str] = None, + delimiter: Optional[str] = None, + extra_index_fields: Optional[List[str]] = None, + block_size: int = 256, + items_per_slot: int = 1024, + uncompressed_blocks: bool = False, + udc_directory: Optional[Path] = None, + max_memory: Optional[str] = None, + temp_directory: Optional[Path] = None, + allow_1000g_chrom_names: bool = False, + warn_on_all_errors: bool = False, + verbose_level: int = 1, +) -> Dict[str, Union[str, List[str]]]: + """ + Converts a BED file to a bigBed file using ucsc-bedtobigbed. + + This tool takes a BED file and a chromosome sizes file to create a binary + bigBed file, which is optimized for display in the UCSC Genome Browser. + + Args: + bed_file: Path to the input BED file. Must exist. + chrom_sizes_file: Path to the chromosome sizes file (e.g., hg38.chrom.sizes). + This file should contain two columns: chromosome name and size. Must exist. + output_bigbed_file: Path for the output bigBed file. Its parent directory will be created if it doesn't exist. + auto_sql_file: Optional path to an autoSql (.as) file defining a custom BED format. + Required for BED files with more than 12 fields or custom fields. Must exist if provided. + bed_type: Optional string specifying the BED format (e.g., "bed3", "bed6", "bed9+2"). + If not specified, the tool attempts to infer it. + delimiter: Optional character to use as a field delimiter if not tab. + By default, the tool expects tab-separated input. + extra_index_fields: Optional list of field names to create extra indexes on. + Each string in the list will be passed as a separate -extraIndex argument. + block_size: The number of items that are grouped into a block in the bigBed file. + Default is 256. Must be a positive integer. + items_per_slot: The number of items that are stored in each slot of the bigBed index. + Default is 1024. Must be a positive integer. + uncompressed_blocks: If True, blocks in the bigBed file will not be compressed. + udc_directory: Optional path to a directory for UDC files (e.g., for remote files). + Must exist if provided. + max_memory: Optional string specifying the maximum memory to use (e.g., "1G", "500M"). + temp_directory: Optional path to a directory for temporary files. + Will be created if it doesn't exist. + allow_1000g_chrom_names: If True, allows 1000Genomes style chromosome names + (e.g., "1" instead of "chr1"). + warn_on_all_errors: If True, warns on all errors, not just the first 1000. + verbose_level: Set verbose level. Default is 1. Must be a non-negative integer. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + + Raises: + FileNotFoundError: If required input files or directories do not exist. + ValueError: If invalid parameter values are provided (e.g., non-positive block_size). + subprocess.CalledProcessError: If the ucsc-bedtobigbed command fails. + """ + # Input validation + if not bed_file.is_file(): + raise FileNotFoundError(f"Input BED file not found: {bed_file}") + if not chrom_sizes_file.is_file(): + raise FileNotFoundError(f"Chromosome sizes file not found: {chrom_sizes_file}") + if auto_sql_file and not auto_sql_file.is_file(): + raise FileNotFoundError(f"AutoSql file not found: {auto_sql_file}") + if udc_directory and not udc_directory.is_dir(): + raise FileNotFoundError(f"UDC directory not found: {udc_directory}") + if block_size <= 0: + raise ValueError("block_size must be a positive integer.") + if items_per_slot <= 0: + raise ValueError("items_per_slot must be a positive integer.") + if verbose_level < 0: + raise ValueError("verbose_level cannot be negative.") + + # Ensure output directory exists + output_bigbed_file.parent.mkdir(parents=True, exist_ok=True) + + command = ["bedToBigBed"] + + # Add optional parameters + if auto_sql_file: + command.append(f"-as={auto_sql_file.resolve()}") + if bed_type: + command.append(f"-type={bed_type}") + if delimiter: + command.append(f"-delim={delimiter}") + + if extra_index_fields: + for field in extra_index_fields: + command.append(f"-extraIndex={field}") + + command.append(f"-blockSize={block_size}") + command.append(f"-itemsPerSlot={items_per_slot}") + + if uncompressed_blocks: + command.append("-unc") + + if udc_directory: + command.append(f"-udcDir={udc_directory.resolve()}") + + if max_memory: + command.append(f"-maxMem={max_memory}") + + if temp_directory: + temp_directory.mkdir(parents=True, exist_ok=True) + command.append(f"-tempDir={temp_directory.resolve()}") + + if allow_1000g_chrom_names: + command.append("-allow1000g") + + if warn_on_all_errors: + command.append("-warnOnAllErrors") + + command.append(f"-verbose={verbose_level}") + + # Add positional arguments + command.extend([ + str(bed_file.resolve()), + str(chrom_sizes_file.resolve()), + str(output_bigbed_file.resolve()) + ]) + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + stdout = process.stdout + stderr = process.stderr + output_files = [str(output_bigbed_file.resolve())] + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + "error": f"bedToBigBed command failed with exit code {e.returncode}", + } + except FileNotFoundError: + return { + "command_executed": " ".join(command), + "stdout": "", + "stderr": "Error: bedToBigBed executable not found. Please ensure it is in your PATH.", + "output_files": [], + "error": "bedToBigBed executable not found.", + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/app/ucsc-bedtobigbed_shim_server.py b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/app/ucsc-bedtobigbed_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..cf0e6d6ebcad0addccd59211a565e9a9094923b7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/app/ucsc-bedtobigbed_shim_server.py @@ -0,0 +1,55 @@ +#!/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-bedtobigbed/app/ucsc-bedtobigbed_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_ucsc_bedtobigbed' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/docker-compose.yml b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..0f62bb91a7bfb557f52f80241dcbf4d06f5bb82b --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-ucsc-bedtobigbed: + build: . + image: mcp-ucsc-bedtobigbed:latest + container_name: mcp-ucsc-bedtobigbed + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=ucsc-bedtobigbed + 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/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/environment.yaml b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d56ac5c51316dd34bc7cf388d0ce1e452431c313 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - ucsc-bedtobigbed + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/requirements.txt b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-bedtobigbed/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_umi_tools/requirements.txt b/Biomni/mcp_generated/mcp_umi_tools/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_umi_tools/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_upimapi/requirements.txt b/Biomni/mcp_generated/mcp_upimapi/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_upimapi/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_vardict-java/Dockerfile b/Biomni/mcp_generated/mcp_vardict-java/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2ec170992dd2cf69a7708eb1bbef9f7fa8650bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_vardict-java/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 vardict-java via conda (e.g., from bioconda) +RUN conda install -c bioconda vardict-java -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/vardict-java_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/vardict-java_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/vardict-java_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_vardict-java/app/requirements.txt b/Biomni/mcp_generated/mcp_vardict-java/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_vardict-java/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_vardict-java/app/vardict-java_server.py b/Biomni/mcp_generated/mcp_vardict-java/app/vardict-java_server.py new file mode 100644 index 0000000000000000000000000000000000000000..299d33fcf43450efd4601c907ebb7e717c3dffd6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_vardict-java/app/vardict-java_server.py @@ -0,0 +1,358 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_vardict_java' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def vardict_java( + reference_fasta: str, + bam_path: str, + sample_name: str, + bed_file: Optional[str] = None, + region: Optional[str] = None, + min_af: float = 0.01, + chrom_col: int = 1, + start_col: int = 2, + end_col: int = 3, + gene_col: int = 4, + min_reads: int = 2, + min_base_qual: float = 22.5, + min_pos: int = 5, + hq_lq_ratio: float = 1.5, + min_map_qual: Optional[float] = None, + mnv_dist: int = 2, + realign: int = 1, + no_sv: bool = False, + all_positions: bool = False, + amplicon_params: str = "10:0.95", + output_file: Optional[str] = None, +) -> Dict[str, Any]: + """ + Run VarDictJava for variant discovery. + + Args: + reference_fasta: Path to the reference genome in FASTA format. + bam_path: Path to BAM file(s). For paired mode, use 'tumor.bam|normal.bam'. + sample_name: Sample name for the output. + bed_file: Path to target regions in BED format. + region: Region to call variants in (e.g., 'chr7:55270300-55270348:EGFR'). + min_af: Minimum allele frequency threshold (default: 0.01). + chrom_col: Column index for chromosome in BED file (default: 1). + start_col: Column index for start position in BED file (default: 2). + end_col: Column index for end position in BED file (default: 3). + gene_col: Column index for gene name in BED file (default: 4). + min_reads: Minimum number of high-quality reads supporting variant (default: 2). + min_base_qual: Minimum mean base quality (phred score) (default: 22.5). + min_pos: Minimum mean position of the variant in reads (default: 5). + hq_lq_ratio: Minimum ratio of high-quality reads to low-quality reads (default: 1.5). + min_map_qual: Minimum mean mapping quality (default: no filtering). + mnv_dist: Combine SNVs into MNV if closer than this distance (default: 2). + realign: Enable realignment of insertions/deletions (1 to enable, 0 to disable). + no_sv: Disable structural variant calling. + all_positions: Output all positions (useful for amplicon mode). + amplicon_params: Criteria for discarding reads far from segments (default: '10:0.95'). + output_file: Path to save the intermediate tabular output. + """ + # Input validation + ref_path = Path(reference_fasta) + if not ref_path.exists(): + return {"error": f"Reference FASTA not found: {reference_fasta}"} + + # Check BAM paths (handle paired pipe syntax) + for b_path in bam_path.split('|'): + if not Path(b_path).exists(): + return {"error": f"BAM file not found: {b_path}"} + + if not bed_file and not region: + return {"error": "Either bed_file or region (-R) must be provided."} + + cmd = [ + "VarDict", + "-G", str(ref_path), + "-f", str(min_af), + "-N", sample_name, + "-b", bam_path, + "-c", str(chrom_col), + "-S", str(start_col), + "-E", str(end_col), + "-g", str(gene_col), + "-r", str(min_reads), + "-q", str(min_base_qual), + "-P", str(min_pos), + "-o", str(hq_lq_ratio), + "-X", str(mnv_dist), + "-k", str(realign), + "-a", amplicon_params + ] + + if region: + cmd.extend(["-R", region]) + if no_sv: + cmd.append("-U") + if all_positions: + cmd.append("-p") + if min_map_qual is not None: + cmd.extend(["-O", str(min_map_qual)]) + + if bed_file: + bed_p = Path(bed_file) + if not bed_p.exists(): + return {"error": f"BED file not found: {bed_file}"} + cmd.append(str(bed_p)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + if output_file: + out_p = Path(output_file) + out_p.write_text(result.stdout) + return { + "command_executed": " ".join(cmd), + "stdout": "Output written to file", + "stderr": result.stderr, + "output_files": [str(out_p)] + } + + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def test_strand_bias( + input_file: str, + output_file: Optional[str] = None, +) -> Dict[str, Any]: + """ + Perform statistical test for strand bias using teststrandbias.R (Single Sample). + + Args: + input_file: Path to the intermediate tabular output from VarDict. + output_file: Path to save the output with strand bias statistics. + """ + in_p = Path(input_file) + if not in_p.exists(): + return {"error": f"Input file not found: {input_file}"} + + # Assuming teststrandbias.R is in the PATH or VarDict directory + cmd = ["teststrandbias.R"] + + try: + with open(in_p, 'r') as f_in: + result = subprocess.run(cmd, stdin=f_in, capture_output=True, text=True, check=True) + + if output_file: + out_p = Path(output_file) + out_p.write_text(result.stdout) + return { + "command_executed": f"cat {input_file} | {' '.join(cmd)}", + "stdout": "Output written to file", + "stderr": result.stderr, + "output_files": [str(out_p)] + } + + return { + "command_executed": f"cat {input_file} | {' '.join(cmd)}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def test_somatic( + input_file: str, + output_file: Optional[str] = None, +) -> Dict[str, Any]: + """ + Perform statistical test for somatic variants using testsomatic.R (Paired Sample). + + Args: + input_file: Path to the intermediate tabular output from VarDict (paired mode). + output_file: Path to save the output with somatic statistics. + """ + in_p = Path(input_file) + if not in_p.exists(): + return {"error": f"Input file not found: {input_file}"} + + cmd = ["testsomatic.R"] + + try: + with open(in_p, 'r') as f_in: + result = subprocess.run(cmd, stdin=f_in, capture_output=True, text=True, check=True) + + if output_file: + out_p = Path(output_file) + out_p.write_text(result.stdout) + return { + "command_executed": f"cat {input_file} | {' '.join(cmd)}", + "stdout": "Output written to file", + "stderr": result.stderr, + "output_files": [str(out_p)] + } + + return { + "command_executed": f"cat {input_file} | {' '.join(cmd)}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def var2vcf_valid( + input_file: str, + sample_name: str, + min_af: float = 0.01, + ext_info: bool = True, + all_variants: bool = False, + output_vcf: Optional[str] = None, +) -> Dict[str, Any]: + """ + Convert VarDict intermediate format to VCF for single sample. + + Args: + input_file: Path to the output from teststrandbias.R. + sample_name: Sample name for the VCF header. + min_af: Minimum allele frequency threshold (default: 0.01). + ext_info: Output extended information (default: True). + all_variants: Output all variants at a position, not just the one with highest AF. + output_vcf: Path to save the final VCF file. + """ + in_p = Path(input_file) + if not in_p.exists(): + return {"error": f"Input file not found: {input_file}"} + + cmd = [ + "var2vcf_valid.pl", + "-N", sample_name, + "-f", str(min_af) + ] + + if ext_info: + cmd.append("-E") + if all_variants: + cmd.append("-A") + + try: + with open(in_p, 'r') as f_in: + result = subprocess.run(cmd, stdin=f_in, capture_output=True, text=True, check=True) + + if output_vcf: + out_p = Path(output_vcf) + out_p.write_text(result.stdout) + return { + "command_executed": f"cat {input_file} | {' '.join(cmd)}", + "stdout": "VCF written to file", + "stderr": result.stderr, + "output_files": [str(out_p)] + } + + return { + "command_executed": f"cat {input_file} | {' '.join(cmd)}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +@mcp.tool() +def var2vcf_paired( + input_file: str, + sample_names: str, + min_af: float = 0.01, + all_variants: bool = False, + min_matches: Optional[int] = None, + somatic_only: bool = False, + output_vcf: Optional[str] = None, +) -> Dict[str, Any]: + """ + Convert VarDict intermediate format to VCF for paired samples. + + Args: + input_file: Path to the output from testsomatic.R. + sample_names: Sample names in 'tumor|normal' format. + min_af: Minimum allele frequency threshold (default: 0.01). + all_variants: Output all variants at a position, not just the one with highest AF. + min_matches: Minimum matches for a variant to be considered (optional). + somatic_only: Only output somatic variants (optional). + output_vcf: Path to save the final VCF file. + """ + in_p = Path(input_file) + if not in_p.exists(): + return {"error": f"Input file not found: {input_file}"} + + cmd = [ + "var2vcf_paired.pl", + "-N", sample_names, + "-f", str(min_af) + ] + + if all_variants: + cmd.append("-A") + if min_matches is not None: + cmd.extend(["-M", str(min_matches)]) + if somatic_only: + cmd.append("-S") + + try: + with open(in_p, 'r') as f_in: + result = subprocess.run(cmd, stdin=f_in, capture_output=True, text=True, check=True) + + if output_vcf: + out_p = Path(output_vcf) + out_p.write_text(result.stdout) + return { + "command_executed": f"cat {input_file} | {' '.join(cmd)}", + "stdout": "VCF written to file", + "stderr": result.stderr, + "output_files": [str(out_p)] + } + + return { + "command_executed": f"cat {input_file} | {' '.join(cmd)}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": str(e) + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_vardict-java/app/vardict-java_shim_server.py b/Biomni/mcp_generated/mcp_vardict-java/app/vardict-java_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f57488428631929e3112c9c5f0e030fb0649b9c9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_vardict-java/app/vardict-java_shim_server.py @@ -0,0 +1,55 @@ +#!/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_vardict-java/app/vardict-java_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_vardict_java' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_vardict-java/docker-compose.yml b/Biomni/mcp_generated/mcp_vardict-java/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..4f5ecc9793000b84ad1ae1f08849fda6abba8cbe --- /dev/null +++ b/Biomni/mcp_generated/mcp_vardict-java/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-vardict-java: + build: . + image: mcp-vardict-java:latest + container_name: mcp-vardict-java + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=vardict-java + 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/Biomni/mcp_generated/mcp_vardict-java/environment.yaml b/Biomni/mcp_generated/mcp_vardict-java/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8fa188efcb53123883dbdc10cd466dadd0e67255 --- /dev/null +++ b/Biomni/mcp_generated/mcp_vardict-java/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - vardict-java + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_vardict-java/requirements.txt b/Biomni/mcp_generated/mcp_vardict-java/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_vardict-java/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_varscan/requirements.txt b/Biomni/mcp_generated/mcp_varscan/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_varscan/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_vcflib/requirements.txt b/Biomni/mcp_generated/mcp_vcflib/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_vcflib/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_wasp2/Dockerfile b/Biomni/mcp_generated/mcp_wasp2/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e064fa62a303e42f97e65ad3b5a821083b7ec379 --- /dev/null +++ b/Biomni/mcp_generated/mcp_wasp2/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 wasp2 via conda (e.g., from bioconda) +RUN conda install -c bioconda wasp2 -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/wasp2_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/wasp2_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/wasp2_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_wasp2/app/requirements.txt b/Biomni/mcp_generated/mcp_wasp2/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_wasp2/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_wasp2/app/wasp2_server.py b/Biomni/mcp_generated/mcp_wasp2/app/wasp2_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a0436135af4d80033b62a88f9a68a15c5f276c9f --- /dev/null +++ b/Biomni/mcp_generated/mcp_wasp2/app/wasp2_server.py @@ -0,0 +1,412 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# Mock decorator for stand-alone execution +class mcp: + def tool(func): + return func + +# Tool: wasp2 +# summary: Allele-specific analysis of next-generation sequencing data with Rust acceleration + +@mcp.tool +def map_make_reads( + input_bam: Path, + variants_vcf: Path, + output_dir: Path, + samples: Optional[List[str]] = None, + is_paired_end: bool = True, + is_sorted: bool = True, + threads: int = 1, +) -> dict: + """ + Generates FASTQ files with swapped alleles for remapping to correct mapping bias. + + This corresponds to the 'wasp2-map make-reads' command. It identifies reads + overlapping heterozygous SNPs and creates new FASTQ files where the alternate + allele is swapped into the read sequence. These FASTQs are then realigned + to check for mapping bias. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if not variants_vcf.is_file(): + raise FileNotFoundError(f"Input VCF file not found: {variants_vcf}") + if threads < 1: + raise ValueError("threads must be a positive integer.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [ + "wasp2-map", + "make-reads", + str(input_bam), + str(variants_vcf), + "-o", + str(output_dir), + "--threads", + str(threads), + ] + + if samples: + cmd.extend(["-s", ",".join(samples)]) + if not is_paired_end: + cmd.append("--no-paired-end") + if not is_sorted: + cmd.append("--no-sorted") + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + # Output files are generated inside the output_dir, including a key JSON file + output_files = list(output_dir.glob("*")) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": { + "remap_directory": str(output_dir), + "generated_files": [str(f) for f in output_files] + } + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": "WASP2 map make-reads failed", + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + +@mcp.tool +def map_filter_remapped( + remapped_bam: Path, + json_file: Path, + output_bam: Path, +) -> dict: + """ + Filters a remapped BAM file to remove reads with mapping bias. + + This corresponds to the 'wasp2-map filter-remapped' command. It uses the + JSON file from 'make-reads' and the realigned BAM to identify and discard + reads that map inconsistently, thus correcting for reference bias. + """ + if not remapped_bam.is_file(): + raise FileNotFoundError(f"Remapped BAM file not found: {remapped_bam}") + if not json_file.is_file(): + raise FileNotFoundError(f"WASP data JSON file not found: {json_file}") + + output_bam.parent.mkdir(parents=True, exist_ok=True) + + cmd = [ + "wasp2-map", + "filter-remapped", + str(remapped_bam), + "-j", + str(json_file), + "-o", + str(output_bam), + ] + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"filtered_bam": str(output_bam)} + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": "WASP2 map filter-remapped failed", + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + +@mcp.tool +def count_variants_bulk( + input_bam: Path, + variants_vcf: Path, + output_file: Path, + samples: Optional[List[str]] = None, + min_mapq: int = 20, + min_baseq: int = 10, + threads: int = 1, +) -> dict: + """ + Counts allele-specific reads from a BAM file for bulk sequencing data. + + This corresponds to the 'wasp2-count count-variants' command. It tallies + reference and alternate allele counts at heterozygous SNP locations for + downstream allelic imbalance analysis. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if not variants_vcf.is_file(): + raise FileNotFoundError(f"Input VCF file not found: {variants_vcf}") + if min_mapq < 0: + raise ValueError("min_mapq must be non-negative.") + if min_baseq < 0: + raise ValueError("min_baseq must be non-negative.") + if threads < 1: + raise ValueError("threads must be a positive integer.") + + output_file.parent.mkdir(parents=True, exist_ok=True) + + cmd = [ + "wasp2-count", + "count-variants", + str(input_bam), + str(variants_vcf), + "-o", + str(output_file), + "--min-mapq", + str(min_mapq), + "--min-baseq", + str(min_baseq), + "--threads", + str(threads), + ] + + if samples: + cmd.extend(["-s", ",".join(samples)]) + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"counts_tsv": str(output_file)} + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": "WASP2 count variants (bulk) failed", + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + +@mcp.tool +def count_variants_sc( + input_bam: Path, + variants_vcf: Path, + barcodes_tsv: Path, + out_file: Path, + samples: List[str], + feature_gtf: Optional[Path] = None, + barcode_tag: str = "CB", + umi_tag: str = "UB", + min_mapq: int = 20, + min_baseq: int = 10, + threads: int = 1, +) -> dict: + """ + Counts allele-specific reads per cell from a single-cell BAM file. + + This corresponds to the 'wasp2-count count-variants-sc' command. It generates + an AnnData (h5ad) object containing allele counts per cell per variant, + suitable for single-cell allelic imbalance analysis. + """ + if not input_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {input_bam}") + if not variants_vcf.is_file(): + raise FileNotFoundError(f"Input VCF file not found: {variants_vcf}") + if not barcodes_tsv.is_file(): + raise FileNotFoundError(f"Barcodes TSV file not found: {barcodes_tsv}") + if feature_gtf and not feature_gtf.is_file(): + raise FileNotFoundError(f"Feature GTF file not found: {feature_gtf}") + if not samples: + raise ValueError("At least one sample must be provided.") + if min_mapq < 0: + raise ValueError("min_mapq must be non-negative.") + if min_baseq < 0: + raise ValueError("min_baseq must be non-negative.") + if threads < 1: + raise ValueError("threads must be a positive integer.") + + out_file.parent.mkdir(parents=True, exist_ok=True) + + cmd = [ + "wasp2-count", + "count-variants-sc", + str(input_bam), + str(variants_vcf), + str(barcodes_tsv), + "--out_file", + str(out_file), + "--samples", + ",".join(samples), + "--barcode-tag", + barcode_tag, + "--umi-tag", + umi_tag, + "--min-mapq", + str(min_mapq), + "--min-baseq", + str(min_baseq), + "--threads", + str(threads), + ] + + if feature_gtf: + cmd.extend(["--feature", str(feature_gtf)]) + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"allele_counts_h5ad": str(out_file)} + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": "WASP2 count variants (single-cell) failed", + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + +@mcp.tool +def analyze_find_imbalance_bulk( + counts_tsv: Path, + output_file: Path, + min_reads: int = 20, + fdr_method: str = "bh", +) -> dict: + """ + Performs statistical tests for allelic imbalance on bulk sequencing count data. + + This corresponds to the 'wasp2-analyze find-imbalance' command. It uses a + beta-binomial likelihood-ratio test to identify significant allelic imbalance + from the counts generated by 'count-variants'. + """ + if not counts_tsv.is_file(): + raise FileNotFoundError(f"Counts TSV file not found: {counts_tsv}") + if min_reads < 1: + raise ValueError("min_reads must be a positive integer.") + + output_file.parent.mkdir(parents=True, exist_ok=True) + + cmd = [ + "wasp2-analyze", + "find-imbalance", + str(counts_tsv), + "-o", + str(output_file), + "--min-reads", + str(min_reads), + "--fdr-method", + fdr_method, + ] + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"imbalance_results_tsv": str(output_file)} + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": "WASP2 find imbalance (bulk) failed", + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + +@mcp.tool +def analyze_find_imbalance_sc( + allele_counts_h5ad: Path, + barcode_groups_tsv: Path, + output_file: Path, + sample: str, + min_reads: int = 20, + fdr_method: str = "bh", +) -> dict: + """ + Performs statistical tests for allelic imbalance on single-cell count data. + + This corresponds to the 'wasp2-analyze find-imbalance-sc' command. It aggregates + counts by cell groups (e.g., cell types) and performs tests for allelic + imbalance within each group. + """ + if not allele_counts_h5ad.is_file(): + raise FileNotFoundError(f"Allele counts H5AD file not found: {allele_counts_h5ad}") + if not barcode_groups_tsv.is_file(): + raise FileNotFoundError(f"Barcode groups TSV file not found: {barcode_groups_tsv}") + if not sample: + raise ValueError("A sample name must be provided.") + if min_reads < 1: + raise ValueError("min_reads must be a positive integer.") + + output_file.parent.mkdir(parents=True, exist_ok=True) + + cmd = [ + "wasp2-analyze", + "find-imbalance-sc", + str(allele_counts_h5ad), + str(barcode_groups_tsv), + "-o", + str(output_file), + "--sample", + sample, + "--min-reads", + str(min_reads), + "--fdr-method", + fdr_method, + ] + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": {"imbalance_results_tsv": str(output_file)} + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": "WASP2 find imbalance (single-cell) failed", + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_wasp2/app/wasp2_shim_server.py b/Biomni/mcp_generated/mcp_wasp2/app/wasp2_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f6ec1b0754d6f8f94cc1e81274489c53ae9175c2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_wasp2/app/wasp2_shim_server.py @@ -0,0 +1,55 @@ +#!/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_wasp2/app/wasp2_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_wasp2' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_wasp2/docker-compose.yml b/Biomni/mcp_generated/mcp_wasp2/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d2833564a9d2fb972e51832f202809305ad72acc --- /dev/null +++ b/Biomni/mcp_generated/mcp_wasp2/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-wasp2: + build: . + image: mcp-wasp2:latest + container_name: mcp-wasp2 + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=wasp2 + 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/Biomni/mcp_generated/mcp_wasp2/environment.yaml b/Biomni/mcp_generated/mcp_wasp2/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..034e7c3a3ee93c5df21ccdecd3f1a6c99883ddc3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_wasp2/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - wasp2 + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_wasp2/requirements.txt b/Biomni/mcp_generated/mcp_wasp2/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_wasp2/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_womtool/Dockerfile b/Biomni/mcp_generated/mcp_womtool/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f8dcad7b877ae7fd7d20ef8f91c6ae35a2e2037b --- /dev/null +++ b/Biomni/mcp_generated/mcp_womtool/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 womtool via conda (e.g., from bioconda) +RUN conda install -c bioconda womtool -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/womtool_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/womtool_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/womtool_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_womtool/app/requirements.txt b/Biomni/mcp_generated/mcp_womtool/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_womtool/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_womtool/app/womtool_server.py b/Biomni/mcp_generated/mcp_womtool/app/womtool_server.py new file mode 100644 index 0000000000000000000000000000000000000000..98c71bab436b04159c38dab9cd5be21ca692b1fb --- /dev/null +++ b/Biomni/mcp_generated/mcp_womtool/app/womtool_server.py @@ -0,0 +1,359 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Literal, Dict, Any + +# Helper function to run the Java command for womtool +def _run_womtool_command( + womtool_jar_path: Path, + action: str, + params: List[str], + java_options: Optional[List[str]] = None, + capture_output_to_file: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Internal helper to execute womtool commands. + + Args: + womtool_jar_path: Path to the womtool.jar executable. + action: The womtool subcommand to execute (e.g., "validate", "inputs"). + params: A list of string arguments specific to the womtool subcommand. + java_options: Optional list of additional Java options (e.g., JVM memory settings). + capture_output_to_file: Optional Path to a file where stdout should be redirected. + If None, stdout is captured as a string. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + """ + if not womtool_jar_path.is_file(): + raise FileNotFoundError(f"womtool JAR file not found at: {womtool_jar_path}") + + command = ["java"] + if java_options: + command.extend(java_options) + command.extend(["-jar", str(womtool_jar_path), action]) + command.extend(params) + + stdout_content = "" + stderr_content = "" + output_files = [] + + try: + if capture_output_to_file: + # Ensure the directory for the output file exists + capture_output_to_file.parent.mkdir(parents=True, exist_ok=True) + with open(capture_output_to_file, "w") as f: + process = subprocess.run( + command, + stdout=f, + stderr=subprocess.PIPE, + check=False, + text=True, + encoding="utf-8" + ) + stderr_content = process.stderr + output_files.append(str(capture_output_to_file)) + else: + process = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + text=True, + encoding="utf-8" + ) + stdout_content = process.stdout + stderr_content = process.stderr + + if process.returncode != 0: + return { + "command_executed": " ".join(command), + "stdout": stdout_content, + "stderr": stderr_content, + "error": f"womtool command failed with exit code {process.returncode}", + "output_files": output_files, + } + + return { + "command_executed": " ".join(command), + "stdout": stdout_content, + "stderr": stderr_content, + "output_files": output_files, + } + + except FileNotFoundError: + return { + "command_executed": " ".join(command), + "stdout": "", + "stderr": "Error: 'java' command not found. Please ensure Java is installed and in your PATH.", + "error": "Java executable not found.", + "output_files": [], + } + except Exception as e: + return { + "command_executed": " ".join(command), + "stdout": stdout_content, + "stderr": stderr_content, + "error": f"An unexpected error occurred: {e}", + "output_files": output_files, + } + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_womtool' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def womtool_validate( + wdl_file: Path, + womtool_jar_path: Path, + list_dependencies: bool = False, + java_options: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Performs full validation of a WDL file including syntax and semantic checking. + Optionally lists files referenced in import statements. + + Args: + wdl_file: Path to the WDL file to validate. + womtool_jar_path: Path to the womtool.jar executable. + list_dependencies: If True, lists files referenced in import statements + for successful validations. + java_options: Optional list of additional Java options (e.g., JVM memory settings). + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + """ + if not wdl_file.is_file(): + raise FileNotFoundError(f"WDL file not found at: {wdl_file}") + + params = [] + if list_dependencies: + params.append("--list-dependencies") + params.append(str(wdl_file)) + + return _run_womtool_command( + womtool_jar_path=womtool_jar_path, + action="validate", + params=params, + java_options=java_options, + ) + + +@mcp.tool() +def womtool_inputs( + wdl_file: Path, + womtool_jar_path: Path, + java_options: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Examines a WDL file with one workflow, computes all the inputs needed for that + workflow, and outputs a JSON template to stdout. + + Args: + wdl_file: Path to the WDL file. + womtool_jar_path: Path to the womtool.jar executable. + java_options: Optional list of additional Java options (e.g., JVM memory settings). + + Returns: + A dictionary containing the command executed, stdout (JSON template), stderr, + and any output files. + """ + if not wdl_file.is_file(): + raise FileNotFoundError(f"WDL file not found at: {wdl_file}") + + params = [str(wdl_file)] + + return _run_womtool_command( + womtool_jar_path=womtool_jar_path, + action="inputs", + params=params, + java_options=java_options, + ) + + +@mcp.tool() +def womtool_highlight( + wdl_file: Path, + output_type: Literal["html", "console"], + womtool_jar_path: Path, + java_options: Optional[List[str]] = None, + output_file: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Reformats and colorizes/tags a WDL file. + Output can be HTML (with tags) or colorized console text. + + Args: + wdl_file: Path to the WDL file to highlight. + output_type: The desired output format ("html" or "console"). + womtool_jar_path: Path to the womtool.jar executable. + java_options: Optional list of additional Java options (e.g., JVM memory settings). + output_file: Optional path to write the output. If 'html' output_type is chosen + and this is not provided, a temporary HTML file will be created. + If 'console' output_type is chosen, output is always to stdout + unless this is provided. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + """ + if not wdl_file.is_file(): + raise FileNotFoundError(f"WDL file not found at: {wdl_file}") + + params = [str(wdl_file), output_type] + + capture_to_file = None + if output_type == "html": + if output_file: + capture_to_file = output_file + else: + # Create a temporary file for HTML output if not specified + temp_output = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html") + temp_output.close() + capture_to_file = Path(temp_output.name) + elif output_file: + # If console output is requested but output_file is provided, redirect stdout + capture_to_file = output_file + + return _run_womtool_command( + womtool_jar_path=womtool_jar_path, + action="highlight", + params=params, + java_options=java_options, + capture_output_to_file=capture_to_file, + ) + + +@mcp.tool() +def womtool_parse( + wdl_file: Path, + womtool_jar_path: Path, + java_options: Optional[List[str]] = None, +) -> Dict[str, Any]: + """ + Compares a WDL file against the grammar and writes out an abstract syntax tree + if it is valid, and a syntax error otherwise. Note that higher-level AST checks + are not done via this sub-command; 'validate' should be used for full validation. + + Args: + wdl_file: Path to the WDL file to parse. + womtool_jar_path: Path to the womtool.jar executable. + java_options: Optional list of additional Java options (e.g., JVM memory settings). + + Returns: + A dictionary containing the command executed, stdout (AST), stderr, + and any output files. + """ + if not wdl_file.is_file(): + raise FileNotFoundError(f"WDL file not found at: {wdl_file}") + + params = [str(wdl_file)] + + return _run_womtool_command( + womtool_jar_path=womtool_jar_path, + action="parse", + params=params, + java_options=java_options, + ) + + +@mcp.tool() +def womtool_graph( + wdl_file: Path, + womtool_jar_path: Path, + all_nodes: bool = False, + java_options: Optional[List[str]] = None, + output_dot_file: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Generates the data-flow graph of a WDL file in .dot format. + Note: graph currently DOES NOT WORK on version 1.0 workflows. + + Args: + wdl_file: Path to the WDL file. + womtool_jar_path: Path to the womtool.jar executable. + all_nodes: If True, all WDL graph nodes (including task declarations and call outputs) + become nodes in the generated DAG. + java_options: Optional list of additional Java options (e.g., JVM memory settings). + output_dot_file: Optional path to write the .dot graph output. If not provided, + a temporary .dot file will be created. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the path + to the generated .dot file. + """ + if not wdl_file.is_file(): + raise FileNotFoundError(f"WDL file not found at: {wdl_file}") + + params = [] + if all_nodes: + params.append("--all") + params.append(str(wdl_file)) + + capture_to_file = output_dot_file + if not capture_to_file: + temp_output = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".dot") + temp_output.close() + capture_to_file = Path(temp_output.name) + + return _run_womtool_command( + womtool_jar_path=womtool_jar_path, + action="graph", + params=params, + java_options=java_options, + capture_output_to_file=capture_to_file, + ) + + +@mcp.tool() +def womtool_womgraph( + wdl_file: Path, + womtool_jar_path: Path, + ancillary_files: Optional[List[Path]] = None, + java_options: Optional[List[str]] = None, + output_dot_file: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Reads a WDL file, converts it to a Workflow Object Model (WOM) representation, + and prints out a graph of the WOM produced in .dot format. + + Args: + wdl_file: Path to the main WDL file. + womtool_jar_path: Path to the womtool.jar executable. + ancillary_files: Optional list of paths to imported WDL files. + java_options: Optional list of additional Java options (e.g., JVM memory settings). + output_dot_file: Optional path to write the .dot graph output. If not provided, + a temporary .dot file will be created. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the path + to the generated .dot file. + """ + if not wdl_file.is_file(): + raise FileNotFoundError(f"WDL file not found at: {wdl_file}") + if ancillary_files: + for f in ancillary_files: + if not f.is_file(): + raise FileNotFoundError(f"Ancillary file not found at: {f}") + + params = [str(wdl_file)] + if ancillary_files: + params.extend([str(f) for f in ancillary_files]) + + capture_to_file = output_dot_file + if not capture_to_file: + temp_output = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".dot") + temp_output.close() + capture_to_file = Path(temp_output.name) + + return _run_womtool_command( + womtool_jar_path=womtool_jar_path, + action="womgraph", + params=params, + java_options=java_options, + capture_output_to_file=capture_to_file, + ) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_womtool/app/womtool_shim_server.py b/Biomni/mcp_generated/mcp_womtool/app/womtool_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..728bb6d60ad998bf0bcafaca3bf5939201d4aa04 --- /dev/null +++ b/Biomni/mcp_generated/mcp_womtool/app/womtool_shim_server.py @@ -0,0 +1,55 @@ +#!/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_womtool/app/womtool_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_womtool' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_womtool/docker-compose.yml b/Biomni/mcp_generated/mcp_womtool/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..33da8392afaa2cd335bdacb040d698d59c495ff4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_womtool/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-womtool: + build: . + image: mcp-womtool:latest + container_name: mcp-womtool + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=womtool + 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/Biomni/mcp_generated/mcp_womtool/environment.yaml b/Biomni/mcp_generated/mcp_womtool/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..345d4dfce4c34e2a40740fd1e0ba7a0709695637 --- /dev/null +++ b/Biomni/mcp_generated/mcp_womtool/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - womtool + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_womtool/requirements.txt b/Biomni/mcp_generated/mcp_womtool/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_womtool/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_yacrd/Dockerfile b/Biomni/mcp_generated/mcp_yacrd/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4f9549fc105d7a52e56c12ef90556549a065d976 --- /dev/null +++ b/Biomni/mcp_generated/mcp_yacrd/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 yacrd via conda (e.g., from bioconda) +RUN conda install -c bioconda yacrd -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/yacrd_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/yacrd_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/yacrd_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_yacrd/app/requirements.txt b/Biomni/mcp_generated/mcp_yacrd/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_yacrd/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_yacrd/app/yacrd_server.py b/Biomni/mcp_generated/mcp_yacrd/app/yacrd_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d7fc8869fbc3561c39841c33829a97dbaae55bda --- /dev/null +++ b/Biomni/mcp_generated/mcp_yacrd/app/yacrd_server.py @@ -0,0 +1,468 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Dict, Any +import tempfile +import shutil + +# Placeholder for the ondisk prefix that will be replaced with a safe temporary path +ONDISK_PREFIX_PLACEHOLDER = "__MCP_ONDISK_PREFIX_PLACEHOLDER__" + +def _run_yacrd_command( + command_args: List[str], + output_files: List[Path], + user_ondisk_prefix: Optional[str] = None +) -> Dict[str, Any]: + """ + Helper function to run yacrd commands and handle output. + + Args: + command_args: A list of command-line arguments for yacrd. + output_files: A list of expected output file paths to check for existence. + user_ondisk_prefix: The user-provided prefix for on-disk temporary files. + If provided, a safe temporary directory will be used. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + temp_dir_obj = None + final_command_args = list(command_args) # Work with a mutable copy + + if user_ondisk_prefix: + temp_dir_obj = tempfile.TemporaryDirectory() + temp_dir_path = Path(temp_dir_obj.name) + # yacrd will create files like temp_dir_path/user_ondisk_prefix.0 + # If user_ondisk_prefix contains path separators, yacrd might delete the parent. + # By using temp_dir_path as the parent, we ensure only our temporary directory is affected. + actual_ondisk_path_for_yacrd = str(temp_dir_path / user_ondisk_prefix) + + # Find and replace the placeholder for -d/--ondisk + try: + ondisk_idx = final_command_args.index(ONDISK_PREFIX_PLACEHOLDER) + final_command_args[ondisk_idx] = actual_ondisk_path_for_yacrd + except ValueError: + # This should not happen if _build_base_options is correctly used with ondisk_prefix + pass + + cmd = ["yacrd"] + final_command_args + command_executed = " ".join(map(str, cmd)) + + stdout = "" + stderr = "" + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + ) + stdout = result.stdout + stderr = result.stderr + except subprocess.CalledProcessError as e: + error_output = { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"yacrd command failed with exit code {e.returncode}", + "output_files": [] + } + return error_output + except FileNotFoundError: + error_output = { + "command_executed": command_executed, + "stdout": "", + "stderr": "yacrd executable not found. Please ensure it is installed and in your PATH.", + "error": "yacrd executable not found", + "output_files": [] + } + return error_output + finally: + if temp_dir_obj: + temp_dir_obj.cleanup() # Ensure cleanup after success or any error + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": [str(f) for f in output_files if f.exists()] + } + +def _build_base_options( + coverage_threshold: int, + ondisk_prefix: Optional[str], + not_coverage_ratio: float, + ondisk_buffer_size: int, + read_buffer_size: int, + threads: int, +) -> List[str]: + """ + Builds the common base options for yacrd. + + Args: + coverage_threshold: If coverage reaches this value, the region is marked as bad. + ondisk_prefix: The user-provided prefix for on-disk temporary files. + not_coverage_ratio: If the ratio of bad region length on total length is lower than + this value, the read is marked as NotCovered. + ondisk_buffer_size: Buffer size for on-disk mode. + read_buffer_size: Buffer size for reading PAF file. + threads: Number of threads use by yacrd. + + Returns: + A list of command-line arguments for the common yacrd options. + """ + options = [] + + if coverage_threshold < 0: + raise ValueError("Coverage threshold must be non-negative.") + options.extend(["-c", str(coverage_threshold)]) + + if ondisk_prefix is not None: + options.extend(["-d", ONDISK_PREFIX_PLACEHOLDER]) # Add placeholder for later replacement + + if not 0.0 <= not_coverage_ratio <= 1.0: + raise ValueError("Not-coverage ratio must be between 0.0 and 1.0.") + options.extend(["-n", str(not_coverage_ratio)]) + + if ondisk_buffer_size <= 0: + raise ValueError("Ondisk buffer size must be positive.") + options.extend(["--ondisk-buffer-size", str(ondisk_buffer_size)]) + + if read_buffer_size <= 0: + raise ValueError("Read buffer size must be positive.") + options.extend(["--read-buffer-size", str(read_buffer_size)]) + + if threads < 0: + raise ValueError("Number of threads must be non-negative.") + options.extend(["-t", str(threads)]) + + return options + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_yacrd' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def yacrd_report( + input_overlap_file: Path, + output_report_file: Path, + coverage_threshold: int = 0, + ondisk_prefix: Optional[str] = None, + not_coverage_ratio: float = 0.8, + ondisk_buffer_size: int = 64000000, + read_buffer_size: int = 8192, + threads: int = 1, +) -> Dict[str, Any]: + """ + Detects chimeric and not-covered regions in long reads based on overlap mapping results + and generates a yacrd report file. + + Args: + input_overlap_file: Path to input overlap file (.paf|.m4|.mhap) or yacrd report (.yacrd). + Format is autodetected and compression is allowed (gz|bzip2|lzma). + output_report_file: Path to the output yacrd report file. + coverage_threshold: If coverage reaches this value, the region is marked as bad. + Default is 0. + ondisk_prefix: yacrd switches to 'ondisk' mode which will reduce memory usage but + increase computation time. The value passed as a parameter is used + as a prefix for the temporary files created by yacrd. If None, + on-disk mode is not used. + not_coverage_ratio: If the ratio of bad region length on total length is lower than + this value, the read is marked as NotCovered. Default is 0.8. + ondisk_buffer_size: With the default value, yacrd in 'ondisk' mode uses around 1 GBytes. + You can increase to reduce runtime but increase memory usage. + Default is 64000000. + read_buffer_size: Control the size of the buffer used to read paf file. Default is 8192. + threads: Number of threads use by yacrd. 0 means all threads available. Default is 1. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not input_overlap_file.is_file(): + raise FileNotFoundError(f"Input overlap file not found: {input_overlap_file}") + if not output_report_file.parent.is_dir(): + output_report_file.parent.mkdir(parents=True, exist_ok=True) + + base_options = _build_base_options( + coverage_threshold, ondisk_prefix, not_coverage_ratio, + ondisk_buffer_size, read_buffer_size, threads + ) + + command_args = [ + "-i", str(input_overlap_file), + "-o", str(output_report_file), + ] + base_options + + result = _run_yacrd_command(command_args, [output_report_file], user_ondisk_prefix=ondisk_prefix) + return result + +@mcp.tool() +def yacrd_filter( + input_overlap_file: Path, + output_report_file: Path, + input_sequence_file: Path, + output_sequence_file: Path, + coverage_threshold: int = 0, + ondisk_prefix: Optional[str] = None, + not_coverage_ratio: float = 0.8, + ondisk_buffer_size: int = 64000000, + read_buffer_size: int = 8192, + threads: int = 1, +) -> Dict[str, Any]: + """ + Filters a sequence file, removing records marked as Chimeric or NotCovered + based on an internally generated yacrd report. + + Args: + input_overlap_file: Path to input overlap file (.paf|.m4|.mhap). + Used to generate the internal yacrd report. + output_report_file: Path to the intermediate yacrd report file that will be generated. + This file is used internally by the filter subcommand. + input_sequence_file: Path to the input sequence file (.fasta|.fastq). + output_sequence_file: Path to the output filtered sequence file. + coverage_threshold: If coverage reaches this value, the region is marked as bad. + Default is 0. + ondisk_prefix: yacrd switches to 'ondisk' mode which will reduce memory usage but + increase computation time. The value passed as a parameter is used + as a prefix for the temporary files created by yacrd. If None, + on-disk mode is not used. + not_coverage_ratio: If the ratio of bad region length on total length is lower than + this value, the read is marked as NotCovered. Default is 0.8. + ondisk_buffer_size: With the default value, yacrd in 'ondisk' mode uses around 1 GBytes. + You can increase to reduce runtime but increase memory usage. + Default is 64000000. + read_buffer_size: Control the size of the buffer used to read paf file. Default is 8192. + threads: Number of threads use by yacrd. 0 means all threads available. Default is 1. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not input_overlap_file.is_file(): + raise FileNotFoundError(f"Input overlap file not found: {input_overlap_file}") + if not input_sequence_file.is_file(): + raise FileNotFoundError(f"Input sequence file not found: {input_sequence_file}") + if not output_report_file.parent.is_dir(): + output_report_file.parent.mkdir(parents=True, exist_ok=True) + if not output_sequence_file.parent.is_dir(): + output_sequence_file.parent.mkdir(parents=True, exist_ok=True) + + base_options = _build_base_options( + coverage_threshold, ondisk_prefix, not_coverage_ratio, + ondisk_buffer_size, read_buffer_size, threads + ) + + command_args = [ + "-i", str(input_overlap_file), + "-o", str(output_report_file), + "filter", + "-i", str(input_sequence_file), + "-o", str(output_sequence_file), + ] + base_options + + result = _run_yacrd_command(command_args, [output_report_file, output_sequence_file], user_ondisk_prefix=ondisk_prefix) + return result + +@mcp.tool() +def yacrd_extract( + input_overlap_file: Path, + output_report_file: Path, + input_sequence_file: Path, + output_sequence_file: Path, + coverage_threshold: int = 0, + ondisk_prefix: Optional[str] = None, + not_coverage_ratio: float = 0.8, + ondisk_buffer_size: int = 64000000, + read_buffer_size: int = 8192, + threads: int = 1, +) -> Dict[str, Any]: + """ + Extracts records marked as Chimeric or NotCovered from a sequence file + based on an internally generated yacrd report. + + Args: + input_overlap_file: Path to input overlap file (.paf|.m4|.mhap). + Used to generate the internal yacrd report. + output_report_file: Path to the intermediate yacrd report file that will be generated. + This file is used internally by the extract subcommand. + input_sequence_file: Path to the input sequence file (.fasta|.fastq). + output_sequence_file: Path to the output extracted sequence file. + coverage_threshold: If coverage reaches this value, the region is marked as bad. + Default is 0. + ondisk_prefix: yacrd switches to 'ondisk' mode which will reduce memory usage but + increase computation time. The value passed as a parameter is used + as a prefix for the temporary files created by yacrd. If None, + on-disk mode is not used. + not_coverage_ratio: If the ratio of bad region length on total length is lower than + this value, the read is marked as NotCovered. Default is 0.8. + ondisk_buffer_size: With the default value, yacrd in 'ondisk' mode uses around 1 GBytes. + You can increase to reduce runtime but increase memory usage. + Default is 64000000. + read_buffer_size: Control the size of the buffer used to read paf file. Default is 8192. + threads: Number of threads use by yacrd. 0 means all threads available. Default is 1. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not input_overlap_file.is_file(): + raise FileNotFoundError(f"Input overlap file not found: {input_overlap_file}") + if not input_sequence_file.is_file(): + raise FileNotFoundError(f"Input sequence file not found: {input_sequence_file}") + if not output_report_file.parent.is_dir(): + output_report_file.parent.mkdir(parents=True, exist_ok=True) + if not output_sequence_file.parent.is_dir(): + output_sequence_file.parent.mkdir(parents=True, exist_ok=True) + + base_options = _build_base_options( + coverage_threshold, ondisk_prefix, not_coverage_ratio, + ondisk_buffer_size, read_buffer_size, threads + ) + + command_args = [ + "-i", str(input_overlap_file), + "-o", str(output_report_file), + "extract", + "-i", str(input_sequence_file), + "-o", str(output_sequence_file), + ] + base_options + + result = _run_yacrd_command(command_args, [output_report_file, output_sequence_file], user_ondisk_prefix=ondisk_prefix) + return result + +@mcp.tool() +def yacrd_split( + input_overlap_file: Path, + output_report_file: Path, + input_sequence_file: Path, + output_sequence_file: Path, + coverage_threshold: int = 0, + ondisk_prefix: Optional[str] = None, + not_coverage_ratio: float = 0.8, + ondisk_buffer_size: int = 64000000, + read_buffer_size: int = 8192, + threads: int = 1, +) -> Dict[str, Any]: + """ + Splits a sequence file, removing bad regions in the middle of reads and + removing NotCovered reads, based on an internally generated yacrd report. + + Args: + input_overlap_file: Path to input overlap file (.paf|.m4|.mhap). + Used to generate the internal yacrd report. + output_report_file: Path to the intermediate yacrd report file that will be generated. + This file is used internally by the split subcommand. + input_sequence_file: Path to the input sequence file (.fasta|.fastq). + output_sequence_file: Path to the output split sequence file. + coverage_threshold: If coverage reaches this value, the region is marked as bad. + Default is 0. + ondisk_prefix: yacrd switches to 'ondisk' mode which will reduce memory usage but + increase computation time. The value passed as a parameter is used + as a prefix for the temporary files created by yacrd. If None, + on-disk mode is not used. + not_coverage_ratio: If the ratio of bad region length on total length is lower than + this value, the read is marked as NotCovered. Default is 0.8. + ondisk_buffer_size: With the default value, yacrd in 'ondisk' mode uses around 1 GBytes. + You can increase to reduce runtime but increase memory usage. + Default is 64000000. + read_buffer_size: Control the size of the buffer used to read paf file. Default is 8192. + threads: Number of threads use by yacrd. 0 means all threads available. Default is 1. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not input_overlap_file.is_file(): + raise FileNotFoundError(f"Input overlap file not found: {input_overlap_file}") + if not input_sequence_file.is_file(): + raise FileNotFoundError(f"Input sequence file not found: {input_sequence_file}") + if not output_report_file.parent.is_dir(): + output_report_file.parent.mkdir(parents=True, exist_ok=True) + if not output_sequence_file.parent.is_dir(): + output_sequence_file.parent.mkdir(parents=True, exist_ok=True) + + base_options = _build_base_options( + coverage_threshold, ondisk_prefix, not_coverage_ratio, + ondisk_buffer_size, read_buffer_size, threads + ) + + command_args = [ + "-i", str(input_overlap_file), + "-o", str(output_report_file), + "split", + "-i", str(input_sequence_file), + "-o", str(output_sequence_file), + ] + base_options + + result = _run_yacrd_command(command_args, [output_report_file, output_sequence_file], user_ondisk_prefix=ondisk_prefix) + return result + +@mcp.tool() +def yacrd_scrubb( + input_overlap_file: Path, + output_report_file: Path, + input_sequence_file: Path, + output_sequence_file: Path, + coverage_threshold: int = 0, + ondisk_prefix: Optional[str] = None, + not_coverage_ratio: float = 0.8, + ondisk_buffer_size: int = 64000000, + read_buffer_size: int = 8192, + threads: int = 1, +) -> Dict[str, Any]: + """ + Removes all bad regions from reads in a sequence file and removes NotCovered reads, + based on an internally generated yacrd report. + + Args: + input_overlap_file: Path to input overlap file (.paf|.m4|.mhap). + Used to generate the internal yacrd report. + output_report_file: Path to the intermediate yacrd report file that will be generated. + This file is used internally by the scrubb subcommand. + input_sequence_file: Path to the input sequence file (.fasta|.fastq). + output_sequence_file: Path to the output scrubbed sequence file. + coverage_threshold: If coverage reaches this value, the region is marked as bad. + Default is 0. + ondisk_prefix: yacrd switches to 'ondisk' mode which will reduce memory usage but + increase computation time. The value passed as a parameter is used + as a prefix for the temporary files created by yacrd. If None, + on-disk mode is not used. + not_coverage_ratio: If the ratio of bad region length on total length is lower than + this value, the read is marked as NotCovered. Default is 0.8. + ondisk_buffer_size: With the default value, yacrd in 'ondisk' mode uses around 1 GBytes. + You can increase to reduce runtime but increase memory usage. + Default is 64000000. + read_buffer_size: Control the size of the buffer used to read paf file. Default is 8192. + threads: Number of threads use by yacrd. 0 means all threads available. Default is 1. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not input_overlap_file.is_file(): + raise FileNotFoundError(f"Input overlap file not found: {input_overlap_file}") + if not input_sequence_file.is_file(): + raise FileNotFoundError(f"Input sequence file not found: {input_sequence_file}") + if not output_report_file.parent.is_dir(): + output_report_file.parent.mkdir(parents=True, exist_ok=True) + if not output_sequence_file.parent.is_dir(): + output_sequence_file.parent.mkdir(parents=True, exist_ok=True) + + base_options = _build_base_options( + coverage_threshold, ondisk_prefix, not_coverage_ratio, + ondisk_buffer_size, read_buffer_size, threads + ) + + command_args = [ + "-i", str(input_overlap_file), + "-o", str(output_report_file), + "scrubb", + "-i", str(input_sequence_file), + "-o", str(output_sequence_file), + ] + base_options + + result = _run_yacrd_command(command_args, [output_report_file, output_sequence_file], user_ondisk_prefix=ondisk_prefix) + return result + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_yacrd/app/yacrd_shim_server.py b/Biomni/mcp_generated/mcp_yacrd/app/yacrd_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..7121c86bb21c1a53721f7749fcb61a11fd83d3ec --- /dev/null +++ b/Biomni/mcp_generated/mcp_yacrd/app/yacrd_shim_server.py @@ -0,0 +1,55 @@ +#!/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_yacrd/app/yacrd_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_yacrd' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + 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/Biomni/mcp_generated/mcp_yacrd/docker-compose.yml b/Biomni/mcp_generated/mcp_yacrd/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..af8d6bf9b7445d6494ec59e2b7b1fd8e6abe8e20 --- /dev/null +++ b/Biomni/mcp_generated/mcp_yacrd/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-yacrd: + build: . + image: mcp-yacrd:latest + container_name: mcp-yacrd + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=yacrd + 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/Biomni/mcp_generated/mcp_yacrd/environment.yaml b/Biomni/mcp_generated/mcp_yacrd/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d1e208d90c43e9cae8eb48e91b1e3d6929a2fe76 --- /dev/null +++ b/Biomni/mcp_generated/mcp_yacrd/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - yacrd + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_yacrd/requirements.txt b/Biomni/mcp_generated/mcp_yacrd/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_yacrd/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp