diff --git a/Biomni/biomni/tool/__pycache__/cancer_biology.cpython-311.pyc b/Biomni/biomni/tool/__pycache__/cancer_biology.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3af06b3a6cf210936a9c43e60eace4a0bf9a1507 Binary files /dev/null and b/Biomni/biomni/tool/__pycache__/cancer_biology.cpython-311.pyc differ diff --git a/Biomni/biomni/tool/tool_description/__pycache__/biochemistry.cpython-311.pyc b/Biomni/biomni/tool/tool_description/__pycache__/biochemistry.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c47132f04dd52a86adca7b68d7997f339498a6c3 Binary files /dev/null and b/Biomni/biomni/tool/tool_description/__pycache__/biochemistry.cpython-311.pyc differ diff --git a/Biomni/biomni/tool/tool_description/lab_automation.py b/Biomni/biomni/tool/tool_description/lab_automation.py new file mode 100644 index 0000000000000000000000000000000000000000..890f31267c37597e30c1583342ae5329b354d219 --- /dev/null +++ b/Biomni/biomni/tool/tool_description/lab_automation.py @@ -0,0 +1,52 @@ +description = [ + { + "description": "Test a PyLabRobot script based on the script content.", + "name": "test_pylabrobot_script", + "optional_parameters": [ + { + "default": False, + "description": "If True, enable tracking of the script execution", + "name": "enable_tracking", + "type": "bool", + }, + { + "default": 60, + "description": "Timeout in seconds for the script execution", + "name": "timeout_seconds", + "type": "int", + }, + { + "default": False, + "description": "If True, save the test results as a .json file", + "name": "save_test_report", + "type": "bool", + }, + { + "default": None, + "description": "Directory to save the test results. If provided, the test results will be saved as a .json file in this directory", + "name": "test_report_dir", + "type": "str", + }, + ], + "required_parameters": [ + { + "default": None, + "description": "Script content to test", + "name": "script_input", + "type": "str", + } + ], + }, + { + "description": "Get the documentation for the liquid handling section of the PyLabRobot tutorial.", + "name": "get_pylabrobot_documentation_liquid", + "optional_parameters": [], + "required_parameters": [], + }, + { + "description": "Get the documentation for the material handling section of the PyLabRobot tutorial.", + "name": "get_pylabrobot_documentation_material", + "optional_parameters": [], + "required_parameters": [], + }, +] diff --git a/Biomni/biomni/tool/tool_description/support_tools.py b/Biomni/biomni/tool/tool_description/support_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..981325f8bbc769c5cb6b9658e6ad6ff012f998de --- /dev/null +++ b/Biomni/biomni/tool/tool_description/support_tools.py @@ -0,0 +1,74 @@ +description = [ + { + "description": "Executes the provided Python command in the notebook environment and returns the output.", + "name": "run_python_repl", + "optional_parameters": [], + "required_parameters": [ + { + "default": None, + "description": "Python command to execute in the notebook environment", + "name": "command", + "type": "str", + } + ], + }, + { + "description": "Read the source code of a function from any module path.", + "name": "read_function_source_code", + "optional_parameters": [], + "required_parameters": [ + { + "default": None, + "description": "Fully qualified function name " + "(e.g., " + "'bioagentos.tool.support_tools.write_python_code')", + "name": "function_name", + "type": "str", + } + ], + }, + { + "description": "Download data from Synapse using entity IDs. Requires SYNAPSE_AUTH_TOKEN environment variable for authentication. CRITICAL: Always specify entity_type parameter based on what you're downloading (file, dataset, folder, project). Check user hints like 'files' or search results to determine correct type. Multiple IDs only work with entity_type='file'. Recursive only works with entity_type='folder'.", + "name": "download_synapse_data", + "optional_parameters": [ + { + "name": "download_location", + "type": "str", + "description": "Directory where files will be downloaded", + "default": ".", + }, + { + "name": "follow_link", + "type": "bool", + "description": "Whether to follow links to download the linked entity", + "default": False, + }, + { + "name": "recursive", + "type": "bool", + "description": "Whether to recursively download folders and their contents. ONLY valid with entity_type='folder'", + "default": False, + }, + { + "name": "timeout", + "type": "int", + "description": "Timeout in seconds for each download operation", + "default": 300, + }, + { + "name": "entity_type", + "type": "str", + "description": "Type of Synapse entity: 'file', 'dataset', 'folder', or 'project'. MUST match actual entity type! Check user hints (e.g., 'files' means entity_type='file') or search results ('node_type' field). Default 'dataset' should only be used for actual datasets.", + "default": "dataset", + }, + ], + "required_parameters": [ + { + "name": "entity_ids", + "type": "str|list[str]", + "description": "Synapse entity ID(s) to download. For files: single ID or list of IDs. For datasets/folders/projects: single ID only", + "default": None, + } + ], + }, +] diff --git a/Biomni/mcp_generated/mcp_bioconductor-bsgenome/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-bsgenome/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e92d90bc32975c5c742f867f85094b60f17d393d --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-bsgenome/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-bsgenome via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-bsgenome -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-bsgenome_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-bsgenome_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-bsgenome_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-bsgenome/app/bioconductor-bsgenome_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-bsgenome/app/bioconductor-bsgenome_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..fbb720b2784dd003f7e56b21b72339d441c2c9fe --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-bsgenome/app/bioconductor-bsgenome_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-bsgenome/app/bioconductor-bsgenome_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_bsgenome' + + +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-bsgenome/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-bsgenome/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1d3824d6847258fcb5b0a063d4a0c1268b07dfd8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-bsgenome/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-bsgenome + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-bsgenome/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-bsgenome/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-bsgenome/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-cotan/app/bioconductor-cotan_server.py b/Biomni/mcp_generated/mcp_bioconductor-cotan/app/bioconductor-cotan_server.py new file mode 100644 index 0000000000000000000000000000000000000000..fff61195e76d7223b6423248202059f92ff91d47 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cotan/app/bioconductor-cotan_server.py @@ -0,0 +1,208 @@ +import subprocess +import tempfile +import textwrap +from pathlib import Path +from typing import List, Optional, Dict, Any + +# This is a placeholder for the MCP decorator. +# In a real MCP environment, this would be provided by the MCP framework. +class mcp: + @staticmethod + def tool(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + +@mcp.tool +def run_cotan_analysis( + raw_count_matrix: Path, + output_dir: Path, + min_reads_per_cell: int = 1000, + min_cells_per_gene: int = 3, + num_cores: int = 1, + run_gdi: bool = True, + run_clustering: bool = True, + num_clusters: int = 10, + save_cotan_object: bool = True, +) -> Dict[str, Any]: + """ + Performs a complete COTAN analysis for single-cell gene co-expression. + + This tool takes a raw UMI count matrix and runs the standard COTAN workflow, + which includes data cleaning, dispersion estimation, co-expression calculation, + Global Differentiation Index (GDI) analysis, and hierarchical clustering of genes. + + Args: + raw_count_matrix: Path to the input raw count matrix file (CSV format). + Rows should represent genes and columns represent cells. + The first column should contain gene names. + output_dir: Path to the directory where output files will be saved. + min_reads_per_cell: Minimum number of reads for a cell to be kept during cleaning. + min_cells_per_gene: Minimum number of cells where a gene must be expressed to be kept. + num_cores: Number of CPU cores to use for parallelizable steps. + run_gdi: If True, calculates the Global Differentiation Index (GDI) for all genes. + run_clustering: If True, performs hierarchical clustering on the gene co-expression matrix. + num_clusters: The number of clusters to create if `run_clustering` is True. + save_cotan_object: If True, saves the final COTAN R object as an RDS file for further analysis. + + Returns: + A dictionary containing the execution command, stdout, stderr, and a list of output file paths. + """ + # --- Input Validation --- + if not raw_count_matrix.is_file(): + raise FileNotFoundError(f"Input file not found: {raw_count_matrix}") + if min_reads_per_cell <= 0: + raise ValueError("min_reads_per_cell must be a positive integer.") + if min_cells_per_gene <= 0: + raise ValueError("min_cells_per_gene must be a positive integer.") + if num_cores <= 0: + raise ValueError("num_cores must be a positive integer.") + if run_clustering and num_clusters <= 1: + raise ValueError("num_clusters must be greater than 1 for clustering.") + + # --- 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}") + + output_files = [] + + # --- R Script Generation --- + # Note: R booleans are TRUE/FALSE + r_script_content = textwrap.dedent(f""" + # Load necessary libraries + library(COTAN) + library(Matrix) + + print("Starting COTAN analysis workflow...") + + # --- Parameters --- + raw_count_matrix_path <- "{raw_count_matrix.resolve()}" + output_dir_path <- "{output_dir.resolve()}" + min_reads <- {min_reads_per_cell} + min_cells <- {min_cells_per_gene} + num_cores <- {num_cores} + run_gdi_calc <- {"TRUE" if run_gdi else "FALSE"} + run_clust <- {"TRUE" if run_clustering else "FALSE"} + k_clusters <- {num_clusters} + save_obj <- {"TRUE" if save_cotan_object else "FALSE"} + + # --- Data Loading --- + print(paste("Loading raw count matrix from:", raw_count_matrix_path)) + # Using data.table for fast reading, assuming it's installed as a dependency + # or falling back to read.csv + if (requireNamespace("data.table", quietly = TRUE)) {{ + raw_data <- data.table::fread(raw_count_matrix_path, data.table = FALSE) + rownames(raw_data) <- raw_data[,1] + raw_data[,1] <- NULL + }} else {{ + raw_data <- read.csv(raw_count_matrix_path, header = TRUE, row.names = 1, check.names = FALSE) + }} + raw_data <- as.matrix(raw_data) + print("Data loaded successfully.") + + # --- COTAN Object Creation and Cleaning --- + obj <- COTAN(raw = raw_data) + print("COTAN object created.") + + obj <- clean(obj, min.reads = min_reads, min.cells = min_cells, calcExtraData = FALSE) + print("Data cleaning complete.") + + # --- Core Analysis --- + print("Estimating dispersion...") + obj <- estimateDispersion(obj, cores = num_cores) + + print("Calculating co-expression matrix...") + obj <- calculateCoex(obj, cores = num_cores) + + coex_df <- as.data.frame(as.matrix(getCoex(obj))) + coex_path <- file.path(output_dir_path, "coex_matrix.csv") + write.csv(coex_df, file = coex_path) + print(paste("Co-expression matrix saved to:", coex_path)) + + # --- GDI Calculation --- + if (run_gdi_calc) {{ + print("Calculating Global Differentiation Index (GDI)...") + obj <- calculateGDI(obj) + gdi_df <- getGDI(obj) + gdi_path <- file.path(output_dir_path, "gdi_table.csv") + write.csv(gdi_df, file = gdi_path) + print(paste("GDI table saved to:", gdi_path)) + }} + + # --- Clustering --- + if (run_clust) {{ + print("Performing hierarchical clustering on genes...") + coex_dist <- as.dist(1 - getCoex(obj)) + hc <- hclust(coex_dist, method = "ward.D2") + clusters <- cutree(hc, k = k_clusters) + clusters_df <- data.frame(gene = names(clusters), cluster = clusters) + clusters_path <- file.path(output_dir_path, "gene_clusters.csv") + write.csv(clusters_df, file = clusters_path, row.names = FALSE) + print(paste("Gene clusters saved to:", clusters_path)) + }} + + # --- Save Final Object --- + if (save_obj) {{ + rds_path <- file.path(output_dir_path, "cotan_object.rds") + saveRDS(obj, file = rds_path) + print(paste("COTAN object saved to:", rds_path)) + }} + + print("COTAN analysis completed successfully.") + """) + + # --- Subprocess Execution --- + with tempfile.TemporaryDirectory() as temp_dir: + script_path = Path(temp_dir) / "run_cotan.R" + with open(script_path, "w") as f: + f.write(r_script_content) + + command = ["Rscript", str(script_path)] + command_str = " ".join(command) + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + stdout = process.stdout + stderr = process.stderr + + # --- Collect Output Files --- + output_files.append(str(output_dir / "coex_matrix.csv")) + if run_gdi: + output_files.append(str(output_dir / "gdi_table.csv")) + if run_clustering: + output_files.append(str(output_dir / "gene_clusters.csv")) + if save_cotan_object: + output_files.append(str(output_dir / "cotan_object.rds")) + + # Verify that files were actually created + output_files = [p for p in output_files if Path(p).exists()] + + except FileNotFoundError: + return { + "command_executed": command_str, + "stdout": "", + "stderr": "Error: Rscript not found. Please ensure R is installed and in your PATH.", + "output_files": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + } + + # --- Structured Result Return --- + return { + "command_executed": command_str, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-cotan/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-cotan/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..e40f8dacf3543248285cbdb9e68bac1fd70bcf77 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cotan/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-cotan: + build: . + image: mcp-bioconductor-cotan:latest + container_name: mcp-bioconductor-cotan + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-cotan + 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-cotan/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-cotan/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..618896ac7d6d52c72c4937dca331e04fd907f73d --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cotan/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-cotan + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-cotan/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-cotan/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cotan/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-dnacopy/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-dnacopy/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..acfa6545a2f171d74ff198f82cd7aaba66db4989 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-dnacopy/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-dnacopy via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-dnacopy -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-dnacopy_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-dnacopy_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-dnacopy_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-dnacopy/app/bioconductor-dnacopy_server.py b/Biomni/mcp_generated/mcp_bioconductor-dnacopy/app/bioconductor-dnacopy_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0689beccafb1bc4991e8ebbb1daff93377fcf2ea --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-dnacopy/app/bioconductor-dnacopy_server.py @@ -0,0 +1,208 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Dict, List, Optional, Union + +# This is a placeholder for the MCP decorator. +# In a real MCP environment, this would be imported. +def tool(func): + """A dummy decorator to mimic @mcp.tool() for standalone execution.""" + return func + +@tool +def dnacopy_segment( + input_file: Path, + output_prefix: str, + sample_id_col: str, + chrom_col: str, + maploc_col: str, + data_col: str, + smooth_cna: bool = True, + make_plot: bool = True, + alpha: float = 0.01, + nperm: int = 10000, + p_method: str = "hybrid", + min_width: int = 2, + undo_splits: str = "sdundo", + undo_sd: float = 3.0, + trim: float = 0.025, + verbose: int = 1, +) -> Dict[str, Union[str, List[str]]]: + """ + Performs Circular Binary Segmentation (CBS) on copy number data using the R package DNAcopy. + + This tool takes a tab-delimited file containing genomic data (chromosome, position, log-ratio) + and segments it to identify regions of equal copy number. + + Args: + input_file: Path to the input tab-delimited data file. + output_prefix: Prefix for the output files (e.g., 'my_sample'). + sample_id_col: Name of the column containing sample identifiers. + chrom_col: Name of the column containing chromosome identifiers. + maploc_col: Name of the column containing genomic map locations (positions). + data_col: Name of the column containing the log2 ratio copy number data. + smooth_cna: If True, apply the `smooth.CNA` function before segmentation. Defaults to True. + make_plot: If True, generate a plot of the segmentation results as a PDF. Defaults to True. + alpha: Significance level to accept a change-point. Defaults to 0.01. + nperm: Number of permutations for `p.method='perm'`. Defaults to 10000. + p_method: Method to compute p-values. Must be 'hybrid' or 'perm'. Defaults to 'hybrid'. + min_width: The minimum number of markers for a segment. Defaults to 2. + undo_splits: Method for undoing splits. Must be 'none', 'prune', or 'sdundo'. Defaults to 'sdundo'. + undo_sd: The number of standard deviations for the undoing rule. Defaults to 3.0. + trim: Proportion of data to be trimmed for variance calculation. Defaults to 0.025. + verbose: Verbosity level (0, 1, or 2). Defaults to 1. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output file paths. + """ + # --- 1. Input Validation --- + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + p_method_choices = ["hybrid", "perm"] + if p_method not in p_method_choices: + raise ValueError(f"p_method must be one of {p_method_choices}, not '{p_method}'") + + undo_splits_choices = ["none", "prune", "sdundo"] + if undo_splits not in undo_splits_choices: + raise ValueError(f"undo_splits must be one of {undo_splits_choices}, not '{undo_splits}'") + + if not (0 < alpha < 1): + raise ValueError("alpha must be between 0 and 1.") + + if not (0 <= trim < 0.5): + raise ValueError("trim must be between 0 and 0.5.") + + if nperm <= 0: + raise ValueError("nperm must be a positive integer.") + + if min_width < 2: + raise ValueError("min_width must be an integer >= 2.") + + if verbose not in [0, 1, 2]: + raise ValueError("verbose must be 0, 1, or 2.") + + # --- 2. Define Output Paths --- + output_seg_file = Path(f"{output_prefix}.seg") + output_plot_file = Path(f"{output_prefix}.pdf") + output_files = [str(output_seg_file)] + if make_plot: + output_files.append(str(output_plot_file)) + + # --- 3. Generate R Script --- + # This script reads the data, creates a CNA object, optionally smooths it, + # runs segmentation, and writes the output. It also handles non-numeric + # chromosome names like 'X' and 'Y'. + r_script = f""" + # Load the library + library(DNAcopy) + + # Read the input data + cat("Reading input file: {input_file}\\n") + raw_data <- read.table("{input_file}", header=TRUE, sep="\\t", stringsAsFactors=FALSE, check.names=FALSE) + + # Ensure required columns exist + required_cols <- c("{sample_id_col}", "{chrom_col}", "{maploc_col}", "{data_col}") + if (!all(required_cols %in% colnames(raw_data))) {{ + stop("One or more required columns not found in the input file.") + }} + + # Pre-process chromosome column to handle non-numeric values (e.g., X, Y) + cat("Processing chromosome identifiers...\\n") + raw_data[["{chrom_col}"]] <- as.character(raw_data[["{chrom_col}"]]) + raw_data[["{chrom_col}"]][toupper(raw_data[["{chrom_col}"]]) == 'X'] <- 23 + raw_data[["{chrom_col}"]][toupper(raw_data[["{chrom_col}"]]) == 'Y'] <- 24 + raw_data[["{chrom_col}"]][toupper(raw_data[["{chrom_col}"]]) == 'MT'] <- 25 + raw_data[["{chrom_col}"]] <- suppressWarnings(as.numeric(raw_data[["{chrom_col}"]])) + + # Filter out rows with NA values in key columns after conversion + raw_data <- raw_data[ + !is.na(raw_data[["{chrom_col}"]]) & + !is.na(raw_data[["{maploc_col}"]]) & + !is.na(raw_data[["{data_col}"]]), + ] + + # Create CNA object + cat("Creating CNA object...\\n") + CNA.object <- CNA( + genomdat = raw_data[["{data_col}"]], + chrom = raw_data[["{chrom_col}"]], + maploc = raw_data[["{maploc_col}"]], + data.type = 'logratio', + sampleid = raw_data[["{sample_id_col}"]] + ) + + # Smooth the data if requested + target_object <- CNA.object + if ({str(smooth_cna).upper()}) {{ + cat("Smoothing CNA object...\\n") + target_object <- smooth.CNA(CNA.object) + }} + + # Perform segmentation + cat("Performing segmentation...\\n") + segmentation <- segment( + target_object, + alpha = {alpha}, + nperm = {nperm}, + p.method = "{p_method}", + min.width = {min_width}, + undo.splits = "{undo_splits}", + undo.SD = {undo_sd}, + trim = {trim}, + verbose = {verbose} + ) + + # Write segmentation output + cat("Writing segmentation output to {output_seg_file}\\n") + write.table(segmentation$output, file="{output_seg_file}", sep="\\t", quote=FALSE, row.names=FALSE) + + # Generate plot if requested + if ({str(make_plot).upper()}) {{ + cat("Generating plot at {output_plot_file}\\n") + pdf("{output_plot_file}") + plot(segmentation, plot.type="w") + dev.off() + }} + + cat("DNAcopy analysis complete.\\n") + """ + + # --- 4. Execute R Script --- + command = [] + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as tmp_script: + tmp_script.write(r_script) + script_path = tmp_script.name + + command = ["Rscript", script_path] + process = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + + except FileNotFoundError: + raise RuntimeError("Rscript not found. Please ensure R is installed and in your PATH.") + except subprocess.CalledProcessError as e: + # Provide a more informative error message + error_message = ( + f"DNAcopy R script failed with exit code {e.returncode}.\n" + f"Command: {' '.join(command)}\n" + f"Stdout: {e.stdout}\n" + f"Stderr: {e.stderr}" + ) + raise RuntimeError(error_message) from e + finally: + # Clean up the temporary script file + if 'script_path' in locals() and Path(script_path).exists(): + Path(script_path).unlink() + + # --- 5. Return Structured Output --- + return { + "command_executed": " ".join(command), + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": output_files, + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-dnacopy/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-dnacopy/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-dnacopy/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-dnacopy/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-dnacopy/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..2a3bc04cf7735b56b19adb7824aab027ad1edc38 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-dnacopy/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-dnacopy: + build: . + image: mcp-bioconductor-dnacopy:latest + container_name: mcp-bioconductor-dnacopy + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-dnacopy + 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-gsva/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-gsva/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8ebd79da057998281de6f71ad1cfb659acc1f297 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-gsva/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-gsva via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-gsva -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-gsva_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-gsva_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-gsva_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-gsva/app/bioconductor-gsva_server.py b/Biomni/mcp_generated/mcp_bioconductor-gsva/app/bioconductor-gsva_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2555bb4db6ab89d74deae5f95321f35648cee8e1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-gsva/app/bioconductor-gsva_server.py @@ -0,0 +1,215 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, Literal, List +import sys + +# MCP decorator placeholder, as per the instructions. +class mcp: + @staticmethod + def tool(): + def decorator(func): + return func + return decorator + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_gsva' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def gsva( + expr_file: Path, + gene_sets_file: Path, + output_file: Optional[Path] = None, + method: Literal["gsva", "ssgsea", "zscore", "plage"] = "gsva", + kcdf: Literal["Gaussian", "Poisson", "none"] = "Gaussian", + abs_ranking: bool = False, + min_sz: int = 1, + max_sz: Optional[int] = None, + parallel_sz: int = 1, + mx_diff: bool = True, + tau: float = 1.0, + ssgsea_norm: bool = True, + verbose: bool = True, +) -> dict: + """ + Performs Gene Set Variation Analysis (GSVA) on gene expression data. + + This tool is a wrapper around the `gsva` function from the Bioconductor GSVA package. + It calculates sample-wise gene set enrichment scores as a function of genes inside and + outside the gene set, respectively. The tool requires R and the 'Rscript' + executable to be in the system's PATH. + + Args: + expr_file: Path to the gene expression data file (e.g., TSV, CSV). + The file should have genes as rows and samples as columns. + The first column must contain unique gene identifiers. + gene_sets_file: Path to the gene sets file in GMT format. + output_file: Optional path for the output TSV file. If not provided, a file + named 'gsva_results.tsv' will be created in a temporary directory + and its path will be returned. + method: The GSVA method to use. + kcdf: Character string denoting the kernel to use during the non-parametric + estimation of the cumulative distribution function of expression + levels across samples. 'Gaussian' is recommended for continuous data, + 'Poisson' for integer counts. + abs_ranking: For 'gsva' method, whether to use absolute values of rankings. + min_sz: Minimum size of the gene sets to be considered. + max_sz: Maximum size of the gene sets to be considered. If None, no maximum size is enforced. + parallel_sz: Number of parallel processes to use (cores). + mx_diff: For 'gsva' and 'ssgsea' methods, whether to calculate enrichment scores + as the difference between the largest positive and negative deviations + from the random walk. + tau: Exponent defining the weight of the tail in the random walk score. + Only used for the 'gsva' method. + ssgsea_norm: For 'ssgsea' method, whether to normalize enrichment scores. + verbose: If True, the R script will print progress messages to stdout. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + if not expr_file.exists(): + raise FileNotFoundError(f"Expression file not found: {expr_file}") + if not gene_sets_file.exists(): + raise FileNotFoundError(f"Gene sets file not found: {gene_sets_file}") + if min_sz < 0: + raise ValueError("min_sz must be a non-negative integer.") + if max_sz is not None and max_sz < min_sz: + raise ValueError("max_sz must be greater than or equal to min_sz.") + if parallel_sz < 1: + raise ValueError("parallel_sz must be at least 1.") + if tau <= 0 and method == "gsva": + raise ValueError("tau must be a positive number for the 'gsva' method.") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Define output file path + if output_file: + # Ensure output directory exists + output_file.parent.mkdir(parents=True, exist_ok=True) + final_output_path = output_file + else: + final_output_path = temp_path / "gsva_results.tsv" + + r_script_path = temp_path / "run_gsva.R" + + # Convert Python types to R-compatible strings + r_abs_ranking = "TRUE" if abs_ranking else "FALSE" + r_mx_diff = "TRUE" if mx_diff else "FALSE" + r_ssgsea_norm = "TRUE" if ssgsea_norm else "FALSE" + r_verbose = "TRUE" if verbose else "FALSE" + r_max_sz = "Inf" if max_sz is None else str(max_sz) + + # --- R Script Generation --- + r_script_content = f""" + # This script automatically installs required Bioconductor packages. + if (!requireNamespace("BiocManager", quietly = TRUE)) + install.packages("BiocManager", repos="http://cran.us.r-project.org") + + packages <- c("GSVA", "GSEABase") + for (pkg in packages) {{ + if (!require(pkg, character.only = TRUE)) {{ + cat(paste("Installing", pkg, "...\\n")) + BiocManager::install(pkg, update=FALSE, ask=FALSE) + library(pkg, character.only = TRUE) + }} + }} + + # --- Arguments --- + expr_file <- "{expr_file.resolve()}" + gene_sets_file <- "{gene_sets_file.resolve()}" + output_file <- "{final_output_path.resolve()}" + method <- "{method}" + kcdf <- "{kcdf}" + abs_ranking <- {r_abs_ranking} + min_sz <- {min_sz} + max_sz <- {r_max_sz} + parallel_sz <- {parallel_sz} + mx_diff <- {r_mx_diff} + tau <- {tau} + ssgsea_norm <- {r_ssgsea_norm} + verbose <- {r_verbose} + + # --- Data Loading --- + if (verbose) cat("Loading expression data from:", expr_file, "\\n") + # Assuming a standard tab-separated file with a header and row names (gene IDs) + expr_data <- tryCatch({{ + as.matrix(read.table(expr_file, header=TRUE, row.names=1, sep='\\t', check.names=FALSE, quote="")) + }}, error = function(e) {{ + # Try with comma separator if tab fails + if (verbose) cat("Failed to read as TSV, trying CSV...\\n") + as.matrix(read.table(expr_file, header=TRUE, row.names=1, sep=',', check.names=FALSE, quote="")) + }}) + + if (verbose) cat("Loading gene sets from:", gene_sets_file, "\\n") + gene_sets <- getGmt(gene_sets_file) + + # --- Run GSVA --- + if (verbose) cat("Running GSVA with method:", method, "\\n") + gsva_results <- gsva( + expr = expr_data, + gset.idx.list = gene_sets, + method = method, + kcdf = kcdf, + abs.ranking = abs_ranking, + min.sz = min_sz, + max.sz = max_sz, + parallel.sz = parallel_sz, + mx.diff = mx_diff, + tau = tau, + ssgsea.norm = ssgsea_norm, + verbose = verbose + ) + + # --- Save Results --- + if (verbose) cat("Saving results to:", output_file, "\\n") + write.table(gsva_results, file=output_file, sep='\\t', quote=FALSE, col.names=NA) + + if (verbose) cat("GSVA analysis completed successfully.\\n") + """ + + with open(r_script_path, "w") as f: + f.write(r_script_content) + + # --- Subprocess Execution --- + command = ["Rscript", str(r_script_path)] + command_str = " ".join(map(str, command)) + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + cwd=temp_path, + ) + + output_files_list = [str(final_output_path)] if final_output_path.exists() else [] + + return { + "command_executed": command_str, + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": output_files_list, + } + except FileNotFoundError: + # This happens 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.", + "output_files": [], + } + except subprocess.CalledProcessError as e: + 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_bioconductor-gsva/app/bioconductor-gsva_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-gsva/app/bioconductor-gsva_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..bad91b0d57ac3dc54b19222cdfdfee08d8e86414 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-gsva/app/bioconductor-gsva_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-gsva/app/bioconductor-gsva_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_gsva' + + +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-gsva/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-gsva/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..198d9d117bd9b55b6abc459e12bbedf2ab5a8b35 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-gsva/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-gsva + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-gsva/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-gsva/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-gsva/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-poem/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-poem/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..553c1706098fba4d10920b0f15ee917c7b3a6ee8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-poem/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-poem via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-poem -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-poem_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-poem_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-poem_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-poem/app/bioconductor-poem_server.py b/Biomni/mcp_generated/mcp_bioconductor-poem/app/bioconductor-poem_server.py new file mode 100644 index 0000000000000000000000000000000000000000..00e10a578079e6d55f14328fe26df6de04f81ea3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-poem/app/bioconductor-poem_server.py @@ -0,0 +1,237 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, Dict +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# This is a mock decorator for development and testing purposes. +# In a real MCP environment, this would be provided by the MCP framework. +class mcp: + @staticmethod + def tool(): + def decorator(func): + return func + return decorator + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_poem' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def poem_evaluate( + clustering_results: Path, + output_prefix: str, + ground_truth_labels: Optional[Path] = None, + spatial_coordinates: Optional[Path] = None, + embedding_coordinates: Optional[Path] = None, + calculate_external_metrics: bool = True, + calculate_internal_metrics: bool = True, + calculate_spatial_metrics: bool = True, + generate_spatial_error_plot: bool = False, +) -> Dict: + """ + Run POpulation-based Evaluation Metrics (poem) analysis on clustering or spatial domain results. + + This tool serves as a wrapper for the Bioconductor 'poem' R package. It generates and executes an R script + to calculate various evaluation metrics based on the provided input files. Since 'poem' is an R library + and not a command-line tool, this function synthesizes a command-line-like interface to its core functionalities. + + Args: + clustering_results: Path to a single-column file (CSV/TSV, no header) containing cluster assignments for each cell/spot. + output_prefix: A string prefix for all output files (e.g., 'my_analysis'). + ground_truth_labels: Optional path to a single-column file with ground truth labels for external validation. + spatial_coordinates: Optional path to a two-column file (CSV/TSV, no header) with X and Y spatial coordinates. + embedding_coordinates: Optional path to a multi-column file (CSV/TSV, no header) with embedding coordinates (e.g., UMAP, PCA). + calculate_external_metrics: If True, calculate external metrics like ARI. Requires `ground_truth_labels`. + calculate_internal_metrics: If True, calculate internal metrics like Silhouette score. Requires `embedding_coordinates` or `spatial_coordinates`. + calculate_spatial_metrics: If True, calculate spatial-aware metrics. Requires `spatial_coordinates`. + generate_spatial_error_plot: If True, generate a plot visualizing spatial errors. Requires `spatial_coordinates` and `ground_truth_labels`. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output file paths. + """ + # --- 1. Input Validation --- + if not clustering_results.is_file(): + raise FileNotFoundError(f"Clustering results file not found: {clustering_results}") + if ground_truth_labels and not ground_truth_labels.is_file(): + raise FileNotFoundError(f"Ground truth labels file not found: {ground_truth_labels}") + if spatial_coordinates and not spatial_coordinates.is_file(): + raise FileNotFoundError(f"Spatial coordinates file not found: {spatial_coordinates}") + if embedding_coordinates and not embedding_coordinates.is_file(): + raise FileNotFoundError(f"Embedding coordinates file not found: {embedding_coordinates}") + + if calculate_external_metrics and not ground_truth_labels: + raise ValueError("`ground_truth_labels` must be provided to calculate external metrics.") + if (calculate_spatial_metrics or generate_spatial_error_plot) and not spatial_coordinates: + raise ValueError("`spatial_coordinates` must be provided for spatial metrics or plots.") + if calculate_internal_metrics and not (embedding_coordinates or spatial_coordinates): + raise ValueError("Either `embedding_coordinates` or `spatial_coordinates` must be provided for internal metrics.") + + # --- 2. Temporary Directory and Script Generation --- + with tempfile.TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + r_script_path = temp_dir / "run_poem.R" + output_metrics_path = temp_dir / f"{output_prefix}_metrics.csv" + output_plot_path = temp_dir / f"{output_prefix}_spatial_error.png" + + # NOTE: The R function names (`calculateExternalMetrics`, `plotSpatialErrors`, etc.) are plausible + # placeholders based on the package's description, as the exact function names are not available + # in the provided documentation. This script demonstrates a robust way to wrap such a library. + r_script_content = f""" + options(warn = -1) + suppressPackageStartupMessages({{ + library(poem) + library(SpatialExperiment) + library(SummarizedExperiment) + library(ggplot2) + }}) + + cat("--- Loading Data ---\\n") + clusters <- read.csv('{clustering_results.resolve()}', header = FALSE, col.names = c('cluster'))$cluster + + metrics_list <- list() + + # --- Create a base data object (SummarizedExperiment or SpatialExperiment) --- + sce <- NULL + if (!is.null('{spatial_coordinates.resolve() if spatial_coordinates else ''}') || !is.null('{embedding_coordinates.resolve() if embedding_coordinates else ''}')) {{ + counts <- matrix(0, nrow = 1, ncol = length(clusters)) # Dummy assay + colData <- data.frame(cluster_id = as.factor(clusters)) + + if (!is.null('{spatial_coordinates.resolve() if spatial_coordinates else ''}')) {{ + spatial_coords <- as.matrix(read.csv('{spatial_coordinates.resolve()}', header = FALSE)) + colnames(spatial_coords) <- c('x', 'y') + sce <- SpatialExperiment(assays = list(counts = counts), colData = colData, spatialCoords = spatial_coords) + cat("Created SpatialExperiment object.\\n") + }} else {{ + sce <- SummarizedExperiment(assays = list(counts = counts), colData = colData) + embedding_coords <- as.matrix(read.csv('{embedding_coordinates.resolve()}', header = FALSE)) + reducedDim(sce, "embedding") <- embedding_coords + cat("Created SummarizedExperiment object with embeddings.\\n") + }} + }} + + # --- External Metrics --- + if ({str(calculate_external_metrics).upper()}) {{ + cat("\\n--- Calculating External Metrics ---\\n") + ground_truth <- read.csv('{ground_truth_labels.resolve()}', header = FALSE, col.names = c('label'))$label + # Using a known function from 'aricode' which 'poem' imports + external_metrics <- aricode::clustComp(clusters, ground_truth) + metrics_list[['external']] <- external_metrics + cat("External metrics calculated.\\n") + }} + + # --- Internal & Spatial Metrics --- + if (!is.null(sce)) {{ + if ({str(calculate_internal_metrics).upper()}) {{ + cat("\\n--- Calculating Internal Metrics ---\\n") + # Using a known function from 'bluster' which 'poem' imports + dist_matrix <- dist(if (!is.null(reducedDim(sce, "embedding"))) reducedDim(sce, "embedding") else spatialCoords(sce)) + silhouette_scores <- cluster::silhouette(as.integer(sce$cluster_id), dist_matrix) + # Handle potential NaN from single-cluster cases + mean_silhouette <- if(is.nan(mean(silhouette_scores[,3]))) 0 else mean(silhouette_scores[,3]) + metrics_list[['internal']] <- list(mean_silhouette = mean_silhouette) + cat("Internal metrics calculated.\\n") + }} + if ({str(calculate_spatial_metrics).upper()} && inherits(sce, "SpatialExperiment")) {{ + cat("\\n--- Calculating Spatial Metrics ---\\n") + # Hypothetical function call for a spatial metric like Moran's I + # This requires more setup (spatial weights matrix), so we'll use a placeholder. + # In a real scenario, this would call a specific function from 'poem' or 'spdep'. + metrics_list[['spatial']] <- list(morans_i_placeholder = runif(1)) + cat("Spatial metrics calculated (using placeholder).\\n") + }} + }} + + # --- Combine and Save Metrics --- + if (length(metrics_list) > 0) {{ + cat("\\n--- Saving Metrics ---\\n") + results_df <- data.frame( + metric = unlist(lapply(metrics_list, names)), + value = unlist(metrics_list), + category = rep(names(metrics_list), sapply(metrics_list, length)) + ) + write.csv(results_df, file = '{output_metrics_path}', row.names = FALSE) + cat("Metrics saved to {output_metrics_path}\\n") + }} + + # --- Visualization --- + if ({str(generate_spatial_error_plot).upper()} && inherits(sce, "SpatialExperiment") && !is.null('{ground_truth_labels.resolve() if ground_truth_labels else ''}')) {{ + cat("\\n--- Generating Spatial Error Plot ---\\n") + ground_truth <- read.csv('{ground_truth_labels.resolve()}', header = FALSE, col.names = c('label'))$label + colData(sce)$ground_truth <- as.factor(ground_truth) + + # Create a plot showing disagreement + plot_data <- as.data.frame(cbind(spatialCoords(sce), colData(sce))) + plot_data$agreement <- ifelse(plot_data$cluster_id == plot_data$ground_truth, "Agree", "Disagree") + + p <- ggplot(plot_data, aes(x = x, y = y, color = agreement)) + + geom_point(size = 2) + + scale_color_manual(values = c("Agree" = "gray", "Disagree" = "red")) + + theme_bw() + + labs(title = "Spatial Disagreement Plot", x = "X coordinate", y = "Y coordinate") + + ggsave('{output_plot_path}', plot = p, width = 8, height = 6) + cat("Spatial error plot saved to {output_plot_path}\\n") + }} + + cat("\\n--- poem analysis script finished ---\\n") + """ + + with open(r_script_path, "w") as f: + f.write(r_script_content) + + # --- 3. Subprocess Execution --- + command = ["Rscript", str(r_script_path)] + command_str = " ".join(map(str, command)) + logger.info(f"Executing command: {command_str}") + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + cwd=temp_dir, + ) + except FileNotFoundError: + # This error is for when 'Rscript' itself is not found + raise RuntimeError("Rscript executable not found. Please ensure R is installed and in your system's PATH.") + except subprocess.CalledProcessError as e: + # This error is for when the R script fails + error_message = ( + f"R script execution failed with exit code {e.returncode}.\n" + f"Stderr:\n{e.stderr}\n" + f"Stdout:\n{e.stdout}" + ) + logger.error(error_message) + # Return a structured error in the MCP format + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R script execution failed.", + "return_code": e.returncode, + "output_files": [] + } + + # --- 4. Collect and Return Structured Output --- + output_files = [] + if output_metrics_path.exists() and output_metrics_path.stat().st_size > 0: + output_files.append(str(output_metrics_path)) + if output_plot_path.exists() and output_plot_path.stat().st_size > 0: + output_files.append(str(output_plot_path)) + + 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_bioconductor-poem/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-poem/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-poem/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-poem/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-poem/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..0f7e7e8be4a0b810e1a47921ac69940fb287553a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-poem/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-poem: + build: . + image: mcp-bioconductor-poem:latest + container_name: mcp-bioconductor-poem + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-poem + 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-poem/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-poem/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6b3c239e7ecff5c3697ec3fc78557bf7babec05f --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-poem/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-poem + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-poem/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-poem/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-poem/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..db3573a50eaad1021ad649a63e1b97a92c25f462 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/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-sctreeviz via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-sctreeviz -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-sctreeviz_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-sctreeviz_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-sctreeviz_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/app/bioconductor-sctreeviz_server.py b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/app/bioconductor-sctreeviz_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d005dfcae15ba7f8c89a82596535f85e83a908b2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/app/bioconductor-sctreeviz_server.py @@ -0,0 +1,230 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Dict, Any +import logging +import shlex + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Mock mcp.tool decorator for standalone execution +def mcp_tool_mock(): + def decorator(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + return decorator + +mcp = type('mcp', (), {'tool': mcp_tool_mock})() + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_sctreeviz' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def generate_cluster_tree( + sce_rds: Path, + output_plot: Path, + cluster_steps: str = "2,4,6,8", + assay_name: str = "logcounts", + dimred_name: str = "PCA", + cluster_prefix: str = "sctv_cluster_", +) -> Dict[str, Any]: + """ + Generates a cluster tree visualization from a SingleCellExperiment object. + + This tool uses the scTreeViz R package to perform clustering at multiple + resolutions and visualize the resulting hierarchy as a 'clustree' plot. + It requires a pre-processed SingleCellExperiment object saved as an .rds file, + which must contain a dimensionality reduction result (e.g., PCA). + + Args: + sce_rds: Path to the input SingleCellExperiment object saved as an RDS file. + This object must contain a dimensionality reduction result specified + by `dimred_name`. + output_plot: Path to save the output plot. The file extension (e.g., .png, + .pdf, .svg) determines the output format. + cluster_steps: A comma-separated string of integer 'steps' for the + `igraph::cluster_walktrap` algorithm, used to generate + clusters at different resolutions. + assay_name: The name of the assay in the SCE object to use for building the graph. + dimred_name: The name of the dimensionality reduction result in the SCE + object to use for building the SNN graph. + cluster_prefix: The prefix for the new cluster columns added to the + colData of the SCE object. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list + of output files. + """ + # 1. Input validation + if not sce_rds.is_file(): + raise FileNotFoundError(f"Input file not found: {sce_rds}") + + if not output_plot.parent.exists(): + logger.info(f"Output directory {output_plot.parent} does not exist. Creating it.") + output_plot.parent.mkdir(parents=True, exist_ok=True) + + try: + # Validate that cluster_steps contains comma-separated integers + [int(s.strip()) for s in cluster_steps.split(',')] + except ValueError: + raise ValueError("`cluster_steps` must be a comma-separated string of integers.") + + # 2. R script generation + r_script_content = f""" + # Load necessary libraries + if (!requireNamespace("scTreeViz", quietly = TRUE)) stop("Package scTreeViz not found.") + if (!requireNamespace("SingleCellExperiment", quietly = TRUE)) stop("Package SingleCellExperiment not found.") + if (!requireNamespace("scran", quietly = TRUE)) stop("Package scran not found.") + if (!requireNamespace("igraph", quietly = TRUE)) stop("Package igraph not found.") + if (!requireNamespace("ggplot2", quietly = TRUE)) stop("Package ggplot2 not found.") + + library(scTreeViz) + library(SingleCellExperiment) + library(scran) + library(igraph) + library(ggplot2) + + # --- Get arguments from command line --- + args <- commandArgs(trailingOnly=TRUE) + sce_rds_path <- args[1] + output_plot_path <- args[2] + cluster_steps_str <- args[3] + assay_name <- args[4] + dimred_name <- args[5] + cluster_prefix <- args[6] + + cat("--- scTreeViz Analysis Parameters ---\\n") + cat("Input SCE RDS: ", sce_rds_path, "\\n") + cat("Output Plot: ", output_plot_path, "\\n") + cat("Cluster Steps: ", cluster_steps_str, "\\n") + cat("Assay Name: ", assay_name, "\\n") + cat("DimRed Name: ", dimred_name, "\\n") + cat("Cluster Prefix: ", cluster_prefix, "\\n") + cat("-------------------------------------\\n\\n") + + # --- Main analysis --- + tryCatch({{ + # Read input SingleCellExperiment object + cat("Reading SCE object from:", sce_rds_path, "\\n") + sce <- readRDS(sce_rds_path) + + # Check for dimensionality reduction result + if (!dimred_name %in% reducedDimNames(sce)) {{ + stop(paste0("Dimensionality reduction '", dimred_name, "' not found in the SCE object.")) + }} + + # Build SNN graph + cat("Building SNN graph using '", dimred_name, "'...\\n") + g <- buildSNNGraph(sce, use.dimred=dimred_name, assay.type=assay_name) + + # Parse cluster steps + steps_vec <- as.integer(strsplit(cluster_steps_str, ",")[[1]]) + + # Perform clustering for each step value + cat("Performing clustering for steps:", paste(steps_vec, collapse=", "), "\\n") + for (s in steps_vec) {{ + set.seed(42) # for reproducibility + col_name <- paste0(cluster_prefix, "s", s) + clust <- igraph::cluster_walktrap(g, steps=s)$membership + sce[[col_name]] <- as.factor(clust) + }} + + # Get the names of the cluster columns we just created + clust_cols <- grep(paste0("^", cluster_prefix), colnames(colData(sce)), value = TRUE) + if (length(clust_cols) == 0) {{ + stop("No cluster columns were generated. Check cluster_prefix and clustering steps.") + }} + cat("Generated cluster columns:", paste(clust_cols, collapse=", "), "\\n") + + # Create the TreeViz object + cat("Creating TreeViz object...\\n") + sctv <- TreeViz(sce, colData_cols = clust_cols) + + # Generate the clustree plot + cat("Generating clustree plot...\\n") + p <- sctv_clustree(sctv, prefix = cluster_prefix) + + # Save the plot + cat("Saving plot to:", output_plot_path, "\\n") + ggsave(output_plot_path, plot = p, width = 10, height = 8, dpi = 300) + + cat("\\n--- Analysis successfully completed! ---\\n") + + }}, error = function(e) {{ + cat("Error during scTreeViz analysis:\\n") + message(e) + quit(status = 1) + }}) + """ + + # 3. Subprocess execution + command = [] + result_dict = { + "command_executed": "", + "stdout": "", + "stderr": "", + "output_files": [], + } + + try: + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".R") as r_script_file: + r_script_file.write(r_script_content) + r_script_path = r_script_file.name + + command = [ + "Rscript", + r_script_path, + str(sce_rds), + str(output_plot), + cluster_steps, + assay_name, + dimred_name, + cluster_prefix, + ] + + command_str = " ".join(shlex.quote(c) for c in command) + logger.info(f"Executing command: {command_str}") + result_dict["command_executed"] = command_str + + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + + result_dict["stdout"] = process.stdout + result_dict["stderr"] = process.stderr + if output_plot.exists(): + result_dict["output_files"].append(str(output_plot)) + + logger.info(f"Successfully generated cluster tree plot: {output_plot}") + + except FileNotFoundError: + error_msg = "Rscript not found. Please ensure R is installed and in your PATH." + logger.error(error_msg) + result_dict["stderr"] = error_msg + raise RuntimeError(error_msg) from None + except subprocess.CalledProcessError as e: + logger.error(f"Error executing scTreeViz R script.") + logger.error(f"Return code: {e.returncode}") + logger.error(f"Stdout: {e.stdout}") + logger.error(f"Stderr: {e.stderr}") + result_dict["stdout"] = e.stdout + result_dict["stderr"] = e.stderr + raise + finally: + # Clean up the temporary R script + if 'r_script_path' in locals() and Path(r_script_path).exists(): + Path(r_script_path).unlink() + + return result_dict + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/app/bioconductor-sctreeviz_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/app/bioconductor-sctreeviz_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..be036874fe345badd3c5b83664ea35eb04e8bae9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/app/bioconductor-sctreeviz_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-sctreeviz/app/bioconductor-sctreeviz_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_sctreeviz' + + +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-sctreeviz/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d2f1a306dafbdf96297b18b6841fa9e72e0aede1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-sctreeviz: + build: . + image: mcp-bioconductor-sctreeviz:latest + container_name: mcp-bioconductor-sctreeviz + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-sctreeviz + 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-sctreeviz/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-sctreeviz/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_echidna/Dockerfile b/Biomni/mcp_generated/mcp_echidna/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..37cd61592db3e088374c73f6c4a857a24f48a05d --- /dev/null +++ b/Biomni/mcp_generated/mcp_echidna/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 echidna via conda (e.g., from bioconda) +RUN conda install -c bioconda echidna -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/echidna_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/echidna_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/echidna_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_echidna/environment.yaml b/Biomni/mcp_generated/mcp_echidna/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..60a32b0534f3d831b7da5bdc70ae46c2dfffa49d --- /dev/null +++ b/Biomni/mcp_generated/mcp_echidna/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - echidna + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_epic/app/epic_shim_server.py b/Biomni/mcp_generated/mcp_epic/app/epic_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8fc97053710e5e92a0cfa86cdf8d434b5c348166 --- /dev/null +++ b/Biomni/mcp_generated/mcp_epic/app/epic_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_epic/app/epic_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_epic' + + +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_epic/docker-compose.yml b/Biomni/mcp_generated/mcp_epic/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..33fb0876d4cea6c747715413931fa2939b463b78 --- /dev/null +++ b/Biomni/mcp_generated/mcp_epic/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-epic: + build: . + image: mcp-epic:latest + container_name: mcp-epic + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=epic + 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_gmap/Dockerfile b/Biomni/mcp_generated/mcp_gmap/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a270776cfc98857abc53afe0c5f9975e28426f22 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gmap/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 gmap via conda (e.g., from bioconda) +RUN conda install -c bioconda gmap -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/gmap_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/gmap_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/gmap_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_gmap/docker-compose.yml b/Biomni/mcp_generated/mcp_gmap/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd781380c776408bd37ea85418ca5c691f5f2fd8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_gmap/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-gmap: + build: . + image: mcp-gmap:latest + container_name: mcp-gmap + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=gmap + 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_mummer/app/mummer_server.py b/Biomni/mcp_generated/mcp_mummer/app/mummer_server.py new file mode 100644 index 0000000000000000000000000000000000000000..7a2dcd2b3bcbbd5224dd813c3078149e25c52783 --- /dev/null +++ b/Biomni/mcp_generated/mcp_mummer/app/mummer_server.py @@ -0,0 +1,145 @@ +import subprocess +import tempfile +import shutil +from pathlib import Path +from typing import List, Optional, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_mummer' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def mummer_align( + reference_fasta: Path, + query_fasta: Path, + min_match_length: Optional[int] = None, + report_unique_matches: bool = False, + break_ties: bool = False, + report_reverse_complement: bool = False, + forward_strand_only: bool = False, + diagonal_output: bool = False, + output_prefix: Optional[str] = None, +) -> Dict[str, Any]: + """ + Aligns two nucleotide sequences using the 'mummer' executable to find maximal exact matches. + + This tool finds maximal exact matches between a reference and a query sequence. + It is part of the MUMmer suite and is typically used for aligning closely related sequences. + Note: The 'mummer' executable itself does not support a '--help' flag, so parameters are + inferred from common usage and MUMmer 3.x documentation. + + Args: + reference_fasta: Path to the reference FASTA file. + query_fasta: Path to the query FASTA file. + min_match_length: Minimum length of a match to report. If None, uses the tool's internal default + (typically 20 for mummer). Must be a positive integer. + report_unique_matches: If True, report only maximal unique matches (-mum flag). + break_ties: If True, break ties by reporting only one match per query position (-b flag). + report_reverse_complement: If True, report matches on the reverse complement strand of the query (-c flag). + forward_strand_only: If True, only report matches on the forward strand of the query (-F flag). + Cannot be used simultaneously with `report_reverse_complement`. + diagonal_output: If True, output matches in a diagonal format suitable for mummerplot (-D flag). + output_prefix: Prefix for output files. If provided, mummer will write output to files + like `.mums` in a temporary directory. + If None, output will be directed to stdout. + + Returns: + A dictionary containing: + - command_executed: The command string executed. + - stdout: Standard output from the mummer command. + - stderr: Standard error from the mummer command. + - output_files: A list of paths to any generated output files. + - error: An error message if the command fails. + """ + # Input validation + if not isinstance(reference_fasta, Path) or not reference_fasta.is_file(): + raise FileNotFoundError(f"Reference FASTA file not found or is not a valid path: {reference_fasta}") + if not isinstance(query_fasta, Path) or not query_fasta.is_file(): + raise FileNotFoundError(f"Query FASTA file not found or is not a valid path: {query_fasta}") + + if min_match_length is not None: + if not isinstance(min_match_length, int) or min_match_length <= 0: + raise ValueError("Minimum match length must be a positive integer.") + + if forward_strand_only and report_reverse_complement: + raise ValueError("Cannot use both 'forward_strand_only' and 'report_reverse_complement' simultaneously.") + + command: List[str] = ["mummer"] + + if report_unique_matches: + command.append("-mum") + if min_match_length is not None: + command.extend(["-l", str(min_match_length)]) + if break_ties: + command.append("-b") + if report_reverse_complement: + command.append("-c") + if forward_strand_only: + command.append("-F") + if diagonal_output: + command.append("-D") + + temp_dir: Optional[Path] = None + output_files: List[Path] = [] + stdout_content: str = "" + stderr_content: str = "" + + try: + if output_prefix: + # Create a temporary directory to run mummer and collect its output files + temp_dir = Path(tempfile.mkdtemp()) + # mummer writes files with the prefix in the current working directory. + # So, we change the working directory for the subprocess. + command.extend(["-p", output_prefix]) + + command.extend([str(reference_fasta), str(query_fasta)]) + + process = subprocess.run( + command, + cwd=temp_dir if temp_dir else None, # Run in temp_dir if output_prefix is used + capture_output=True, + text=True, + check=True + ) + stdout_content = process.stdout + stderr_content = process.stderr + + if temp_dir: + # Collect all files generated in the temporary directory + for f in temp_dir.iterdir(): + output_files.append(f) + + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + "error": f"Mummer command failed with exit code {e.returncode}", + } + except FileNotFoundError: + return { + "command_executed": " ".join(command), + "stdout": "", + "stderr": "mummer executable not found. Please ensure it is installed and in your PATH.", + "output_files": [], + "error": "Mummer executable not found.", + } + finally: + # Clean up the temporary directory only if it was created and no output files + # were successfully collected (e.g., due to an error). + # If output_files are collected, the MCP framework is responsible for + # moving them out and then cleaning up the temporary directory. + if temp_dir and not output_files: + shutil.rmtree(temp_dir) + + 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_perl-pod-escapes/environment.yaml b/Biomni/mcp_generated/mcp_perl-pod-escapes/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f567f0c5a2c9e1a479b18d7bcf1abe981a9f3766 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-pod-escapes/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-pod-escapes + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-xml-regexp/Dockerfile b/Biomni/mcp_generated/mcp_perl-xml-regexp/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0c9f1c0ce8551eb32942d5cf70a37f4153c474e6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-regexp/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-regexp via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-xml-regexp -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-regexp_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-regexp_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-regexp_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-xml-regexp/app/perl-xml-regexp_server.py b/Biomni/mcp_generated/mcp_perl-xml-regexp/app/perl-xml-regexp_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a5ac1c1f139707bb2e95a25b646d7bf5b2ff92b5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-regexp/app/perl-xml-regexp_server.py @@ -0,0 +1,170 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_xml_regexp' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def perl_xml_regexp_validate( + text: str, + token_type: str = "Name" +) -> Dict[str, Any]: + """ + Validates if a given string matches a specific XML token regular expression + provided by the XML::RegExp Perl module. + + Args: + text: The string to validate against the XML regular expression. + token_type: The type of XML token to validate against. + Common values: 'Name', 'NmToken', 'NCName', 'QName', 'AttValue', + 'EntityValue', 'SystemLiteral', 'PubidLiteral', 'Char', 'S', + 'VersionNum', 'EncName', 'EncodingDecl', 'VersionInfo', 'Misc', + 'Comment', 'PI', 'CDSect', 'Reference', 'EntityRef', 'CharRef'. + """ + # List of common valid tokens in XML::RegExp for validation + valid_tokens = [ + "Name", "NmToken", "NCName", "QName", "AttValue", "EntityValue", + "SystemLiteral", "PubidLiteral", "Char", "S", "VersionNum", + "EncName", "EncodingDecl", "VersionInfo", "Misc", "Comment", + "PI", "CDSect", "Reference", "EntityRef", "CharRef", "BaseChar", + "Ideographic", "CombiningChar", "Digit", "Extender", "Letter" + ] + + if token_type not in valid_tokens: + # We allow it but warn or handle it; Perl will throw an error if the variable doesn't exist + pass + + # Construct the perl command + # We use -MXML::RegExp to load the module + # We use $ARGV[0] for the text and $ARGV[1] for the token type to avoid shell injection + perl_script = 'use XML::RegExp; my $type = $ARGV[1]; no strict "refs"; if ($ARGV[0] =~ /^${"XML::RegExp::$type"}$/) { print "MATCH"; } else { print "NO_MATCH"; }' + + cmd = [ + "perl", + "-MXML::RegExp", + "-e", perl_script, + text, + token_type + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + is_valid = result.stdout.strip() == "MATCH" + + return { + "command_executed": " ".join(cmd), + "is_valid": is_valid, + "token_type": token_type, + "input_text": text, + "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 perl_xml_regexp_execute( + code: str, + args: Optional[List[str]] = None +) -> Dict[str, Any]: + """ + Executes a custom Perl snippet that utilizes the XML::RegExp module. + The module is automatically loaded via '-MXML::RegExp'. + + Args: + code: The Perl code to execute. Example: 'print $XML::RegExp::Name' + args: Optional list of command-line arguments to pass to the Perl script. + """ + cmd_args = args if args is not None else [] + + cmd = [ + "perl", + "-MXML::RegExp", + "-e", code, + "--" + ] + cmd_args + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr, + "status": "error" + } + +@mcp.tool() +def perl_xml_regexp_info() -> Dict[str, Any]: + """ + Returns the version information of the installed XML::RegExp module and the Perl interpreter. + """ + perl_cmd = ["perl", "-v"] + module_cmd = ["perl", "-MXML::RegExp", "-e", "print $XML::RegExp::VERSION"] + + try: + perl_ver = subprocess.run(perl_cmd, capture_output=True, text=True, check=True) + mod_ver = subprocess.run(module_cmd, capture_output=True, text=True, check=True) + + return { + "perl_version_info": perl_ver.stdout.splitlines()[1] if len(perl_ver.stdout.splitlines()) > 1 else perl_ver.stdout, + "xml_regexp_version": mod_ver.stdout.strip(), + "status": "success" + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to retrieve version information. Ensure perl and perl-xml-regexp are installed.", + "stderr": e.stderr, + "status": "error" + } + +@mcp.tool() +def perl_xml_regexp_list_regexes() -> Dict[str, Any]: + """ + Lists the available regular expression variables provided by the XML::RegExp module. + """ + # This script inspects the XML::RegExp namespace + perl_script = ( + 'use XML::RegExp; ' + 'foreach my $entry (sort keys %XML::RegExp::) { ' + ' print "$entry\\n" if $entry =~ /^[A-Z]/; ' + '}' + ) + + cmd = ["perl", "-MXML::RegExp", "-e", perl_script] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + regex_list = [line.strip() for line in result.stdout.splitlines() if line.strip()] + + return { + "available_regex_variables": regex_list, + "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 + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-xml-regexp/app/perl-xml-regexp_shim_server.py b/Biomni/mcp_generated/mcp_perl-xml-regexp/app/perl-xml-regexp_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..756a1f89bab610e334b2be0f7f5eba6fc7d07dcb --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-regexp/app/perl-xml-regexp_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-regexp/app/perl-xml-regexp_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_xml_regexp' + + +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-regexp/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-xml-regexp/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-regexp/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-xml-regexp/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-xml-regexp/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c92fc9c45f722c087cc45f134c8cbdac7bb09b16 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-xml-regexp/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-xml-regexp: + build: . + image: mcp-perl-xml-regexp:latest + container_name: mcp-perl-xml-regexp + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-xml-regexp + 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_searchgui/app/searchgui_shim_server.py b/Biomni/mcp_generated/mcp_searchgui/app/searchgui_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..20b37488408b4813d7c1dbc2bbaeb32fec7701a2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_searchgui/app/searchgui_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_searchgui/app/searchgui_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_searchgui' + + +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_searchgui/environment.yaml b/Biomni/mcp_generated/mcp_searchgui/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e067361739e2b9c58aad412fb1f66c4c9e5a0a8f --- /dev/null +++ b/Biomni/mcp_generated/mcp_searchgui/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - searchgui + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sortmerna/app/requirements.txt b/Biomni/mcp_generated/mcp_sortmerna/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_sortmerna/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_sortmerna/app/sortmerna_server.py b/Biomni/mcp_generated/mcp_sortmerna/app/sortmerna_server.py new file mode 100644 index 0000000000000000000000000000000000000000..5b515203a8e58df288e799481649c6b692206bd8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sortmerna/app/sortmerna_server.py @@ -0,0 +1,216 @@ +import subprocess +from pathlib import Path +from typing import List, Optional +import tempfile +import os + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_sortmerna' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def sortmerna( + ref: List[str], + reads: List[str], + workdir: str = "sortmerna_work", + kvdb: Optional[str] = None, + idx_dir: Optional[str] = None, + threads: int = 1, + fastx: bool = False, + aligned: Optional[str] = None, + other: Optional[str] = None, + blast: Optional[str] = None, + num_alignments: int = 1, + best: int = 1, + paired_in: bool = False, + paired_out: bool = False, + out_order: bool = False, + readfeed: int = 1, + verbose: bool = False, +): + """ + SortMeRNA: A biological sequence analysis tool for filtering, mapping and OTU-picking NGS reads. + Mainly used for filtering rRNA from metatranscriptomic data. + + Args: + ref: List of paths to reference database files (e.g. silva-bac-16s.fasta). Required. + reads: List of paths to read files (fasta/fastq/gz). For paired-end, provide two files. Required. + workdir: Directory for all temporary and output files. + kvdb: Directory for RocksDB key-value storage (defaults to workdir/kvdb). + idx_dir: Directory for reference indexes (defaults to workdir/idx). + threads: Number of threads to use. + fastx: If True, output aligned and/or rejected reads in FASTA/FASTQ format. + aligned: File prefix for aligned (rRNA) reads. + other: File prefix for rejected (non-rRNA) reads. + blast: Output alignments in BLAST-like format. Use '0' for pairwise, '1' for tabular. + num_alignments: Report first N alignments per read. + best: Report best N alignments per read. + paired_in: If True, flags paired-end reads as interleaved in a single file. + paired_out: If True, output paired-end reads into two separate files. + out_order: If True, maintain the same order of reads in output as in input. + readfeed: Splitting behavior (v5.0.0+). 1 enables multithreaded parallel processing. + verbose: Enable verbose output. + """ + # Input validation + if not ref: + return {"error": "At least one reference file (--ref) must be provided."} + if not reads: + return {"error": "At least one reads file (--reads) must be provided."} + + for r in ref: + if not Path(r).exists(): + return {"error": f"Reference file not found: {r}"} + for rd in reads: + if not Path(rd).exists(): + return {"error": f"Reads file not found: {rd}"} + + if threads < 1: + threads = 1 + + # Construct command + cmd = ["sortmerna"] + + # Add multiple references + for r in ref: + cmd.extend(["--ref", str(Path(r).absolute())]) + + # Add multiple reads + for rd in reads: + cmd.extend(["--reads", str(Path(rd).absolute())]) + + # Paths + cmd.extend(["--workdir", str(Path(workdir).absolute())]) + if kvdb: + cmd.extend(["--kvdb", str(Path(kvdb).absolute())]) + if idx_dir: + cmd.extend(["--idx-dir", str(Path(idx_dir).absolute())]) + + # Options + cmd.extend(["--threads", str(threads)]) + cmd.extend(["--num_alignments", str(num_alignments)]) + cmd.extend(["--best", str(best)]) + cmd.extend(["--readfeed", str(readfeed)]) + + if fastx: + cmd.append("--fastx") + if aligned: + cmd.extend(["--aligned", aligned]) + if other: + cmd.extend(["--other", other]) + if blast is not None: + cmd.extend(["--blast", str(blast)]) + if paired_in: + cmd.append("--paired_in") + if paired_out: + cmd.append("--paired_out") + if out_order: + cmd.append("--out_order") + if verbose: + cmd.append("-v") + + try: + # Ensure workdir exists + Path(workdir).mkdir(parents=True, exist_ok=True) + + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + # Collect output files from workdir/out + output_path = Path(workdir) / "out" + output_files = [] + if output_path.exists(): + output_files = [str(f) for f in output_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" + } + except Exception as e: + return {"error": str(e), "status": "failed"} + +@mcp.tool() +def sortmerna_split( + files: List[str], + num_splits: int = 8, + workdir: str = "sortmerna_split_out", +): + """ + Utility to perform multithreaded splitting of large sequence files prior to SortMeRNA processing. + Uses rapidgzip and pigz internally (if available in the environment). + + Args: + files: List of sequence files to split. + num_splits: Number of chunks to split the files into. + workdir: Working directory to store split chunks and descriptors. + """ + if not files: + return {"error": "No files provided for splitting."} + + for f in files: + if not Path(f).exists(): + return {"error": f"File not found: {f}"} + + if num_splits < 1: + num_splits = 1 + + # Based on the documentation: python run.py split --file --num-splits --workdir + # Note: This assumes 'run.py' is available or the logic is accessible via the sortmerna environment. + # Since run.py is a helper script in the repo, we attempt to call it via python. + + cmd = ["python", "run.py", "split"] + for f in files: + cmd.extend(["--file", str(Path(f).absolute())]) + + cmd.extend(["--num-splits", str(num_splits)]) + cmd.extend(["--workdir", str(Path(workdir).absolute())]) + + try: + Path(workdir).mkdir(parents=True, exist_ok=True) + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + output_files = [str(f) for f in Path(workdir).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": "Ensure run.py from the SortMeRNA repository is in the current directory or PYTHONPATH.", + "status": "failed" + } + except Exception as e: + return {"error": str(e), "status": "failed"} + +@mcp.tool() +def sortmerna_version(): + """Check the installed version of SortMeRNA.""" + try: + result = subprocess.run(["sortmerna", "--version"], capture_output=True, text=True, check=True) + return { + "command_executed": "sortmerna --version", + "stdout": result.stdout, + "stderr": result.stderr + } + 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_sortmerna/requirements.txt b/Biomni/mcp_generated/mcp_sortmerna/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sortmerna/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_subread/Dockerfile b/Biomni/mcp_generated/mcp_subread/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b97a577a9a5954e0f1131f7d9a10ae19f173bc49 --- /dev/null +++ b/Biomni/mcp_generated/mcp_subread/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 subread via conda (e.g., from bioconda) +RUN conda install -c bioconda subread -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/subread_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/subread_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/subread_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_subread/app/__pycache__/subread_shim_server.cpython-311.pyc b/Biomni/mcp_generated/mcp_subread/app/__pycache__/subread_shim_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..487c070003a76e5661e5adc50d0a1e94507acaf3 Binary files /dev/null and b/Biomni/mcp_generated/mcp_subread/app/__pycache__/subread_shim_server.cpython-311.pyc differ diff --git a/Biomni/mcp_generated/mcp_subread/app/requirements.txt b/Biomni/mcp_generated/mcp_subread/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_subread/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_subread/app/subread_server.py b/Biomni/mcp_generated/mcp_subread/app/subread_server.py new file mode 100644 index 0000000000000000000000000000000000000000..8549fc236645f88e8f6418321cb624242f840d7a --- /dev/null +++ b/Biomni/mcp_generated/mcp_subread/app/subread_server.py @@ -0,0 +1,436 @@ +from typing import Optional, List +import subprocess +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_subread' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def subread_buildindex( + basename: str, + reference_fasta: str, + full_index: bool = False, + memory_limit: int = 8000, + index_split: bool = False, +): + """ + Build an index for the reference genome using subread-buildindex. + + :param basename: Base name of the index to be created. + :param reference_fasta: Path to the reference genome FASTA file. + :param full_index: If True, build a full index (larger but faster). Default is False (gapped index). + :param memory_limit: Memory usage limit in MB. Default is 8000. + :param index_split: If True, the index will be split into multiple chunks. + """ + fasta_path = Path(reference_fasta) + if not fasta_path.exists(): + return {"error": f"Reference FASTA file not found: {reference_fasta}"} + + cmd = ["subread-buildindex", "-o", basename] + if full_index: + cmd.append("-F") + if index_split: + cmd.append("-s") + cmd.extend(["-B", str(memory_limit)]) + cmd.append(str(fasta_path)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [f"{basename}.{ext}" for ext in ["files"]] # Index creates multiple files + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def subread_align( + index: str, + read1: str, + output: str, + read2: Optional[str] = None, + is_rna: bool = False, + nthreads: int = 1, + n_best_locations: int = 1, + min_votes: int = 3, + max_indels: int = 5, + sort_by_coordinates: bool = False, + keep_read_order: bool = False, + unique_mapping: bool = False, +): + """ + Align reads to a reference genome using subread-align. + + :param index: Base name of the index. + :param read1: Path to the first read file (FASTQ/FASTA). + :param output: Path to the output file (SAM/BAM). + :param read2: Path to the second read file for paired-end data. + :param is_rna: Set to True for RNA-seq reads, False for genomic DNA-seq reads. + :param nthreads: Number of threads to use. + :param n_best_locations: Number of best mapping locations to report. + :param min_votes: Minimum number of subreads required for a reporting a hit. + :param max_indels: Maximum number of indels allowed in the alignment. + :param sort_by_coordinates: If True, output location-sorted BAM file. + :param keep_read_order: Keep the same order of reads in the output as in the input. + :param unique_mapping: If True, only uniquely mapped reads are reported. + """ + if not Path(read1).exists(): + return {"error": f"Read file 1 not found: {read1}"} + + cmd = ["subread-align", "-i", index, "-r", read1, "-o", output] + + if read2: + if not Path(read2).exists(): + return {"error": f"Read file 2 not found: {read2}"} + cmd.extend(["-R", read2]) + + cmd.extend(["-t", "1" if is_rna else "0"]) + cmd.extend(["-T", str(nthreads)]) + cmd.extend(["-n", str(n_best_locations)]) + cmd.extend(["-m", str(min_votes)]) + cmd.extend(["-I", str(max_indels)]) + + if sort_by_coordinates: + cmd.append("--sortReadsByCoordinates") + if keep_read_order: + cmd.append("--keepReadOrder") + if unique_mapping: + cmd.append("-u") + + 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] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def subjunc( + index: str, + read1: str, + output: str, + read2: Optional[str] = None, + nthreads: int = 1, + n_best_locations: int = 1, + sort_by_coordinates: bool = False, + keep_read_order: bool = False, + all_junctions: bool = False, +): + """ + Align RNA-seq reads and detect exon-exon junctions using subjunc. + + :param index: Base name of the index. + :param read1: Path to the first read file. + :param output: Path to the output file. + :param read2: Path to the second read file for paired-end data. + :param nthreads: Number of threads. + :param n_best_locations: Number of best mapping locations to report. + :param sort_by_coordinates: Output location-sorted BAM. + :param keep_read_order: Keep input read order. + :param all_junctions: If True, report all discovered junctions. + """ + if not Path(read1).exists(): + return {"error": f"Read file 1 not found: {read1}"} + + cmd = ["subjunc", "-i", index, "-r", read1, "-o", output] + + if read2: + if not Path(read2).exists(): + return {"error": f"Read file 2 not found: {read2}"} + cmd.extend(["-R", read2]) + + cmd.extend(["-T", str(nthreads)]) + cmd.extend(["-n", str(n_best_locations)]) + + if sort_by_coordinates: + cmd.append("--sortReadsByCoordinates") + if keep_read_order: + cmd.append("--keepReadOrder") + if all_junctions: + cmd.append("-J") + + 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] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def feature_counts( + input_files: List[str], + annotation: str, + output: str, + format: str = "GTF", + feature_type: str = "exon", + attribute_type: str = "gene_id", + is_paired_end: bool = False, + count_read_pairs: bool = False, + strand_specificity: int = 0, + nthreads: int = 1, + min_mapping_quality: int = 0, + allow_multi_overlap: bool = False, + fraction: bool = False, + extra_attributes: Optional[str] = None, +): + """ + Count reads to genomic features using featureCounts. + + :param input_files: List of input BAM/SAM files. + :param annotation: Path to the annotation file (GTF/GFF/SAF). + :param output: Path to the output count file. + :param format: Annotation format: 'GTF' or 'SAF'. Default is 'GTF'. + :param feature_type: Feature type in GTF annotation (e.g., 'exon', 'gene'). + :param attribute_type: Attribute type in GTF annotation (e.g., 'gene_id', 'transcript_id'). + :param is_paired_end: If True, input reads are paired-end. + :param count_read_pairs: If True, count read pairs instead of individual reads. + :param strand_specificity: 0 (unstranded), 1 (stranded), 2 (reversely stranded). + :param nthreads: Number of threads. + :param min_mapping_quality: Minimum mapping quality score. + :param allow_multi_overlap: If True, reads overlapping multiple features will be counted. + :param fraction: If True, use fractional counts for multi-overlapping reads. + :param extra_attributes: Extra attributes to be included in the output. + """ + if not Path(annotation).exists(): + return {"error": f"Annotation file not found: {annotation}"} + + valid_inputs = [] + for f in input_files: + if Path(f).exists(): + valid_inputs.append(f) + else: + return {"error": f"Input file not found: {f}"} + + cmd = ["featureCounts", "-a", annotation, "-o", output] + cmd.extend(["-F", format]) + cmd.extend(["-t", feature_type]) + cmd.extend(["-g", attribute_type]) + cmd.extend(["-s", str(strand_specificity)]) + cmd.extend(["-T", str(nthreads)]) + cmd.extend(["-Q", str(min_mapping_quality)]) + + if is_paired_end: + cmd.append("-p") + if count_read_pairs: + cmd.append("--countReadPairs") + if allow_multi_overlap: + cmd.append("-O") + if fraction: + cmd.append("--fraction") + if extra_attributes: + cmd.extend(["--extraAttributes", extra_attributes]) + + cmd.extend(valid_inputs) + + 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, f"{output}.summary"] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def sublong( + index: str, + read: str, + output: str, + nthreads: int = 1, + n_best_locations: int = 1, +): + """ + Align long reads (Nanopore/PacBio) using sublong. + + :param index: Base name of the index. + :param read: Path to the long-read file. + :param output: Path to the output file. + :param nthreads: Number of threads. + :param n_best_locations: Number of best mapping locations to report. + """ + if not Path(read).exists(): + return {"error": f"Read file not found: {read}"} + + cmd = ["sublong", "-i", index, "-r", read, "-o", output] + cmd.extend(["-T", str(nthreads)]) + cmd.extend(["-n", str(n_best_locations)]) + + 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] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def exact_snp( + reference: str, + input_bam: str, + output_vcf: str, + annotation: Optional[str] = None, + min_base_quality: int = 13, + min_snp_fraction: float = 0.1, + min_coverage: int = 1, + nthreads: int = 1, +): + """ + Call SNPs using exactSNP. + + :param reference: Path to the reference genome FASTA file. + :param input_bam: Path to the input BAM/SAM file. + :param output_vcf: Path to the output VCF file. + :param annotation: Path to the gene annotation file (optional). + :param min_base_quality: Minimum base quality score. Default is 13. + :param min_snp_fraction: Minimum fraction of reads supporting a SNP. Default is 0.1. + :param min_coverage: Minimum coverage required for SNP calling. Default is 1. + :param nthreads: Number of threads. + """ + if not Path(reference).exists(): + return {"error": f"Reference file not found: {reference}"} + if not Path(input_bam).exists(): + return {"error": f"Input BAM file not found: {input_bam}"} + + cmd = ["exactSNP", "-g", reference, "-i", input_bam, "-o", output_vcf] + cmd.extend(["-Q", str(min_base_quality)]) + cmd.extend(["-f", str(min_snp_fraction)]) + cmd.extend(["-c", str(min_coverage)]) + cmd.extend(["-T", str(nthreads)]) + + if annotation: + if not Path(annotation).exists(): + return {"error": f"Annotation file not found: {annotation}"} + cmd.extend(["-a", annotation]) + + 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_vcf] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def flatten_gtf( + input_gtf: str, + output_saf: str, + combine_overlapping: bool = True, +): + """ + Flatten a GTF/GFF annotation into a SAF format annotation. + + :param input_gtf: Path to the input GTF/GFF file. + :param output_saf: Path to the output SAF file. + :param combine_overlapping: If True, combine overlapping exons. If False, chop into bins. + """ + if not Path(input_gtf).exists(): + return {"error": f"Input GTF file not found: {input_gtf}"} + + cmd = ["flattenGTF", "-i", input_gtf, "-o", output_saf] + if not combine_overlapping: + cmd.append("-C") # Chop into non-overlapping bins + + 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_saf] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def remove_dup( + input_file: str, + output_file: str, + remove_duplicates: bool = True, +): + """ + Remove or mark duplicate reads in a BAM/SAM file. + + :param input_file: Path to the input BAM/SAM file. + :param output_file: Path to the output BAM/SAM file. + :param remove_duplicates: If True, remove duplicates. If False, mark them. + """ + if not Path(input_file).exists(): + return {"error": f"Input file not found: {input_file}"} + + cmd = ["removeDup", "-i", input_file, "-o", output_file] + if remove_duplicates: + cmd.append("-r") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_file] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_subread/app/subread_shim_server.py b/Biomni/mcp_generated/mcp_subread/app/subread_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1bb33132d22eea3eafd2155b4d88d9e0d099b281 --- /dev/null +++ b/Biomni/mcp_generated/mcp_subread/app/subread_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_subread/app/subread_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_subread' + + +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_subread/requirements.txt b/Biomni/mcp_generated/mcp_subread/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_subread/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_tabix/Dockerfile b/Biomni/mcp_generated/mcp_tabix/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..927298ae7c7ef6904600460b0a9b64ca1b57e0dc --- /dev/null +++ b/Biomni/mcp_generated/mcp_tabix/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 tabix via conda (e.g., from bioconda) +RUN conda install -c bioconda tabix -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/tabix_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/tabix_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/tabix_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_tabix/app/__pycache__/tabix_server.cpython-311.pyc b/Biomni/mcp_generated/mcp_tabix/app/__pycache__/tabix_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d0d47a2a58366af464f8695f5104f4c3c49cbac Binary files /dev/null and b/Biomni/mcp_generated/mcp_tabix/app/__pycache__/tabix_server.cpython-311.pyc differ diff --git a/Biomni/mcp_generated/mcp_tabix/app/__pycache__/tabix_shim_server.cpython-311.pyc b/Biomni/mcp_generated/mcp_tabix/app/__pycache__/tabix_shim_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d7f88b63323fa5bd28b4daf05b056898a88c6c4 Binary files /dev/null and b/Biomni/mcp_generated/mcp_tabix/app/__pycache__/tabix_shim_server.cpython-311.pyc differ diff --git a/Biomni/mcp_generated/mcp_tabix/app/requirements.txt b/Biomni/mcp_generated/mcp_tabix/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_tabix/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_tabix/app/tabix_server.py b/Biomni/mcp_generated/mcp_tabix/app/tabix_server.py new file mode 100644 index 0000000000000000000000000000000000000000..01475cb34a84101d49c386b349d15f1b29c2be94 --- /dev/null +++ b/Biomni/mcp_generated/mcp_tabix/app/tabix_server.py @@ -0,0 +1,197 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Literal + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_tabix' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def tabix( + in_tab_bgz: Path, + regions: Optional[List[str]] = None, + preset: Literal["gff", "bed", "sam", "vcf", "psltbl"] = "gff", + sequence_name_column: int = 1, + start_column: int = 4, + end_column: int = 5, + skip_first_lines: int = 0, + comment_char: str = "#", + replace_header_file: Optional[Path] = None, + bed_regions: bool = False, + zero_based_coordinate: bool = False, + print_header_lines: bool = False, + print_only_header_lines: bool = False, + list_chromosome_names: bool = False, + force_overwrite_index: bool = False, +) -> dict: + """ + Indexes a TAB-delimited file or retrieves regions from an indexed file. + + This tool can create a tabix index (.tbi or .csi) for a BGZF-compressed, + tab-delimited file, or query specific genomic regions from an already + indexed file. + + Args: + in_tab_bgz: Path to the input BGZF-compressed, tab-delimited file. + Must exist and be readable. + regions: A list of genomic regions to query (e.g., ["chr1:100-200", "chr2"]). + If not provided, tabix will attempt to create an index for the file. + preset: Format preset for indexing. Choose from "gff", "bed", "sam", "vcf", "psltbl". + Defaults to "gff". + sequence_name_column: Column for the sequence name (e.g., chromosome). 1-based. + Defaults to 1. + start_column: Column for the start coordinate. 1-based. Defaults to 4. + end_column: Column for the end coordinate. 1-based. Can be identical to start_column. + Defaults to 5. + skip_first_lines: Skip the first INT lines. Defaults to 0. + comment_char: Symbol for comment/meta lines. Defaults to "#". Must be a single character. + replace_header_file: Path to a file whose content will replace the header of the input file. + If provided, must exist and be readable. + bed_regions: Treat the first region argument as a BED file containing regions to query. + If True, `regions` must contain at least one path to a BED file. + zero_based_coordinate: Use zero-based coordinates for start/end columns. Defaults to False. + print_header_lines: Print header lines along with data lines when querying. Defaults to False. + print_only_header_lines: Print only the header lines. Mutually exclusive with `print_header_lines`. + Defaults to False. + list_chromosome_names: List chromosome names present in the file. Mutually exclusive with + region queries and index creation options. Defaults to False. + force_overwrite_index: Force to overwrite an existing index file. Defaults to False. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + """ + # Input validation + if not in_tab_bgz.is_file(): + raise FileNotFoundError(f"Input file not found: {in_tab_bgz}") + + if replace_header_file: + if not replace_header_file.is_file(): + raise FileNotFoundError(f"Header replacement file not found: {replace_header_file}") + + if not (1 <= sequence_name_column): + raise ValueError("Sequence name column must be 1 or greater.") + if not (1 <= start_column): + raise ValueError("Start column must be 1 or greater.") + if not (1 <= end_column): + raise ValueError("End column must be 1 or greater.") + if not (0 <= skip_first_lines): + raise ValueError("Number of lines to skip must be non-negative.") + + if len(comment_char) != 1: + raise ValueError("Comment character must be a single character.") + + # Mutual exclusivity checks + if print_header_lines and print_only_header_lines: + raise ValueError("Options -h (print_header_lines) and -H (print_only_header_lines) are mutually exclusive.") + + if list_chromosome_names: + if regions: + raise ValueError("Cannot specify regions when listing chromosome names (-l).") + if bed_regions: + raise ValueError("Cannot specify BED regions when listing chromosome names (-l).") + # While other indexing options (preset, columns, etc.) might be technically allowed, + # they are usually irrelevant for -l and might lead to unexpected behavior or be ignored. + # The current implementation allows them, but tabix might ignore them. + + if bed_regions: + if not regions: + raise ValueError("If 'bed_regions' is True, 'regions' must be provided and contain at least one BED file path.") + # The help states "region1 is a BED file". This implies the first element of `regions` is a path. + if not Path(regions[0]).is_file(): + raise FileNotFoundError(f"The first region specified for BED regions (-B) must be a valid BED file path: {regions[0]}") + + command = ["tabix"] + + # Add options + if preset != "gff": # Only add if not default + command.extend(["-p", preset]) + if sequence_name_column != 1: + command.extend(["-s", str(sequence_name_column)]) + if start_column != 4: + command.extend(["-b", str(start_column)]) + if end_column != 5: + command.extend(["-e", str(end_column)]) + if skip_first_lines != 0: + command.extend(["-S", str(skip_first_lines)]) + if comment_char != "#": + command.extend(["-c", comment_char]) + if replace_header_file: + command.extend(["-r", str(replace_header_file)]) + if bed_regions: + command.append("-B") + if zero_based_coordinate: + command.append("-0") + if print_header_lines: + command.append("-h") + if print_only_header_lines: + command.append("-H") + if list_chromosome_names: + command.append("-l") + if force_overwrite_index: + command.append("-f") + + # Add input file + command.append(str(in_tab_bgz)) + + # Add regions + if regions: + command.extend(regions) + + command_str = " ".join(command) + stdout = "" + stderr = "" + output_files = [] + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True + ) + stdout = process.stdout + stderr = process.stderr + + # Determine if an index file was created/overwritten. + # This happens if no regions are specified (indexing mode) or if -f is used. + # The index file is typically .tbi or .csi. + # For tabix 0.2.5, .tbi is the standard. + # If regions are provided, it's a query, not an index creation. + if not regions or force_overwrite_index: + # Check for .tbi index + index_file_tbi = in_tab_bgz.with_suffix(in_tab_bgz.suffix + ".tbi") + if index_file_tbi.exists(): + output_files.append(str(index_file_tbi)) + else: + # Check for .csi index (used by newer htslib/tabix for larger chromosomes) + index_file_csi = in_tab_bgz.with_suffix(in_tab_bgz.suffix + ".csi") + if index_file_csi.exists(): + output_files.append(str(index_file_csi)) + + except subprocess.CalledProcessError as e: + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + "error": f"tabix command failed with exit code {e.returncode}" + } + except FileNotFoundError: + return { + "command_executed": command_str, + "stdout": "", + "stderr": "tabix command not found. Is it installed and in your PATH?", + "output_files": [], + "error": "tabix executable not found." + } + + 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_tabix/app/tabix_shim_server.py b/Biomni/mcp_generated/mcp_tabix/app/tabix_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..dc71b1098881374372ccd090cc7a9b7fe9f4137f --- /dev/null +++ b/Biomni/mcp_generated/mcp_tabix/app/tabix_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_tabix/app/tabix_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_tabix' + + +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_tabix/docker-compose.yml b/Biomni/mcp_generated/mcp_tabix/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..53ab6a2ea5390fc22bc88972319f506779b8c6b5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_tabix/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-tabix: + build: . + image: mcp-tabix:latest + container_name: mcp-tabix + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=tabix + 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_tabix/environment.yaml b/Biomni/mcp_generated/mcp_tabix/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cb95ca11f53710b290ec4898530d3a991d1afd16 --- /dev/null +++ b/Biomni/mcp_generated/mcp_tabix/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - tabix + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_tabix/requirements.txt b/Biomni/mcp_generated/mcp_tabix/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_tabix/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_talon/Dockerfile b/Biomni/mcp_generated/mcp_talon/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b5bd5e8f318ea57317d28e48f83befca9efe5842 --- /dev/null +++ b/Biomni/mcp_generated/mcp_talon/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 talon via conda (e.g., from bioconda) +RUN conda install -c bioconda talon -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/talon_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/talon_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/talon_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_talon/app/requirements.txt b/Biomni/mcp_generated/mcp_talon/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_talon/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_talon/app/talon_server.py b/Biomni/mcp_generated/mcp_talon/app/talon_server.py new file mode 100644 index 0000000000000000000000000000000000000000..13925f3264759d79830f3e18f5cd0c2c1aed1f10 --- /dev/null +++ b/Biomni/mcp_generated/mcp_talon/app/talon_server.py @@ -0,0 +1,545 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List + +# Set up logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +# 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(): + def decorator(func): + return func + return decorator + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_talon' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def talon_initialize_database( + fasta: Path, + gtf: Path, + annotation: str, + l: int = 500, + idprefix: str = "TALON", + outprefix: str = "talon" +) -> dict: + """ + Initializes a TALON database from a reference genome FASTA and a GTF annotation. + + Args: + fasta: Path to the reference genome FASTA file. + gtf: Path to the reference genome GTF file. + annotation: Name for the annotation being processed. + l: Length of the 5-prime novelty buffer in bases. Default is 500. + idprefix: Prefix for all TALON-generated gene and transcript IDs. Default is 'TALON'. + outprefix: Prefix for output files. The database will be named .db. Default is 'talon'. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not fasta.is_file(): + raise FileNotFoundError(f"Input FASTA file not found: {fasta}") + if not gtf.is_file(): + raise FileNotFoundError(f"Input GTF file not found: {gtf}") + if l < 0: + raise ValueError("Novelty buffer length 'l' must be a non-negative integer.") + + output_db = Path(f"{outprefix}.db") + + cmd = [ + "talon_initialize_database", + "--f", str(fasta), + "--g", str(gtf), + "--a", annotation, + "--l", str(l), + "--idprefix", idprefix, + "--o", outprefix + ] + + 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": [str(output_db.resolve())] + } + except subprocess.CalledProcessError as e: + logging.error(f"Error executing talon_initialize_database: {e.stderr}") + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "TALON command failed", + "return_code": e.returncode + } + +@mcp.tool() +def talon_label_reads( + sam_file: Path, + db: Path, + threads: int = 1, + max_internal_prime: int = 50, + outprefix: str = "talon" +) -> dict: + """ + Labels reads in a SAM file based on a TALON database. + + Args: + sam_file: Path to the SAM file of reads aligned to the genome. + db: Path to the TALON database file. + threads: Number of threads to use. Default is 1. + max_internal_prime: Max distance allowed between read ends and transcript ends + to consider the read an internal prime. Default is 50. + outprefix: Prefix for output files. Default is 'talon'. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not sam_file.is_file(): + raise FileNotFoundError(f"Input SAM file not found: {sam_file}") + if not db.is_file(): + raise FileNotFoundError(f"TALON database file not found: {db}") + if threads <= 0: + raise ValueError("Number of threads must be a positive integer.") + if max_internal_prime < 0: + raise ValueError("max_internal_prime must be a non-negative integer.") + + output_sam = Path(f"{outprefix}_labeled.sam") + + cmd = [ + "talon_label_reads", + "--f", str(sam_file), + "--d", str(db), + "--t", str(threads), + "--ar", str(max_internal_prime), + "--o", outprefix + ] + + 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": [str(output_sam.resolve())] + } + except subprocess.CalledProcessError as e: + logging.error(f"Error executing talon_label_reads: {e.stderr}") + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "TALON command failed", + "return_code": e.returncode + } + +@mcp.tool() +def talon( + config: Path, + db: Path, + build: Optional[str] = None, + min_coverage: float = 0.9, + min_identity: float = 0.8, + outprefix: str = "talon" +) -> dict: + """ + Runs the main TALON tool to identify novel genes and transcripts from long read data. + + Args: + config: Path to a file indicating sample, platform, and SAM file location for each input. + db: Path to the TALON database file. + build: Genome build name (e.g., 'hg38'). + min_coverage: Minimum query coverage to accept a read. Default is 0.9. + min_identity: Minimum query identity to accept a read. Default is 0.8. + outprefix: Prefix for output files. Default is 'talon'. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not config.is_file(): + raise FileNotFoundError(f"Input config file not found: {config}") + if not db.is_file(): + raise FileNotFoundError(f"TALON database file not found: {db}") + if not (0.0 <= min_coverage <= 1.0): + raise ValueError("min_coverage must be between 0.0 and 1.0.") + if not (0.0 <= min_identity <= 1.0): + raise ValueError("min_identity must be between 0.0 and 1.0.") + + cmd = [ + "talon", + "--f", str(config), + "--db", str(db), + "--cov", str(min_coverage), + "--identity", str(min_identity), + "--o", outprefix + ] + if build: + cmd.extend(["--build", build]) + + output_files = [ + str(Path(f"{outprefix}_talon.log").resolve()), + str(Path(f"{outprefix}_unfiltered_transcripts.tsv").resolve()), + str(Path(f"{outprefix}_read_annot.tsv").resolve()), + str(Path(f"{outprefix}_abundance.tsv").resolve()) + ] + + 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: + logging.error(f"Error executing talon: {e.stderr}") + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "TALON command failed", + "return_code": e.returncode + } + +@mcp.tool() +def talon_summarize( + db: Path, + outprefix: str = "talon" +) -> dict: + """ + Generates a summary of the contents of a TALON database. + + Args: + db: Path to the TALON database file. + outprefix: Prefix for output files. Default is 'talon'. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not db.is_file(): + raise FileNotFoundError(f"TALON database file not found: {db}") + + output_summary = Path(f"{outprefix}_summary.tsv") + + cmd = [ + "talon_summarize", + "--db", str(db), + "--o", outprefix + ] + + 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": [str(output_summary.resolve())] + } + except subprocess.CalledProcessError as e: + logging.error(f"Error executing talon_summarize: {e.stderr}") + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "TALON command failed", + "return_code": e.returncode + } + +@mcp.tool() +def talon_filter_transcripts( + db: Path, + annotation: Optional[str] = None, + datasets: Optional[str] = None, + max_frac_a: float = 0.5, + min_count: int = 1, + min_datasets: int = 1, + outprefix: str = "talon" +) -> dict: + """ + Filters transcripts based on read support and other quality metrics. + + Args: + db: Path to the TALON database file. + annotation: Annotation name. If not provided, all annotations are used. + datasets: Comma-separated list of datasets to include. If not provided, all are used. + max_frac_a: Maximum fraction of A's downstream of the 3' end. Default is 0.5. + min_count: Minimum number of reads supporting a transcript. Default is 1. + min_datasets: Minimum number of datasets a transcript must be observed in. Default is 1. + outprefix: Prefix for output files. Default is 'talon'. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not db.is_file(): + raise FileNotFoundError(f"TALON database file not found: {db}") + if not (0.0 <= max_frac_a <= 1.0): + raise ValueError("max_frac_a must be between 0.0 and 1.0.") + if min_count < 1: + raise ValueError("min_count must be at least 1.") + if min_datasets < 1: + raise ValueError("min_datasets must be at least 1.") + + cmd = [ + "talon_filter_transcripts", + "--db", str(db), + "--maxFracA", str(max_frac_a), + "--minCount", str(min_count), + "--minDatasets", str(min_datasets), + "--o", outprefix + ] + if annotation: + cmd.extend(["--a", annotation]) + if datasets: + cmd.extend(["--datasets", datasets]) + + output_files = [ + str(Path(f"{outprefix}_talon_whitelist.csv").resolve()), + str(Path(f"{outprefix}_talon_filtered_transcripts.tsv").resolve()) + ] + + 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: + logging.error(f"Error executing talon_filter_transcripts: {e.stderr}") + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "TALON command failed", + "return_code": e.returncode + } + +@mcp.tool() +def talon_abundance( + db: Path, + annotation: str, + datasets: Optional[str] = None, + build: Optional[str] = None, + outprefix: str = "talon" +) -> dict: + """ + Computes abundance estimates for genes and transcripts. + + Args: + db: Path to the TALON database file. + annotation: Annotation name to use for the analysis. + datasets: Comma-separated list of datasets to include. If not provided, all are used. + build: Genome build name. + outprefix: Prefix for output files. Default is 'talon'. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not db.is_file(): + raise FileNotFoundError(f"TALON database file not found: {db}") + + cmd = [ + "talon_abundance", + "--db", str(db), + "-a", annotation, + "--o", outprefix + ] + if datasets: + cmd.extend(["--d", datasets]) + if build: + cmd.extend(["--build", build]) + + output_files = [ + str(Path(f"{outprefix}_abundance.tsv").resolve()), + str(Path(f"{outprefix}_read_annot.tsv").resolve()), + str(Path(f"{outprefix}_gene_abundance.tsv").resolve()), + str(Path(f"{outprefix}_transcript_abundance.tsv").resolve()) + ] + + 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: + logging.error(f"Error executing talon_abundance: {e.stderr}") + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "TALON command failed", + "return_code": e.returncode + } + +@mcp.tool() +def talon_create_gtf( + db: Path, + annotation: str, + build: Optional[str] = None, + datasets: Optional[str] = None, + outprefix: str = "talon" +) -> dict: + """ + Creates a GTF file of novel transcripts from a TALON database. + + Args: + db: Path to the TALON database file. + annotation: Annotation name to use for the analysis. + build: Genome build name. + datasets: Comma-separated list of datasets to include. If not provided, all are used. + outprefix: Prefix for output files. Default is 'talon'. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not db.is_file(): + raise FileNotFoundError(f"TALON database file not found: {db}") + + output_gtf = Path(f"{outprefix}_talon.gtf") + + cmd = [ + "talon_create_GTF", + "--db", str(db), + "-a", annotation, + "--o", outprefix + ] + if build: + cmd.extend(["--build", build]) + if datasets: + cmd.extend(["--datasets", datasets]) + + 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": [str(output_gtf.resolve())] + } + except subprocess.CalledProcessError as e: + logging.error(f"Error executing talon_create_GTF: {e.stderr}") + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "TALON command failed", + "return_code": e.returncode + } + +@mcp.tool() +def talon_get_sjs( + db: Path, + annotation: Optional[str] = None, + build: Optional[str] = None, + datasets: Optional[str] = None, + outprefix: str = "talon" +) -> dict: + """ + Outputs a splice junction file from a TALON database. + + Args: + db: Path to the TALON database file. + annotation: Annotation name to use. If not provided, all annotations are used. + build: Genome build name. + datasets: Comma-separated list of datasets to include. If not provided, all are used. + outprefix: Prefix for output files. Default is 'talon'. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # Input validation + if not db.is_file(): + raise FileNotFoundError(f"TALON database file not found: {db}") + + output_sj = Path(f"{outprefix}_talon_SJ.tsv") + + cmd = [ + "talon_get_sjs", + "--db", str(db), + "--o", outprefix + ] + if annotation: + cmd.extend(["-a", annotation]) + if build: + cmd.extend(["--build", build]) + if datasets: + cmd.extend(["--datasets", datasets]) + + 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": [str(output_sj.resolve())] + } + except subprocess.CalledProcessError as e: + logging.error(f"Error executing talon_get_sjs: {e.stderr}") + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "error": "TALON command failed", + "return_code": e.returncode + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_talon/app/talon_shim_server.py b/Biomni/mcp_generated/mcp_talon/app/talon_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..47f90db707b3910bf4840bea4f9cf8970556ff61 --- /dev/null +++ b/Biomni/mcp_generated/mcp_talon/app/talon_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_talon/app/talon_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_talon' + + +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_talon/requirements.txt b/Biomni/mcp_generated/mcp_talon/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_talon/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/app/requirements.txt b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/app/ucsc-gtftogenepred_server.py b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/app/ucsc-gtftogenepred_server.py new file mode 100644 index 0000000000000000000000000000000000000000..80c46a5ad886c94674b142a4b41461dd7f94d242 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/app/ucsc-gtftogenepred_server.py @@ -0,0 +1,145 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_ucsc_gtftogenepred' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def ucsc_gtftogenepred( + input_gtf: str, + output_gp: str, + gene_db: bool = False, + all_errors: bool = False, + ignore_groups_without_exons: bool = False, + info_out: Optional[str] = None, + source_out: Optional[str] = None, + gene_name_as_name2: bool = False, + include_version: bool = False, + implied_stop: bool = False, + simple: bool = False, + prefix: Optional[str] = None, +) -> Dict[str, Any]: + """ + Convert a GTF file to a genePred file using UCSC gtfToGenePred. + + Args: + input_gtf: Path to the input GTF file. + output_gp: Path to the output genePred file. + gene_db: Use gene_id for name, transcript_id for name2. + all_errors: Process all errors, don't stop at first. + ignore_groups_without_exons: Skip transcripts with no exons. + info_out: Write info about each transcript to this file. + source_out: Write source of each transcript to this file. + gene_name_as_name2: Use gene_name for name2 field. + include_version: Include version number in transcript/gene IDs. + implied_stop: GTF doesn't include stop codon in CDS, add it. + simple: Simple GTF (no gene_id/transcript_id). + prefix: Prefix to add to transcript names. + """ + # Input validation + input_path = Path(input_gtf) + if not input_path.exists(): + return { + "command_executed": "", + "stdout": "", + "stderr": f"Error: Input GTF file '{input_gtf}' does not exist.", + "output_files": [] + } + + output_path = Path(output_gp) + if not output_path.parent.exists(): + try: + output_path.parent.mkdir(parents=True, exist_ok=True) + except Exception as e: + return { + "command_executed": "", + "stdout": "", + "stderr": f"Error: Could not create directory for output file '{output_gp}': {str(e)}", + "output_files": [] + } + + # Build command + # The binary name in the UCSC suite is gtfToGenePred + cmd = ["gtfToGenePred"] + + # Add boolean flags + if gene_db: + cmd.append("-geneDb") + if all_errors: + cmd.append("-allErrors") + if ignore_groups_without_exons: + cmd.append("-ignoreGroupsWithoutExons") + if gene_name_as_name2: + cmd.append("-geneNameAsName2") + if include_version: + cmd.append("-includeVersion") + if implied_stop: + cmd.append("-impliedStop") + if simple: + cmd.append("-simple") + + # Add options with values + if info_out: + info_out_path = Path(info_out) + if not info_out_path.parent.exists(): + info_out_path.parent.mkdir(parents=True, exist_ok=True) + cmd.append(f"-infoOut={info_out}") + + if source_out: + source_out_path = Path(source_out) + if not source_out_path.parent.exists(): + source_out_path.parent.mkdir(parents=True, exist_ok=True) + cmd.append(f"-sourceOut={source_out}") + + if prefix: + cmd.append(f"-prefix={prefix}") + + # Add positional arguments + cmd.append(str(input_path)) + cmd.append(str(output_path)) + + try: + # Execute the tool + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + + # Collect output files + output_files = [str(output_path)] + if info_out: + output_files.append(str(info_out)) + if source_out: + output_files.append(str(source_out)) + + 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": f"Error during execution: {e.stderr}", + "output_files": [] + } + except FileNotFoundError: + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Error: 'gtfToGenePred' command not found. Please ensure UCSC tools are installed and in your PATH.", + "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_ucsc-gtftogenepred/app/ucsc-gtftogenepred_shim_server.py b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/app/ucsc-gtftogenepred_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d6303ffe70dec7a86ea5decf04c21af0edbcb849 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/app/ucsc-gtftogenepred_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-gtftogenepred/app/ucsc-gtftogenepred_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_ucsc_gtftogenepred' + + +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-gtftogenepred/docker-compose.yml b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c5c85f65bd2fcedff7c2801c431de17bf3d9f3cb --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-ucsc-gtftogenepred: + build: . + image: mcp-ucsc-gtftogenepred:latest + container_name: mcp-ucsc-gtftogenepred + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=ucsc-gtftogenepred + 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-gtftogenepred/requirements.txt b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_ucsc-gtftogenepred/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp