diff --git a/Biomni/mcp_generated/mcp_bamtools/Dockerfile b/Biomni/mcp_generated/mcp_bamtools/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0c00395bd1b2d1ebcdee0a4e47e9bae314fa2abd --- /dev/null +++ b/Biomni/mcp_generated/mcp_bamtools/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 bamtools via conda (e.g., from bioconda) +RUN conda install -c bioconda bamtools -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 bamtools_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/bamtools_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/bamtools_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bamtools/app/bamtools_server.py b/Biomni/mcp_generated/mcp_bamtools/app/bamtools_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0573b3d9e985224a666a67d90414f8ad96cf0093 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bamtools/app/bamtools_server.py @@ -0,0 +1,712 @@ +import subprocess +import shlex +import os +from pathlib import Path +from typing import List, Optional + +# In a real MCP environment, the 'mcp' object with its decorators +# would be provided by the framework. This is a placeholder for standalone validation. +class _MCP: + def tool(self, *args, **kwargs): + def decorator(f): + return f + return decorator +mcp = _MCP() + +### Bamtools Tool Definitions ### + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bamtools' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def convert( + in_bam: Path, + format: str, + out: Optional[Path] = None, + region: Optional[str] = None, +) -> dict: + """ + Converts a BAM file to various other formats. + + Args: + in_bam: The input BAM file. + format: The output format. Must be one of: bed, fasta, fastq, json, pileup, sam, yaml. + out: The output filename. If not provided, output is sent to stdout. + region: Only convert alignments that overlap this region (e.g., "ref:start-end"). + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + if not in_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {in_bam}") + + valid_formats = {"bed", "fasta", "fastq", "json", "pileup", "sam", "yaml"} + if format not in valid_formats: + raise ValueError(f"Invalid format '{format}'. Must be one of {valid_formats}") + + cmd = ["bamtools", "convert", "-in", str(in_bam), "-format", format] + output_files = [] + + if out: + cmd.extend(["-out", str(out)]) + output_files.append(str(out)) + if region: + cmd.extend(["-region", region]) + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools convert failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def count(in_bams: List[Path]) -> dict: + """ + Prints the number of alignments in one or more BAM files. + + Args: + in_bams: A list of input BAM files. + + Returns: + A dictionary containing the command executed, stdout (the count), and stderr. + """ + if not in_bams: + raise ValueError("At least one input BAM file must be provided.") + + cmd = ["bamtools", "count"] + for bam_file in in_bams: + if not bam_file.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam_file}") + cmd.extend(["-in", str(bam_file)]) + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools count failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def coverage(in_bams: List[Path]) -> dict: + """ + Prints coverage statistics from one or more BAM files. + + Args: + in_bams: A list of input BAM files. + + Returns: + A dictionary containing the command executed, stdout (coverage stats), and stderr. + """ + if not in_bams: + raise ValueError("At least one input BAM file must be provided.") + + cmd = ["bamtools", "coverage"] + for bam_file in in_bams: + if not bam_file.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam_file}") + cmd.extend(["-in", str(bam_file)]) + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools coverage failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def filter_bam( + in_bams: List[Path], + out_bam: Optional[Path] = None, + region: Optional[str] = None, + property_filters: Optional[List[str]] = None, + tag_filters: Optional[List[str]] = None, + script: Optional[Path] = None, + list_file: Optional[Path] = None, + force_compression: bool = False, +) -> dict: + """ + Filters BAM file(s) by user-specified criteria. + + Args: + in_bams: The input BAM file(s). + out_bam: The output BAM file. If not provided, output is sent to stdout. + region: Only keeps alignments that overlap this region. + property_filters: List of "TAG:VALUE" strings to filter by BamAlignment property. + tag_filters: List of "TAG:VALUE" strings to filter by BamAlignment tag. + script: A file containing a list of filter rules. + list_file: A file containing a list of read names to be kept. + force_compression: If True, the output BAM file will be compressed. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + if not in_bams: + raise ValueError("At least one input BAM file must be provided.") + + cmd = ["bamtools", "filter"] + output_files = [] + + for bam_file in in_bams: + if not bam_file.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam_file}") + cmd.extend(["-in", str(bam_file)]) + + if out_bam: + cmd.extend(["-out", str(out_bam)]) + output_files.append(str(out_bam)) + if region: + cmd.extend(["-region", region]) + if property_filters: + for p_filter in property_filters: + cmd.extend(["-property", p_filter]) + if tag_filters: + for t_filter in tag_filters: + cmd.extend(["-tag", t_filter]) + if script: + if not script.is_file(): + raise FileNotFoundError(f"Script file not found: {script}") + cmd.extend(["-script", str(script)]) + if list_file: + if not list_file.is_file(): + raise FileNotFoundError(f"List file not found: {list_file}") + cmd.extend(["-list", str(list_file)]) + if force_compression: + cmd.append("-forceCompression") + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools filter failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def header(in_bam: Path) -> dict: + """ + Prints the header from a BAM file. + + Args: + in_bam: The input BAM file. + + Returns: + A dictionary containing the command executed, stdout (the header), and stderr. + """ + if not in_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {in_bam}") + + cmd = ["bamtools", "header", "-in", str(in_bam)] + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools header failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def index(in_bam: Path) -> dict: + """ + Generates an index for a BAM file. The index file (.bai) is created in the same directory. + + Args: + in_bam: The input BAM file to index. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the path to the generated index file. + """ + if not in_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {in_bam}") + + cmd = ["bamtools", "index", "-in", str(in_bam)] + output_index_file = in_bam.with_suffix(in_bam.suffix + '.bai') + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + if not output_index_file.is_file(): + raise FileNotFoundError(f"Expected index file was not created: {output_index_file}") + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_index_file)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools index failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def merge( + in_bams: List[Path], + out_bam: Optional[Path] = None, + region: Optional[str] = None, + force: bool = False, +) -> dict: + """ + Merges multiple BAM files into a single file. + + Args: + in_bams: A list of input BAM files to merge. Must contain at least two files. + out_bam: The output BAM file. If not provided, output is sent to stdout. + region: Merges only alignments that overlap this region. + force: Forces merge, even if headers are not identical. Uses the first BAM file's header. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + if len(in_bams) < 2: + raise ValueError("At least two input BAM files must be provided for merging.") + + cmd = ["bamtools", "merge"] + output_files = [] + + for bam_file in in_bams: + if not bam_file.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam_file}") + cmd.extend(["-in", str(bam_file)]) + + if out_bam: + cmd.extend(["-out", str(out_bam)]) + output_files.append(str(out_bam)) + if region: + cmd.extend(["-region", region]) + if force: + cmd.append("-force") + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools merge failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def random( + in_bams: List[Path], + out_bam: Optional[Path] = None, + n: int = 100, + seed: int = 0, +) -> dict: + """ + Selects a random number of alignments from existing BAM file(s). + + Args: + in_bams: The input BAM file(s). + out_bam: The output BAM file. If not provided, output is sent to stdout. + n: Number of alignments to select. + seed: Random seed. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + if not in_bams: + raise ValueError("At least one input BAM file must be provided.") + + cmd = ["bamtools", "random", "-n", str(n), "-seed", str(seed)] + output_files = [] + + for bam_file in in_bams: + if not bam_file.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam_file}") + cmd.extend(["-in", str(bam_file)]) + + if out_bam: + cmd.extend(["-out", str(out_bam)]) + output_files.append(str(out_bam)) + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools random failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def resolve(in_bam: Path, out_bam: Path) -> dict: + """ + Resolves paired-end reads, marking the IsProperPair flag as needed. + + Args: + in_bam: The input BAM file. + out_bam: The output BAM file. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the output file path. + """ + if not in_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {in_bam}") + + cmd = ["bamtools", "resolve", "-in", str(in_bam), "-out", str(out_bam)] + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(out_bam)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools resolve failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def revert(in_bam: Path, out_bam: Path) -> dict: + """ + Removes duplicate marks and restores original base qualities from a BAM file. + + Args: + in_bam: The input BAM file. + out_bam: The output BAM file. + + Returns: + A dictionary containing the command executed, stdout, stderr, and the output file path. + """ + if not in_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {in_bam}") + + cmd = ["bamtools", "revert", "-in", str(in_bam), "-out", str(out_bam)] + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(out_bam)], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools revert failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def sort( + in_bam: Path, + out_bam: Optional[Path] = None, + by: str = "position", + order: str = "ascending", + n: int = 1000, + mem: int = 512, +) -> dict: + """ + Sorts a BAM file by position or name. + + Args: + in_bam: The input BAM file. + out_bam: The output BAM file. If not provided, output is sent to stdout. + by: Sort by 'position' or 'name'. + order: Sort in 'ascending' or 'descending' order. + n: Max number of alignments to buffer in memory. + mem: Max memory to use (in megabytes). + + Returns: + A dictionary containing the command executed, stdout, stderr, and output files. + """ + if not in_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {in_bam}") + if by not in ["position", "name"]: + raise ValueError("Sort 'by' must be 'position' or 'name'.") + if order not in ["ascending", "descending"]: + raise ValueError("Sort 'order' must be 'ascending' or 'descending'.") + + cmd = ["bamtools", "sort", "-in", str(in_bam), "-by", by, "-order", order, "-n", str(n), "-mem", str(mem)] + output_files = [] + + if out_bam: + cmd.extend(["-out", str(out_bam)]) + output_files.append(str(out_bam)) + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools sort failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def split( + in_bam: Path, + output_dir: Path, + stub: Optional[str] = None, + by: Optional[str] = None, + reference: bool = False, + mapped: bool = False, + paired: bool = False, + tag: Optional[str] = None, +) -> dict: + """ + Splits a BAM file based on a specified property, creating multiple output BAM files. + + Args: + in_bam: The input BAM file. + output_dir: The directory where output files will be created. + stub: The prefix for output file names. Defaults to the input file name. + by: Split file by a specific property. + reference: Split by reference sequence. + mapped: Split into mapped and unmapped files. + paired: Split into paired and single-end files. + tag: Split by the value of a specific tag. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of created files. + """ + if not in_bam.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {in_bam}") + + split_options = [by is not None, reference, mapped, paired, tag is not None] + if sum(split_options) != 1: + raise ValueError("Exactly one split criterion (by, reference, mapped, paired, or tag) must be specified.") + + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = ["bamtools", "split", "-in", str(in_bam)] + + # Use the stub to direct output to the specified directory + output_stub = stub if stub else in_bam.stem + cmd.extend(["-stub", str(output_dir / output_stub)]) + + if by: + cmd.extend(["-by", by]) + if reference: + cmd.append("-reference") + if mapped: + cmd.append("-mapped") + if paired: + cmd.append("-paired") + if tag: + cmd.extend(["-tag", tag]) + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + # Find all files created in the output directory + created_files = [str(p) for p in output_dir.glob(f"{output_stub}*")] + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": created_files, + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools split failed with exit code {e.returncode}", + "output_files": [], + } + +@mcp.tool() +def stats(in_bams: List[Path], insert: bool = False) -> dict: + """ + Prints basic statistics from one or more BAM files. + + Args: + in_bams: A list of input BAM files. + insert: If True, print insert size summary. + + Returns: + A dictionary containing the command executed, stdout (the stats), and stderr. + """ + if not in_bams: + raise ValueError("At least one input BAM file must be provided.") + + cmd = ["bamtools", "stats"] + for bam_file in in_bams: + if not bam_file.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bam_file}") + cmd.extend(["-in", str(bam_file)]) + + if insert: + cmd.append("-insert") + + command_executed = shlex.join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": f"bamtools stats failed with exit code {e.returncode}", + "output_files": [], + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bamtools/app/bamtools_shim_server.py b/Biomni/mcp_generated/mcp_bamtools/app/bamtools_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..81abfae345f18d686d3e93f6df132e63b4d0f735 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bamtools/app/bamtools_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_bamtools/app/bamtools_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bamtools' + + +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_bamtools/docker-compose.yml b/Biomni/mcp_generated/mcp_bamtools/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..efe15438bf81dc42bdebaf6b8a684c7d8c8ae3f6 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bamtools/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bamtools: + build: . + image: mcp-bamtools:latest + container_name: mcp-bamtools + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bamtools + 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_bamtools/environment.yaml b/Biomni/mcp_generated/mcp_bamtools/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cd56f2319dab67d6b24a5b2974f63480e998a013 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bamtools/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bamtools + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bamtools/requirements.txt b/Biomni/mcp_generated/mcp_bamtools/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bamtools/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..40770fc4cf44910bbe4a77f31ad3f18c226c857b --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/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-alabaster.spatial via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-alabaster.spatial -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-alabaster.spatial_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-alabaster.spatial_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-alabaster.spatial_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..9077879ad1f81983e4ba6ace465ba860f645b5be --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-alabaster.spatial: + build: . + image: mcp-bioconductor-alabaster.spatial:latest + container_name: mcp-bioconductor-alabaster.spatial + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-alabaster.spatial + 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-alabaster.spatial/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..329b888afd15efdc23d52fc58fe6ff2376c71013 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-alabaster.spatial + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-cardspa/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-cardspa/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..53a99bf8cbf88e922ebdad2fbfe1ce6dd59da719 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cardspa/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-cardspa via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-cardspa -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-cardspa_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-cardspa_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-cardspa_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_server.py b/Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_server.py new file mode 100644 index 0000000000000000000000000000000000000000..90c0f13ea1cf94688a5ffb7d36f2230cb81fb38b --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_server.py @@ -0,0 +1,189 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Dict, List, Literal, Union + +# This is a placeholder for the MCP framework's decorator. +# The user's environment will provide the actual mcp.tool implementation. +class Mcp: + def tool(self): + def decorator(f): + return f + return decorator +mcp = Mcp() + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_cardspa' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def cardspa( + sce_object: Path, + output_path: Path, + phenotype_col: str, + location_col_1: str = "row", + location_col_2: str = "col", + method: Literal["Giotto", "SpaGCN", "BayesSpace"] = "Giotto", + num_results: int = 10, + num_core: int = 1, + min_cells: int = 10, + min_pairs: int = 10, +) -> Dict[str, Union[str, List[str], int]]: + """ + Performs spatial analysis on single-cell data using the cardspa R package. + + This tool wraps the main `cardspa` function, which can utilize different + backend methods like Giotto, SpaGCN, or BayesSpace to identify significant + co-enrichment of cell types in spatial transcriptomics data. + + Args: + sce_object: Path to the input SingleCellExperiment object in RDS format. + output_path: Path to save the output results object in RDS format. + phenotype_col: The column name in the SCE object's colData that contains cell type annotations. + location_col_1: The column name for the first spatial coordinate (e.g., 'row', 'x'). + location_col_2: The column name for the second spatial coordinate (e.g., 'col', 'y'). + method: The computational method to use for the analysis. + num_results: The number of top co-enriched cell type pairs to return. + num_core: The number of CPU cores to use for parallel processing. + min_cells: The minimum number of cells required for a cell type to be included in the analysis. + min_pairs: The minimum number of interacting pairs for a result to be considered significant. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + In case of an error, it includes an error message and return code. + """ + # --- Input Validation --- + if not sce_object.is_file(): + raise FileNotFoundError(f"Input file not found: {sce_object}") + + if not output_path.parent.is_dir(): + try: + output_path.parent.mkdir(parents=True, exist_ok=True) + except OSError as e: + raise NotADirectoryError( + f"Output directory does not exist and could not be created: {output_path.parent}" + ) from e + + if num_results <= 0: + raise ValueError("num_results must be a positive integer.") + if num_core <= 0: + raise ValueError("num_core must be a positive integer.") + if min_cells <= 0: + raise ValueError("min_cells must be a positive integer.") + if min_pairs <= 0: + raise ValueError("min_pairs must be a positive integer.") + + # R script to be executed. It includes package installation for portability. + r_script_content = """ + # run_cardspa.R + options(warn=-1) + suppressPackageStartupMessages({ + if (!requireNamespace("argparser", quietly = TRUE)) { + install.packages("argparser", repos = "http://cran.us.r-project.org", quiet = TRUE) + } + if (!requireNamespace("BiocManager", quietly = TRUE)) { + install.packages("BiocManager", repos = "http://cran.us.r-project.org", quiet = TRUE) + } + if (!requireNamespace("cardspa", quietly = TRUE)) { + BiocManager::install("cardspa", update=FALSE, ask=FALSE, quiet = TRUE) + } + if (!requireNamespace("SingleCellExperiment", quietly = TRUE)) { + BiocManager::install("SingleCellExperiment", update=FALSE, ask=FALSE, quiet = TRUE) + } + library(argparser) + library(cardspa) + library(SingleCellExperiment) + }) + + p <- arg_parser("Run cardspa analysis") + p <- add_argument(p, "--sce_object", help="Path to the input SingleCellExperiment RDS file", type="character") + p <- add_argument(p, "--output_path", help="Path to save the output RDS file", type="character") + p <- add_argument(p, "--phenotype_col", help="Column name for cell types", type="character") + p <- add_argument(p, "--location_col_1", help="First column name for spatial coordinates", default="row", type="character") + p <- add_argument(p, "--location_col_2", help="Second column name for spatial coordinates", default="col", type="character") + p <- add_argument(p, "--method", help="Method for spatial analysis (Giotto, SpaGCN, BayesSpace)", default="Giotto", type="character") + p <- add_argument(p, "--num_results", help="Number of top results to return", default=10, type="integer") + p <- add_argument(p, "--num_core", help="Number of cores for parallel processing", default=1, type="integer") + p <- add_argument(p, "--min_cells", help="Minimum number of cells for a cell type", default=10, type="integer") + p <- add_argument(p, "--min_pairs", help="Minimum number of interacting pairs", default=10, type="integer") + + argv <- parse_args(p) + + if (is.na(argv$sce_object) || is.na(argv$output_path) || is.na(argv$phenotype_col)) { + stop("Missing required arguments: --sce_object, --output_path, or --phenotype_col", call. = FALSE) + } + + cat("Reading input SCE object from:", argv$sce_object, "\\n") + sce <- readRDS(argv$sce_object) + + cat("Running cardspa analysis with method:", argv$method, "\\n") + results <- cardspa( + sce_object = sce, + location_col = c(argv$location_col_1, argv$location_col_2), + phenotype_col = argv$phenotype_col, + method = argv$method, + num_results = argv$num_results, + num_core = argv$num_core, + min_cells = argv$min_cells, + min_pairs = argv$min_pairs + ) + + cat("Saving results to:", argv$output_path, "\\n") + saveRDS(results, file = argv$output_path) + cat("cardspa analysis complete. Results saved to:", argv$output_path, "\\n") + """ + + # --- Subprocess Execution --- + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file: + r_script_file.write(r_script_content) + r_script_path = Path(r_script_file.name) + + cmd = [ + "Rscript", str(r_script_path), + "--sce_object", str(sce_object.resolve()), + "--output_path", str(output_path.resolve()), + "--phenotype_col", phenotype_col, + "--location_col_1", location_col_1, + "--location_col_2", location_col_2, + "--method", method, + "--num_results", str(num_results), + "--num_core", str(num_core), + "--min_cells", str(min_cells), + "--min_pairs", str(min_pairs), + ] + command_executed = " ".join(cmd) + + try: + result = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + encoding='utf-8' + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_path.resolve())] + } + except FileNotFoundError: + raise RuntimeError("Rscript command not found. Please ensure R is installed and in your system's PATH.") + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "error": "cardspa execution failed.", + "return_code": e.returncode, + "output_files": [] + } + finally: + # Ensure temporary script is always removed + if r_script_path.exists(): + r_script_path.unlink() + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..25c23e07c509922070c4f2044a858ef525f0713f --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_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-cardspa/app/bioconductor-cardspa_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_cardspa' + + +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-cardspa/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-cardspa/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cardspa/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-cardspa/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-cardspa/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c97f759a1c64d8bf5d0c061ab68e38a779fc9afd --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cardspa/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-cardspa: + build: . + image: mcp-bioconductor-cardspa:latest + container_name: mcp-bioconductor-cardspa + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-cardspa + 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-cardspa/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-cardspa/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7ea224b0da926f170bcd200087a22fdb529c04e4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cardspa/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-cardspa + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-cardspa/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-cardspa/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cardspa/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-cellid/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-cellid/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..fe989065a06a14a78840560dd420a1b13cad37f1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cellid/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-cellid via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-cellid -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-cellid_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-cellid_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-cellid_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_server.py b/Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_server.py new file mode 100644 index 0000000000000000000000000000000000000000..4b60eb5f3c1bcb0f0ded2531f154eab5d6d066b1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_server.py @@ -0,0 +1,288 @@ +import subprocess +import os +from pathlib import Path +from typing import Optional, List, Union +import tempfile + +def run_r_command(script_content: str): + """Helper function to execute R code via Rscript""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(script_content) + tmp_path = tmp.name + + try: + result = subprocess.run( + ["Rscript", tmp_path], + capture_output=True, + text=True, + check=True + ) + return result.stdout, result.stderr + except subprocess.CalledProcessError as e: + raise RuntimeError(f"R execution failed: {e.stderr}\nStdout: {e.stdout}") + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_cellid' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def cellid_run_mca( + input_rds: str, + output_rds: str, + nmcs: int = 50, + assay: str = "RNA", + slot: str = "counts", + features: Optional[List[str]] = None +): + """ + Performs Multiple Correspondence Analysis (MCA) on a SingleCellExperiment or Seurat object. + MCA allows for the simultaneous representation of cells and genes in a common low-dimensional space. + """ + input_path = Path(input_rds) + output_path = Path(output_rds) + + if not input_path.exists(): + return {"error": f"Input file {input_rds} not found."} + + if nmcs <= 0: + return {"error": "nmcs must be a positive integer."} + + features_r = "NULL" + if features: + features_formatted = ", ".join([f"'{f}'" for f in features]) + features_r = f"c({features_formatted})" + + r_script = f""" + library(CelliD) + library(Seurat) + library(SingleCellExperiment) + + obj <- readRDS("{input_path}") + + # Run MCA + obj <- RunMCA(obj, nmcs = {nmcs}, assay = "{assay}", slot = "{slot}", features = {features_r}) + + saveRDS(obj, "{output_path}") + """ + + try: + stdout, stderr = run_r_command(r_script) + return { + "command_executed": "RunMCA", + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_path)] + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def cellid_get_cell_gene_set( + input_rds: str, + output_csv: str, + dims: int = 50, + n_features: int = 200 +): + """ + Extracts the top gene signatures for each individual cell based on MCA coordinates. + """ + input_path = Path(input_rds) + output_path = Path(output_csv) + + if not input_path.exists(): + return {"error": f"Input file {input_rds} not found."} + + if dims <= 0 or n_features <= 0: + return {"error": "dims and n_features must be positive integers."} + + r_script = f""" + library(CelliD) + library(Seurat) + library(SingleCellExperiment) + library(data.table) + + obj <- readRDS("{input_path}") + + # Extract gene sets per cell + # dims is treated as 1:dims + gene_sets <- GetCellGeneSet(obj, dims = 1:{dims}, n.features = {n_features}) + + # Convert list to a data table for export + # Each column is a cell, rows are top genes + max_len <- max(sapply(gene_sets, length)) + dt <- as.data.frame(lapply(gene_sets, function(x) c(x, rep(NA, max_len - length(x))))) + + write.csv(dt, "{output_path}", row.names = FALSE) + """ + + try: + stdout, stderr = run_r_command(r_script) + return { + "command_executed": "GetCellGeneSet", + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_path)] + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def cellid_run_cell_hgt( + input_rds: str, + pathways_gmt: str, + output_rds: str, + dims: int = 50, + n_features: int = 200, + min_size: int = 5 +): + """ + Performs cell-type assignment or functional enrichment using a Hypergeometric Test (HGT). + Matches per-cell gene signatures against a reference database (GMT format). + """ + input_path = Path(input_rds) + gmt_path = Path(pathways_gmt) + output_path = Path(output_rds) + + if not input_path.exists(): + return {"error": f"Input RDS {input_rds} not found."} + if not gmt_path.exists(): + return {"error": f"GMT file {pathways_gmt} not found."} + + r_script = f""" + library(CelliD) + library(Seurat) + library(SingleCellExperiment) + library(fgsea) + + obj <- readRDS("{input_path}") + pathways <- gmtPathways("{gmt_path}") + + # Run Hypergeometric Test + # Returns a matrix of -log10(p-value) + hgt_matrix <- RunCellHGT(obj, pathways = pathways, dims = 1:{dims}, n.features = {n_features}, minSize = {min_size}) + + # If Seurat, we can add it as an assay or metadata + if(inherits(obj, "Seurat")) {{ + obj[["HGT"]] <- CreateAssayObject(counts = hgt_matrix) + }} else {{ + # For SCE, add to altExp + altExp(obj, "HGT") <- SummarizedExperiment(assays = list(counts = hgt_matrix)) + }} + + saveRDS(obj, "{output_path}") + """ + + try: + stdout, stderr = run_r_command(r_script) + return { + "command_executed": "RunCellHGT", + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_path)] + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def cellid_run_gsea( + input_rds: str, + pathways_gmt: str, + output_rds: str, + dims: int = 50 +): + """ + Performs Gene Set Enrichment Analysis (GSEA) on cells using the MCA gene rankings. + """ + input_path = Path(input_rds) + gmt_path = Path(pathways_gmt) + output_path = Path(output_rds) + + if not input_path.exists(): + return {"error": f"Input RDS {input_rds} not found."} + if not gmt_path.exists(): + return {"error": f"GMT file {pathways_gmt} not found."} + + r_script = f""" + library(CelliD) + library(Seurat) + library(SingleCellExperiment) + library(fgsea) + + obj <- readRDS("{input_path}") + pathways <- gmtPathways("{gmt_path}") + + # Run GSEA + # This calculates enrichment scores for each cell and each pathway + gsea_res <- RunGSEA(obj, pathways = pathways, dims = 1:{dims}) + + # Save results (usually returns a matrix of NES) + if(inherits(obj, "Seurat")) {{ + obj[["GSEA"]] <- CreateAssayObject(counts = gsea_res) + }} else {{ + altExp(obj, "GSEA") <- SummarizedExperiment(assays = list(counts = gsea_res)) + }} + + saveRDS(obj, "{output_path}") + """ + + try: + stdout, stderr = run_r_command(r_script) + return { + "command_executed": "RunGSEA", + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_path)] + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def cellid_run_group_mca( + input_rds: str, + group_by: str, + output_rds: str, + nmcs: int = 50, + assay: str = "RNA", + slot: str = "counts" +): + """ + Performs MCA on grouped cells (e.g., by cluster or cell type) rather than individual cells. + Useful for extracting group-specific gene signatures. + """ + input_path = Path(input_rds) + output_path = Path(output_rds) + + if not input_path.exists(): + return {"error": f"Input file {input_rds} not found."} + + r_script = f""" + library(CelliD) + library(Seurat) + library(SingleCellExperiment) + + obj <- readRDS("{input_path}") + + # Run Group MCA + # This aggregates cells by the specified metadata column before MCA + group_mca <- RunGroupMCA(obj, group.by = "{group_by}", nmcs = {nmcs}, assay = "{assay}", slot = "{slot}") + + saveRDS(group_mca, "{output_path}") + """ + + try: + stdout, stderr = run_r_command(r_script) + return { + "command_executed": "RunGroupMCA", + "stdout": stdout, + "stderr": stderr, + "output_files": [str(output_path)] + } + except Exception as e: + return {"error": str(e)} + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3cab3103b60f82d274eb4ef8e2fdf3f23a988380 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_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-cellid/app/bioconductor-cellid_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_cellid' + + +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-cellid/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-cellid/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..1314ea956fa156847970266461a3cfd9e0b3637f --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cellid/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-cellid: + build: . + image: mcp-bioconductor-cellid:latest + container_name: mcp-bioconductor-cellid + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-cellid + 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-cellid/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-cellid/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f08c7d868e99c3a997a959a96e68214878543b56 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cellid/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-cellid + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-cellid/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-cellid/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-cellid/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-ggspavis/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..cce68a8789108203349e557d81008954a56da088 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/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-ggspavis via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-ggspavis -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-ggspavis_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-ggspavis_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-ggspavis_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_server.py b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f9ea5025816106c39ac370357d544a8e318c4a81 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_server.py @@ -0,0 +1,403 @@ +import subprocess +import tempfile +import json +import shlex +from pathlib import Path +from typing import Optional, List, Literal + +# This R script acts as a generic command-line wrapper for ggspavis functions. +# It is written to a temporary file and executed by the Python functions below. +# It dynamically installs required R packages, making the tool self-contained. +R_SCRIPT = """ +# ggspavis_runner.R +# A generic runner for ggspavis functions + +# Suppress startup messages for cleaner output +suppressPackageStartupMessages({ + # Check if packages are installed, install if not + if (!requireNamespace("argparse", quietly = TRUE)) install.packages("argparse", repos = "http://cran.us.r-project.org") + if (!requireNamespace("jsonlite", quietly = TRUE)) install.packages("jsonlite", repos = "http://cran.us.r-project.org") + if (!requireNamespace("ggplot2", quietly = TRUE)) install.packages("ggplot2", repos = "http://cran.us.r-project.org") + if (!requireNamespace("BiocManager", quietly = TRUE)) install.packages("BiocManager", repos = "http://cran.us.r-project.org") + + # Use BiocManager to install Bioconductor packages + if (!requireNamespace("SpatialExperiment", quietly = TRUE)) BiocManager::install("SpatialExperiment", update = FALSE, ask = FALSE) + if (!requireNamespace("ggspavis", quietly = TRUE)) BiocManager::install("ggspavis", update = FALSE, ask = FALSE) +}) + +library(argparse) +library(jsonlite) +library(ggplot2) +library(SpatialExperiment) +library(ggspavis) + +# Main function to parse arguments and call the appropriate ggspavis function +main <- function() { + parser <- ArgumentParser(description = "A command-line wrapper for ggspavis functions.") + + parser$add_argument("func", help = "The ggspavis function to call (e.g., plotSpots, plotFeature).") + parser$add_argument("--spe_path", required = TRUE, help = "Path to the SpatialExperiment object (.rds file).") + parser$add_argument("--output_path", required = TRUE, help = "Path to save the output plot.") + parser$add_argument("--width", type = "double", default = 7, help = "Width of the output plot in inches.") + parser$add_argument("--height", type = "double", default = 7, help = "Height of the output plot in inches.") + parser$add_argument("--json_args", type = "character", default = "{}", help = "A JSON string of arguments for the ggspavis function.") + + args <- parser$parse_args() + + # Load SpatialExperiment object + if (!file.exists(args$spe_path)) { + stop("Input SPE file not found: ", args$spe_path) + } + spe <- readRDS(args$spe_path) + + # Parse JSON arguments for the target function + func_args <- fromJSON(args$json_args) + + # Add the main 'spe' object to the arguments list + func_args <- c(list(spe = spe), func_args) + + # Check if the requested function exists in the ggspavis package + if (!exists(args$func, where = "package:ggspavis", mode = "function")) { + stop("Function '", args$func, "' not found in ggspavis package.") + } + + # Dynamically call the ggspavis function with the parsed arguments + plot_obj <- do.call(args$func, func_args) + + # Save the generated plot to the specified output file + ggsave( + filename = args$output_path, + plot = plot_obj, + width = args$width, + height = args$height, + dpi = 300 + ) + + cat("Plot successfully saved to", args$output_path, "\\n") +} + +# Execute the main function and handle potential errors +tryCatch({ + main() +}, error = function(e) { + message("Error: ", e$message) + quit(status = 1) +}) +""" + +def _run_ggspavis( + function_name: str, + spe_path: Path, + output_path: Path, + plot_width: float, + plot_height: float, + json_args: dict, +) -> dict: + """ + Helper function to execute the ggspavis R script wrapper. + + Args: + function_name: The ggspavis function to execute (e.g., 'plotSpots'). + spe_path: Path to the input SpatialExperiment RDS file. + output_path: Path to save the output plot image. + plot_width: Width of the output plot in inches. + plot_height: Height of the output plot in inches. + json_args: A dictionary of arguments for the ggspavis function. + + Returns: + A dictionary containing the command executed, stdout, stderr, and output file path. + """ + # Ensure output directory exists + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Use a temporary file to store and execute the R script + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file: + r_script_path = Path(r_script_file.name) + r_script_file.write(R_SCRIPT) + + command = [ + "Rscript", + str(r_script_path), + function_name, + "--spe_path", str(spe_path), + "--output_path", str(output_path), + "--width", str(plot_width), + "--height", str(plot_height), + "--json_args", json.dumps(json_args), + ] + + command_executed = " ".join(shlex.quote(c) for c in command) + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + stdout = process.stdout + stderr = process.stderr + output_files = {"plot": str(output_path)} if output_path.exists() else {} + + except FileNotFoundError: + # This error occurs if 'Rscript' is not in the system's PATH + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'Rscript' command not found. Please ensure R is installed and in your system's PATH.", + "output_files": {}, + } + except subprocess.CalledProcessError as e: + # This error occurs if the R script returns a non-zero exit code + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": {}, + } + finally: + # Clean up the temporary R script file + r_script_path.unlink() + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_ggspavis' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def ggspavis_plot_spots( + spe_path: Path, + output_filename: str, + fill: Optional[str] = None, + highlight: Optional[str] = None, + facet_by: Optional[str] = None, + assay: str = "logcounts", + palette: str = "viridis", + point_size: float = 1.5, + point_alpha: float = 1.0, + plot_width: float = 7.0, + plot_height: float = 7.0, +): + """ + Generates a spatial plot of spots/capture locations using ggspavis::plotSpots. + + Args: + spe_path: Path to the input SpatialExperiment object (.rds file). + output_filename: Name of the output plot file (e.g., 'plot.png'). + fill: Column name in colData or a feature name to use for spot fill color. + highlight: Column name in colData to use for highlighting spots. + facet_by: Column name in colData to use for faceting the plot. + assay: Name of the assay to use if 'fill' is a feature name. + palette: Color palette for continuous values (e.g., 'viridis', 'magma'). + point_size: Size of the points representing spots. + point_alpha: Alpha transparency of the points. + plot_width: Width of the output plot in inches. + plot_height: Height of the output plot in inches. + """ + # Input validation + if not spe_path.is_file(): + raise FileNotFoundError(f"Input file not found: {spe_path}") + if point_size <= 0: + raise ValueError("point_size must be positive.") + if not (0.0 <= point_alpha <= 1.0): + raise ValueError("point_alpha must be between 0.0 and 1.0.") + + # Prepare arguments for the R function + json_args = { + "fill": fill, + "highlight": highlight, + "facet_by": facet_by, + "assay": assay, + "palette": palette, + "point_size": point_size, + "point_alpha": point_alpha, + } + # Filter out None values so they are not passed in the JSON + json_args = {k: v for k, v in json_args.items() if v is not None} + + output_path = Path(output_filename) + + return _run_ggspavis( + function_name="plotSpots", + spe_path=spe_path, + output_path=output_path, + plot_width=plot_width, + plot_height=plot_height, + json_args=json_args, + ) + + +@mcp.tool() +def ggspavis_plot_feature( + spe_path: Path, + output_filename: str, + feature: str, + assay: str = "logcounts", + platform: Literal["Visium", "ST", "VisiumHD", "mIF", "seqFISH", "MERFISH", "Xenium"] = "Visium", + palette: str = "viridis", + point_size: float = 1.5, + plot_width: float = 7.0, + plot_height: float = 7.0, +): + """ + Visualizes the expression of a single feature (e.g., gene) in spatial context using ggspavis::plotFeature. + + Args: + spe_path: Path to the input SpatialExperiment object (.rds file). + output_filename: Name of the output plot file (e.g., 'feature_plot.png'). + feature: Name of the feature (gene) to plot from the specified assay. + assay: Name of the assay containing the feature. + platform: The spatial transcriptomics platform. + palette: Color palette for continuous expression values. + point_size: Size of the points. + plot_width: Width of the output plot in inches. + plot_height: Height of the output plot in inches. + """ + # Input validation + if not spe_path.is_file(): + raise FileNotFoundError(f"Input file not found: {spe_path}") + if not feature: + raise ValueError("A feature name must be provided.") + + # Prepare arguments for the R function + json_args = { + "feature": feature, + "assay": assay, + "platform": platform, + "palette": palette, + "point_size": point_size, + } + json_args = {k: v for k, v in json_args.items() if v is not None} + + output_path = Path(output_filename) + + return _run_ggspavis( + function_name="plotFeature", + spe_path=spe_path, + output_path=output_path, + plot_width=plot_width, + plot_height=plot_height, + json_args=json_args, + ) + + +@mcp.tool() +def ggspavis_plot_dim_red( + spe_path: Path, + output_filename: str, + reduction_type: str, + x_axis: Optional[str] = None, + y_axis: Optional[str] = None, + color_by: Optional[str] = None, + shape_by: Optional[str] = None, + assay: str = "logcounts", + point_size: float = 1.5, + plot_width: float = 7.0, + plot_height: float = 7.0, +): + """ + Plots dimensionality reduction results (e.g., UMAP, PCA) using ggspavis::plotDimRed. + + Args: + spe_path: Path to the input SpatialExperiment object (.rds file). + output_filename: Name of the output plot file (e.g., 'umap.png'). + reduction_type: Name of the dimensionality reduction result in reducedDims(spe). + x_axis: Column name for the x-axis (e.g., 'UMAP_1'). Defaults to type_1. + y_axis: Column name for the y-axis (e.g., 'UMAP_2'). Defaults to type_2. + color_by: Column name in colData or feature name to use for point color. + shape_by: Column name in colData to use for point shape. + assay: Name of assay to use if 'color_by' is a feature name. + point_size: Size of the points. + plot_width: Width of the output plot in inches. + plot_height: Height of the output plot in inches. + """ + # Input validation + if not spe_path.is_file(): + raise FileNotFoundError(f"Input file not found: {spe_path}") + if not reduction_type: + raise ValueError("A reduction_type must be provided.") + + # Prepare arguments for the R function + json_args = { + "type": reduction_type, + "x_axis": x_axis, + "y_axis": y_axis, + "color_by": color_by, + "shape_by": shape_by, + "assay": assay, + "point_size": point_size, + } + json_args = {k: v for k, v in json_args.items() if v is not None} + + output_path = Path(output_filename) + + return _run_ggspavis( + function_name="plotDimRed", + spe_path=spe_path, + output_path=output_path, + plot_width=plot_width, + plot_height=plot_height, + json_args=json_args, + ) + + +@mcp.tool() +def ggspavis_plot_label( + spe_path: Path, + output_filename: str, + label: str, + platform: Literal["Visium", "ST", "VisiumHD", "mIF", "seqFISH", "MERFISH", "Xenium"] = "Visium", + palette: Optional[str] = None, + point_size: float = 1.5, + plot_width: float = 7.0, + plot_height: float = 7.0, +): + """ + Visualizes spatial clusters or other categorical labels using ggspavis::plotLabel. + + Args: + spe_path: Path to the input SpatialExperiment object (.rds file). + output_filename: Name of the output plot file (e.g., 'clusters.png'). + label: Column name in colData containing the labels to plot. + platform: The spatial transcriptomics platform. + palette: Optional color palette for labels. If None, ggplot2 default is used. + point_size: Size of the points. + plot_width: Width of the output plot in inches. + plot_height: Height of the output plot in inches. + """ + # Input validation + if not spe_path.is_file(): + raise FileNotFoundError(f"Input file not found: {spe_path}") + if not label: + raise ValueError("A label column name must be provided.") + + # Prepare arguments for the R function + json_args = { + "label": label, + "platform": platform, + "palette": palette, + "point_size": point_size, + } + json_args = {k: v for k, v in json_args.items() if v is not None} + + output_path = Path(output_filename) + + return _run_ggspavis( + function_name="plotLabel", + spe_path=spe_path, + output_path=output_path, + plot_width=plot_width, + plot_height=plot_height, + json_args=json_args, + ) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e2e48c22659a19491db77b7dbe62b14178cfbf2d --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_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-ggspavis/app/bioconductor-ggspavis_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_ggspavis' + + +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-ggspavis/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-ggspavis/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..941920ed4e6d089e1a71dab40598b95cf6239402 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-ggspavis: + build: . + image: mcp-bioconductor-ggspavis:latest + container_name: mcp-bioconductor-ggspavis + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-ggspavis + 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-ggspavis/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..282746043f724ae679e2f7e8ad158a7cb4b97be5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-ggspavis + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-ggspavis/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-ggspavis/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-mastr/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-mastr/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..7a616939e747f440f980604cbf5026f178643614 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-mastr/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-mastr via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-mastr -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-mastr_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-mastr_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-mastr_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_server.py b/Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_server.py new file mode 100644 index 0000000000000000000000000000000000000000..abeaf91bb53784150aa7a76a52c90883cba0bad9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_server.py @@ -0,0 +1,208 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, List, Union + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_mastr' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def mastr_align( + fasta_input: str, + output_alignment: str, + iterations: int = 100, + temperature: float = 1.0, + cooling_rate: float = 0.9, + gap_open: float = -10.0, + gap_extend: float = -1.0, + use_structure: bool = True, + r_path: str = "Rscript" +): + """ + Perform Multiple Alignment of Structural RNAs (MASTR) using the Bioconductor mastr package. + + Args: + fasta_input: Path to the input FASTA file containing RNA sequences. + output_alignment: Path where the resulting alignment (FASTA format) will be saved. + iterations: Number of iterations for the simulated annealing (default: 100). + temperature: Initial temperature for simulated annealing (default: 1.0). + cooling_rate: Cooling rate for simulated annealing (default: 0.9). + gap_open: Penalty for opening a gap (default: -10.0). + gap_extend: Penalty for extending a gap (default: -1.0). + use_structure: Whether to incorporate secondary structure prediction (default: True). + r_path: Path to the Rscript executable (default: 'Rscript'). + """ + # Input validation + input_path = Path(fasta_input) + if not input_path.exists(): + return {"error": f"Input file not found: {fasta_input}"} + + output_path = Path(output_alignment) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if iterations <= 0: + return {"error": "Iterations must be a positive integer."} + + if not (0 < cooling_rate < 1): + return {"error": "Cooling rate must be between 0 and 1."} + + # Create an R script to execute the mastr package + # Note: mastr is a Bioconductor package, so we use Rscript to interface with it. + r_script_content = f""" + library(mastr) + library(Biostrings) + + # Load sequences + seqs <- readRNAStringSet("{input_path.absolute()}") + + # Define parameters + params <- list( + iterations = {iterations}, + temp = {temperature}, + cooling = {cooling_rate}, + gapOpen = {gap_open}, + gapExtend = {gap_extend}, + useStructure = {"TRUE" if use_structure else "FALSE"} + ) + + # Run MASTR alignment + # Note: mastr() is the primary function in the Bioconductor package + alignment_result <- mastr(seqs, + iterations = params$iterations, + temp = params$temp, + cooling = params$cooling, + gapOpen = params$gapOpen, + gapExtend = params$gapExtend) + + # Write output + writeXStringSet(alignment_result, "{output_path.absolute()}") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script_content) + tmp_path = tmp.name + + try: + # Execute the R script + process = subprocess.run( + [r_path, tmp_path], + capture_output=True, + text=True, + check=True + ) + + return { + "command_executed": f"{r_path} {tmp_path}", + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [str(output_path.absolute())], + "status": "Success" + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": f"{r_path} {tmp_path}", + "stdout": e.stdout, + "stderr": e.stderr, + "error": "R execution failed. Ensure 'mastr' and 'Biostrings' packages are installed in R.", + "status": "Error" + } + finally: + # Cleanup temporary R script + if Path(tmp_path).exists(): + Path(tmp_path).unlink() + +@mcp.tool() +def mastr_score_alignment( + alignment_file: str, + r_path: str = "Rscript" +): + """ + Calculate the structural conservation score for an existing RNA alignment using mastr metrics. + + Args: + alignment_file: Path to the aligned FASTA/Stockholm file. + r_path: Path to the Rscript executable. + """ + input_path = Path(alignment_file) + if not input_path.exists(): + return {"error": f"Alignment file not found: {alignment_file}"} + + r_script_content = f""" + library(mastr) + library(Biostrings) + + aln <- readRNAStringSet("{input_path.absolute()}") + # Calculate conservation score based on mastr's internal scoring + # This typically involves base-pairing probabilities and covariance + score <- mastr:::calculateScore(aln) # Accessing internal scoring if exported or via wrapper + cat("Alignment_Score:", score, "\\n") + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script_content) + tmp_path = tmp.name + + try: + process = subprocess.run( + [r_path, tmp_path], + capture_output=True, + text=True, + check=True + ) + + return { + "command_executed": f"{r_path} {tmp_path}", + "stdout": process.stdout, + "stderr": process.stderr, + "status": "Success" + } + except subprocess.CalledProcessError as e: + return { + "error": str(e), + "stderr": e.stderr, + "status": "Error" + } + finally: + if Path(tmp_path).exists(): + Path(tmp_path).unlink() + +@mcp.tool() +def mastr_check_dependencies(r_path: str = "Rscript"): + """ + Verify if R and the required Bioconductor mastr package are installed and accessible. + + Args: + r_path: Path to the Rscript executable. + """ + r_script_content = """ + if (!requireNamespace("mastr", quietly = TRUE)) { + stop("Package 'mastr' not found. Please install it via BiocManager::install('mastr')") + } + if (!requireNamespace("Biostrings", quietly = TRUE)) { + stop("Package 'Biostrings' not found.") + } + cat("All dependencies are satisfied.\\n") + """ + + try: + process = subprocess.run( + [r_path, "-e", r_script_content], + capture_output=True, + text=True, + check=True + ) + return { + "status": "Ready", + "stdout": process.stdout + } + except Exception as e: + return { + "status": "Missing Dependencies", + "error": str(e) + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a671b26ab593f961cee08273390fecd9cdb54764 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_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-mastr/app/bioconductor-mastr_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_mastr' + + +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-mastr/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-mastr/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-mastr/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-mastr/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-mastr/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-mastr/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..70ad418d4a9082937d70718a7a788888f0f4525b --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/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-rhdf5filters via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-rhdf5filters -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-rhdf5filters_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-rhdf5filters_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-rhdf5filters_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/bioconductor-rhdf5filters_server.py b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/bioconductor-rhdf5filters_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0f0736b143afb43ad4371abc491eaa320ada41bd --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/bioconductor-rhdf5filters_server.py @@ -0,0 +1,78 @@ +import subprocess +import tempfile +import shutil +from pathlib import Path +from typing import Dict, Any, List + +# MCP decorator is commented out as per instructions +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_rhdf5filters' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def check_installation() -> Dict[str, Any]: + """ + Verifies the installation of the bioconductor-rhdf5filters R library. + + bioconductor-rhdf5filters is an R library that provides HDF5 compression + filters and does not have a direct command-line interface. This function + executes a simple R script to confirm that the library is installed and + can be loaded in the R environment, and it reports the installed version. + """ + # Pre-flight check for Rscript executable + if not shutil.which("Rscript"): + raise FileNotFoundError("Rscript executable not found in PATH. R is required to run this tool.") + + r_script_content = """ + if (!requireNamespace("rhdf5filters", quietly = TRUE)) { + cat("Error: The 'rhdf5filters' package is not installed.\\n", file = stderr()) + quit(status = 1) + } + ver <- as.character(packageVersion("rhdf5filters")) + cat(paste0("Successfully loaded 'rhdf5filters' version: ", ver, "\\n")) + quit(status = 0) + """ + + command_executed = "" + script_path = "" + try: + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".R", encoding='utf-8') as tmp_r_script: + script_path = tmp_r_script.name + tmp_r_script.write(r_script_content) + + cmd: List[str] = ["Rscript", script_path] + command_executed = " ".join(cmd) + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + encoding='utf-8' + ) + + return { + "command_executed": command_executed, + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [] + } + + except subprocess.CalledProcessError as e: + # This will catch the case where the R script exits with a non-zero status + # (e.g., package not installed) and return a structured error. + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "error": "R script execution failed. The 'rhdf5filters' package may not be installed or accessible." + } + finally: + # Ensure the temporary file is cleaned up + if script_path and Path(script_path).exists(): + Path(script_path).unlink() + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..81832d0f80640700360671a525f10b97f2382efa --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-rhdf5filters: + build: . + image: mcp-bioconductor-rhdf5filters:latest + container_name: mcp-bioconductor-rhdf5filters + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-rhdf5filters + 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-rhdf5filters/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a0adbb1473ea9468bd807b23ff418eeac7f03f44 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-rhdf5filters + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-rsubread/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-rsubread/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e4dec6ab2174115a3f1326b6fd37bc802e9c6b00 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rsubread/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-rsubread via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-rsubread -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-rsubread_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-rsubread_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-rsubread_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_server.py b/Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_server.py new file mode 100644 index 0000000000000000000000000000000000000000..e3c0511e467738b789b0138f423a705373287d4f --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_server.py @@ -0,0 +1,322 @@ +import subprocess +import os +from pathlib import Path +from typing import Optional, List, Union +import tempfile + +def run_r_command(command: str): + """Helper to execute R commands via Rscript""" + try: + # We use -e to execute the expression directly + full_cmd = ["Rscript", "-e", f"library(Rsubread); {command}"] + result = subprocess.run(full_cmd, capture_output=True, text=True, check=True) + return result.stdout, result.stderr + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Rsubread execution failed: {e.stderr}") + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_rsubread' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def rsubread_buildindex( + basename: str, + reference: str, + gapped_index: bool = False, + index_memory: int = 8000, + colorspace: bool = False, + split_index: bool = False +): + """ + Builds an index for a reference genome. + + Args: + basename: Base name of the index files to be generated. + reference: Path to the reference genome file (FASTA format). + gapped_index: If True, a gapped index will be built (saves memory). + index_memory: Memory usage in MB. + colorspace: If True, index is built for color-space reads (SOLiD). + split_index: If True, the index will be split into multiple blocks. + """ + ref_path = Path(reference) + if not ref_path.exists(): + return {"error": f"Reference file {reference} not found."} + + # Construct R command + r_cmd = ( + f"buildindex(basename='{basename}', reference='{reference}', " + f"gappedIndex={'TRUE' if gapped_index else 'FALSE'}, " + f"indexMemory={index_memory}, colorspace={'TRUE' if colorspace else 'FALSE'}, " + f"splitIndex={'TRUE' if split_index else 'FALSE'})" + ) + + try: + stdout, stderr = run_r_command(r_cmd) + return { + "command_executed": f"Rsubread::buildindex({basename})", + "stdout": stdout, + "stderr": stderr, + "output_files": [str(Path(f"{basename}.{ext}")) for ext in ["files"]] # Rsubread creates multiple files + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def rsubread_align( + index: str, + readfile1: str, + readfile2: Optional[str] = None, + output_file: str = "output.bam", + input_format: str = "gzFASTQ", + output_format: str = "BAM", + nthreads: int = 1, + type: str = "dna", + phred_offset: int = 33, + unique: bool = False, + n_best_locations: int = 1, + max_mismatches: int = 3 +): + """ + Aligns reads to a reference genome using the Subread aligner. + + Args: + index: Base name of the index. + readfile1: Path to the first read file. + readfile2: Path to the second read file (for paired-end). + output_file: Name of the output file. + input_format: Format of input reads ('FASTQ', 'gzFASTQ', 'SAM', 'BAM'). + output_format: Format of output ('BAM', 'SAM'). + nthreads: Number of threads to use. + type: Type of sequencing ('dna' or 'rna'). + phred_offset: Phred score offset (33 or 64). + unique: If True, only uniquely mapped reads are reported. + n_best_locations: Number of best mapping locations to report. + max_mismatches: Maximum number of mismatches allowed. + """ + if not Path(readfile1).exists(): + return {"error": f"Read file {readfile1} not found."} + + read2_param = f"'{readfile2}'" if readfile2 else "NULL" + + r_cmd = ( + f"align(index='{index}', readfile1='{readfile1}', readfile2={read2_param}, " + f"output_file='{output_file}', input_format='{input_format}', output_format='{output_format}', " + f"nthreads={nthreads}, type='{type}', phredOffset={phred_offset}, " + f"unique={'TRUE' if unique else 'FALSE'}, nBestLocations={n_best_locations}, " + f"maxMismatches={max_mismatches})" + ) + + try: + stdout, stderr = run_r_command(r_cmd) + return { + "command_executed": "Rsubread::align", + "stdout": stdout, + "stderr": stderr, + "output_files": [output_file] + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def rsubread_subjunc( + index: str, + readfile1: str, + readfile2: Optional[str] = None, + output_file: str = "output_subjunc.bam", + input_format: str = "gzFASTQ", + output_format: str = "BAM", + nthreads: int = 1, + phred_offset: int = 33, + unique: bool = False +): + """ + Aligns RNA-seq reads and detects exon-exon junctions using the Subjunc aligner. + + Args: + index: Base name of the index. + readfile1: Path to the first read file. + readfile2: Path to the second read file (for paired-end). + output_file: Name of the output file. + input_format: Format of input reads. + output_format: Format of output. + nthreads: Number of threads. + phred_offset: Phred score offset. + unique: If True, only uniquely mapped reads are reported. + """ + read2_param = f"'{readfile2}'" if readfile2 else "NULL" + + r_cmd = ( + f"subjunc(index='{index}', readfile1='{readfile1}', readfile2={read2_param}, " + f"output_file='{output_file}', input_format='{input_format}', output_format='{output_format}', " + f"nthreads={nthreads}, phredOffset={phred_offset}, unique={'TRUE' if unique else 'FALSE'})" + ) + + try: + stdout, stderr = run_r_command(r_cmd) + return { + "command_executed": "Rsubread::subjunc", + "stdout": stdout, + "stderr": stderr, + "output_files": [output_file] + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def rsubread_featurecounts( + files: List[str], + annot_ext: str, + is_gtf_annotation_file: bool = True, + gtf_feature_type: str = "exon", + gtf_attr_type: str = "gene_id", + use_meta_features: bool = True, + allow_multi_overlap: bool = False, + is_paired_end: bool = False, + nthreads: int = 1, + strand_specific: int = 0, + count_multi_mapping_reads: bool = False +): + """ + Assigns mapped reads to genomic features (e.g. genes, exons). + + Args: + files: List of paths to alignment files (BAM/SAM). + annot_ext: Path to the annotation file (GTF/GFF/SAF). + is_gtf_annotation_file: Whether the annotation file is in GTF format. + gtf_feature_type: Feature type in GTF to be used (e.g. 'exon'). + gtf_attr_type: Attribute type in GTF to be used (e.g. 'gene_id'). + use_meta_features: If True, reads are counted for meta-features (e.g. genes). + allow_multi_overlap: If True, a read is allowed to be assigned to multiple features. + is_paired_end: If True, reads are treated as paired-end. + nthreads: Number of threads. + strand_specific: Strand specificity (0: unstranded, 1: stranded, 2: reversely stranded). + count_multi_mapping_reads: If True, multi-mapping reads will be counted. + """ + # Convert Python list to R vector + files_r = "c(" + ",".join([f"'{f}'" for f in files]) + ")" + + r_cmd = ( + f"fc <- featureCounts(files={files_r}, annot.ext='{annot_ext}', " + f"isGTFAnnotationFile={'TRUE' if is_gtf_annotation_file else 'FALSE'}, " + f"GTF.featureType='{gtf_feature_type}', GTF.attrType='{gtf_attr_type}', " + f"useMetaFeatures={'TRUE' if use_meta_features else 'FALSE'}, " + f"allowMultiOverlap={'TRUE' if allow_multi_overlap else 'FALSE'}, " + f"isPairedEnd={'TRUE' if is_paired_end else 'FALSE'}, " + f"nthreads={nthreads}, strandSpecific={strand_specific}, " + f"countMultiMappingReads={'TRUE' if count_multi_mapping_reads else 'FALSE'}); " + f"write.table(fc$counts, file='counts.txt', sep='\\t', quote=FALSE); " + f"write.table(fc$stat, file='stats.txt', sep='\\t', quote=FALSE)" + ) + + try: + stdout, stderr = run_r_command(r_cmd) + return { + "command_executed": "Rsubread::featureCounts", + "stdout": stdout, + "stderr": stderr, + "output_files": ["counts.txt", "stats.txt"] + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def rsubread_qualityscores( + filename: str, + input_format: str = "gzFASTQ", + offset: int = 33, + nreads: int = 10000 +): + """ + Extracts quality scores from a read file. + + Args: + filename: Path to the read file. + input_format: Format of the input file. + offset: Phred score offset. + nreads: Number of reads to extract quality scores from. + """ + if not Path(filename).exists(): + return {"error": f"File {filename} not found."} + + r_cmd = ( + f"qs <- qualityScores(filename='{filename}', input_format='{input_format}', " + f"offset={offset}, nreads={nreads}); " + f"write.table(qs, file='quality_scores.txt', sep='\\t', quote=FALSE)" + ) + + try: + stdout, stderr = run_r_command(r_cmd) + return { + "command_executed": "Rsubread::qualityScores", + "stdout": stdout, + "stderr": stderr, + "output_files": ["quality_scores.txt"] + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def rsubread_atgccontent( + filename: str, + input_format: str = "gzFASTQ", + basewise: bool = True +): + """ + Calculates ATGC content of reads. + + Args: + filename: Path to the read file. + input_format: Format of the input file. + basewise: If True, calculate content for each base position. + """ + r_cmd = ( + f"atgc <- atgcContent(filename='{filename}', input_format='{input_format}', " + f"basewise={'TRUE' if basewise else 'FALSE'}); " + f"write.table(atgc, file='atgc_content.txt', sep='\\t', quote=FALSE)" + ) + + try: + stdout, stderr = run_r_command(r_cmd) + return { + "command_executed": "Rsubread::atgcContent", + "stdout": stdout, + "stderr": stderr, + "output_files": ["atgc_content.txt"] + } + except Exception as e: + return {"error": str(e)} + +@mcp.tool() +def rsubread_propmapped( + files: List[str], + count_multi_mapping_reads: bool = False +): + """ + Calculates the proportion of mapped reads in BAM/SAM files. + + Args: + files: List of paths to alignment files. + count_multi_mapping_reads: Whether to include multi-mapping reads. + """ + files_r = "c(" + ",".join([f"'{f}'" for f in files]) + ")" + + r_cmd = ( + f"pm <- propMapped(files={files_r}, " + f"countMultiMappingReads={'TRUE' if count_multi_mapping_reads else 'FALSE'}); " + f"write.table(pm, file='prop_mapped.txt', sep='\\t', quote=FALSE, row.names=FALSE)" + ) + + try: + stdout, stderr = run_r_command(r_cmd) + return { + "command_executed": "Rsubread::propMapped", + "stdout": stdout, + "stderr": stderr, + "output_files": ["prop_mapped.txt"] + } + except Exception as e: + return {"error": str(e)} + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..61a4db6756b948241f2f00ce7ba70d886e6b5057 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_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-rsubread/app/bioconductor-rsubread_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_rsubread' + + +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-rsubread/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-rsubread/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rsubread/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-rtracklayer/app/bioconductor-rtracklayer_server.py b/Biomni/mcp_generated/mcp_bioconductor-rtracklayer/app/bioconductor-rtracklayer_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d68fa4f35a1715e991d5843bb988c2cb66583da9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-rtracklayer/app/bioconductor-rtracklayer_server.py @@ -0,0 +1,340 @@ +import subprocess +import tempfile +import re +from pathlib import Path +from typing import Optional, List + +# @mcp.tool() is a placeholder for the actual decorator. +# The code is written to be compatible with the MCP framework. + +def mcp_tool_placeholder(): + def decorator(func): + return func + return decorator + +@mcp_tool_placeholder() +def rtracklayer_convert( + input_file: Path, + output_file: Path, + input_format: Optional[str] = None, + output_format: Optional[str] = None, + which: Optional[str] = None, + genome: Optional[str] = None, + gff_feature_type_filter: Optional[str] = None, + gff_version: Optional[str] = None, + export_source: Optional[str] = None, + index_output: bool = False, +): + """ + Converts genomic data files from one format to another using rtracklayer. + + This tool wraps the core import() and export() functionality of the rtracklayer + R package, allowing for conversion between formats like BED, GFF, GTF, WIG, + and BigWig. It also supports filtering by genomic region. + + Args: + input_file: Path to the input genomic data file. + output_file: Path to write the converted output file. + input_format: The format of the input file (e.g., 'bed', 'gff', 'bigWig'). + If not provided, rtracklayer will guess from the file extension. + output_format: The format for the output file. If not provided, rtracklayer + will guess from the file extension. + which: A specific genomic region to process, in the format 'chr:start-end' + (e.g., 'chr1:1000-2000'). This is highly effective for indexed files. + genome: The genome assembly name (e.g., 'hg19', 'mm10'). + gff_feature_type_filter: For GFF/GTF import, a comma-separated list of + feature types to keep (e.g., 'exon,gene'). + gff_version: Specify the GFF version ('1', '2', or '3') for import/export. + export_source: The 'source' field to use for GFF/GTF export. + index_output: If True, create a tabix index (.tbi) for the output file. + Applicable to formats like BED and GFF. + """ + # 1. Input Validation + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + ALLOWED_FORMATS = ["bed", "gff", "gff1", "gff2", "gff3", "gtf", "wig", "bigWig", "bedGraph", "2bit", "fasta"] + if input_format and input_format not in ALLOWED_FORMATS: + raise ValueError(f"Invalid input_format '{input_format}'. Allowed formats: {ALLOWED_FORMATS}") + if output_format and output_format not in ALLOWED_FORMATS: + raise ValueError(f"Invalid output_format '{output_format}'. Allowed formats: {ALLOWED_FORMATS}") + + if gff_version and gff_version not in ["1", "2", "3"]: + raise ValueError("gff_version must be one of '1', '2', or '3'.") + + if which and not re.match(r"^\w+:\d+-\d+$", which): + raise ValueError("Invalid 'which' format. Expected 'chr:start-end' (e.g., 'chr1:1000-5000').") + + # 2. Build R Script + r_script_lines = [ + "suppressPackageStartupMessages(library(rtracklayer))", + "suppressPackageStartupMessages(library(GenomicRanges))", + "", + "# --- Import Arguments ---", + f"import_args <- list(con = '{input_file.resolve()}')", + ] + if input_format: + r_script_lines.append(f"import_args$format <- '{input_format}'") + if which: + r_script_lines.append(f"import_args$which <- GRanges('{which}')") + if genome: + r_script_lines.append(f"import_args$genome <- '{genome}'") + if gff_feature_type_filter: + r_vector = 'c(' + ', '.join([f'"{f.strip()}"' for f in gff_feature_type_filter.split(',')]) + ')' + r_script_lines.append(f"import_args$feature.type <- {r_vector}") + if gff_version: + r_script_lines.append(f"import_args$version <- '{gff_version}'") + + r_script_lines.extend([ + "", + "# --- Import Data ---", + "gr_object <- do.call(rtracklayer::import, import_args)", + "", + "# --- Export Arguments ---", + f"export_args <- list(object = gr_object, con = '{output_file.resolve()}')", + ]) + + if output_format: + r_script_lines.append(f"export_args$format <- '{output_format}'") + if export_source: + r_script_lines.append(f"export_args$source <- '{export_source}'") + if gff_version: + r_script_lines.append(f"export_args$version <- '{gff_version}'") + if index_output: + r_script_lines.append("export_args$index <- TRUE") + + r_script_lines.extend([ + "", + "# --- Export Data ---", + "do.call(rtracklayer::export, export_args)", + "cat('Conversion successful.\\n')" + ]) + + r_script = "\n".join(r_script_lines) + + # 3. Execute R Script + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False, dir=".") as tmp_script: + script_path = Path(tmp_script.name) + tmp_script.write(r_script) + + command = ["Rscript", str(script_path.name)] + command_str = " ".join(command) + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + cwd=Path.cwd() + ) + + output_files = [str(output_file)] + index_file = Path(f"{output_file}.tbi") + if index_output and index_file.exists(): + output_files.append(str(index_file)) + + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Rscript execution failed with return code {e.returncode}.\n" + f"Command: {command_str}\n" + f"Stderr: {e.stderr}\n" + f"Stdout: {e.stdout}" + ) from e + finally: + script_path.unlink() + + +@mcp_tool_placeholder() +def rtracklayer_liftover( + input_file: Path, + output_file: Path, + chain_file: Path, + input_format: Optional[str] = None, + unmapped_output_file: Optional[Path] = None, +): + """ + Lifts over genomic coordinates from one assembly to another using a chain file. + + This tool wraps the liftOver() function from rtracklayer, which is essential + for translating genomic features between different reference genome versions + (e.g., hg18 to hg19). + + Args: + input_file: Path to the input genomic data file (e.g., BED, GFF) with + coordinates to be lifted over. + output_file: Path to write the output file with lifted coordinates. + chain_file: Path to the liftOver chain file (.chain or .chain.gz). + input_format: The format of the input file (e.g., 'bed', 'gff'). If not + provided, rtracklayer will guess from the file extension. + unmapped_output_file: Optional path to write features that could not be + mapped to the new assembly. + """ + # 1. Input Validation + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + if not chain_file.exists(): + raise FileNotFoundError(f"Chain file not found: {chain_file}") + + # 2. Build R Script + r_script_lines = [ + "suppressPackageStartupMessages(library(rtracklayer))", + "", + "# --- Import Data ---", + f"ch <- rtracklayer::import.chain('{chain_file.resolve()}')", + f"gr <- rtracklayer::import('{input_file.resolve()}'" + (f", format = '{input_format}'" if input_format else "") + ")", + "", + "# --- Perform LiftOver ---", + "lifted_grl <- rtracklayer::liftOver(gr, ch)", + "lifted_gr <- unlist(lifted_grl)", + "", + "# --- Export Lifted Data ---", + f"rtracklayer::export(lifted_gr, '{output_file.resolve()}')", + ] + + if unmapped_output_file: + r_script_lines.extend([ + "", + "# --- Handle Unmapped Regions ---", + "unmapped_indices <- elementNROWS(lifted_grl) == 0", + "unmapped_gr <- gr[unmapped_indices]", + f"if (length(unmapped_gr) > 0) {{", + f" rtracklayer::export(unmapped_gr, '{unmapped_output_file.resolve()}')", + f"}}", + ]) + + r_script_lines.append("cat('LiftOver successful.\\n')") + r_script = "\n".join(r_script_lines) + + # 3. Execute R Script + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False, dir=".") as tmp_script: + script_path = Path(tmp_script.name) + tmp_script.write(r_script) + + command = ["Rscript", str(script_path.name)] + command_str = " ".join(command) + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + cwd=Path.cwd() + ) + + output_files = [str(output_file)] + if unmapped_output_file and unmapped_output_file.exists(): + output_files.append(str(unmapped_output_file)) + + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files + } + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Rscript execution failed with return code {e.returncode}.\n" + f"Command: {command_str}\n" + f"Stderr: {e.stderr}\n" + f"Stdout: {e.stdout}" + ) from e + finally: + script_path.unlink() + + +@mcp_tool_placeholder() +def rtracklayer_ucsc_query( + output_file: Path, + genome: str, + track: str, + which: Optional[str] = None, + output_format: Optional[str] = None, +): + """ + Downloads data directly from the UCSC Table Browser. + + This tool provides an interface to query and download genomic tracks from the + UCSC database, saving them to a local file in a specified format. + + Args: + output_file: Path to write the downloaded data. + genome: The UCSC genome assembly to query (e.g., 'hg19', 'mm10'). + track: The name of the track/table to download (e.g., 'knownGene', 'cpgIslandExt'). + which: A specific genomic region to query, in the format 'chr:start-end' + (e.g., 'chr1:1000-2000'). If not provided, the whole genome is queried. + output_format: The format for the output file (e.g., 'bed', 'gff'). If not + provided, rtracklayer will guess from the file extension. + """ + # 1. Input Validation + if which and not re.match(r"^\w+:\d+-\d+$", which): + raise ValueError("Invalid 'which' format. Expected 'chr:start-end' (e.g., 'chr1:1000-5000').") + + # 2. Build R Script + r_script_lines = [ + "suppressPackageStartupMessages(library(rtracklayer))", + "suppressPackageStartupMessages(library(GenomicRanges))", + "", + "# --- Build UCSC Data Source ---", + f"ucsc_args <- list(genome = '{genome}', tablename = '{track}')", + ] + if which: + r_script_lines.append(f"ucsc_args$range <- GRanges('{which}')") + + r_script_lines.extend([ + "ucsc_data_source <- do.call(UCSCData, ucsc_args)", + "", + "# --- Import Data from UCSC ---", + "gr_object <- rtracklayer::import(ucsc_data_source)", + "", + "# --- Export Data to File ---", + f"export_args <- list(object = gr_object, con = '{output_file.resolve()}')", + ]) + if output_format: + r_script_lines.append(f"export_args$format <- '{output_format}'") + + r_script_lines.extend([ + "do.call(rtracklayer::export, export_args)", + "cat('UCSC data downloaded successfully.\\n')" + ]) + r_script = "\n".join(r_script_lines) + + # 3. Execute R Script + with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False, dir=".") as tmp_script: + script_path = Path(tmp_script.name) + tmp_script.write(r_script) + + command = ["Rscript", str(script_path.name)] + command_str = " ".join(command) + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + cwd=Path.cwd() + ) + + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_file)] + } + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Rscript execution failed with return code {e.returncode}.\n" + f"Command: {command_str}\n" + f"Stderr: {e.stderr}\n" + f"Stdout: {e.stdout}" + ) from e + finally: + script_path.unlink() \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-scdotplot/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3d4c868a815aa829265425f503d9268db6cc690e --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/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-scdotplot via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-scdotplot -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-scdotplot_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-scdotplot_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-scdotplot_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_server.py b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_server.py new file mode 100644 index 0000000000000000000000000000000000000000..529c7f8de6c5b17dd6802bc546bcb9e635f3ba8a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_server.py @@ -0,0 +1,273 @@ +import subprocess +import tempfile +import json +from pathlib import Path +from typing import List, Optional, Union + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_scdotplot' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def sc_dot_plot( + input_path: str, + features: List[str], + group_by: str, + output_path: str = "dotplot.pdf", + cluster_cols: bool = True, + cluster_rows: bool = True, + assay: str = "RNA", + slot: str = "data", + scale: bool = True, + cap: float = 2.5, + annotation_cols: Optional[List[str]] = None, + annotation_rows: Optional[List[str]] = None, +) -> dict: + """ + Generate a clustered single-cell RNA-seq dot plot using the scDotPlot package. + + Args: + input_path: Path to the input file (RDS format containing Seurat or SingleCellExperiment object). + features: List of gene names or features to plot. + group_by: Metadata column name to group cells by (e.g., 'ident' or 'cluster'). + output_path: Path where the resulting plot (PDF, PNG, etc.) will be saved. + cluster_cols: Whether to perform hierarchical clustering on the columns (features). + cluster_rows: Whether to perform hierarchical clustering on the rows (cell groups). + assay: Name of the assay to use (for Seurat objects). + slot: Name of the slot to use (e.g., 'data', 'counts'). + scale: Whether to scale the expression data. + cap: Value at which to cap the scaled expression data. + annotation_cols: Optional list of metadata columns to use for annotating the columns. + annotation_rows: Optional list of metadata columns to use for annotating the rows. + """ + # Input validation + input_file = Path(input_path) + if not input_file.exists(): + return {"error": f"Input file not found: {input_path}"} + + output_file = Path(output_path) + output_file.parent.mkdir(parents=True, exist_ok=True) + + # Prepare R script + features_r = ", ".join([f"'{f}'" for f in features]) + + # Handle optional annotations + ann_cols_r = "NULL" + if annotation_cols: + ann_cols_r = "c(" + ", ".join([f"'{a}'" for a in annotation_cols]) + ")" + + ann_rows_r = "NULL" + if annotation_rows: + ann_rows_r = "c(" + ", ".join([f"'{a}'" for a in annotation_rows]) + ")" + + r_script = f""" + library(scDotPlot) + library(Seurat) + library(SingleCellExperiment) + library(ggplot2) + + # Load object + obj <- readRDS('{input_path}') + + # Generate plot + p <- scDotPlot( + object = obj, + features = c({features_r}), + group.by = '{group_by}', + cluster.cols = {str(cluster_cols).upper()}, + cluster.rows = {str(cluster_rows).upper()}, + assay = '{assay}', + slot = '{slot}', + scale = {str(scale).upper()}, + cap = {cap}, + annotation_cols = {ann_cols_r}, + annotation_rows = {ann_rows_r} + ) + + # Save plot + ggsave('{output_path}', plot = p) + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script) + tmp_path = tmp.name + + try: + result = subprocess.run( + ["Rscript", tmp_path], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"scDotPlot on {input_path}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(output_file.absolute())] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr, + "command_executed": f"Rscript {tmp_path}" + } + finally: + Path(tmp_path).unlink(missing_ok=True) + +@mcp.tool() +def sc_dot_plot_data( + input_path: str, + features: List[str], + group_by: str, + output_csv: str = "dotplot_data.csv", + assay: str = "RNA", + slot: str = "data", + scale: bool = True, + cap: float = 2.5, +) -> dict: + """ + Extract the underlying data frame used for generating a scDotPlot. + + Args: + input_path: Path to the input file (RDS format). + features: List of gene names or features. + group_by: Metadata column name to group cells by. + output_csv: Path to save the extracted data as a CSV file. + assay: Name of the assay to use. + slot: Name of the slot to use. + scale: Whether to scale the expression data. + cap: Value at which to cap the scaled expression data. + """ + input_file = Path(input_path) + if not input_file.exists(): + return {"error": f"Input file not found: {input_path}"} + + features_r = ", ".join([f"'{f}'" for f in features]) + + r_script = f""" + library(scDotPlot) + library(Seurat) + library(SingleCellExperiment) + + obj <- readRDS('{input_path}') + + # Extract data using the internal data preparation logic + plot_data <- scDotPlotData( + object = obj, + features = c({features_r}), + group.by = '{group_by}', + assay = '{assay}', + slot = '{slot}', + scale = {str(scale).upper()}, + cap = {cap} + ) + + write.csv(plot_data, '{output_csv}', row.names = FALSE) + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script) + tmp_path = tmp.name + + try: + result = subprocess.run( + ["Rscript", tmp_path], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"scDotPlotData on {input_path}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(Path(output_csv).absolute())] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + finally: + Path(tmp_path).unlink(missing_ok=True) + +@mcp.tool() +def sc_dot_plot_custom_df( + data_path: str, + features_col: str, + groups_col: str, + avg_exp_col: str, + pct_exp_col: str, + output_path: str = "dotplot_from_df.pdf", + cluster_cols: bool = True, + cluster_rows: bool = True, +) -> dict: + """ + Generate a clustered dot plot from a pre-calculated data frame instead of a Seurat/SCE object. + + Args: + data_path: Path to a CSV file containing the summarized expression data. + features_col: Column name in the CSV containing gene/feature names. + groups_col: Column name in the CSV containing cell group/cluster names. + avg_exp_col: Column name in the CSV containing average expression values. + pct_exp_col: Column name in the CSV containing percent of cells expressing the gene. + output_path: Path where the resulting plot will be saved. + cluster_cols: Whether to perform hierarchical clustering on columns. + cluster_rows: Whether to perform hierarchical clustering on rows. + """ + data_file = Path(data_path) + if not data_file.exists(): + return {"error": f"Data file not found: {data_path}"} + + r_script = f""" + library(scDotPlot) + library(ggplot2) + + # Load data frame + df <- read.csv('{data_path}') + + # Generate plot using the data frame method + # Note: scDotPlot handles data frames by mapping columns + p <- scDotPlot( + object = df, + features = '{features_col}', + group.by = '{groups_col}', + avg.exp = '{avg_exp_col}', + pct.exp = '{pct_exp_col}', + cluster.cols = {str(cluster_cols).upper()}, + cluster.rows = {str(cluster_rows).upper()} + ) + + ggsave('{output_path}', plot = p) + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp: + tmp.write(r_script) + tmp_path = tmp.name + + try: + result = subprocess.run( + ["Rscript", tmp_path], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"scDotPlot on data frame {data_path}", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(Path(output_path).absolute())] + } + except subprocess.CalledProcessError as e: + return { + "error": "R execution failed", + "stdout": e.stdout, + "stderr": e.stderr + } + finally: + Path(tmp_path).unlink(missing_ok=True) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..193ae85627d294bd324cb66c7627176ff2da21e2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_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-scdotplot/app/bioconductor-scdotplot_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_scdotplot' + + +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-scdotplot/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-scdotplot/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..75f17138ca6bc36ee85fef8d3f0095d8611fcb9c --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-scdotplot: + build: . + image: mcp-bioconductor-scdotplot:latest + container_name: mcp-bioconductor-scdotplot + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-scdotplot + 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-scdotplot/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a13eaadf703def4a3951f4ee0662665909ca7c19 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-scdotplot + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-scdotplot/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-scdotplot/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-spaniel/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-spaniel/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9dbd68e1fb55260579019aed1a41043a433e47dd --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-spaniel/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-spaniel via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-spaniel -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-spaniel_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-spaniel_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-spaniel_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-spaniel/app/bioconductor-spaniel_server.py b/Biomni/mcp_generated/mcp_bioconductor-spaniel/app/bioconductor-spaniel_server.py new file mode 100644 index 0000000000000000000000000000000000000000..383899a909a5fa655059751218ee4001f707d2d0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-spaniel/app/bioconductor-spaniel_server.py @@ -0,0 +1,152 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List + +# 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_spaniel' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def rscript( + script_file: Optional[Path] = None, + expressions: Optional[List[str]] = None, + script_args: Optional[List[str]] = None, + version: bool = False, + verbose: bool = False, + default_packages: Optional[str] = None, + vanilla: bool = False, + save: bool = False, + no_environ: bool = False, + no_site_file: bool = False, + no_init_file: bool = False, + restore: bool = False +): + """ + Executes an R script or R expressions using the Rscript command-line tool. + + This tool serves as a wrapper for Rscript, the non-interactive R interpreter. + It can be used to run bioinformatics scripts, such as those utilizing + Bioconductor packages. You must provide either a script file to execute or + a list of R expressions. + + Args: + script_file: Path to the R script file to be executed. + expressions: A list of R expressions to execute. Use instead of script_file. + script_args: A list of arguments to be passed to the R script itself. + version: If True, print the R version and exit. + verbose: If True, enable verbose output, printing progress information. + default_packages: A comma-separated list of package names to be loaded by default. + vanilla: If True, combines --no-save, --no-restore, --no-site-file, + --no-init-file, and --no-environ. Overrides individual flags. + save: If True, save the workspace at the end of the session. Ignored if vanilla is True. + no_environ: If True, do not read site and user environment files. Ignored if vanilla is True. + no_site_file: If True, do not read the site-wide Rprofile. Ignored if vanilla is True. + no_init_file: If True, do not read the user R profile. Ignored if vanilla is True. + restore: If True, restore previously saved objects at startup. Ignored if vanilla is True. + + Returns: + A dictionary containing the executed command, stdout, stderr, and a list of output files. + Note: Rscript itself does not define output files; any files created are determined by the + script being executed. This tool does not track those files. + """ + # --- Input Validation --- + if version: + if script_file or expressions: + logger.warning("--version is set; other execution arguments like script_file and expressions will be ignored.") + elif not script_file and not expressions: + raise ValueError("You must provide either 'script_file' or 'expressions' to execute.") + elif script_file and expressions: + raise ValueError("You cannot provide both 'script_file' and 'expressions'. Please choose one.") + + if script_file and not script_file.is_file(): + raise FileNotFoundError(f"The specified script file does not exist: {script_file}") + + # --- Command Construction --- + cmd = ["Rscript"] + + if version: + cmd.append("--version") + else: + # Add R environment options + if vanilla: + cmd.append("--vanilla") + else: + if save: + cmd.append("--save") + if no_environ: + cmd.append("--no-environ") + if no_site_file: + cmd.append("--no-site-file") + if no_init_file: + cmd.append("--no-init-file") + if restore: + cmd.append("--restore") + + # Add other options + if verbose: + cmd.append("--verbose") + if default_packages: + cmd.append(f"--default-packages={default_packages}") + + # Add the script file or expressions to execute + if script_file: + cmd.append(str(script_file)) + elif expressions: + for expr in expressions: + cmd.extend(["-e", expr]) + + # Add arguments for the script itself + if script_args: + cmd.extend(script_args) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + stdout = result.stdout + stderr = result.stderr + logger.info("Rscript executed successfully.") + + except FileNotFoundError: + logger.error("Rscript not found. Is R installed and in your system's PATH?") + raise + except subprocess.CalledProcessError as e: + logger.error(f"Rscript execution failed with return code {e.returncode}.") + logger.error(f"STDOUT: {e.stdout}") + logger.error(f"STDERR: {e.stderr}") + # Re-raise to signal failure to the MCP controller + raise e + + # --- Structured Result Return --- + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": [] # Rscript itself doesn't manage output files + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_bioconductor-spaniel/app/bioconductor-spaniel_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-spaniel/app/bioconductor-spaniel_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3909ca613c50cb3c7a92be6b6649bd731aee34e9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-spaniel/app/bioconductor-spaniel_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-spaniel/app/bioconductor-spaniel_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_spaniel' + + +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-spaniel/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-spaniel/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..e8bdb553b274706a2c79f0789f4397e161629dc9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-spaniel/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-spaniel: + build: . + image: mcp-bioconductor-spaniel:latest + container_name: mcp-bioconductor-spaniel + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-spaniel + 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-spaniel/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-spaniel/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0ae5308d6c56dea2d8bb0f7777b29c3031a0218 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-spaniel/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-spaniel + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-spaniel/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-spaniel/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-spaniel/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/Dockerfile b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d59501990993d1eeeb40a8a91a09ed7342475cc9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/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-tenxxeniumdata via conda (e.g., from bioconda) +RUN conda install -c bioconda bioconductor-tenxxeniumdata -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-tenxxeniumdata_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-tenxxeniumdata_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-tenxxeniumdata_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/app/bioconductor-tenxxeniumdata_shim_server.py b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/app/bioconductor-tenxxeniumdata_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ff6d78a33a24d9dea20011bd3c4969c3fe3f8a9f --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/app/bioconductor-tenxxeniumdata_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-tenxxeniumdata/app/bioconductor-tenxxeniumdata_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_bioconductor_tenxxeniumdata' + + +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-tenxxeniumdata/app/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/docker-compose.yml b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..e2167c28c7b4737a4b32594ef03d88c34d6db8b9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-bioconductor-tenxxeniumdata: + build: . + image: mcp-bioconductor-tenxxeniumdata:latest + container_name: mcp-bioconductor-tenxxeniumdata + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=bioconductor-tenxxeniumdata + 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-tenxxeniumdata/environment.yaml b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f34874e474d51c77838554d6d2270d5488ada46a --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - bioconductor-tenxxeniumdata + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/requirements.txt b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-tenxxeniumdata/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_bioconductor-zlibbioc/app/bioconductor-zlibbioc_server.py b/Biomni/mcp_generated/mcp_bioconductor-zlibbioc/app/bioconductor-zlibbioc_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1b847d6f7acb9e8356173aadef2c326f9fd877c7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_bioconductor-zlibbioc/app/bioconductor-zlibbioc_server.py @@ -0,0 +1,180 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Dict, Any + +# Assume @mcp.tool is imported from the mcp library +# from mcp import tool + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_bioconductor_zlibbioc' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_rscript( + r_script_file: Optional[Path] = None, + expressions: Optional[List[str]] = None, + r_script_args: Optional[List[str]] = None, + help_flag: bool = False, + version_flag: bool = False, + verbose: bool = False, + default_packages: Optional[str] = None, + save: bool = False, + no_environ: bool = False, + no_site_file: bool = False, + no_init_file: bool = False, + restore: bool = False, + vanilla: bool = False, +) -> Dict[str, Any]: + """ + Executes an R script file or R expressions using the Rscript command. + + This tool provides an interface to the Rscript command, allowing users to run R code + from the command line, either by specifying a script file or by providing R expressions + directly. It supports various Rscript options for controlling the R session behavior. + + Args: + r_script_file: Path to an R script file to execute. Mutually exclusive with 'expressions'. + The file must exist. + expressions: A list of R expressions to execute. Each string in the list will be + passed with a separate '-e' flag. Mutually exclusive with 'r_script_file'. + r_script_args: A list of arguments to be passed to the R script itself. These are + appended after the script file or expressions. + help_flag: If True, print usage information and exit. When True, all other parameters + related to script execution are ignored. + version_flag: If True, print version information and exit. When True, all other parameters + related to script execution are ignored. + verbose: If True, print information on progress during Rscript execution. + default_packages: A comma-separated list of package names, or 'NULL', to load by default. + If None, this option is not included. + save: If True, save the workspace at the end of the session. Cannot be used with 'vanilla'. + no_environ: If True, don't read the site and user environment files. + This is implied by 'vanilla'. + no_site_file: If True, don't read the site-wide Rprofile. + This is implied by 'vanilla'. + no_init_file: If True, don't read the user R profile. + This is implied by 'vanilla'. + restore: If True, restore previously saved objects at startup. Cannot be used with 'vanilla'. + vanilla: If True, combine --no-save, --no-restore, --no-site-file, --no-init-file, + and --no-environ. Cannot be used with 'save' or 'restore'. + + Returns: + A dictionary containing the command executed, stdout, stderr, and any output files. + 'output_files' will typically be empty unless the R script itself creates files + in a known location. + """ + cmd = ["Rscript"] + output_files: List[Path] = [] + + # Handle --help and --version first, as they are standalone and override other options + if help_flag: + cmd.append("--help") + elif version_flag: + cmd.append("--version") + else: + # Input validation for script/expressions + if r_script_file and expressions: + raise ValueError("Cannot specify both 'r_script_file' and 'expressions'. Choose one.") + if not r_script_file and not expressions: + raise ValueError("Either 'r_script_file' or 'expressions' must be provided.") + + # Validate --vanilla conflicts + if vanilla: + conflicting_options = [] + if save: + conflicting_options.append("--save") + if restore: + conflicting_options.append("--restore") + if conflicting_options: + raise ValueError( + f"Cannot use --vanilla with conflicting options: {', '.join(conflicting_options)}. " + "--vanilla implies --no-save and --no-restore." + ) + + # Add general options + if verbose: + cmd.append("--verbose") + if default_packages is not None: + cmd.append(f"--default-packages={default_packages}") + + # Add R session options + if vanilla: + cmd.append("--vanilla") + else: + # Only add these if vanilla is not set, to avoid redundancy and potential conflicts + if save: + cmd.append("--save") + if no_environ: + cmd.append("--no-environ") + if no_site_file: + cmd.append("--no-site-file") + if no_init_file: + cmd.append("--no-init-file") + if restore: + cmd.append("--restore") + + # Handle script file vs expressions + if r_script_file: + if not r_script_file.is_file(): + raise FileNotFoundError(f"R script file not found: {r_script_file}") + cmd.append(str(r_script_file)) + elif expressions: + for expr in expressions: + cmd.extend(["-e", expr]) + + # Add arguments to the R script + if r_script_args: + cmd.extend(r_script_args) + + try: + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + stdout = process.stdout + stderr = process.stderr + command_executed = " ".join(cmd) + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } + except FileNotFoundError: + # This specific error means the 'Rscript' executable itself was not found + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": "Error: Rscript command not found. Please ensure R is installed and 'Rscript' is in your system's PATH.", + "output_files": [], + } + except subprocess.CalledProcessError as e: + # This means Rscript ran, but exited with a non-zero status + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + } + except ValueError as e: + # Catch validation errors before subprocess call + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": f"Input validation error: {e}", + "output_files": [], + } + except Exception as e: + # Catch any other unexpected errors + return { + "command_executed": " ".join(cmd), + "stdout": "", + "stderr": f"An unexpected error occurred: {e}", + "output_files": [], + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_c-ares/Dockerfile b/Biomni/mcp_generated/mcp_c-ares/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c838dfe6abb8ea5aa3d5f168267e9f6a01cd4028 --- /dev/null +++ b/Biomni/mcp_generated/mcp_c-ares/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 c-ares via conda (e.g., from bioconda) +RUN conda install -c bioconda c-ares -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/c-ares_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/c-ares_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/c-ares_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_c-ares/app/c-ares_server.py b/Biomni/mcp_generated/mcp_c-ares/app/c-ares_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b4dc5d6a3b83f3ef65213298654f4ad3c3dc047b --- /dev/null +++ b/Biomni/mcp_generated/mcp_c-ares/app/c-ares_server.py @@ -0,0 +1,211 @@ +import subprocess +import logging +from typing import Optional, List + +# MCP-decorated functions for the c-ares command-line tools: adig and ahost + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_c_ares' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def adig( + hostname: str, + query_type: Optional[str] = None, + query_class: Optional[str] = None, + servers: Optional[List[str]] = None, + debug: bool = False, + use_tcp: bool = False, + use_udp: bool = False, + flags: Optional[List[str]] = None, + tcp_timeout: Optional[int] = None, + udp_timeout: Optional[int] = None, + retries: Optional[int] = None, +): + """ + Performs an asynchronous DNS lookup using the adig utility. + + adig is a command-line tool similar to dig that uses the c-ares library + for asynchronous DNS requests. + + Args: + hostname: The hostname to query. + query_type: The query type (e.g., A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, TXT). Defaults to 'A'. + query_class: The query class (e.g., IN, CH, HS, ANY). Defaults to 'IN'. + servers: A list of DNS servers to use for the query. + debug: Enable debug output. + use_tcp: Use TCP for the query instead of UDP. + use_udp: Use UDP for the query (this is the default). + flags: A list of c-ares flags to set (e.g., 'usevc', 'norecurse'). + tcp_timeout: Set the TCP timeout in seconds. + udp_timeout: Set the UDP timeout in seconds. + retries: Set the number of retries. + """ + # Input validation + if use_tcp and use_udp: + raise ValueError("Cannot specify both use_tcp (-s) and use_udp (-u).") + + valid_flags = {"usevc", "primary", "igntc", "norecurse", "stayopen", "noaliases"} + if flags: + for flag in flags: + if flag not in valid_flags: + raise ValueError(f"Invalid flag '{flag}'. Valid flags are: {', '.join(valid_flags)}") + + if tcp_timeout is not None and tcp_timeout < 0: + raise ValueError("tcp_timeout must be a non-negative integer.") + + if udp_timeout is not None and udp_timeout < 0: + raise ValueError("udp_timeout must be a non-negative integer.") + + if retries is not None and retries < 0: + raise ValueError("retries must be a non-negative integer.") + + # Command construction + cmd = ["adig"] + + if debug: + cmd.append("-d") + if use_tcp: + cmd.append("-s") + if use_udp: + cmd.append("-u") + + if flags: + for flag in flags: + cmd.extend(["-f", flag]) + + if tcp_timeout is not None: + cmd.extend(["-T", str(tcp_timeout)]) + + if udp_timeout is not None: + cmd.extend(["-U", str(udp_timeout)]) + + if retries is not None: + cmd.extend(["-r", str(retries)]) + + if servers: + for server in servers: + cmd.append(f"@{server}") + + # Positional arguments + cmd.append(hostname) + if query_type: + cmd.append(query_type) + if query_class: + cmd.append(query_class) + + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + # Subprocess execution + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'adig' command not found. Is c-ares installed and in the system's PATH?", + "output_files": [] + } + except subprocess.CalledProcessError as e: + logging.error(f"adig execution failed: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +@mcp.tool() +def ahost( + host_or_address: str, + debug: bool = False, + flags: Optional[List[str]] = None, + family: Optional[str] = None, +): + """ + Performs an asynchronous DNS lookup for a hostname or IP address using the ahost utility. + + ahost is a command-line tool similar to host that uses the c-ares library + for asynchronous DNS requests. + + Args: + host_or_address: The hostname or IP address to look up. + debug: Enable debug output. + flags: A list of c-ares flags to set (e.g., 'usevc', 'norecurse'). + family: Address family to search for (one of 'inet', 'inet6'). + """ + # Input validation + valid_flags = {"usevc", "primary", "igntc", "norecurse", "stayopen", "noaliases"} + if flags: + for flag in flags: + if flag not in valid_flags: + raise ValueError(f"Invalid flag '{flag}'. Valid flags are: {', '.join(valid_flags)}") + + valid_families = {"inet", "inet6"} + if family and family not in valid_families: + raise ValueError(f"Invalid family '{family}'. Valid families are: 'inet', 'inet6'.") + + # Command construction + cmd = ["ahost"] + + if debug: + cmd.append("-d") + + if flags: + for flag in flags: + cmd.extend(["-f", flag]) + + if family: + cmd.extend(["-s", family]) + + # Positional argument + cmd.append(host_or_address) + + command_executed = " ".join(cmd) + logging.info(f"Executing command: {command_executed}") + + # Subprocess execution + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'ahost' command not found. Is c-ares installed and in the system's PATH?", + "output_files": [] + } + except subprocess.CalledProcessError as e: + logging.error(f"ahost execution failed: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_c-ares/app/c-ares_shim_server.py b/Biomni/mcp_generated/mcp_c-ares/app/c-ares_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..602d0306a62db55fa0fe089b950b413501c958e5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_c-ares/app/c-ares_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_c-ares/app/c-ares_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_c_ares' + + +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_c-ares/app/requirements.txt b/Biomni/mcp_generated/mcp_c-ares/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_c-ares/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_c-ares/docker-compose.yml b/Biomni/mcp_generated/mcp_c-ares/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d40aed9ba9aa4997be56d2b66dc70055654731f9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_c-ares/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-c-ares: + build: . + image: mcp-c-ares:latest + container_name: mcp-c-ares + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=c-ares + 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_c-ares/environment.yaml b/Biomni/mcp_generated/mcp_c-ares/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e37f8a3c620056dc5b7f9490d6e486291739c21d --- /dev/null +++ b/Biomni/mcp_generated/mcp_c-ares/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - c-ares + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_c-ares/requirements.txt b/Biomni/mcp_generated/mcp_c-ares/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_c-ares/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_cellqc/Dockerfile b/Biomni/mcp_generated/mcp_cellqc/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..cf857a360bb89dc8448c35e587d8923712660aa2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cellqc/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 cellqc via conda (e.g., from bioconda) +RUN conda install -c bioconda cellqc -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/cellqc_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/cellqc_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/cellqc_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_cellqc/app/cellqc_server.py b/Biomni/mcp_generated/mcp_cellqc/app/cellqc_server.py new file mode 100644 index 0000000000000000000000000000000000000000..da91d312e64afc13435adc98f74efafc702fb8eb --- /dev/null +++ b/Biomni/mcp_generated/mcp_cellqc/app/cellqc_server.py @@ -0,0 +1,180 @@ +import subprocess +import tempfile +import yaml +import logging +from pathlib import Path +from typing import Optional, List, Dict + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# MCP decorator is not defined here, but is expected to be in the execution environment. +# We will use a placeholder class for demonstration and type-hinting. +class mcp: + @staticmethod + def tool(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + +@mcp.tool +def run_cellqc( + samples_file: Path, + output_dir: Path, + numthreads: int = 12, + cbtag: str = "CB", + retag: str = "RE", + dropkick_skip: bool = False, + dropkick_method: str = "multiotsu", + dropkick_numthreads: int = 1, + filterbycount_mincount: int = 500, + filterbycount_minfeature: int = 300, + filterbycount_mito: int = 10, + doubletfinder_skip: bool = False, + doubletfinder_findpK: bool = False, + doubletfinder_numthreads: int = 5, + doubletfinder_pK: float = 0.01, + scpred_skip: bool = True, + scpred_reference: Optional[Path] = None, + scpred_threshold: float = 0.9, + dryrun: bool = False, +) -> Dict: + """ + Runs the cellqc standardized quality control pipeline for single-cell RNA-Seq data. + + This tool takes a sample sheet and various QC parameters to execute a Snakemake + workflow that filters empty droplets, removes doublets, and annotates cell types, + producing clean feature-count matrices. + """ + # 1. Input Validation + if not samples_file.is_file(): + raise FileNotFoundError(f"Sample file not found: {samples_file}") + + if not scpred_skip: + if scpred_reference is None: + raise ValueError("scpred.reference must be provided when scpred.skip is False.") + if not scpred_reference.is_file(): + raise FileNotFoundError(f"scPred reference file not found: {scpred_reference}") + + allowed_dropkick_methods = ["multiotsu", "otsu", "li", "mean"] + if dropkick_method not in allowed_dropkick_methods: + raise ValueError(f"dropkick.method must be one of {allowed_dropkick_methods}, not '{dropkick_method}'.") + + if numthreads <= 0: + raise ValueError("numthreads must be a positive integer.") + if filterbycount_mincount < 0 or filterbycount_minfeature < 0: + raise ValueError("mincount and minfeature must be non-negative.") + if not (0 <= filterbycount_mito <= 100): + raise ValueError("mito (mitochondria percentage) must be between 0 and 100.") + + # 2. Create Output Directory + output_dir.mkdir(parents=True, exist_ok=True) + + # 3. Generate YAML Config from parameters + config_data = { + "numthreads": numthreads, + "cbtag": cbtag, + "retag": retag, + "dropkick": { + "skip": dropkick_skip, + "method": dropkick_method, + "numthreads": dropkick_numthreads, + }, + "filterbycount": { + "mincount": filterbycount_mincount, + "minfeature": filterbycount_minfeature, + "mito": filterbycount_mito, + }, + "doubletfinder": { + "skip": doubletfinder_skip, + "findpK": doubletfinder_findpK, + "numthreads": doubletfinder_numthreads, + "pK": doubletfinder_pK, + }, + "scpred": { + "skip": scpred_skip, + "reference": str(scpred_reference) if scpred_reference else None, + "threshold": scpred_threshold, + }, + } + + # 4. Subprocess Execution + # The cellqc command-line interface is not explicitly documented. We assume a common + # pattern for Snakemake wrappers: passing sample/config files and core options as arguments. + # The wrapper is expected to handle the integration with the underlying Snakemake pipeline. + + command_executed = "" + stdout_str = "" + stderr_str = "" + output_files: List[str] = [] + + try: + with tempfile.NamedTemporaryFile(mode='w', suffix=".yaml", delete=False, dir=output_dir) as tmp_config: + yaml.dump(config_data, tmp_config, default_flow_style=False) + config_path = Path(tmp_config.name) + + # Base command + cmd = [ + "cellqc", + "--samples", str(samples_file.resolve()), + "--config", str(config_path.resolve()), + "--outdir", str(output_dir.resolve()), + "--cores", str(numthreads) + ] + + if dryrun: + cmd.append("--dryrun") + + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + cwd=output_dir # Run in the output directory + ) + + stdout_str = process.stdout + stderr_str = process.stderr + + # 5. Collect Output Files + result_dir = output_dir / "result" + postproc_dir = output_dir / "postproc" + + if result_dir.is_dir(): + output_files.extend(str(p.resolve()) for p in result_dir.glob("*.h5seurat")) + output_files.extend(str(p.resolve()) for p in result_dir.glob("*.h5ad")) + if (result_dir / "report.html").is_file(): + output_files.append(str((result_dir / "report.html").resolve())) + + if postproc_dir.is_dir(): + output_files.extend(str(p.resolve()) for p in postproc_dir.glob("*.h5ad")) + + except FileNotFoundError: + raise RuntimeError("`cellqc` command not found. Please ensure it is installed and in your PATH.") + except subprocess.CalledProcessError as e: + logger.error(f"cellqc pipeline failed with exit code {e.returncode}") + logger.error(f"STDOUT: {e.stdout}") + logger.error(f"STDERR: {e.stderr}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + "error": "Pipeline execution failed." + } + finally: + # Clean up the temporary config file + if 'config_path' in locals() and config_path.exists(): + config_path.unlink() + + # 6. Return Structured Output + return { + "command_executed": command_executed, + "stdout": stdout_str, + "stderr": stderr_str, + "output_files": output_files + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_cellqc/app/cellqc_shim_server.py b/Biomni/mcp_generated/mcp_cellqc/app/cellqc_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3b06dc1a293ca096424fe4f8e1a9369650fcb74e --- /dev/null +++ b/Biomni/mcp_generated/mcp_cellqc/app/cellqc_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_cellqc/app/cellqc_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_cellqc' + + +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_cellqc/app/requirements.txt b/Biomni/mcp_generated/mcp_cellqc/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f62ce0c56db2b5d134cff98f7fb1625023cd2874 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cellqc/app/requirements.txt @@ -0,0 +1 @@ +PyYAML==6.0.3 diff --git a/Biomni/mcp_generated/mcp_cellqc/docker-compose.yml b/Biomni/mcp_generated/mcp_cellqc/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..041c8a5419777a6e83688b9368c771a416445f4e --- /dev/null +++ b/Biomni/mcp_generated/mcp_cellqc/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-cellqc: + build: . + image: mcp-cellqc:latest + container_name: mcp-cellqc + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=cellqc + 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_cellqc/environment.yaml b/Biomni/mcp_generated/mcp_cellqc/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e255270dae33a2936ffcb379ad4835edf6a9aea7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cellqc/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - cellqc + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_cellqc/requirements.txt b/Biomni/mcp_generated/mcp_cellqc/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..c841f3ed8ad31753f4f5da0ef0b754abc017e725 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cellqc/requirements.txt @@ -0,0 +1,3 @@ +PyYAML==6.0.3 +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_constellations/Dockerfile b/Biomni/mcp_generated/mcp_constellations/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f69e44d668aedbe7dbc4df45f73ce70f647ad9fd --- /dev/null +++ b/Biomni/mcp_generated/mcp_constellations/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 constellations via conda (e.g., from bioconda) +RUN conda install -c bioconda constellations -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/constellations_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/constellations_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/constellations_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_constellations/app/constellations_server.py b/Biomni/mcp_generated/mcp_constellations/app/constellations_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2691aa04a2224c06161510200a93610b5a80591f --- /dev/null +++ b/Biomni/mcp_generated/mcp_constellations/app/constellations_server.py @@ -0,0 +1,100 @@ +import subprocess +import json +from pathlib import Path +from typing import Optional + +# MCP-ready code for the 'constellations' package. +# Based on the analysis, 'constellations' is a data package providing JSON definitions +# for SARS-CoV-2 mutations, intended for use by other tools like 'scorpio' and 'pangolin'. +# It does not have a command-line interface of its own. The error message from the +# --help attempt confirms this. +# +# Therefore, instead of wrapping a non-existent CLI, this MCP tool will provide +# information about the package and how to access its data, which is the primary +# function of this package. + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_constellations' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def constellations_info(): + """ + Provides information about the constellations data package. + + The 'constellations' package is a data-only package containing descriptions of + constellations of mutations for the SARS-CoV-2 virus. It does not provide a + command-line executable. Instead, it installs a collection of JSON files used by + other tools like 'scorpio' and 'pangolin' to classify viral lineages. + + This function returns the path to the installed constellations data directory. + """ + # The 'constellations' package does not have a CLI. + # The command `constellations --help` fails with a ModuleNotFoundError. + # This function will instead provide information about the package's purpose + # and locate the data files it provides. + + command_to_find_path = [ + "python", + "-c", + "import constellations; import os; print(os.path.dirname(constellations.__file__))" + ] + + command_executed = " ".join(command_to_find_path) + stdout_message = "" + stderr_message = "" + output_files = {} + + try: + # Execute a python command to find the installation path of the package + process = subprocess.run( + command_to_find_path, + capture_output=True, + text=True, + check=True + ) + + package_path = Path(process.stdout.strip()) + constellations_dir = package_path / "constellations" + + if not constellations_dir.is_dir(): + raise FileNotFoundError(f"Constellations data directory not found at: {constellations_dir}") + + # List the contents of the constellations directory + files = [str(f) for f in constellations_dir.iterdir() if f.is_file()] + + stdout_message = ( + "The 'constellations' package is a data package for SARS-CoV-2 mutations.\n" + "It does not have a direct command-line interface.\n" + "The data files are located in the directory listed in 'output_files'.\n" + f"Found {len(files)} definition files." + ) + + output_files = { + "constellations_data_directory": str(constellations_dir), + "definition_files": files + } + + stderr_message = process.stderr + + except FileNotFoundError as e: + stderr_message = str(e) + stdout_message = "Could not locate the constellations data directory. The package might not be installed correctly." + except subprocess.CalledProcessError as e: + stdout_message = "Failed to locate the constellations package path." + stderr_message = e.stderr + except Exception as e: + stdout_message = "An unexpected error occurred." + stderr_message = str(e) + + + return { + "command_executed": command_executed, + "stdout": stdout_message, + "stderr": stderr_message, + "output_files": output_files + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_constellations/app/constellations_shim_server.py b/Biomni/mcp_generated/mcp_constellations/app/constellations_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..5d796a0737475fe681629b2999be5153986ea204 --- /dev/null +++ b/Biomni/mcp_generated/mcp_constellations/app/constellations_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_constellations/app/constellations_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_constellations' + + +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_constellations/app/requirements.txt b/Biomni/mcp_generated/mcp_constellations/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_constellations/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_constellations/docker-compose.yml b/Biomni/mcp_generated/mcp_constellations/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d0d68dfd5d9d4689f1674199d184eebf729b1803 --- /dev/null +++ b/Biomni/mcp_generated/mcp_constellations/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-constellations: + build: . + image: mcp-constellations:latest + container_name: mcp-constellations + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=constellations + 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_constellations/environment.yaml b/Biomni/mcp_generated/mcp_constellations/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8c02af7b6017bcad5d5bed0e888d7c9dd3fe573c --- /dev/null +++ b/Biomni/mcp_generated/mcp_constellations/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - constellations + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_constellations/requirements.txt b/Biomni/mcp_generated/mcp_constellations/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_constellations/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_cooler/app/cooler_server.py b/Biomni/mcp_generated/mcp_cooler/app/cooler_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9849939c118258ddd36adb671c80160993ae92b7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cooler/app/cooler_server.py @@ -0,0 +1,738 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, List, Dict, Any + +# Helper function to handle subprocess execution and error reporting +def _run_cooler_command(cmd: List[str], output_path: Optional[Path] = None) -> Dict[str, Any]: + """ + Executes a cooler command using subprocess, captures output, and handles errors. + + Args: + cmd: A list of strings representing the command and its arguments. + output_path: The path to the primary output file, if one is created. + + Returns: + A dictionary containing the execution details and results. + """ + command_str = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + output_files = [str(output_path)] if output_path else [] + # If output is redirected to stdout and no file is created + if not output_files and result.stdout: + # Create a temporary file to store stdout if it's substantial + if len(result.stdout) > 1024: # Heuristic for large output + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', prefix='cooler_stdout_') as tmp: + tmp.write(result.stdout) + output_files.append(tmp.name) + + return { + "command_executed": command_str, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except FileNotFoundError: + return { + "command_executed": command_str, + "stdout": "", + "stderr": "Error: 'cooler' command not found. Please ensure it is installed and in your PATH.", + "output_files": [], + "error": "Command not found", + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_str, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + "error": "Command failed with exit code {}".format(e.returncode), + } + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_cooler' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def cooler_cload_tabix( + bins_path: Path, + pairs_path: Path, + cool_path: Path, + format_str: str = "COO", + metadata: Optional[Path] = None, + assembly: Optional[str] = None, + chunksize: int = 1000000, + zero_based: bool = False, + field: Optional[List[str]] = None, + count_as_float: bool = False, + append: bool = False, + no_symmetric_upper: bool = False, +) -> Dict[str, Any]: + """ + Load a contact matrix from a tabix-indexed pairs file. + + Args: + bins_path: Path to a BED-like file containing genomic bins. + pairs_path: Path to a sorted, bgzipped and tabix-indexed contact list file. + cool_path: Output cooler path. + format_str: Storage format for pixels. One of 'COO', 'CSR', 'Dense'. + metadata: Path to a JSON file containing user-defined metadata. + assembly: Name of genome assembly. + chunksize: The number of pixels loaded in each chunk. + zero_based: If set, coordinates in the contact list are 0-based. + field: Specify which columns to load and how, e.g., 'count=4:dtype=int'. Can be used multiple times. + count_as_float: Store the 'count' column as a float. + append: Append to an existing cooler file. + no_symmetric_upper: Create a non-symmetric cooler. + """ + if not bins_path.is_file(): + raise FileNotFoundError(f"Bins file not found: {bins_path}") + if not pairs_path.is_file(): + raise FileNotFoundError(f"Pairs file not found: {pairs_path}") + if format_str not in ["COO", "CSR", "Dense"]: + raise ValueError("format_str must be one of 'COO', 'CSR', 'Dense'") + + cmd = [ + "cooler", "cload", "tabix", + str(bins_path), + str(pairs_path), + str(cool_path), + ] + cmd.extend(["--format", format_str]) + if metadata: + if not metadata.is_file(): + raise FileNotFoundError(f"Metadata file not found: {metadata}") + cmd.extend(["--metadata", str(metadata)]) + if assembly: + cmd.extend(["--assembly", assembly]) + cmd.extend(["--chunksize", str(chunksize)]) + if zero_based: + cmd.append("--zero-based") + if field: + for f in field: + cmd.extend(["--field", f]) + if count_as_float: + cmd.append("--count-as-float") + if append: + cmd.append("--append") + if no_symmetric_upper: + cmd.append("--no-symmetric-upper") + + return _run_cooler_command(cmd, cool_path) + +@mcp.tool() +def cooler_cload_pairix( + bins_path: Path, + pairs_path: Path, + cool_path: Path, + metadata: Optional[Path] = None, + assembly: Optional[str] = None, + chunksize: int = 1000000, + zero_based: bool = False, + field: Optional[List[str]] = None, + count_as_float: bool = False, + append: bool = False, + no_symmetric_upper: bool = False, +) -> Dict[str, Any]: + """ + Load a contact matrix from a pairix-indexed pairs file. + + Args: + bins_path: Path to a BED-like file containing genomic bins. + pairs_path: Path to a sorted, bgzipped and pairix-indexed contact list file. + cool_path: Output cooler path. + metadata: Path to a JSON file containing user-defined metadata. + assembly: Name of genome assembly. + chunksize: The number of pixels loaded in each chunk. + zero_based: If set, coordinates in the contact list are 0-based. + field: Specify which columns to load and how, e.g., 'count=4:dtype=int'. Can be used multiple times. + count_as_float: Store the 'count' column as a float. + append: Append to an existing cooler file. + no_symmetric_upper: Create a non-symmetric cooler. + """ + if not bins_path.is_file(): + raise FileNotFoundError(f"Bins file not found: {bins_path}") + if not pairs_path.is_file(): + raise FileNotFoundError(f"Pairs file not found: {pairs_path}") + + cmd = [ + "cooler", "cload", "pairix", + str(bins_path), + str(pairs_path), + str(cool_path), + ] + if metadata: + if not metadata.is_file(): + raise FileNotFoundError(f"Metadata file not found: {metadata}") + cmd.extend(["--metadata", str(metadata)]) + if assembly: + cmd.extend(["--assembly", assembly]) + cmd.extend(["--chunksize", str(chunksize)]) + if zero_based: + cmd.append("--zero-based") + if field: + for f in field: + cmd.extend(["--field", f]) + if count_as_float: + cmd.append("--count-as-float") + if append: + cmd.append("--append") + if no_symmetric_upper: + cmd.append("--no-symmetric-upper") + + return _run_cooler_command(cmd, cool_path) + +@mcp.tool() +def cooler_csort( + pairs_path: Path, + output_file: Optional[Path] = None, + nproc: int = 8, + chunksize: int = 5000000, + tmp_dir: Optional[Path] = None, + memory: str = "4G", +) -> Dict[str, Any]: + """ + Sort a contact list file (pairs format). + + Args: + pairs_path: Path to a pairs file. Use '-' for stdin. + output_file: Output file path. If not specified, outputs to stdout. + nproc: Number of processes to use for sorting. + chunksize: Number of lines to process per chunk. + tmp_dir: Temporary directory for sorting. + memory: Maximum memory to use for sorting, e.g., '10G'. + """ + if str(pairs_path) != "-" and not pairs_path.is_file(): + raise FileNotFoundError(f"Pairs file not found: {pairs_path}") + + cmd = ["cooler", "csort", str(pairs_path)] + if output_file: + cmd.extend(["-o", str(output_file)]) + cmd.extend(["-p", str(nproc)]) + cmd.extend(["-c", str(chunksize)]) + if tmp_dir: + tmp_dir.mkdir(parents=True, exist_ok=True) + cmd.extend(["-T", str(tmp_dir)]) + cmd.extend(["-M", memory]) + + return _run_cooler_command(cmd, output_file) + +@mcp.tool() +def cooler_load( + bins_path: Path, + pixels_path: Path, + cool_path: Path, + format_str: str = "coo", + metadata: Optional[Path] = None, + assembly: Optional[str] = None, + chunksize: int = 1000000, + field: Optional[List[str]] = None, + count_as_float: bool = False, + no_symmetric_upper: bool = False, +) -> Dict[str, Any]: + """ + Load a contact matrix from a tabular file. + + Args: + bins_path: Path to a BED-like file containing genomic bins. + pixels_path: Path to a file containing pixel data. + cool_path: Output cooler path. + format_str: Format of the pixel file. One of 'coo' or 'bg2'. + metadata: Path to a JSON file containing user-defined metadata. + assembly: Name of genome assembly. + chunksize: The number of pixels loaded in each chunk. + field: Specify which columns to load and how. + count_as_float: Store the 'count' column as a float. + no_symmetric_upper: Create a non-symmetric cooler. + """ + if not bins_path.is_file(): + raise FileNotFoundError(f"Bins file not found: {bins_path}") + if not pixels_path.is_file(): + raise FileNotFoundError(f"Pixels file not found: {pixels_path}") + if format_str not in ["coo", "bg2"]: + raise ValueError("format_str must be one of 'coo' or 'bg2'") + + cmd = ["cooler", "load", str(bins_path), str(pixels_path), str(cool_path)] + cmd.extend(["--format", format_str]) + if metadata: + if not metadata.is_file(): + raise FileNotFoundError(f"Metadata file not found: {metadata}") + cmd.extend(["--metadata", str(metadata)]) + if assembly: + cmd.extend(["--assembly", assembly]) + cmd.extend(["--chunksize", str(chunksize)]) + if field: + for f in field: + cmd.extend(["--field", f]) + if count_as_float: + cmd.append("--count-as-float") + if no_symmetric_upper: + cmd.append("--no-symmetric-upper") + + return _run_cooler_command(cmd, cool_path) + +@mcp.tool() +def cooler_merge( + cool_path: Path, + in_paths: List[Path], + chunksize: int = 1000000, + merge_attrs: str = "union", +) -> Dict[str, Any]: + """ + Merge multiple cooler files. + + Args: + cool_path: Output cooler path. + in_paths: List of at least two input cooler paths. + chunksize: The number of pixels loaded in each chunk. + merge_attrs: Strategy for merging attributes. One of 'union', 'intersection', 'first', 'last', 'unique', 'fail'. + """ + if len(in_paths) < 2: + raise ValueError("At least two input cooler paths are required for merging.") + for p in in_paths: + if not p.is_file(): + raise FileNotFoundError(f"Input cooler file not found: {p}") + if merge_attrs not in ["union", "intersection", "first", "last", "unique", "fail"]: + raise ValueError(f"Invalid merge_attrs strategy: {merge_attrs}") + + cmd = ["cooler", "merge", str(cool_path)] + cmd.extend([str(p) for p in in_paths]) + cmd.extend(["--chunksize", str(chunksize)]) + cmd.extend(["--merge-attrs", merge_attrs]) + + return _run_cooler_command(cmd, cool_path) + +@mcp.tool() +def cooler_coarsen( + cool_path: Path, + factor: int, + output_file: Optional[Path] = None, + nproc: int = 8, + chunksize: int = 1000000, + agg: str = "sum", +) -> Dict[str, Any]: + """ + Coarsen a cooler to a lower resolution. + + Args: + cool_path: Path to a cooler file. + factor: The coarsening factor. + output_file: Output file path. If not specified, appends to the input file. + nproc: Number of processes to use. + chunksize: Number of pixels to process per chunk. + agg: Aggregation function for value columns. Default is 'sum'. + """ + if not cool_path.is_file(): + raise FileNotFoundError(f"Input cooler file not found: {cool_path}") + if factor <= 0: + raise ValueError("Factor must be a positive integer.") + + cmd = ["cooler", "coarsen", str(cool_path)] + cmd.extend(["-k", str(factor)]) + if output_file: + cmd.extend(["-o", str(output_file)]) + cmd.extend(["-p", str(nproc)]) + cmd.extend(["--chunksize", str(chunksize)]) + cmd.extend(["--agg", agg]) + + return _run_cooler_command(cmd, output_file if output_file else cool_path) + +@mcp.tool() +def cooler_zoomify( + cool_path: Path, + output_file: Optional[Path] = None, + resolutions: Optional[str] = None, + nproc: int = 1, + chunksize: int = 1000000, + balance: bool = False, + balance_args: Optional[str] = None, + legacy: bool = False, +) -> Dict[str, Any]: + """ + Generate a multi-resolution cooler file. + + Args: + cool_path: Path to a cooler file. + output_file: Output file path. If not specified, creates a new file with a .mcool extension. + resolutions: Comma-separated list of target resolutions. + nproc: Number of processes to use. + chunksize: The number of pixels loaded in each chunk. + balance: Apply balancing to each resolution. + balance_args: Arguments to pass to the balancing function. + legacy: Use the legacy layout for the multi-resolution file. + """ + if not cool_path.is_file(): + raise FileNotFoundError(f"Input cooler file not found: {cool_path}") + + cmd = ["cooler", "zoomify", str(cool_path)] + if output_file: + cmd.extend(["-o", str(output_file)]) + if resolutions: + cmd.extend(["--resolutions", resolutions]) + cmd.extend(["-p", str(nproc)]) + cmd.extend(["--chunksize", str(chunksize)]) + if balance: + cmd.append("--balance") + if balance_args: + cmd.extend(["--balance-args", balance_args]) + if legacy: + cmd.append("--legacy") + + out_path = output_file if output_file else Path(str(cool_path).replace(".cool", ".mcool")) + return _run_cooler_command(cmd, out_path) + +@mcp.tool() +def cooler_balance( + cool_path: Path, + nproc: int = 8, + chunksize: int = 1000000, + mad_max: int = 5, + min_nnz: int = 10, + min_count: int = 0, + ignore_diags: int = 2, + tol: float = 1e-05, + max_iters: int = 200, + cis_only: bool = False, + trans_only: bool = False, + name: str = "weight", + force: bool = False, + check: bool = False, + append: bool = False, + stdout: bool = False, +) -> Dict[str, Any]: + """ + Balance a cooler matrix using iterative correction. + + Args: + cool_path: Path to a cooler file. + nproc: Number of processes to use. + chunksize: Number of pixels to process per chunk. + mad_max: Filter out bins with MAD > mad_max. + min_nnz: Filter out bins with < min_nnz contacts. + min_count: Filter out bins with < min_count contacts. + ignore_diags: Number of diagonals to ignore. + tol: Tolerance for convergence. + max_iters: Maximum number of iterations. + cis_only: Only balance cis data. + trans_only: Only balance trans data. + name: Name of the weight column to create. + force: Overwrite existing weights. + check: Check if the matrix is balanced. + append: Append weights to the file instead of overwriting. + stdout: Print weights to stdout instead of writing to the file. + """ + if not cool_path.is_file(): + raise FileNotFoundError(f"Input cooler file not found: {cool_path}") + if cis_only and trans_only: + raise ValueError("Cannot specify both --cis-only and --trans-only.") + + cmd = ["cooler", "balance", str(cool_path)] + cmd.extend(["-p", str(nproc)]) + cmd.extend(["--chunksize", str(chunksize)]) + cmd.extend(["--mad-max", str(mad_max)]) + cmd.extend(["--min-nnz", str(min_nnz)]) + cmd.extend(["--min-count", str(min_count)]) + cmd.extend(["--ignore-diags", str(ignore_diags)]) + cmd.extend(["--tol", str(tol)]) + cmd.extend(["--max-iters", str(max_iters)]) + if cis_only: + cmd.append("--cis-only") + if trans_only: + cmd.append("--trans-only") + cmd.extend(["--name", name]) + if force: + cmd.append("--force") + if check: + cmd.append("--check") + if append: + cmd.append("--append") + if stdout: + cmd.append("--stdout") + + # If not writing to stdout, the command modifies the input file. + output_file = None if stdout else cool_path + return _run_cooler_command(cmd, output_file) + +@mcp.tool() +def cooler_dump( + cool_path: Path, + output_file: Optional[Path] = None, + table: str = "pixels", + header: bool = False, + columns: Optional[str] = None, + range_str: Optional[str] = None, + range2_str: Optional[str] = None, + fill_lower: Optional[str] = None, + join: bool = False, + annotate: Optional[str] = None, + chunksize: Optional[int] = None, + one_based_starts: bool = False, + na_rep: str = "nan", +) -> Dict[str, Any]: + """ + Dump data from a cooler file to a text stream. + + Args: + cool_path: Path to a cooler file. + output_file: Output file path. If not specified, outputs to stdout. + table: Which table to dump: 'chroms', 'bins', 'pixels', or 'indexes'. + header: Print the header. + columns: Comma-separated list of columns to print. + range_str: Genomic range, e.g., 'chr1:1000-2000'. + range2_str: Second genomic range for cis/trans queries. + fill_lower: How to fill the lower triangle: 'zero' or 'copy'. + join: Join with bin coordinates (for pixels table). + annotate: Annotate bins with a BED-like file. Format: '[FIELD=]PATH[:COLUMN]'. + chunksize: Number of rows to dump at a time. + one_based_starts: Use 1-based starts in output. + na_rep: String representation for missing values. + """ + if not cool_path.is_file(): + raise FileNotFoundError(f"Input cooler file not found: {cool_path}") + if table not in ["chroms", "bins", "pixels", "indexes"]: + raise ValueError("table must be one of 'chroms', 'bins', 'pixels', 'indexes'") + if fill_lower and fill_lower not in ["zero", "copy"]: + raise ValueError("fill_lower must be one of 'zero' or 'copy'") + + cmd = ["cooler", "dump", str(cool_path)] + if output_file: + cmd.extend(["-o", str(output_file)]) + cmd.extend(["--table", table]) + if header: + cmd.append("--header") + if columns: + cmd.extend(["--columns", columns]) + if range_str: + cmd.extend(["--range", range_str]) + if range2_str: + cmd.extend(["--range2", range2_str]) + if fill_lower: + cmd.extend(["--fill-lower", fill_lower]) + if join: + cmd.append("--join") + if annotate: + cmd.extend(["--annotate", annotate]) + if chunksize: + cmd.extend(["--chunksize", str(chunksize)]) + if one_based_starts: + cmd.append("--one-based-starts") + cmd.extend(["--na-rep", na_rep]) + + return _run_cooler_command(cmd, output_file) + +@mcp.tool() +def cooler_show( + cool_path: Path, + range_str: str, + output_file: Optional[Path] = None, + range2_str: Optional[str] = None, + dpi: int = 100, + cmap: str = "YlOrRd", + zmin: Optional[float] = None, + zmax: Optional[float] = None, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + norm: str = "log", + balance_weight: Optional[str] = "weight", + as_pixels: bool = False, + max_size: int = 500, +) -> Dict[str, Any]: + """ + Show a cooler's pixels as a matrix image. + + Args: + cool_path: Path to a cooler file. + range_str: Genomic range to display. + output_file: Output image file path. If not specified, shows interactively (not supported in MCP). + range2_str: Second genomic range for trans views. + dpi: Dots per inch for the output image. + cmap: Colormap for the matrix. + zmin: Minimum of the color scale (before norm). + zmax: Maximum of the color scale (before norm). + vmin: Minimum of the color scale (after norm). + vmax: Maximum of the color scale (after norm). + norm: Normalization for the color scale: 'log' or 'linear'. + balance_weight: Name of the balancing weight column to use. Set to None to disable balancing. + as_pixels: Show individual pixels instead of a heatmap. + max_size: Maximum size of the matrix to display. + """ + if not cool_path.is_file(): + raise FileNotFoundError(f"Input cooler file not found: {cool_path}") + if norm not in ["log", "linear"]: + raise ValueError("norm must be 'log' or 'linear'") + if not output_file: + raise ValueError("output_file must be specified for non-interactive use.") + + cmd = ["cooler", "show", str(cool_path), range_str] + cmd.extend(["-o", str(output_file)]) + if range2_str: + cmd.extend(["--range2", range2_str]) + cmd.extend(["--dpi", str(dpi)]) + cmd.extend(["--cmap", cmap]) + if zmin is not None: + cmd.extend(["--zmin", str(zmin)]) + if zmax is not None: + cmd.extend(["--zmax", str(zmax)]) + if vmin is not None: + cmd.extend(["--vmin", str(vmin)]) + if vmax is not None: + cmd.extend(["--vmax", str(vmax)]) + cmd.extend(["--norm", norm]) + if balance_weight: + cmd.extend(["--balance", balance_weight]) + else: + cmd.append("--no-balance") + if as_pixels: + cmd.append("--as-pixels") + cmd.extend(["--max-size", str(max_size)]) + + return _run_cooler_command(cmd, output_file) + +@mcp.tool() +def cooler_info( + cool_path: Path, + field: Optional[str] = None, + metadata: bool = False, + output_file: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Display information about a cooler file. + + Args: + cool_path: Path to a cooler file. + field: A specific field to retrieve from the info dictionary. + metadata: Show the full metadata dictionary. + output_file: Output file path. If not specified, outputs to stdout. + """ + if not cool_path.is_file(): + raise FileNotFoundError(f"Input cooler file not found: {cool_path}") + + cmd = ["cooler", "info", str(cool_path)] + if field: + cmd.extend(["--field", field]) + if metadata: + cmd.append("--metadata") + if output_file: + cmd.extend(["-o", str(output_file)]) + + return _run_cooler_command(cmd, output_file) + +@mcp.tool() +def cooler_list( + cool_path: Path, + long_format: bool = False, +) -> Dict[str, Any]: + """ + List the contents of a cooler file. + + Args: + cool_path: Path to a cooler file (cool or mcool). + long_format: Use long listing format. + """ + if not cool_path.is_file(): + raise FileNotFoundError(f"Input cooler file not found: {cool_path}") + + cmd = ["cooler", "list", str(cool_path)] + if long_format: + cmd.append("-l") + + return _run_cooler_command(cmd) + +@mcp.tool() +def cooler_copy( + uri1: str, + uri2: str, + chunksize: int = 1000000, + append: bool = False, + no_index: bool = False, +) -> Dict[str, Any]: + """ + Copy a cooler from one file to another. + + Args: + uri1: Source cooler URI, e.g., 'path/to/file.cool::/path/inside'. + uri2: Destination cooler URI. + chunksize: The number of pixels loaded in each chunk. + append: Append to an existing cooler file. + no_index: Do not copy the bin-offset index. + """ + # Basic validation for file paths in URIs + src_path_str = uri1.split('::')[0] + if not Path(src_path_str).is_file(): + raise FileNotFoundError(f"Source cooler file not found: {src_path_str}") + + cmd = ["cooler", "copy", uri1, uri2] + cmd.extend(["--chunksize", str(chunksize)]) + if append: + cmd.append("--append") + if no_index: + cmd.append("--no-index") + + dest_path = Path(uri2.split('::')[0]) + return _run_cooler_command(cmd, dest_path) + +@mcp.tool() +def cooler_makebins( + chromsizes_path: Path, + binsize: int, + output_file: Optional[Path] = None, + id_decorator: Optional[str] = None, + name_col: Optional[str] = None, +) -> Dict[str, Any]: + """ + Create a genome-wide bin segmentation. + + Args: + chromsizes_path: Path to a chromsizes file. + binsize: The bin size in base pairs. + output_file: Output file path. If not specified, outputs to stdout. + id_decorator: A format string to generate bin IDs. + name_col: Name of the column for bin names. + """ + if not chromsizes_path.is_file(): + raise FileNotFoundError(f"Chromsizes file not found: {chromsizes_path}") + if binsize <= 0: + raise ValueError("Bin size must be a positive integer.") + + cmd = ["cooler", "makebins", str(chromsizes_path), str(binsize)] + if output_file: + cmd.extend(["-o", str(output_file)]) + if id_decorator: + cmd.extend(["--id-decorator", id_decorator]) + if name_col: + cmd.extend(["--name-col", name_col]) + + return _run_cooler_command(cmd, output_file) + +@mcp.tool() +def cooler_digest( + chromsizes_path: Path, + enzyme: str, + output_file: Optional[Path] = None, +) -> Dict[str, Any]: + """ + Create a genome-wide bin segmentation from restriction enzyme digestion. + + Args: + chromsizes_path: Path to a chromsizes file. + enzyme: Name of the restriction enzyme. + output_file: Output file path. If not specified, outputs to stdout. + """ + if not chromsizes_path.is_file(): + raise FileNotFoundError(f"Chromsizes file not found: {chromsizes_path}") + + cmd = ["cooler", "digest", str(chromsizes_path), enzyme] + if output_file: + cmd.extend(["-o", str(output_file)]) + + return _run_cooler_command(cmd, output_file) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_coreutils/app/coreutils_server.py b/Biomni/mcp_generated/mcp_coreutils/app/coreutils_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b861c96471ae67f14c76826d4d1cc027a7d44927 --- /dev/null +++ b/Biomni/mcp_generated/mcp_coreutils/app/coreutils_server.py @@ -0,0 +1,499 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Dict, Any + +# MCP-ready implementations for a selection of GNU Core Utilities. +# Note: coreutils is a suite of many individual tools. This file provides +# MCP wrappers for a commonly used subset of those tools. + +def _run_command(command: List[str], output_file: Optional[Path] = None) -> Dict[str, Any]: + """Helper function to run a command and handle output.""" + try: + if output_file: + with open(output_file, "w") as f: + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + stdout=f + ) + # Read back a snippet of the output file for the stdout key + # to avoid returning massive amounts of data. + with open(output_file, "r") as f: + stdout_snippet = f.read(1024) + if len(stdout_snippet) == 1024: + stdout_snippet += "\n... (output truncated)" + return { + "command_executed": " ".join(command), + "stdout": stdout_snippet, + "stderr": result.stderr, + "output_files": [str(output_file)] + } + else: + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True + ) + return { + "command_executed": " ".join(command), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except FileNotFoundError: + return { + "command_executed": " ".join(command), + "stdout": "", + "stderr": f"Error: The command '{command[0]}' was not found. Ensure coreutils is in the system's PATH.", + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_coreutils' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def ls( + path: Optional[Path] = None, + all_files: bool = False, + almost_all: bool = False, + long_format: bool = False, + recursive: bool = False, + human_readable: bool = False, + reverse: bool = False, + sort_by_time: bool = False +) -> Dict[str, Any]: + """ + List information about files and directories. + + This tool is a wrapper for the GNU 'ls' command. + """ + command = ["ls"] + if all_files: + command.append("-a") + if almost_all: + command.append("-A") + if long_format: + command.append("-l") + if recursive: + command.append("-R") + if human_readable: + command.append("-h") + if reverse: + command.append("-r") + if sort_by_time: + command.append("-t") + + if path: + if not path.exists(): + raise ValueError(f"Path does not exist: {path}") + command.append(str(path)) + + return _run_command(command) + +@mcp.tool() +def cp( + source: List[Path], + destination: Path, + recursive: bool = False, + force: bool = False, + update: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """ + Copy files and directories. + + This tool is a wrapper for the GNU 'cp' command. + """ + if not source: + raise ValueError("At least one source path must be provided.") + + command = ["cp"] + if recursive: + command.append("-R") + if force: + command.append("-f") + if update: + command.append("-u") + if verbose: + command.append("-v") + + for src_path in source: + if not src_path.exists(): + raise FileNotFoundError(f"Source path does not exist: {src_path}") + command.append(str(src_path)) + + command.append(str(destination)) + + result = _run_command(command) + + # Determine output files + output_files = [] + if destination.is_dir(): + for src in source: + output_files.append(str(destination / src.name)) + else: + output_files.append(str(destination)) + result["output_files"] = output_files + + return result + +@mcp.tool() +def mv( + source: List[Path], + destination: Path, + force: bool = False, + update: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """ + Move (rename) files and directories. + + This tool is a wrapper for the GNU 'mv' command. + """ + if not source: + raise ValueError("At least one source path must be provided.") + + command = ["mv"] + if force: + command.append("-f") + if update: + command.append("-u") + if verbose: + command.append("-v") + + for src_path in source: + if not src_path.exists(): + raise FileNotFoundError(f"Source path does not exist: {src_path}") + command.append(str(src_path)) + + command.append(str(destination)) + + result = _run_command(command) + + # Determine output files + output_files = [] + if destination.is_dir(): + for src in source: + output_files.append(str(destination / src.name)) + else: + output_files.append(str(destination)) + result["output_files"] = output_files + + return result + +@mcp.tool() +def rm( + files: List[Path], + force: bool = False, + recursive: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """ + Remove files or directories. + + This tool is a wrapper for the GNU 'rm' command. Use with caution. + """ + if not files: + raise ValueError("At least one file or directory to remove must be provided.") + + command = ["rm"] + if force: + command.append("-f") + if recursive: + command.append("-r") + if verbose: + command.append("-v") + + for file_path in files: + # We don't check for existence here, as 'rm -f' handles it. + command.append(str(file_path)) + + return _run_command(command) + +@mcp.tool() +def mkdir( + directories: List[Path], + parents: bool = False, + verbose: bool = False, + mode: Optional[str] = None +) -> Dict[str, Any]: + """ + Create directories. + + This tool is a wrapper for the GNU 'mkdir' command. + """ + if not directories: + raise ValueError("At least one directory to create must be provided.") + + command = ["mkdir"] + if parents: + command.append("-p") + if verbose: + command.append("-v") + if mode: + command.extend(["-m", mode]) + + for dir_path in directories: + command.append(str(dir_path)) + + result = _run_command(command) + result["output_files"] = [str(d) for d in directories] + return result + +@mcp.tool() +def cat( + files: List[Path], + output_file: Optional[Path] = None, + number_lines: bool = False, + number_nonblank: bool = False, + squeeze_blank: bool = False +) -> Dict[str, Any]: + """ + Concatenate files and print on the standard output. + + This tool is a wrapper for the GNU 'cat' command. + """ + if not files: + raise ValueError("At least one input file must be provided.") + + command = ["cat"] + if number_lines: + command.append("-n") + if number_nonblank: + command.append("-b") + if squeeze_blank: + command.append("-s") + + for file_path in files: + if not file_path.is_file(): + raise FileNotFoundError(f"Input file not found: {file_path}") + command.append(str(file_path)) + + return _run_command(command, output_file=output_file) + +@mcp.tool() +def wc( + files: List[Path], + lines: bool = False, + words: bool = False, + bytes: bool = False, + chars: bool = False, + max_line_length: bool = False +) -> Dict[str, Any]: + """ + Print newline, word, and byte counts for each file. + + This tool is a wrapper for the GNU 'wc' command. + If no flags are set, default 'wc' behavior (lines, words, bytes) is used. + """ + command = ["wc"] + if lines: + command.append("-l") + if words: + command.append("-w") + if bytes: + command.append("-c") + if chars: + command.append("-m") + if max_line_length: + command.append("-L") + + if not files: + raise ValueError("At least one input file must be provided.") + + for file_path in files: + if not file_path.is_file(): + raise FileNotFoundError(f"Input file not found: {file_path}") + command.append(str(file_path)) + + return _run_command(command) + +@mcp.tool() +def sort( + input_files: List[Path], + output_file: Optional[Path] = None, + unique: bool = False, + numeric_sort: bool = False, + reverse: bool = False, + key: Optional[str] = None, + field_separator: Optional[str] = None +) -> Dict[str, Any]: + """ + Sort lines of text files. + + This tool is a wrapper for the GNU 'sort' command. + """ + if not input_files: + raise ValueError("At least one input file must be provided.") + + command = ["sort"] + if unique: + command.append("-u") + if numeric_sort: + command.append("-n") + if reverse: + command.append("-r") + if key: + command.extend(["-k", key]) + if field_separator: + command.extend(["-t", field_separator]) + if output_file: + command.extend(["-o", str(output_file)]) + + for file_path in input_files: + if not file_path.is_file(): + raise FileNotFoundError(f"Input file not found: {file_path}") + command.append(str(file_path)) + + result = _run_command(command) + if output_file: + result["output_files"] = [str(output_file)] + return result + +@mcp.tool() +def uniq( + input_file: Path, + output_file: Optional[Path] = None, + count: bool = False, + repeated_only: bool = False, + unique_only: bool = False, + ignore_case: bool = False +) -> Dict[str, Any]: + """ + Report or omit repeated lines from a sorted file. + + This tool is a wrapper for the GNU 'uniq' command. Input file should be sorted. + """ + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + command = ["uniq"] + if count: + command.append("-c") + if repeated_only: + command.append("-d") + if unique_only: + command.append("-u") + if ignore_case: + command.append("-i") + + command.append(str(input_file)) + if output_file: + command.append(str(output_file)) + + result = _run_command(command) + if output_file: + result["output_files"] = [str(output_file)] + return result + +@mcp.tool() +def head( + files: List[Path], + lines: int = 10, + bytes: Optional[int] = None, + output_file: Optional[Path] = None +) -> Dict[str, Any]: + """ + Output the first part of files. + + This tool is a wrapper for the GNU 'head' command. + """ + if not files: + raise ValueError("At least one input file must be provided.") + if bytes is not None and lines != 10: + raise ValueError("Cannot specify both 'lines' and 'bytes'.") + + command = ["head"] + if bytes is not None: + command.extend(["-c", str(bytes)]) + else: + command.extend(["-n", str(lines)]) + + for file_path in files: + if not file_path.is_file(): + raise FileNotFoundError(f"Input file not found: {file_path}") + command.append(str(file_path)) + + return _run_command(command, output_file=output_file) + +@mcp.tool() +def tail( + files: List[Path], + lines: int = 10, + bytes: Optional[int] = None, + follow: bool = False, + output_file: Optional[Path] = None +) -> Dict[str, Any]: + """ + Output the last part of files. + + This tool is a wrapper for the GNU 'tail' command. + Note: 'follow' mode may not be suitable for non-interactive server environments. + """ + if not files: + raise ValueError("At least one input file must be provided.") + if bytes is not None and lines != 10: + raise ValueError("Cannot specify both 'lines' and 'bytes'.") + + command = ["tail"] + if follow: + command.append("-f") + if bytes is not None: + command.extend(["-c", str(bytes)]) + else: + command.extend(["-n", str(lines)]) + + for file_path in files: + if not file_path.is_file(): + raise FileNotFoundError(f"Input file not found: {file_path}") + command.append(str(file_path)) + + return _run_command(command, output_file=output_file) + +@mcp.tool() +def chmod( + mode: str, + files: List[Path], + recursive: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """ + Change file mode bits. + + This tool is a wrapper for the GNU 'chmod' command. + """ + if not files: + raise ValueError("At least one file or directory must be provided.") + if not mode: + raise ValueError("A mode must be specified (e.g., '755', 'u+x').") + + command = ["chmod"] + if recursive: + command.append("-R") + if verbose: + command.append("-v") + + command.append(mode) + + for file_path in files: + if not file_path.exists(): + raise FileNotFoundError(f"File or directory not found: {file_path}") + command.append(str(file_path)) + + return _run_command(command) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_cospar/Dockerfile b/Biomni/mcp_generated/mcp_cospar/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..bd0d606cfe3dd89e61ce40d207cefee33692f41d --- /dev/null +++ b/Biomni/mcp_generated/mcp_cospar/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 cospar via conda (e.g., from bioconda) +RUN conda install -c bioconda cospar -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/cospar_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/cospar_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/cospar_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_cospar/app/cospar_shim_server.py b/Biomni/mcp_generated/mcp_cospar/app/cospar_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..407daa4215e3dd43e03666bb6b551fa3e1254ffd --- /dev/null +++ b/Biomni/mcp_generated/mcp_cospar/app/cospar_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_cospar/app/cospar_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_cospar' + + +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_cospar/app/requirements.txt b/Biomni/mcp_generated/mcp_cospar/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..2071df08468a2d45a7f42b0b738f0caf2dc0a0eb --- /dev/null +++ b/Biomni/mcp_generated/mcp_cospar/app/requirements.txt @@ -0,0 +1,3 @@ +anndata==0.12.11 +cospar==0.5.0 +numpy==2.4.4 diff --git a/Biomni/mcp_generated/mcp_cospar/docker-compose.yml b/Biomni/mcp_generated/mcp_cospar/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b104b4c777f8e4699d5549f4d207179fa4eeb6ee --- /dev/null +++ b/Biomni/mcp_generated/mcp_cospar/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-cospar: + build: . + image: mcp-cospar:latest + container_name: mcp-cospar + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=cospar + 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_cospar/environment.yaml b/Biomni/mcp_generated/mcp_cospar/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1f2897f3651e08be78a7ac89600a1abec2f79716 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cospar/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - cospar + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_cospar/requirements.txt b/Biomni/mcp_generated/mcp_cospar/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..aec8cc71f9aa53ce09c139626c530d392672ee50 --- /dev/null +++ b/Biomni/mcp_generated/mcp_cospar/requirements.txt @@ -0,0 +1,5 @@ +anndata==0.12.11 +cospar==0.5.0 +numpy==2.4.4 +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_kallisto/Dockerfile b/Biomni/mcp_generated/mcp_kallisto/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..be0ff34dfe4e8ccad8b8a12f7c1db222cd600347 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kallisto/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 kallisto via conda (e.g., from bioconda) +RUN conda install -c bioconda kallisto -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/kallisto_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/kallisto_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/kallisto_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_kallisto/app/__pycache__/kallisto_shim_server.cpython-311.pyc b/Biomni/mcp_generated/mcp_kallisto/app/__pycache__/kallisto_shim_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4173f1fe28c530e3464f67a4505c83b395a649d Binary files /dev/null and b/Biomni/mcp_generated/mcp_kallisto/app/__pycache__/kallisto_shim_server.cpython-311.pyc differ diff --git a/Biomni/mcp_generated/mcp_kallisto/app/kallisto_server.py b/Biomni/mcp_generated/mcp_kallisto/app/kallisto_server.py new file mode 100644 index 0000000000000000000000000000000000000000..b1fd2d8a3e5faca631e2fc0a7614d0e07065d36c --- /dev/null +++ b/Biomni/mcp_generated/mcp_kallisto/app/kallisto_server.py @@ -0,0 +1,464 @@ +import subprocess +from pathlib import Path +from typing import List, Optional, Union + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_kallisto' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def kallisto_index( + fasta_files: List[str], + index: str, + kmer_size: int = 31, + d_list: Optional[str] = None, + make_unique: bool = False, + aa: bool = False, + distinguish: bool = False, + threads: int = 1, + min_size: Optional[int] = None, + ec_max_size: Optional[int] = None, +): + """ + Builds a kallisto index from a FASTA formatted file of target sequences. + + :param fasta_files: List of FASTA files (plaintext or gzipped) to build the index from. + :param index: Filename for the kallisto index to be constructed. + :param kmer_size: k-mer (odd) length (default: 31, max value: 31). + :param d_list: Path to a FASTA-file containing sequences to mask from quantification. + :param make_unique: Replace repeated target names with unique names. + :param aa: Generate index from a FASTA-file containing amino acid sequences. + :param distinguish: Generate index where sequences are distinguished by the sequence name. + :param threads: Number of threads to use (default: 1). + :param min_size: Length of minimizers (default: automatically chosen). + :param ec_max_size: Maximum number of targets in an equivalence class (default: no maximum). + """ + # Validation + if kmer_size > 31 or kmer_size % 2 == 0: + return {"error": "kmer_size must be an odd integer <= 31"} + + for f in fasta_files: + if not Path(f).exists(): + return {"error": f"Input FASTA file not found: {f}"} + + # kallisto index is single-threaded in common releases and does not accept -t. + # Keep the threads parameter for API compatibility, but do not pass it here. + cmd = ["kallisto", "index", "-i", index, "-k", str(kmer_size)] + + if d_list: + cmd.extend(["-d", d_list]) + if make_unique: + cmd.append("--make-unique") + if aa: + cmd.append("--aa") + if distinguish: + cmd.append("--distinguish") + if min_size is not None: + cmd.extend(["-m", str(min_size)]) + if ec_max_size is not None: + cmd.extend(["-e", str(ec_max_size)]) + + cmd.extend(fasta_files) + + 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": [index] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def kallisto_quant( + fastq_files: List[str], + index: str, + output_dir: str, + bootstrap_samples: int = 0, + seed: int = 42, + plaintext: bool = False, + single: bool = False, + single_overhang: bool = False, + fr_stranded: bool = False, + rf_stranded: bool = False, + fragment_length: Optional[float] = None, + sd: Optional[float] = None, + threads: int = 1, + bias: bool = False, + fusion: bool = False, + pseudobam: bool = False, + genomebam: bool = False, + gtf: Optional[str] = None, + chromosomes: Optional[str] = None +): + """ + Runs the kallisto quantification algorithm on RNA-Seq reads. + + :param fastq_files: List of FASTQ files. For paired-end, provide pairs (e.g., [R1, R2]). + :param index: Filename for the kallisto index to be used. + :param output_dir: Directory to write output to. + :param bootstrap_samples: Number of bootstrap samples (default: 0). + :param seed: Seed for the bootstrap sampling (default: 42). + :param plaintext: Output plaintext instead of HDF5. + :param single: Quantify single-end reads. + :param single_overhang: Include reads where unobserved rest of fragment is predicted to lie outside a transcript. + :param fr_stranded: Strand specific reads, first read forward. + :param rf_stranded: Strand specific reads, first read reverse. + :param fragment_length: Estimated average fragment length (Required for --single). + :param sd: Estimated standard deviation of fragment length (Required for --single). + :param threads: Number of threads to use (default: 1). + :param bias: Learn parameters for a model of sequences specific bias (Legacy/0.48.0). + :param fusion: Look for reads from fusion genes (Legacy/0.48.0). + :param pseudobam: Output pseudoalignments to a BAM file (Legacy/0.48.0). + :param genomebam: Project transcript alignments to genome coordinates (Legacy/0.48.0). + :param gtf: GTF file for genomebam (Required if --genomebam is used). + :param chromosomes: Length of genomic chromosomes for BAM header. + """ + # Validation + if not Path(index).exists(): + return {"error": f"Index file not found: {index}"} + + if single: + if fragment_length is None or sd is None: + return {"error": "fragment_length (-l) and sd (-s) are required when using --single"} + + if genomebam and not gtf: + return {"error": "GTF file (--gtf) is required when using --genomebam"} + + Path(output_dir).mkdir(parents=True, exist_ok=True) + + cmd = ["kallisto", "quant", "-i", index, "-o", output_dir, "-b", str(bootstrap_samples), "--seed", str(seed), "-t", str(threads)] + + if plaintext: cmd.append("--plaintext") + if single: cmd.append("--single") + if single_overhang: cmd.append("--single-overhang") + if fr_stranded: cmd.append("--fr-stranded") + if rf_stranded: cmd.append("--rf-stranded") + if fragment_length is not None: cmd.extend(["-l", str(fragment_length)]) + if sd is not None: cmd.extend(["-s", str(sd)]) + if bias: cmd.append("--bias") + if fusion: cmd.append("--fusion") + if pseudobam: cmd.append("--pseudobam") + if genomebam: cmd.append("--genomebam") + if gtf: cmd.extend(["--gtf", gtf]) + if chromosomes: cmd.extend(["--chromosomes", chromosomes]) + + cmd.extend(fastq_files) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": output_dir + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def kallisto_bus( + fastq_files: List[str], + index: str, + output_dir: str, + technology: str, + batch: Optional[str] = None, + threads: int = 1, + bam: bool = False, + num: bool = False, + num_reads: Optional[int] = None, + tag: Optional[str] = None, + fr_stranded: bool = False, + rf_stranded: bool = False, + unstranded: bool = False, + paired: bool = False, + long_reads: bool = False, + aa: bool = False, + inleaved: bool = False, + batch_barcodes: bool = False, + verbose: bool = False +): + """ + Generates BUS files for single-cell sequencing data. + + :param fastq_files: List of FASTQ files. + :param index: Filename for the kallisto index to be used for pseudoalignment. + :param output_dir: Directory to write output to. + :param technology: Single-cell technology used (e.g., 10xv2, 10xv3, or custom format). + :param batch: Process files listed in a batch file. + :param threads: Number of threads to use (default: 1). + :param bam: Input file is a BAM file. + :param num: Output number of read in flag column (incompatible with --bam). + :param num_reads: Maximum number of reads to process from supplied input. + :param tag: 5' tag sequence to identify UMI reads for certain technologies. + :param fr_stranded: Strand specific reads for UMI-tagged reads, first read forward. + :param rf_stranded: Strand specific reads for UMI-tagged reads, first read reverse. + :param unstranded: Treat all read as non-strand-specific. + :param paired: Treat reads as paired. + :param long_reads: Treat reads as long. + :param aa: Align to index generated from amino acid sequences. + :param inleaved: Specifies that input is an interleaved FASTQ file. + :param batch_barcodes: Records both batch and extracted barcode in BUS file. + :param verbose: Print out progress information every 1M processed reads. + """ + if not Path(index).exists(): + return {"error": f"Index file not found: {index}"} + + Path(output_dir).mkdir(parents=True, exist_ok=True) + + cmd = ["kallisto", "bus", "-i", index, "-o", output_dir, "-x", technology, "-t", str(threads)] + + if batch: cmd.extend(["-B", batch]) + if bam: cmd.append("-b") + if num: cmd.append("-n") + if num_reads is not None: cmd.extend(["-N", str(num_reads)]) + if tag: cmd.extend(["-T", tag]) + if fr_stranded: cmd.append("--fr-stranded") + if rf_stranded: cmd.append("--rf-stranded") + if unstranded: cmd.append("--unstranded") + if paired: cmd.append("--paired") + if long_reads: cmd.append("--long") + if aa: cmd.append("--aa") + if inleaved: cmd.append("--inleaved") + if batch_barcodes: cmd.append("--batch-barcodes") + if verbose: cmd.append("--verbose") + + cmd.extend(fastq_files) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": output_dir + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def kallisto_quant_tcc( + output_dir: str, + index: Optional[str] = None, + txnames: Optional[str] = None, + ec_file: Optional[str] = None, + fragment_file: Optional[str] = None, + long_reads: bool = False, + platform: Optional[str] = None, + fragment_length: Optional[float] = None, + sd: Optional[float] = None, + threads: int = 1, + genemap: Optional[str] = None, + gtf: Optional[str] = None, + bootstrap_samples: int = 0, + matrix_to_files: bool = False, + matrix_to_directories: bool = False, + seed: int = 42, + plaintext: bool = False +): + """ + Runs the EM algorithm to produce estimated counts from a transcript-compatibility-counts matrix file. + + :param output_dir: Directory to write output to. + :param index: Filename for the kallisto index (required if txnames not supplied). + :param txnames: File with names of transcripts (required if index not supplied). + :param ec_file: File containing equivalence classes. + :param fragment_file: File containing fragment length distribution. + :param long_reads: Use version of EM for long reads. + :param platform: [PacBio or ONT] used for sequencing. + :param fragment_length: Estimated average fragment length. + :param sd: Estimated standard deviation of fragment length. + :param threads: Number of threads to use (default: 1). + :param genemap: File for mapping transcripts to genes. + :param gtf: GTF file for transcriptome information. + :param bootstrap_samples: Number of bootstrap samples (default: 0). + :param matrix_to_files: Reorganize matrix output into abundance tsv files. + :param matrix_to_directories: Reorganize matrix output into abundance tsv files across multiple directories. + :param seed: Seed for the bootstrap sampling (default: 42). + :param plaintext: Output plaintext only, not HDF5. + """ + if not index and not txnames: + return {"error": "Either index (-i) or txnames (-T) must be provided."} + + Path(output_dir).mkdir(parents=True, exist_ok=True) + + cmd = ["kallisto", "quant-tcc", "-o", output_dir, "-t", str(threads), "-b", str(bootstrap_samples), "--seed", str(seed)] + + if index: cmd.extend(["-i", index]) + if txnames: cmd.extend(["-T", txnames]) + if ec_file: cmd.extend(["-e", ec_file]) + if fragment_file: cmd.extend(["-f", fragment_file]) + if long_reads: cmd.append("--long") + if platform: cmd.extend(["-p", platform]) + if fragment_length is not None: cmd.extend(["-l", str(fragment_length)]) + if sd is not None: cmd.extend(["-s", str(sd)]) + if genemap: cmd.extend(["-g", genemap]) + if gtf: cmd.extend(["-G", gtf]) + if matrix_to_files: cmd.append("--matrix-to-files") + if matrix_to_directories: cmd.append("--matrix-to-directories") + if plaintext: cmd.append("--plaintext") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": output_dir + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def kallisto_h5dump( + h5_file: str, + output_dir: str +): + """ + Converts HDF5-formatted results (abundance.h5) to plaintext. + + :param h5_file: Path to the abundance.h5 file. + :param output_dir: Directory to write output to. + """ + if not Path(h5_file).exists(): + return {"error": f"HDF5 file not found: {h5_file}"} + + Path(output_dir).mkdir(parents=True, exist_ok=True) + + cmd = ["kallisto", "h5dump", "-o", output_dir, h5_file] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": output_dir + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def kallisto_inspect( + index_file: str, + threads: int = 1 +): + """ + Inspects and gives information about a kallisto index. + + :param index_file: Path to the kallisto index file. + :param threads: Number of threads to use. + """ + if not Path(index_file).exists(): + return {"error": f"Index file not found: {index_file}"} + + cmd = ["kallisto", "inspect", index_file, "-t", str(threads)] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def kallisto_version(): + """Prints kallisto version information.""" + cmd = ["kallisto", "version"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return {"version": result.stdout.strip()} + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +@mcp.tool() +def kallisto_cite(): + """Prints citation information for kallisto.""" + cmd = ["kallisto", "cite"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return {"citation": result.stdout.strip()} + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +@mcp.tool() +def kallisto_bus_list_technologies(): + """Lists all single-cell technologies supported by kallisto bus.""" + cmd = ["kallisto", "bus", "-l"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return {"supported_technologies": result.stdout} + except subprocess.CalledProcessError as e: + # kallisto bus -l might return exit code 0 or 1 depending on version + return {"supported_technologies": e.stdout if e.stdout else e.stderr} + +@mcp.tool() +def kallisto_merge( + output_dir: str, + input_dirs: List[str] +): + """ + Merges several batch runs. + + :param output_dir: Directory to write merged output to. + :param input_dirs: List of directories containing kallisto results to merge. + """ + Path(output_dir).mkdir(parents=True, exist_ok=True) + + cmd = ["kallisto", "merge", "-o", output_dir] + cmd.extend(input_dirs) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_directory": output_dir + } + 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_kallisto/app/requirements.txt b/Biomni/mcp_generated/mcp_kallisto/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_kallisto/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_kallisto/docker-compose.yml b/Biomni/mcp_generated/mcp_kallisto/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..ae6743240ef416df1b2d63ae865bc94a2e88660a --- /dev/null +++ b/Biomni/mcp_generated/mcp_kallisto/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-kallisto: + build: . + image: mcp-kallisto:latest + container_name: mcp-kallisto + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=kallisto + 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_kallisto/environment.yaml b/Biomni/mcp_generated/mcp_kallisto/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..67ed8c709fefd02655e0912c4987f578b8b1a42b --- /dev/null +++ b/Biomni/mcp_generated/mcp_kallisto/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - kallisto + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_kallisto/requirements.txt b/Biomni/mcp_generated/mcp_kallisto/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_kallisto/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_libsequence/Dockerfile b/Biomni/mcp_generated/mcp_libsequence/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2100e99912a4b8e8e2314275f3860a7dbeed4d3a --- /dev/null +++ b/Biomni/mcp_generated/mcp_libsequence/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 libsequence via conda (e.g., from bioconda) +RUN conda install -c bioconda libsequence -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/libsequence_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/libsequence_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/libsequence_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_libsequence/app/libsequence_server.py b/Biomni/mcp_generated/mcp_libsequence/app/libsequence_server.py new file mode 100644 index 0000000000000000000000000000000000000000..3f189484348cc6ecc28f2d8bee7505b7f41fc635 --- /dev/null +++ b/Biomni/mcp_generated/mcp_libsequence/app/libsequence_server.py @@ -0,0 +1,51 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Any + +# NO NEED to import mcp as per instructions, assuming it's available in the execution environment. + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_libsequence' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def libsequence_library_info(): + """ + Provides information about the libsequence C++ class library. + + Based on the provided documentation, libsequence is a C++ class library + for evolutionary genetics and does not expose direct command-line executables + or subcommands that can be run via `subprocess.run`. + + This function serves to inform users about the nature of the tool as described + in its documentation, rather than executing a command-line program. + """ + tool_name = "libsequence" + summary = "A C++ class library for evolutionary genetics." + description = "A C++ class library for evolutionary genetics." + home_url = "http://molpopgen.github.io/libsequence/" + + # Since the provided documentation indicates libsequence is a C++ class library + # and does not describe any command-line executables, we cannot perform + # a `subprocess.run` operation. Instead, we return structured information + # explaining this fact. + info_message = ( + f"Tool: {tool_name}\n" + f"Summary: {summary}\n" + f"Description: {description}\n" + f"Home URL: {home_url}\n\n" + "The provided documentation indicates that libsequence is a C++ class library " + "and does not expose command-line functions or executables. Therefore, " + "no `subprocess.run` operation can be performed for this tool as a direct CLI." + ) + + return { + "command_executed": f"No direct command-line executable found for '{tool_name}' based on documentation.", + "stdout": info_message, + "stderr": "", + "output_files": [], + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_libsequence/app/requirements.txt b/Biomni/mcp_generated/mcp_libsequence/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_libsequence/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_libsequence/docker-compose.yml b/Biomni/mcp_generated/mcp_libsequence/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..158fcea9ce40c28e902d44a7b7a619e13de4d21d --- /dev/null +++ b/Biomni/mcp_generated/mcp_libsequence/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-libsequence: + build: . + image: mcp-libsequence:latest + container_name: mcp-libsequence + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=libsequence + 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_libsequence/environment.yaml b/Biomni/mcp_generated/mcp_libsequence/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7800304e4bfd85c7a76a2528cadef6f263efa430 --- /dev/null +++ b/Biomni/mcp_generated/mcp_libsequence/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - libsequence + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_libsequence/requirements.txt b/Biomni/mcp_generated/mcp_libsequence/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_libsequence/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_minimap2/app/minimap2_server.py b/Biomni/mcp_generated/mcp_minimap2/app/minimap2_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0b1e4658a661283088378d33dee02de1f775abfa --- /dev/null +++ b/Biomni/mcp_generated/mcp_minimap2/app/minimap2_server.py @@ -0,0 +1,257 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Union +import tempfile + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_minimap2' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def minimap2_index( + target_fa: str, + output_mmi: str, + preset: Optional[str] = None, + k: Optional[int] = None, + w: Optional[int] = None, + H: bool = False, + I: Optional[str] = None, +): + """ + Indexes target sequences for faster mapping with minimap2. + + Args: + target_fa: Path to the target FASTA file. + output_mmi: Path where the index (.mmi) will be saved. + preset: Preset (e.g., map-ont, lr:hq, map-hifi, sr, etc.). Applied before other options. + k: K-mer size (no larger than 28). + w: Minimizer window size. + H: Use homopolymer-compressed k-mer (preferable for PacBio). + I: Split index for every ~NUM input bases (e.g., 8G, 4M). + """ + target_path = Path(target_fa) + if not target_path.exists(): + return {"error": f"Target file {target_fa} not found."} + + cmd = ["minimap2"] + + if preset: + cmd.extend(["-x", preset]) + if k is not None: + if k > 28: + return {"error": "k-mer size (k) cannot be larger than 28."} + cmd.extend(["-k", str(k)]) + if w is not None: + cmd.extend(["-w", str(w)]) + if H: + cmd.append("-H") + if I: + cmd.extend(["-I", I]) + + cmd.extend(["-d", output_mmi, target_fa]) + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_mmi] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def minimap2_align( + target: str, + queries: List[str], + output_file: Optional[str] = None, + preset: Optional[str] = None, + sam_format: bool = False, + threads: int = 3, + k: Optional[int] = None, + w: Optional[int] = None, + H: bool = False, + I: Optional[str] = None, + f: Optional[float] = None, + g: Optional[str] = None, + G: Optional[str] = None, + F: Optional[int] = None, + r: Optional[str] = None, + n: Optional[int] = None, + m: Optional[int] = None, + X: bool = False, + p: Optional[float] = None, + N: Optional[int] = None, + A: Optional[int] = None, + B: Optional[int] = None, + O: Optional[str] = None, + E: Optional[str] = None, + z: Optional[str] = None, + s: Optional[int] = None, + u: Optional[str] = None, + J: Optional[int] = None, + L: bool = False, + R: Optional[str] = None, + write_cigar_paf: bool = False, + cs_tag: Optional[str] = None, + ds_tag: bool = False, + MD_tag: bool = False, + eqx: bool = False, + soft_clip_supplementary: bool = False, + minibatch_size: Optional[str] = None, + junc_bed: Optional[str] = None, + junc_bonus: Optional[int] = None, + secondary: bool = True, +): + """ + Performs sequence alignment using minimap2. + + Args: + target: Path to target.fa or target.idx. + queries: List of paths to query sequence files (FASTA/FASTQ). + output_file: Path to output file. If not provided, results are returned in stdout. + preset: Preset (map-ont, lr:hq, map-hifi, map-pb, map-iclr, asm5, asm10, asm20, splice, splice:hq, splice:sr, sr, ava-pb, ava-ont). + sam_format: Output in SAM format (default is PAF). + threads: Number of threads to use. + k: K-mer size (no larger than 28). + w: Minimizer window size. + H: Use homopolymer-compressed k-mer. + I: Split index for every ~NUM input bases. + f: Filter out top FLOAT fraction of repetitive minimizers. + g: Stop chain elongation if there are no minimizers in INT-bp. + G: Max intron length (effective with splice mode). + F: Max fragment length (effective with -xsr or fragment mode). + r: Chaining/alignment bandwidth and long-join bandwidth (e.g., "500,20000"). + n: Minimal number of minimizers on a chain. + m: Minimal chaining score. + X: Skip self and dual mappings (for all-vs-all mode). + p: Min secondary-to-primary score ratio. + N: Retain at most INT secondary alignments. + A: Matching score. + B: Mismatch penalty. + O: Gap open penalty (e.g., "4,24"). + E: Gap extension penalty (e.g., "2,1"). + z: Z-drop score and inversion Z-drop score (e.g., "400,200"). + s: Minimal peak DP alignment score. + u: How to find GT-AG. f:transcript strand, b:both strands, n:don't match. + J: Splice mode. 0: original, 1: miniprot. + L: Write CIGAR with >65535 ops at the CG tag. + R: SAM read group line (e.g., '@RG\\tID:foo\\tSM:bar'). + write_cigar_paf: Output CIGAR in PAF (cg tag). + cs_tag: Output the cs tag ('short' or 'long'). + ds_tag: Output the ds tag (extension to cs). + MD_tag: Output the MD tag. + eqx: Write =/X CIGAR operators. + soft_clip_supplementary: Use soft clipping for supplementary alignments. + minibatch_size: Minibatch size for mapping (e.g., 500M). + junc_bed: Gene annotations in BED12 or intron positions in 5-column BED. + junc_bonus: Score bonus for a splice donor or acceptor found in annotation. + secondary: Whether to output secondary alignments. + """ + target_path = Path(target) + if not target_path.exists(): + return {"error": f"Target file {target} not found."} + + for q in queries: + if not Path(q).exists(): + return {"error": f"Query file {q} not found."} + + cmd = ["minimap2"] + + # Preset must be applied first + if preset: + cmd.extend(["-x", preset]) + + # Indexing/Mapping Options + if k is not None: + if k > 28: return {"error": "k-mer size (k) cannot be larger than 28."} + cmd.extend(["-k", str(k)]) + if w is not None: cmd.extend(["-w", str(w)]) + if H: cmd.append("-H") + if I: cmd.extend(["-I", I]) + if f is not None: cmd.extend(["-f", str(f)]) + if g: cmd.extend(["-g", g]) + if G: cmd.extend(["-G", G]) + if F is not None: cmd.extend(["-F", str(F)]) + if r: cmd.extend(["-r", r]) + if n is not None: cmd.extend(["-n", str(n)]) + if m is not None: cmd.extend(["-m", str(m)]) + if X: cmd.append("-X") + if p is not None: cmd.extend(["-p", str(p)]) + if N is not None: cmd.extend(["-N", str(N)]) + + # Alignment Options + if A is not None: cmd.extend(["-A", str(A)]) + if B is not None: cmd.extend(["-B", str(B)]) + if O: cmd.extend(["-O", O]) + if E: cmd.extend(["-E", E]) + if z: cmd.extend(["-z", z]) + if s is not None: cmd.extend(["-s", str(s)]) + if u: cmd.extend(["-u", u]) + if J is not None: cmd.extend(["-J", str(J)]) + if junc_bed: cmd.extend(["--junc-bed", junc_bed]) + if junc_bonus is not None: cmd.extend(["--junc-bonus", str(junc_bonus)]) + + # I/O Options + if sam_format: cmd.append("-a") + if output_file: cmd.extend(["-o", output_file]) + if L: cmd.append("-L") + if R: cmd.extend(["-R", R]) + if write_cigar_paf: cmd.append("-c") + if cs_tag: + cmd.append(f"--cs={cs_tag}" if cs_tag in ["short", "long"] else "--cs") + if ds_tag: cmd.append("--ds") + if MD_tag: cmd.append("--MD") + if eqx: cmd.append("--eqx") + if soft_clip_supplementary: cmd.append("-Y") + if minibatch_size: cmd.extend(["-K", minibatch_size]) + if not secondary: cmd.append("--secondary=no") + + cmd.extend(["-t", str(threads)]) + + # Positional arguments + cmd.append(target) + cmd.extend(queries) + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + + output_info = { + "command_executed": " ".join(cmd), + "stdout": result.stdout if not output_file else f"Output written to {output_file}", + "stderr": result.stderr, + } + if output_file: + output_info["output_files"] = [output_file] + + return output_info + + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def minimap2_get_version(): + """Returns the version of minimap2 installed.""" + try: + result = subprocess.run(["minimap2", "--version"], check=True, capture_output=True, text=True) + return { + "version": result.stdout.strip(), + "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_moments/app/moments_server.py b/Biomni/mcp_generated/mcp_moments/app/moments_server.py new file mode 100644 index 0000000000000000000000000000000000000000..cdb945e4c23a18a4e64bbef98065166ff6befd81 --- /dev/null +++ b/Biomni/mcp_generated/mcp_moments/app/moments_server.py @@ -0,0 +1,453 @@ +import subprocess +import sys +from pathlib import Path +from typing import Optional, List, Union +import json +import tempfile + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_moments' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def moments_vcf_to_sfs( + vcf_path: str, + pop_info_path: str, + populations: List[str], + projections: List[int], + output_path: str, + polarized: bool = False, +): + """ + Parses a VCF file and generates a Site Frequency Spectrum (SFS). + + :param vcf_path: Path to the input VCF file. + :param pop_info_path: Path to the population information file (mapping individuals to populations). + :param populations: List of population IDs to include in the SFS. + :param projections: List of sample sizes (number of chromosomes) to project down to for each population. + :param output_path: Path to save the resulting SFS file (usually .fs). + :param polarized: Whether the SFS should be polarized (requires ancestral allele information in VCF). + """ + vcf_p = Path(vcf_path) + pop_p = Path(pop_info_path) + out_p = Path(output_path) + + if not vcf_p.exists(): + return {"error": f"VCF file not found: {vcf_path}"} + if not pop_p.exists(): + return {"error": f"Population info file not found: {pop_info_path}"} + if len(populations) != len(projections): + return {"error": "The number of populations must match the number of projections."} + + # Construct Python script to run moments + python_script = f""" +import moments +import sys + +try: + sfs = moments.Spectrum.from_vcf( + '{vcf_path}', + popfile='{pop_info_path}', + populations={populations}, + projections={projections}, + polarized={polarized} + ) + sfs.to_file('{output_path}') + print("Successfully created SFS.") +except Exception as e: + print(f"Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"moments.Spectrum.from_vcf('{vcf_path}', ...)", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_path] + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to parse VCF to SFS", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def moments_vcf_to_ld_stats( + vcf_path: str, + pop_info_path: str, + populations: List[str], + output_path: str, + bins: Optional[List[float]] = None, + use_cache: bool = True, +): + """ + Computes Linkage Disequilibrium (LD) statistics from a VCF file. + + :param vcf_path: Path to the input VCF file. + :param pop_info_path: Path to the population information file. + :param populations: List of population IDs to include. + :param output_path: Path to save the computed LD statistics (usually .hdf5 or .p). + :param bins: Recombination distance bins (list of floats). If None, default bins are used. + :param use_cache: Whether to use caching for intermediate steps. + """ + vcf_p = Path(vcf_path) + pop_p = Path(pop_info_path) + + if not vcf_p.exists(): + return {"error": f"VCF file not found: {vcf_path}"} + if not pop_p.exists(): + return {"error": f"Population info file not found: {pop_info_path}"} + + bins_str = str(bins) if bins is not None else "None" + + python_script = f""" +import moments.LD +import sys + +try: + ld_stats = moments.LD.Parsing.compute_ld_statistics( + '{vcf_path}', + pop_file='{pop_info_path}', + pops={populations}, + bins={bins_str}, + use_cache={use_cache} + ) + # moments.LD statistics are typically saved using pickle or hdf5 + import pickle + with open('{output_path}', 'wb') as f: + pickle.dump(ld_stats, f) + print("Successfully computed LD statistics.") +except Exception as e: + print(f"Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": "moments.LD.Parsing.compute_ld_statistics(...)", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_path] + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to compute LD statistics", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def moments_simulate_sfs_demes( + demes_path: str, + sample_sizes: List[int], + output_path: str, + sampled_demes: Optional[List[str]] = None, +): + """ + Simulates an SFS from a demographic model specified in a Demes file. + + :param demes_path: Path to the Demes YAML file. + :param sample_sizes: List of sample sizes for each population. + :param output_path: Path to save the simulated SFS. + :param sampled_demes: List of deme IDs to sample. If None, all leaf demes are sampled. + """ + if not Path(demes_path).exists(): + return {"error": f"Demes file not found: {demes_path}"} + + sampled_demes_str = str(sampled_demes) if sampled_demes is not None else "None" + + python_script = f""" +import moments +import sys + +try: + sfs = moments.Spectrum.from_demes( + '{demes_path}', + sampled_demes={sampled_demes_str}, + sample_sizes={sample_sizes} + ) + sfs.to_file('{output_path}') + print("Successfully simulated SFS from Demes.") +except Exception as e: + print(f"Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": "moments.Spectrum.from_demes(...)", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_path] + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to simulate SFS from Demes", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def moments_infer_sfs( + sfs_path: str, + model_file: str, + model_func_name: str, + p0: List[float], + lower_bound: List[float], + upper_bound: List[float], + output_path: str, + maxiter: int = 100, + verbose: int = 1, +): + """ + Fits a demographic model to an observed SFS using log-likelihood optimization. + + :param sfs_path: Path to the observed SFS file. + :param model_file: Path to a Python script containing the demographic model function. + :param model_func_name: Name of the function in model_file that returns a simulated SFS. + :param p0: Initial parameter guesses. + :param lower_bound: Lower bounds for parameters. + :param upper_bound: Upper bounds for parameters. + :param output_path: Path to save the optimized parameters and results (JSON). + :param maxiter: Maximum number of optimization iterations. + :param verbose: Verbosity level for optimization. + """ + if not Path(sfs_path).exists(): + return {"error": f"SFS file not found: {sfs_path}"} + if not Path(model_file).exists(): + return {"error": f"Model file not found: {model_file}"} + + python_script = f""" +import moments +import sys +import json +import importlib.util + +# Load the user-defined model +spec = importlib.util.spec_from_file_location("user_model", "{model_file}") +user_model = importlib.util.module_from_spec(spec) +spec.loader.exec_module(user_model) +model_func = getattr(user_model, "{model_func_name}") + +try: + data = moments.Spectrum.from_file('{sfs_path}') + ns = data.sample_sizes + + # Wrap model to ensure it uses correct sample sizes + def func_to_optimize(params): + return model_func(params, ns) + + popt = moments.Inference.optimize_log( + {p0}, data, func_to_optimize, + lower_bound={lower_bound}, + upper_bound={upper_bound}, + maxiter={maxiter}, + verbose={verbose} + ) + + model_sfs = func_to_optimize(popt) + ll = moments.Inference.ll_multinom(model_sfs, data) + + results = {{ + "optimized_params": popt.tolist(), + "max_log_likelihood": float(ll), + "model_func": "{model_func_name}" + }} + + with open('{output_path}', 'w') as f: + json.dump(results, f, indent=4) + print("Successfully completed SFS inference.") +except Exception as e: + print(f"Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": "moments.Inference.optimize_log(...)", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_path] + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed SFS inference", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def moments_simulate_two_locus( + sample_size: int, + theta: float, + rho: float, + output_path: str, + gamma: float = 0.0, + sel_type: str = "additive", +): + """ + Simulates a two-locus frequency spectrum. + + :param sample_size: Number of chromosomes to sample. + :param theta: Population-scaled mutation rate (4Nu). + :param rho: Population-scaled recombination rate (4Nr). + :param output_path: Path to save the two-locus spectrum. + :param gamma: Population-scaled selection coefficient (2Ns). + :param sel_type: Type of selection ('additive', 'dominant', etc.). + """ + if sample_size <= 0: + return {"error": "Sample size must be positive."} + if theta < 0 or rho < 0: + return {"error": "Theta and Rho must be non-negative."} + + python_script = f""" +import moments.TwoLocus +import sys + +try: + # Simulate steady state two-locus spectrum + tls = moments.TwoLocus.TLS.steady_state( + {sample_size}, theta={theta}, rho={rho}, gamma={gamma}, sel_type='{sel_type}' + ) + tls.to_file('{output_path}') + print("Successfully simulated Two-Locus spectrum.") +except Exception as e: + print(f"Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": "moments.TwoLocus.TLS.steady_state(...)", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_path] + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to simulate Two-Locus spectrum", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def moments_ld_infer_demography( + ld_stats_path: str, + model_file: str, + model_func_name: str, + p0: List[float], + lower_bound: List[float], + upper_bound: List[float], + output_path: str, + maxiter: int = 100, +): + """ + Fits a demographic model to LD statistics. + + :param ld_stats_path: Path to the computed LD statistics (pickle file). + :param model_file: Path to a Python script containing the LD demographic model. + :param model_func_name: Name of the function in model_file. + :param p0: Initial parameter guesses. + :param lower_bound: Lower bounds for parameters. + :param upper_bound: Upper bounds for parameters. + :param output_path: Path to save the optimized parameters (JSON). + :param maxiter: Maximum number of optimization iterations. + """ + if not Path(ld_stats_path).exists(): + return {"error": f"LD stats file not found: {ld_stats_path}"} + if not Path(model_file).exists(): + return {"error": f"Model file not found: {model_file}"} + + python_script = f""" +import moments.LD +import sys +import json +import pickle +import importlib.util + +# Load the user-defined model +spec = importlib.util.spec_from_file_location("user_model", "{model_file}") +user_model = importlib.util.module_from_spec(spec) +spec.loader.exec_module(user_model) +model_func = getattr(user_model, "{model_func_name}") + +try: + with open('{ld_stats_path}', 'rb') as f: + data = pickle.load(f) + + popt = moments.LD.Inference.optimize_log( + {p0}, data, model_func, + lower_bound={lower_bound}, + upper_bound={upper_bound}, + maxiter={maxiter}, + verbose=True + ) + + results = {{ + "optimized_params": popt.tolist(), + "model_func": "{model_func_name}" + }} + + with open('{output_path}', 'w') as f: + json.dump(results, f, indent=4) + print("Successfully completed LD inference.") +except Exception as e: + print(f"Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + [sys.executable, "-c", python_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": "moments.LD.Inference.optimize_log(...)", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [output_path] + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed LD inference", + "stdout": e.stdout, + "stderr": e.stderr + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_moments/docker-compose.yml b/Biomni/mcp_generated/mcp_moments/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..8af11c0bc2cd75533e71d0c9cfeb80ad2723f487 --- /dev/null +++ b/Biomni/mcp_generated/mcp_moments/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-moments: + build: . + image: mcp-moments:latest + container_name: mcp-moments + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=moments + 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_moments/requirements.txt b/Biomni/mcp_generated/mcp_moments/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_moments/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_parasail-python/Dockerfile b/Biomni/mcp_generated/mcp_parasail-python/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f02664f0502a44bcd1fa9cc10f243539788a8c38 --- /dev/null +++ b/Biomni/mcp_generated/mcp_parasail-python/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install parasail-python via conda (e.g., from bioconda) +RUN conda install -c bioconda parasail-python -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/parasail-python_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/parasail-python_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/parasail-python_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_parasail-python/app/parasail-python_server.py b/Biomni/mcp_generated/mcp_parasail-python/app/parasail-python_server.py new file mode 100644 index 0000000000000000000000000000000000000000..07a576099a2824f0a59d9cbce988857443f2fdc3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_parasail-python/app/parasail-python_server.py @@ -0,0 +1,176 @@ +import subprocess +from pathlib import Path +from typing import Dict, Any, List +import logging + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# MCP decorator placeholder, as per instructions. +# In a real MCP environment, this would be `from mcp import tool`. +class mcp: + @staticmethod + def tool(): + def decorator(f): + return f + return decorator + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_parasail_python' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def parasail_aligner( + a_file: Path, + b_file: Path, + function: str = "sw_trace_striped_sat", + matrix: str = "blosum62", + open_penalty: int = 10, + extend_penalty: int = 1, + threads: int = 1, + xml: bool = False, + header: bool = False, + profile: bool = False, + stats: bool = False, + is_global: bool = False, + semi_global: bool = False, + diag: bool = False, + scan: bool = False, + nogap: bool = False, + saturated: bool = False, + non_saturated: bool = False, + print_score_matrix: bool = False, +) -> Dict[str, Any]: + """ + Performs pairwise sequence alignment using the parasail library command-line tool. + + This tool is a wrapper for the 'parasail_aligner' command-line utility, + which provides access to the SIMD-accelerated pairwise alignment functions + of the parasail library. It aligns sequences from two FASTA files. + + Args: + a_file: Path to the first sequence file (FASTA format). + b_file: Path to the second sequence file (FASTA format). + function: The alignment function to use (e.g., 'sw_trace_striped_sat'). + matrix: The substitution matrix to use (e.g., 'blosum62') or path to a matrix file. + open_penalty: The penalty for opening a gap. + extend_penalty: The penalty for extending a gap. + threads: The number of threads to use for the computation. + xml: If True, output results in XML format. + header: If True, include a header in the output. + profile: If True, use profile alignment. + stats: If True, use stats alignment. + is_global: If True, perform a global alignment (Needleman-Wunsch). + semi_global: If True, perform a semi-global alignment. + diag: If True, perform a diagonal alignment. + scan: If True, perform a scan alignment. + nogap: If True, perform a nogap alignment. + saturated: If True, use a saturated implementation. + non_saturated: If True, use a non-saturated implementation. + print_score_matrix: If True, print the score matrix in the output. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # 1. Input Validation + if not a_file.is_file(): + raise FileNotFoundError(f"Input file 'a_file' not found at {a_file}") + if not b_file.is_file(): + raise FileNotFoundError(f"Input file 'b_file' not found at {b_file}") + + if open_penalty < 0: + raise ValueError("open_penalty must be a non-negative integer.") + if extend_penalty < 0: + raise ValueError("extend_penalty must be a non-negative integer.") + if threads <= 0: + raise ValueError("threads must be a positive integer.") + + if saturated and non_saturated: + raise ValueError("Cannot specify both saturated (-z) and non-saturated (-n) implementations simultaneously.") + + alignment_modes = { + "global": is_global, + "semi_global": semi_global, + "diag": diag, + "scan": scan, + "nogap": nogap, + } + selected_modes = [mode for mode, selected in alignment_modes.items() if selected] + if len(selected_modes) > 1: + raise ValueError( + f"Only one alignment mode can be selected at a time. Found: {', '.join(selected_modes)}" + ) + + # 2. Command Construction + cmd: List[str] = [ + "parasail_aligner", + "-a", str(a_file), + "-b", str(b_file), + "-f", function, + "-m", matrix, + "-o", str(open_penalty), + "-e", str(extend_penalty), + "-t", str(threads), + ] + + if xml: cmd.append("-x") + if header: cmd.append("-c") + if profile: cmd.append("-p") + if stats: cmd.append("-s") + if is_global: cmd.append("-l") + if semi_global: cmd.append("-i") + if diag: cmd.append("-d") + if scan: cmd.append("-w") + if nogap: cmd.append("-g") + if saturated: cmd.append("-z") + if non_saturated: cmd.append("-n") + if print_score_matrix: cmd.append("-r") + + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + + # 3. Subprocess Execution and Error Handling + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + except FileNotFoundError: + error_message = "The 'parasail_aligner' executable was not found. Please ensure it is installed and in your system's PATH." + logger.error(error_message) + # Return a structured error that conforms to MCP expectations + return { + "error": error_message, + "command_executed": command_executed, + "stdout": "", + "stderr": "", + "return_code": 127, + "output_files": [] + } + except subprocess.CalledProcessError as e: + logger.error(f"Parasail aligner failed with exit code {e.returncode}") + logger.error(f"Stderr: {e.stderr}") + return { + "error": "Parasail aligner failed with a non-zero exit code.", + "return_code": e.returncode, + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + + # 4. Structured Result Return + # parasail_aligner prints results to stdout, so there are no explicit output files. + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [], + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_parasail-python/app/parasail-python_shim_server.py b/Biomni/mcp_generated/mcp_parasail-python/app/parasail-python_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d9c944efd0b81e95bd1ff78cf5b1e6416e49d69e --- /dev/null +++ b/Biomni/mcp_generated/mcp_parasail-python/app/parasail-python_shim_server.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_parasail-python/app/parasail-python_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_parasail_python' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + code = source_server.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(source_server)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(source_server), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_parasail-python/app/requirements.txt b/Biomni/mcp_generated/mcp_parasail-python/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_parasail-python/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_parasail-python/docker-compose.yml b/Biomni/mcp_generated/mcp_parasail-python/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..cf00e0fd87375604bb47bfb3228d473761e5dcb9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_parasail-python/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-parasail-python: + build: . + image: mcp-parasail-python:latest + container_name: mcp-parasail-python + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=parasail-python + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_parasail-python/environment.yaml b/Biomni/mcp_generated/mcp_parasail-python/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9816ad719203c7f825b022d954d3993432361214 --- /dev/null +++ b/Biomni/mcp_generated/mcp_parasail-python/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - parasail-python + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_parasail-python/requirements.txt b/Biomni/mcp_generated/mcp_parasail-python/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_parasail-python/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_pasta/Dockerfile b/Biomni/mcp_generated/mcp_pasta/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0278c4e0a58a47e385a3444c112296d1084a4840 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pasta/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 pasta via conda (e.g., from bioconda) +RUN conda install -c bioconda pasta -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/pasta_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/pasta_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/pasta_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_pasta/app/pasta_server.py b/Biomni/mcp_generated/mcp_pasta/app/pasta_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c76568eb1b3387d22e7fe253f2ece47f8f354430 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pasta/app/pasta_server.py @@ -0,0 +1,163 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Dict, Any + +# Assume 'pasta' is the executable name available in the system's PATH. +# If it's a Python script (e.g., run_pasta.py), the command would need to be +# `python run_pasta.py ...` and `pasta_executable` would be `["python", "run_pasta.py"]`. +PASTA_EXECUTABLE = "pasta" + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_pasta' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def align_sequences( + input_fasta: Path, + output_prefix: str, + num_iterations: int = 3, + threads: int = 1, + memory_limit: Optional[str] = None, + temp_dir: Optional[Path] = None, + keep_intermediate: bool = False, + verbose: bool = False, + force_overwrite: bool = False, +) -> Dict[str, Any]: + """ + Performs multiple sequence alignment using the PASTA (Practical Alignment using SATé and MAFFT) tool. + + PASTA is designed for large-scale phylogenetic inference by providing accurate + multiple sequence alignments. + + Args: + input_fasta: Path to the input FASTA file containing unaligned sequences. + This file must exist. + output_prefix: A prefix for the output files. PASTA will generate files + like .fasta (the alignment), .log, etc. + The output files will be placed in the current working directory + or a specified temporary directory if `temp_dir` is used. + num_iterations: Number of iterations for the PASTA algorithm. + Defaults to 3. Must be a positive integer. + threads: Number of threads (CPU cores) to use for parallel processing. + Defaults to 1. Must be a positive integer. + memory_limit: Memory limit for PASTA, e.g., "4G" for 4 gigabytes. + If not specified, PASTA will use available memory. + temp_dir: Optional path to a directory for temporary files. If not provided, + PASTA will use its default temporary directory behavior, which + might be in the current working directory or a system default. + If provided, the directory must exist. + keep_intermediate: If True, PASTA will keep intermediate files generated + during the alignment process. Defaults to False. + verbose: If True, PASTA will print more detailed progress information to stderr. + Defaults to False. + force_overwrite: If True, PASTA will overwrite existing output files + without prompting. Defaults to False. + + Returns: + A dictionary containing: + - command_executed (str): The full command string executed. + - stdout (str): Standard output from the PASTA command. + - stderr (str): Standard error from the PASTA command. + - output_files (List[Path]): A list of paths to the generated output files. + + Raises: + FileNotFoundError: If the input_fasta file does not exist. + ValueError: If num_iterations or threads are not positive integers, + or if output_prefix is empty. + subprocess.CalledProcessError: If the PASTA command returns a non-zero exit code. + """ + # Input validation + if not input_fasta.is_file(): + raise FileNotFoundError(f"Input FASTA file not found: {input_fasta}") + if not input_fasta.suffix.lower() in [".fasta", ".fa", ".fna", ".fas"]: + # This is a common convention, but PASTA might accept others. + # Adding a warning or strict check can be useful. + pass # For now, just a soft check. + + if not isinstance(num_iterations, int) or num_iterations <= 0: + raise ValueError(f"num_iterations must be a positive integer, got {num_iterations}") + if not isinstance(threads, int) or threads <= 0: + raise ValueError(f"threads must be a positive integer, got {threads}") + if not output_prefix: + raise ValueError("output_prefix cannot be an empty string.") + + if temp_dir: + if not temp_dir.is_dir(): + raise FileNotFoundError(f"Temporary directory not found: {temp_dir}") + + command = [ + PASTA_EXECUTABLE, + "-i", str(input_fasta), + "-o", output_prefix, + "-j", str(num_iterations), + "-t", str(threads), + ] + + if memory_limit: + command.extend(["-m", memory_limit]) + if temp_dir: + command.extend(["--tempdir", str(temp_dir)]) + if keep_intermediate: + command.append("--keep-intermediate") + if verbose: + command.append("--verbose") + if force_overwrite: + command.append("--force") + + stdout: str = "" + stderr: str = "" + output_files: List[Path] = [] + + try: + process = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + cwd=temp_dir if temp_dir else None # Run in temp_dir if specified, otherwise current dir + ) + stdout = process.stdout + stderr = process.stderr + + # PASTA typically outputs .fasta and .log + # The actual output directory depends on `cwd` and `temp_dir`. + # If temp_dir is used, output files are relative to temp_dir. + # If not, they are relative to the current working directory. + output_dir = temp_dir if temp_dir else Path.cwd() + + aligned_fasta = output_dir / f"{output_prefix}.fasta" + log_file = output_dir / f"{output_prefix}.log" + + if aligned_fasta.is_file(): + output_files.append(aligned_fasta) + if log_file.is_file(): + output_files.append(log_file) + + # PASTA might generate other files depending on its internal workings + # (e.g., intermediate trees, alignment parts). + # A more robust approach might involve parsing the log or checking common patterns. + # For now, we'll just capture the primary expected outputs. + + except FileNotFoundError: + stderr = f"Error: PASTA executable '{PASTA_EXECUTABLE}' not found. " \ + "Please ensure it is installed and available in your system's PATH." + raise + except subprocess.CalledProcessError as e: + stderr = f"PASTA command failed with exit code {e.returncode}:\n{e.stderr}" + raise + except Exception as e: + stderr = f"An unexpected error occurred: {e}" + raise + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_pasta/app/pasta_shim_server.py b/Biomni/mcp_generated/mcp_pasta/app/pasta_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..36196be923977cdcef2ce08f3f62b9bf80c3aff4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pasta/app/pasta_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_pasta/app/pasta_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_pasta' + + +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_pasta/app/requirements.txt b/Biomni/mcp_generated/mcp_pasta/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_pasta/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_pasta/docker-compose.yml b/Biomni/mcp_generated/mcp_pasta/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..28a05ccd2e1681c6ed6ad2f0efe59a330b16e21d --- /dev/null +++ b/Biomni/mcp_generated/mcp_pasta/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-pasta: + build: . + image: mcp-pasta:latest + container_name: mcp-pasta + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=pasta + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-business-isbn-data/Dockerfile b/Biomni/mcp_generated/mcp_perl-business-isbn-data/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..45e9ae622d589b3c90e57baf8a731ba77a37d08c --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-business-isbn-data/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-business-isbn-data via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-business-isbn-data -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-business-isbn-data_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-business-isbn-data_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-business-isbn-data_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-business-isbn-data/app/perl-business-isbn-data_shim_server.py b/Biomni/mcp_generated/mcp_perl-business-isbn-data/app/perl-business-isbn-data_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..a88cd4339488369737aa687a363e746e0613bede --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-business-isbn-data/app/perl-business-isbn-data_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-business-isbn-data/app/perl-business-isbn-data_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_business_isbn_data' + + +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-business-isbn-data/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-business-isbn-data/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..3ae6a649cd0c30a94e425e20c628f855f27d3476 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-business-isbn-data/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-business-isbn-data: + build: . + image: mcp-perl-business-isbn-data:latest + container_name: mcp-perl-business-isbn-data + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-business-isbn-data + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-business-isbn-data/environment.yaml b/Biomni/mcp_generated/mcp_perl-business-isbn-data/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..208fe07a766682b2cceafea20c40fe1204e961ca --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-business-isbn-data/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-business-isbn-data + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-constant/environment.yaml b/Biomni/mcp_generated/mcp_perl-constant/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a7ee160d72e2ebc00f01f3c82414a5bfdc8e49bd --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-constant/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-constant + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-error/app/perl-error_server.py b/Biomni/mcp_generated/mcp_perl-error/app/perl-error_server.py new file mode 100644 index 0000000000000000000000000000000000000000..c27a4266a1fae9653038a0a8b6515133353d5c72 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-error/app/perl-error_server.py @@ -0,0 +1,159 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict + +# @mcp.tool() is a placeholder for the actual decorator. +# The final code should be used in an environment where mcp is defined. + +def tool_placeholder(func): + """A placeholder decorator for mcp.tool.""" + return func + +@tool_placeholder +def run_perl( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + execute_program: Optional[List[str]] = None, + execute_program_extended: Optional[List[str]] = None, + check_syntax_only: bool = False, + record_separator: Optional[str] = None, + autosplit: bool = False, + split_pattern: Optional[str] = None, + loop_around_program: bool = False, + loop_and_print: bool = False, + in_place_edit_extension: Optional[str] = None, + include_directories: Optional[List[Path]] = None, + use_modules: Optional[List[str]] = None, + no_use_modules: Optional[List[str]] = None, + enable_switch_parsing: bool = False, + search_path_for_program: bool = False, + unicode_features: Optional[str] = None, + run_under_debugger: bool = False, + debugger_module: Optional[str] = None, + debugging_flags: Optional[str] = None, + no_sitecustomize: bool = False, + enable_line_ending_processing: bool = False, + line_terminator_octal: Optional[str] = None, + tainting_warnings: bool = False, + tainting_checks: bool = False, + dump_core: bool = False, + allow_unsafe_operations: bool = False, + print_version: bool = False, + print_configuration: bool = False, + config_variable: Optional[str] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + disable_all_warnings: bool = False, + ignore_text_before_shebang: bool = False, + change_directory: Optional[Path] = None, +) -> Dict[str, any]: + """ + Executes the Perl interpreter with specified switches and a program file or one-liners. + + This tool wraps the generic 'perl' command-line executable. The name 'perl-error' + refers to a Perl module for exception handling, not a standalone command-line tool. + This function provides the functionality of the Perl interpreter itself, which is + used to run scripts that may depend on modules like Error.pm. + """ + # --- Input Validation --- + if not any([programfile, execute_program, execute_program_extended, print_version, print_configuration]): + raise ValueError("A 'programfile', 'execute_program', 'execute_program_extended', 'print_version', or 'print_configuration' must be provided.") + + if programfile and (execute_program or execute_program_extended): + raise ValueError("'programfile' cannot be used with 'execute_program' or 'execute_program_extended'.") + + if programfile: + if not programfile.is_file(): + raise FileNotFoundError(f"Program file not found or is not a regular file: {programfile}") + + if debugger_module and not run_under_debugger: + raise ValueError("'debugger_module' can only be specified when 'run_under_debugger' is True.") + + if config_variable and not print_configuration: + raise ValueError("'config_variable' can only be specified when 'print_configuration' is True.") + + if line_terminator_octal and not enable_line_ending_processing: + raise ValueError("'line_terminator_octal' can only be specified when 'enable_line_ending_processing' is True.") + + if change_directory and not ignore_text_before_shebang: + raise ValueError("'change_directory' can only be specified when 'ignore_text_before_shebang' is True.") + + if include_directories: + for directory in include_directories: + if not directory.is_dir(): + raise NotADirectoryError(f"Include directory not found or not a directory: {directory}") + + if change_directory and not change_directory.is_dir(): + raise NotADirectoryError(f"Directory for '-x' option not found or not a directory: {change_directory}") + + # --- Command Construction --- + cmd = ["perl"] + + if record_separator is not None: cmd.append(f"-0{record_separator}") + if autosplit: cmd.append("-a") + if unicode_features is not None: cmd.append(f"-C{unicode_features}") + if check_syntax_only: cmd.append("-c") + if run_under_debugger: cmd.append(f"-d{':' + debugger_module if debugger_module else ''}") + if debugging_flags is not None: cmd.append(f"-D{debugging_flags}") + if execute_program: + for prog_line in execute_program: cmd.extend(["-e", prog_line]) + if execute_program_extended: + for prog_line in execute_program_extended: cmd.extend(["-E", prog_line]) + if no_sitecustomize: cmd.append("-f") + if split_pattern is not None: cmd.append(f"-F{split_pattern}") + if in_place_edit_extension is not None: cmd.append(f"-i{in_place_edit_extension}") + if include_directories: + for directory in include_directories: cmd.extend(["-I", str(directory)]) + if enable_line_ending_processing: cmd.append(f"-l{line_terminator_octal if line_terminator_octal else ''}") + if use_modules: + for module in use_modules: cmd.append(f"-M{module}") + if no_use_modules: + for module in no_use_modules: cmd.append(f"-m{module}") + if loop_around_program: cmd.append("-n") + if loop_and_print: cmd.append("-p") + if enable_switch_parsing: cmd.append("-s") + if search_path_for_program: cmd.append("-S") + if tainting_warnings: cmd.append("-t") + if tainting_checks: cmd.append("-T") + if dump_core: cmd.append("-u") + if allow_unsafe_operations: cmd.append("-U") + if print_version: cmd.append("-v") + if print_configuration: cmd.append(f"-V{':' + config_variable if config_variable else ''}") + if enable_warnings: cmd.append("-w") + if enable_all_warnings: cmd.append("-W") + if ignore_text_before_shebang: + if change_directory: cmd.extend(["-x", str(change_directory)]) + else: cmd.append("-x") + if disable_all_warnings: cmd.append("-X") + + if programfile: + cmd.append(str(programfile)) + if arguments: + cmd.extend(arguments) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [] + } + except FileNotFoundError: + raise RuntimeError("Error: 'perl' command not found. Please ensure Perl is installed and in your system's PATH.") + except subprocess.CalledProcessError as e: + # Return a structured error, but do not raise an exception from the MCP tool itself + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + "error": f"Perl process exited with non-zero status {e.returncode}." + } diff --git a/Biomni/mcp_generated/mcp_perl-exception-class/Dockerfile b/Biomni/mcp_generated/mcp_perl-exception-class/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6116c8502aa9651cd86b58054e1d3d7315f36279 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-exception-class/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-exception-class via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-exception-class -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-exception-class_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-exception-class_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-exception-class_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-exception-class/app/perl-exception-class_server.py b/Biomni/mcp_generated/mcp_perl-exception-class/app/perl-exception-class_server.py new file mode 100644 index 0000000000000000000000000000000000000000..d4976f06083558122db43a3e2c2be2ded6b77286 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-exception-class/app/perl-exception-class_server.py @@ -0,0 +1,235 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional, Dict, Any + +# This is a placeholder for the MCP decorator. +# In a real MCP environment, this would be: +# from mcp import tool as mcp_tool +class mcp: + @staticmethod + def tool(func): + return func + +@mcp.tool +def run_perl( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + program_lines: Optional[List[str]] = None, + program_lines_extended: Optional[List[str]] = None, + record_separator_octal: Optional[str] = None, + autosplit: bool = False, + unicode_features: Optional[str] = None, + check_syntax_only: bool = False, + debugger: Optional[str] = None, + debugging_flags: Optional[str] = None, + no_sitecustomize: bool = False, + split_pattern: Optional[str] = None, + in_place_edit: Optional[str] = None, + include_directories: Optional[List[Path]] = None, + line_ending_processing_octal: Optional[str] = None, + use_modules: Optional[List[str]] = None, + use_modules_no_imports: Optional[List[str]] = None, + no_modules: Optional[List[str]] = None, + no_modules_no_imports: Optional[List[str]] = None, + loop_around_program: bool = False, + loop_and_print: bool = False, + parse_switches_after_program: bool = False, + search_path_for_program: bool = False, + tainting_warnings: bool = False, + tainting_checks: bool = False, + dump_core: bool = False, + allow_unsafe_operations: bool = False, + print_version: bool = False, + print_config: Optional[str] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + ignore_preamble_dir: Optional[str] = None, + disable_all_warnings: bool = False, +) -> Dict[str, Any]: + """ + Executes the Perl interpreter. This tool is a wrapper for the `perl` command, + which is the execution environment for Perl modules like Exception-Class. + + Args: + programfile: Path to the Perl program file to execute. + arguments: List of arguments to pass to the program file. + program_lines: List of strings, each executed as a line of the program (-e). + program_lines_extended: Like program_lines, but enables all optional features (-E). + record_separator_octal: Specify record separator as an octal value (-0). + autosplit: Enable autosplit mode with -n or -p (splits $_ into @F) (-a). + unicode_features: Enable the listed Unicode features (-C). + check_syntax_only: Check syntax only; does not execute the program (-c). + debugger: Run the program under the specified debugger (-d[:debugger]). + debugging_flags: Set debugging flags (-D). + no_sitecustomize: Don't do $sitelib/sitecustomize.pl at startup (-f). + split_pattern: Specify the split() pattern for the -a switch (-F). + in_place_edit: Edit <> files in place. If a value is supplied, it is used as the backup extension (-i[extension]). + include_directories: List of directories to add to @INC (-I). + line_ending_processing_octal: Enable line ending processing and specify the line terminator (-l[octal]). + use_modules: Execute 'use module...' before the program (-Mmodule). + use_modules_no_imports: Execute 'use module ()' before the program (-mmodule). + no_modules: Execute 'no module...' before the program (-M-module). + no_modules_no_imports: Execute 'no module ()' before the program (-m-module). + loop_around_program: Assume a "while (<>) { ... }" loop around the program (-n). + loop_and_print: Assume a loop like -n but also print the line, like sed (-p). + parse_switches_after_program: Enable rudimentary parsing for switches after the program file (-s). + search_path_for_program: Look for the program file using the PATH environment variable (-S). + tainting_warnings: Enable tainting warnings (-t). + tainting_checks: Enable tainting checks (-T). + dump_core: Dump core after parsing the program (-u). + allow_unsafe_operations: Allow unsafe operations (-U). + print_version: Print version, patchlevel, and license, then exit (-v). + print_config: Print configuration summary or a single Config.pm variable, then exit (-V[:variable]). + enable_warnings: Enable many useful warnings (-w). + enable_all_warnings: Enable all warnings (-W). + ignore_preamble_dir: Ignore text before #!perl line. Optionally cd to the specified directory (-x[directory]). + disable_all_warnings: Disable all warnings (-X). + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list of output files. + """ + # --- Input Validation --- + program_source_count = sum( + p is not None for p in [programfile, program_lines, program_lines_extended] + ) + if program_source_count > 1: + raise ValueError( + "Only one of 'programfile', 'program_lines', or 'program_lines_extended' can be specified." + ) + + is_info_command = print_version or (print_config is not None) + if program_source_count == 0 and not is_info_command: + raise ValueError( + "A program source ('programfile', 'program_lines', or 'program_lines_extended') must be provided." + ) + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"Program file not found: {programfile}") + + if include_directories: + for directory in include_directories: + if not directory.is_dir(): + raise NotADirectoryError(f"Include directory not found: {directory}") + + # --- Command Construction --- + cmd = ["perl"] + + if record_separator_octal is not None: + cmd.append(f"-0{record_separator_octal}") + if autosplit: + cmd.append("-a") + if unicode_features is not None: + cmd.append(f"-C{unicode_features}") + if check_syntax_only: + cmd.append("-c") + if debugger is not None: + cmd.append(f"-d:{debugger}" if debugger else "-d") + if debugging_flags is not None: + cmd.append(f"-D{debugging_flags}") + if no_sitecustomize: + cmd.append("-f") + if split_pattern is not None: + cmd.append(f"-F{split_pattern}") + if in_place_edit is not None: + cmd.append(f"-i{in_place_edit}") + if include_directories: + for directory in include_directories: + cmd.append(f"-I{directory}") + if line_ending_processing_octal is not None: + cmd.append(f"-l{line_ending_processing_octal}") + if use_modules: + for module in use_modules: + cmd.append(f"-M{module}") + if use_modules_no_imports: + for module in use_modules_no_imports: + cmd.append(f"-m{module}") + if no_modules: + for module in no_modules: + cmd.append(f"-M-{module}") + if no_modules_no_imports: + for module in no_modules_no_imports: + cmd.append(f"-m-{module}") + if loop_around_program: + cmd.append("-n") + if loop_and_print: + cmd.append("-p") + if parse_switches_after_program: + cmd.append("-s") + if search_path_for_program: + cmd.append("-S") + if tainting_warnings: + cmd.append("-t") + if tainting_checks: + cmd.append("-T") + if dump_core: + cmd.append("-u") + if allow_unsafe_operations: + cmd.append("-U") + if print_version: + cmd.append("-v") + if print_config is not None: + cmd.append(f"-V:{print_config}" if print_config else "-V") + if enable_warnings: + cmd.append("-w") + if enable_all_warnings: + cmd.append("-W") + if ignore_preamble_dir is not None: + cmd.append(f"-x{ignore_preamble_dir}") + if disable_all_warnings: + cmd.append("-X") + + # --- Program and Arguments --- + if program_lines: + for line in program_lines: + cmd.extend(["-e", line]) + if program_lines_extended: + for line in program_lines_extended: + cmd.extend(["-E", line]) + + if programfile: + cmd.append(str(programfile)) + + if arguments: + cmd.extend(arguments) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + stdout = result.stdout + stderr = result.stderr + except FileNotFoundError: + return { + "error": "Perl executable not found. Please ensure it is in your PATH.", + "command_executed": command_executed, + "stdout": "", + "stderr": "FileNotFoundError: 'perl' not found.", + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "error": "Perl execution failed.", + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "output_files": [] + } + + # --- Structured Result Return --- + output_files = [] + if programfile and in_place_edit is not None: + output_files.append(str(programfile)) + + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": output_files, + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-exception-class/app/perl-exception-class_shim_server.py b/Biomni/mcp_generated/mcp_perl-exception-class/app/perl-exception-class_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9fbe390507eb51a880db1748c4fdbdd6fd5b25c1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-exception-class/app/perl-exception-class_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-exception-class/app/perl-exception-class_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_exception_class' + + +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-exception-class/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-exception-class/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-exception-class/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-exception-class/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-exception-class/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..26fc6034a589d9b309709752d48a2b94347e1314 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-exception-class/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-exception-class: + build: . + image: mcp-perl-exception-class:latest + container_name: mcp-perl-exception-class + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-exception-class + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-exception-class/environment.yaml b/Biomni/mcp_generated/mcp_perl-exception-class/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..12401e3821580643f0a2302537548ee906f1b54e --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-exception-class/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-exception-class + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-exception-class/requirements.txt b/Biomni/mcp_generated/mcp_perl-exception-class/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-exception-class/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-extutils-makemaker/Dockerfile b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9903a24227323d44ec06ba9f1bd33ccd62745a74 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/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-extutils-makemaker via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-extutils-makemaker -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-extutils-makemaker_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-extutils-makemaker_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-extutils-makemaker_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-extutils-makemaker/app/perl-extutils-makemaker_server.py b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/app/perl-extutils-makemaker_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1eeff31b8792d3e3e8b6bb58863a68fae2f2fe42 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/app/perl-extutils-makemaker_server.py @@ -0,0 +1,334 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List, Dict + +# The mcp.tool decorator is a placeholder for the actual Model Context Protocol decorator. +# In a real MCP server, you would import it, e.g., `from mcp import tool`. +class mcp: + def tool(*args, **kwargs): + def decorator(f): + return f + return decorator + +# Set up logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_extutils_makemaker' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def run_makefile_pl( + project_dir: Path, + makefile_pl: str = "Makefile.PL", + output_makefile: str = "Makefile", + prefix: Optional[Path] = None, + lib: Optional[Path] = None, + install_base: Optional[Path] = None, + destdir: Optional[Path] = None, + installdirs: Optional[str] = None, + uninst: bool = False, + verbose: bool = False, + linktype: Optional[str] = None, + optimize: Optional[str] = None, + extra_args: Optional[List[str]] = None, +) -> Dict: + """ + Generates a Makefile from a Makefile.PL script using ExtUtils::MakeMaker. + + This tool simulates the 'perl Makefile.PL' command, which is the first step + in the standard Perl module installation process. It allows specifying + common installation and build parameters. + + Args: + project_dir: The directory containing the Makefile.PL script. + makefile_pl: The name of the Perl Makefile script to execute. + output_makefile: The expected name of the generated Makefile. + prefix: Corresponds to PREFIX. Top-level installation directory. + lib: Corresponds to LIB. Directory for architecture-independent files. + install_base: Corresponds to INSTALL_BASE. A simpler way to specify a custom installation location. + destdir: Corresponds to DESTDIR. For staging installations. + installdirs: Corresponds to INSTALLDIRS. Installation scheme ('site', 'vendor', or 'perl'). + uninst: Corresponds to UNINST=1. If true, create an uninstall target. + verbose: Corresponds to VERBOSE=1. If true, enables verbose output. + linktype: Corresponds to LINKTYPE. Link type ('static' or 'dynamic'). + optimize: Corresponds to OPTIMIZE. Compiler optimization flags. + extra_args: A list of additional arguments in 'KEY=VALUE' format. + + Returns: + A dictionary containing the command executed, stdout, stderr, return code, and a list of output files. + """ + # 1. Input Validation + if not project_dir.is_dir(): + raise ValueError(f"Project directory not found: {project_dir}") + + makefile_pl_path = project_dir / makefile_pl + if not makefile_pl_path.is_file(): + raise FileNotFoundError(f"Makefile.PL script not found: {makefile_pl_path}") + + if installdirs and installdirs not in ["site", "vendor", "perl"]: + raise ValueError(f"Invalid value for installdirs: '{installdirs}'. Must be one of 'site', 'vendor', 'perl'.") + + if linktype and linktype not in ["static", "dynamic"]: + raise ValueError(f"Invalid value for linktype: '{linktype}'. Must be one of 'static' or 'dynamic'.") + + # 2. Command Construction + cmd = ["perl", makefile_pl] + + if prefix: + cmd.append(f"PREFIX={prefix.resolve()}") + if lib: + cmd.append(f"LIB={lib.resolve()}") + if install_base: + cmd.append(f"INSTALL_BASE={install_base.resolve()}") + if destdir: + cmd.append(f"DESTDIR={destdir.resolve()}") + if installdirs: + cmd.append(f"INSTALLDIRS={installdirs}") + if uninst: + cmd.append("UNINST=1") + if verbose: + cmd.append("VERBOSE=1") + if linktype: + cmd.append(f"LINKTYPE={linktype}") + if optimize: + cmd.append(f"OPTIMIZE={optimize}") + if extra_args: + for arg in extra_args: + if "=" not in arg or arg.startswith("="): + raise ValueError(f"Invalid extra argument format: '{arg}'. Must be in 'KEY=VALUE' format.") + cmd.extend(extra_args) + + command_executed = " ".join(map(str, cmd)) + logger.info(f"Executing command: {command_executed} in {project_dir}") + + # 3. Subprocess Execution + try: + result = subprocess.run( + cmd, + cwd=project_dir, + capture_output=True, + text=True, + check=True, + ) + except FileNotFoundError: + logger.error("Error: 'perl' command not found. Please ensure Perl is installed and in your PATH.") + raise + except subprocess.CalledProcessError as e: + logger.error(f"Command failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "output_files": [], + } + + # 4. Output Handling + output_files = [] + generated_makefile_path = project_dir / output_makefile + if generated_makefile_path.exists(): + output_files.append(str(generated_makefile_path)) + logger.info(f"Successfully generated Makefile: {generated_makefile_path}") + else: + logger.warning(f"Expected Makefile not found at: {generated_makefile_path}") + + # 5. Structured Result Return + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "return_code": result.returncode, + "output_files": output_files, + } + +@mcp.tool() +def make_build( + project_dir: Path, + makefile: str = "Makefile", + target: str = "all", + jobs: Optional[int] = None, +) -> Dict: + """ + Runs 'make' to build a Perl module after Makefile.PL has been run. + + Args: + project_dir: The directory containing the Makefile. + makefile: The name of the Makefile to use. + target: The make target to build (e.g., 'all', 'clean'). + jobs: The number of parallel jobs to run (corresponds to 'make -j'). + + Returns: + A dictionary containing the command executed, stdout, stderr, and return code. + """ + # 1. Input Validation + if not project_dir.is_dir(): + raise ValueError(f"Project directory not found: {project_dir}") + makefile_path = project_dir / makefile + if not makefile_path.is_file(): + raise FileNotFoundError(f"Makefile not found: {makefile_path}. Did you run 'run_makefile_pl' first?") + if jobs is not None and jobs < 1: + raise ValueError("Number of jobs must be a positive integer.") + + # 2. Command Construction + cmd = ["make", "-f", str(makefile_path.name)] + if jobs: + cmd.append(f"-j{jobs}") + cmd.append(target) + + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed} in {project_dir}") + + # 3. Subprocess Execution + try: + result = subprocess.run( + cmd, + cwd=project_dir, + capture_output=True, + text=True, + check=True, + ) + except FileNotFoundError: + logger.error("Error: 'make' command not found. Please ensure 'make' is installed and in your PATH.") + raise + except subprocess.CalledProcessError as e: + logger.error(f"Make build failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + + # 4. Structured Result Return + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "return_code": result.returncode, + } + +@mcp.tool() +def make_test( + project_dir: Path, + makefile: str = "Makefile", + verbose: bool = False, +) -> Dict: + """ + Runs 'make test' to execute the test suite for a Perl module. + + Args: + project_dir: The directory containing the Makefile. + makefile: The name of the Makefile to use. + verbose: If true, runs tests verbosely (TEST_VERBOSE=1). + + Returns: + A dictionary containing the command executed, stdout, stderr, and return code. + """ + # 1. Input Validation + if not project_dir.is_dir(): + raise ValueError(f"Project directory not found: {project_dir}") + makefile_path = project_dir / makefile + if not makefile_path.is_file(): + raise FileNotFoundError(f"Makefile not found: {makefile_path}. Did you run 'run_makefile_pl' first?") + + # 2. Command Construction + cmd = ["make", "-f", str(makefile_path.name), "test"] + if verbose: + cmd.append("TEST_VERBOSE=1") + + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed} in {project_dir}") + + # 3. Subprocess Execution + try: + result = subprocess.run( + cmd, + cwd=project_dir, + capture_output=True, + text=True, + check=True, + ) + except FileNotFoundError: + logger.error("Error: 'make' command not found. Please ensure 'make' is installed and in your PATH.") + raise + except subprocess.CalledProcessError as e: + logger.error(f"Make test failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + + # 4. Structured Result Return + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "return_code": result.returncode, + } + +@mcp.tool() +def make_install( + project_dir: Path, + makefile: str = "Makefile", +) -> Dict: + """ + Runs 'make install' to install a Perl module. + Note: This may require elevated (e.g., root) privileges depending on the installation prefix. + + Args: + project_dir: The directory containing the Makefile. + makefile: The name of the Makefile to use. + + Returns: + A dictionary containing the command executed, stdout, stderr, and return code. + """ + # 1. Input Validation + if not project_dir.is_dir(): + raise ValueError(f"Project directory not found: {project_dir}") + makefile_path = project_dir / makefile + if not makefile_path.is_file(): + raise FileNotFoundError(f"Makefile not found: {makefile_path}. Did you run 'run_makefile_pl' first?") + + # 2. Command Construction + cmd = ["make", "-f", str(makefile_path.name), "install"] + + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed} in {project_dir}") + + # 3. Subprocess Execution + try: + result = subprocess.run( + cmd, + cwd=project_dir, + capture_output=True, + text=True, + check=True, + ) + except FileNotFoundError: + logger.error("Error: 'make' command not found. Please ensure 'make' is installed and in your PATH.") + raise + except subprocess.CalledProcessError as e: + logger.error(f"Make install failed with exit code {e.returncode}") + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + } + + # 4. Structured Result Return + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "return_code": result.returncode, + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-extutils-makemaker/app/perl-extutils-makemaker_shim_server.py b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/app/perl-extutils-makemaker_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..88dfccf9e0873f78869429fe4db766a48c430d2c --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/app/perl-extutils-makemaker_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-extutils-makemaker/app/perl-extutils-makemaker_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_extutils_makemaker' + + +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-extutils-makemaker/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-extutils-makemaker/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..7a275d0ddcca2df1beff86dfb541f70a55e24939 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-extutils-makemaker: + build: . + image: mcp-perl-extutils-makemaker:latest + container_name: mcp-perl-extutils-makemaker + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-extutils-makemaker + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-extutils-makemaker/environment.yaml b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..73db9ee6c6f84cbea47adee38cd01ad4b43317e9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-extutils-makemaker + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-extutils-makemaker/requirements.txt b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-extutils-makemaker/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-html-tidy/Dockerfile b/Biomni/mcp_generated/mcp_perl-html-tidy/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c26f55dadf2fbbc25a520d36eb64428d030579f0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-html-tidy/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-html-tidy via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-html-tidy -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-html-tidy_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-html-tidy_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-html-tidy_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-html-tidy/app/perl-html-tidy_server.py b/Biomni/mcp_generated/mcp_perl-html-tidy/app/perl-html-tidy_server.py new file mode 100644 index 0000000000000000000000000000000000000000..f2d6acf3895d60b99b3053dd8985566a752b07eb --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-html-tidy/app/perl-html-tidy_server.py @@ -0,0 +1,173 @@ +import subprocess +import tempfile +from pathlib import Path +from typing import List, Optional + +# @mcp.tool() decorator is not needed for the final code as per instructions. +# It is assumed to be present in the execution environment. + +def perl_interpreter( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + execute_program: Optional[List[str]] = None, + execute_program_extended: Optional[List[str]] = None, + record_separator: Optional[str] = None, + autosplit: bool = False, + unicode_features: Optional[str] = None, + check_syntax_only: bool = False, + run_debugger: Optional[str] = None, + debugging_flags: Optional[str] = None, + no_sitecustomize: bool = False, + split_pattern: Optional[str] = None, + in_place_edit: Optional[str] = None, + include_directories: Optional[List[Path]] = None, + line_ending_processing: Optional[str] = None, + modules_use: Optional[List[str]] = None, + modules_use_no_import: Optional[List[str]] = None, + loop_around_program: bool = False, + loop_and_print: bool = False, + parse_switches: bool = False, + search_path: bool = False, + tainting_warnings: bool = False, + tainting_checks: bool = False, + dump_core: bool = False, + allow_unsafe: bool = False, + print_version: bool = False, + print_config: Optional[str] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + disable_all_warnings: bool = False, + ignore_pre_script_text: Optional[str] = None, +) -> dict: + """ + Executes the Perl interpreter with a given script or command. + + This tool is a wrapper for the 'perl' command-line interpreter. + It allows running Perl scripts or one-liners with a wide range of interpreter options. + Note: The original tool request was for 'perl-html-tidy', but the provided + help documentation was for the generic 'perl' interpreter. + """ + # --- Input Validation --- + if not programfile and not execute_program and not execute_program_extended and not print_version and not print_config: + raise ValueError("Either 'programfile', 'execute_program', 'execute_program_extended', 'print_version', or 'print_config' must be provided.") + + if programfile and (execute_program or execute_program_extended): + raise ValueError("'programfile' cannot be used with 'execute_program' or 'execute_program_extended'.") + + if programfile: + if not programfile.is_file(): + raise FileNotFoundError(f"Program file not found: {programfile}") + + if arguments and not programfile: + raise ValueError("'arguments' can only be provided when a 'programfile' is specified.") + + warning_flags = [enable_warnings, enable_all_warnings, disable_all_warnings] + if sum(warning_flags) > 1: + raise ValueError("Only one of 'enable_warnings', 'enable_all_warnings', or 'disable_all_warnings' can be True.") + + # --- Command Construction --- + cmd = ["perl"] + + if record_separator is not None: + cmd.append(f"-0{record_separator}") + if autosplit: + cmd.append("-a") + if unicode_features is not None: + cmd.append(f"-C{unicode_features}") + if check_syntax_only: + cmd.append("-c") + if run_debugger is not None: + cmd.append(f"-d:{run_debugger}" if run_debugger else "-d") + if debugging_flags is not None: + cmd.append(f"-D{debugging_flags}") + if execute_program: + for prog in execute_program: + cmd.extend(["-e", prog]) + if execute_program_extended: + for prog in execute_program_extended: + cmd.extend(["-E", prog]) + if no_sitecustomize: + cmd.append("-f") + if split_pattern is not None: + cmd.append(f"-F{split_pattern}") + if in_place_edit is not None: + cmd.append(f"-i{in_place_edit}") + if include_directories: + for directory in include_directories: + cmd.extend(["-I", str(directory)]) + if line_ending_processing is not None: + cmd.append(f"-l{line_ending_processing}") + if modules_use: + for module in modules_use: + cmd.append(f"-M{module}") + if modules_use_no_import: + for module in modules_use_no_import: + cmd.append(f"-m{module}") + if loop_around_program: + cmd.append("-n") + if loop_and_print: + cmd.append("-p") + if parse_switches: + cmd.append("-s") + if search_path: + cmd.append("-S") + if tainting_warnings: + cmd.append("-t") + if tainting_checks: + cmd.append("-T") + if dump_core: + cmd.append("-u") + if allow_unsafe: + cmd.append("-U") + if print_version: + cmd.append("-v") + if print_config is not None: + cmd.append(f"-V:{print_config}" if print_config else "-V") + if enable_warnings: + cmd.append("-w") + if enable_all_warnings: + cmd.append("-W") + if disable_all_warnings: + cmd.append("-X") + if ignore_pre_script_text is not None: + cmd.append(f"-x{ignore_pre_script_text}" if ignore_pre_script_text else "-x") + + if programfile: + cmd.append(str(programfile)) + + if arguments: + cmd.extend(arguments) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + stdout = result.stdout + stderr = result.stderr + except FileNotFoundError: + return { + "command_executed": command_executed, + "stdout": "", + "stderr": "Error: 'perl' command not found. Please ensure Perl is installed and in the system's PATH.", + "output_files": [] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [] + } + + # --- Structured Result Return --- + return { + "command_executed": command_executed, + "stdout": stdout, + "stderr": stderr, + "output_files": [] + } \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-html-tidy/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-html-tidy/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..e975547ee8d5272952c93b6961bcc1ffe6f04fb0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-html-tidy/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-html-tidy: + build: . + image: mcp-perl-html-tidy:latest + container_name: mcp-perl-html-tidy + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-html-tidy + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-html-tidy/environment.yaml b/Biomni/mcp_generated/mcp_perl-html-tidy/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e2b8e4506f7de60f10933827abe7063919afe1fa --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-html-tidy/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-html-tidy + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-html-tidy/requirements.txt b/Biomni/mcp_generated/mcp_perl-html-tidy/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-html-tidy/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-image-info/Dockerfile b/Biomni/mcp_generated/mcp_perl-image-info/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..01051f3910d24337130099ec8055312365ee7680 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-info/Dockerfile @@ -0,0 +1,40 @@ + +FROM python:3.10-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Miniconda +RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p /opt/conda && rm /tmp/miniconda.sh + +# Add conda to PATH +ENV PATH="/opt/conda/bin:$PATH" + +# Install perl-image-info via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-image-info -y && conda clean -a + +# Install Python dependencies +RUN pip install uv +RUN uv pip install --system fastmcp + +# Create app directory +WORKDIR /app + +# Copy your MCP server +COPY app/perl-image-info_server.py /app/ + +# Create workspace and output directories +RUN mkdir -p /app/workspace /app/output + +# Make sure the server script is executable +RUN chmod +x /app/perl-image-info_server.py + +# Expose port for MCP over HTTP (optional) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)" + +# Default command runs the MCP server via stdio +CMD ["python", "/app/perl-image-info_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-image-info/app/perl-image-info_server.py b/Biomni/mcp_generated/mcp_perl-image-info/app/perl-image-info_server.py new file mode 100644 index 0000000000000000000000000000000000000000..fdf5b76c8c224429338e568dbc34a94b88b7aa12 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-info/app/perl-image-info_server.py @@ -0,0 +1,168 @@ +import subprocess +import tempfile +import json +from pathlib import Path +from typing import Dict, Any, List + +# Note: The execution of these tools requires a Perl environment with the +# Image::Info and JSON modules installed. The conda package `perl-image-info` +# provides Image::Info, and `perl-json` is a standard module usually +# available in modern Perl installations. + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_image_info' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def get_image_info( + image_file: Path, +) -> Dict[str, Any]: + """ + Extracts detailed meta information from an image file using Perl's Image::Info module. + + This function wraps the `image_info` subroutine, returning a dictionary of metadata + including dimensions, color type, resolution, and format-specific information + (e.g., EXIF data for JPEGs). + + Args: + image_file: The path to the input image file. Supported formats include + AVIF, BMP, GIF, ICO, JPEG, PNG, PBM/PGM/PPM, SVG, TIFF, + WBMP, WEBP, XBM, and XPM. + + Returns: + A dictionary containing the command execution details and the extracted image metadata. + """ + if not image_file.is_file(): + raise FileNotFoundError(f"Input file not found: {image_file}") + + perl_script_content = f""" + use strict; + use warnings; + use Image::Info qw(image_info); + use JSON; + + my $file = shift @ARGV; + die "Usage: $0 \\n" unless defined $file; + + my $info = image_info($file); + + # Encode the resulting Perl hash reference to JSON for easy parsing in Python. + print encode_json($info); + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".pl", delete=True) as tmp_script: + tmp_script.write(perl_script_content) + tmp_script.flush() + + cmd = ["perl", tmp_script.name, str(image_file)] + command_executed = " ".join(cmd) + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + encoding='utf-8' + ) + + try: + metadata = json.loads(process.stdout) + except json.JSONDecodeError: + # This case handles if the script runs but outputs non-JSON text + raise ValueError(f"Failed to parse JSON output from Perl script. stdout: {process.stdout}") + + return { + "command_executed": command_executed, + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [], + "image_metadata": metadata + } + + except subprocess.CalledProcessError as e: + # This case handles if the perl script itself fails (e.g., file not found, module missing) + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + "error": f"Perl script execution failed with return code {e.returncode}." + } + +@mcp.tool() +def get_image_type( + image_file: Path, +) -> Dict[str, Any]: + """ + Quickly determines the type of an image file using Perl's Image::Info module. + + This function wraps the `image_type` subroutine, which is a faster alternative + to get_image_info. It only identifies the file type (e.g., 'GIF', 'JPEG', 'PNG') + without parsing the full image structure. + + Args: + image_file: The path to the input image file. + + Returns: + A dictionary containing the command execution details and the determined image type. + """ + if not image_file.is_file(): + raise FileNotFoundError(f"Input file not found: {image_file}") + + perl_script_content = f""" + use strict; + use warnings; + use Image::Info qw(image_type); + use JSON; + + my $file = shift @ARGV; + die "Usage: $0 \\n" unless defined $file; + + my $type_info = image_type($file); + + # Encode the resulting Perl hash reference to JSON for easy parsing in Python. + print encode_json($type_info); + """ + + with tempfile.NamedTemporaryFile(mode='w', suffix=".pl", delete=True) as tmp_script: + tmp_script.write(perl_script_content) + tmp_script.flush() + + cmd = ["perl", tmp_script.name, str(image_file)] + command_executed = " ".join(cmd) + + try: + process = subprocess.run( + cmd, + check=True, + capture_output=True, + text=True, + encoding='utf-8' + ) + + try: + type_info = json.loads(process.stdout) + except json.JSONDecodeError: + raise ValueError(f"Failed to parse JSON output from Perl script. stdout: {process.stdout}") + + return { + "command_executed": command_executed, + "stdout": process.stdout, + "stderr": process.stderr, + "output_files": [], + "image_type_info": type_info + } + + except subprocess.CalledProcessError as e: + return { + "command_executed": command_executed, + "stdout": e.stdout, + "stderr": e.stderr, + "output_files": [], + "error": f"Perl script execution failed with return code {e.returncode}." + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-image-info/app/perl-image-info_shim_server.py b/Biomni/mcp_generated/mcp_perl-image-info/app/perl-image-info_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..01d376efa8606346a0f31fa36d7d78d3c23c095b --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-info/app/perl-image-info_shim_server.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + + +SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_perl-image-info/app/perl-image-info_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_image_info' + + +class _ShimMCP: + @staticmethod + def tool(*args, **kwargs): + if args and callable(args[0]) and len(args) == 1 and not kwargs: + return args[0] + def _decorator(fn): + return fn + return _decorator + + +def _resolve_source_server(): + if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name: + return LOCAL_SERVER + return SOURCE_SERVER + + +def _load_functions(): + source_server = _resolve_source_server() + code = source_server.read_text(encoding="utf-8") + tree = ast.parse(code, filename=str(source_server)) + function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")] + namespace = { + "__name__": "__mcp_source__", + "mcp": _ShimMCP(), + } + exec(compile(code, str(source_server), "exec"), namespace, namespace) + loaded = [] + for name in function_names: + fn = namespace.get(name) + if callable(fn): + loaded.append(fn) + return loaded + + +mcp = FastMCP(SERVER_NAME) +for _fn in _load_functions(): + mcp.tool()(_fn) + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-image-info/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-image-info/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-info/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-image-info/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-image-info/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..8fcc40e647a8a607d4a141d9f946b8a37b541da2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-info/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-image-info: + build: . + image: mcp-perl-image-info:latest + container_name: mcp-perl-image-info + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-image-info + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-image-info/environment.yaml b/Biomni/mcp_generated/mcp_perl-image-info/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a62f79b1dba34d53737252beba75f1a48533dda9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-info/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-image-info + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-image-info/requirements.txt b/Biomni/mcp_generated/mcp_perl-image-info/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-image-info/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-libxml-perl/Dockerfile b/Biomni/mcp_generated/mcp_perl-libxml-perl/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..08b89036ef919de80b6e4ad1450f549f803d6b21 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-libxml-perl/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-libxml-perl via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-libxml-perl -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-libxml-perl_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-libxml-perl_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-libxml-perl_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-libxml-perl/app/perl-libxml-perl_server.py b/Biomni/mcp_generated/mcp_perl-libxml-perl/app/perl-libxml-perl_server.py new file mode 100644 index 0000000000000000000000000000000000000000..9e968ffc6f32a600871da03f3d57c1030d4c6c8c --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-libxml-perl/app/perl-libxml-perl_server.py @@ -0,0 +1,188 @@ +import subprocess +from pathlib import Path +from typing import Optional, List +import logging + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_libxml_perl' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def perl_interpreter( + programfile: Optional[Path] = None, + arguments: Optional[List[str]] = None, + execute_program: Optional[List[str]] = None, + execute_program_extended: Optional[List[str]] = None, + check_syntax_only: bool = False, + run_debugger: bool = False, + debugger_module: Optional[str] = None, + debugging_flags: Optional[str] = None, + enable_warnings: bool = False, + enable_all_warnings: bool = False, + disable_all_warnings: bool = False, + taint_warnings: bool = False, + taint_checks: bool = False, + include_dirs: Optional[List[Path]] = None, + use_modules: Optional[List[str]] = None, + use_modules_no_import: Optional[List[str]] = None, + no_modules: Optional[List[str]] = None, + loop_program: bool = False, + loop_and_print: bool = False, + autosplit: bool = False, + split_pattern: Optional[str] = None, + in_place_edit: bool = False, + backup_extension: Optional[str] = None, + line_ending_processing: Optional[str] = None, + record_separator: Optional[str] = None, + parse_switches: bool = False, + search_path: bool = False, + dump_core: bool = False, + allow_unsafe: bool = False, + no_sitecustomize: bool = False, + unicode_features: Optional[str] = None, + extract_script: bool = False, + change_dir_before_extract: Optional[Path] = None, + print_version: bool = False, + print_config: bool = False, + config_variable: Optional[str] = None, +): + """ + Executes scripts using the Perl interpreter. + + This tool is a comprehensive wrapper for the `perl` command-line executable. + It allows for running Perl scripts from files or directly as one-liners, + and provides access to a wide range of Perl's execution switches for + debugging, module loading, and input/output processing. + """ + # --- Input Validation --- + if not any([programfile, execute_program, execute_program_extended, print_version, print_config]): + raise ValueError("Either a program file, a program string (-e or -E), or an informational flag (-v or -V) must be provided.") + + if programfile and not programfile.is_file(): + raise FileNotFoundError(f"Program file not found: {programfile}") + + if loop_program and loop_and_print: + raise ValueError("Cannot use -n (loop_program) and -p (loop_and_print) simultaneously.") + + if enable_all_warnings and disable_all_warnings: + raise ValueError("Cannot use -W (enable_all_warnings) and -X (disable_all_warnings) simultaneously.") + + if backup_extension is not None and not in_place_edit: + raise ValueError("backup_extension (-i) requires in_place_edit (-i) to be True.") + + if config_variable and not print_config: + raise ValueError("config_variable (-V:variable) requires print_config (-V) to be True.") + + if debugger_module and not run_debugger: + raise ValueError("debugger_module (-d:debugger) requires run_debugger (-d) to be True.") + + if change_dir_before_extract and not extract_script: + raise ValueError("change_dir_before_extract (-x directory) requires extract_script (-x) to be True.") + + # --- Command Construction --- + cmd = ["perl"] + + if record_separator is not None: cmd.append(f"-0{record_separator}") + if autosplit: cmd.append("-a") + if unicode_features is not None: cmd.append(f"-C{unicode_features}") + if check_syntax_only: cmd.append("-c") + if run_debugger: + debug_flag = "-d" + if debugger_module: debug_flag += f":{debugger_module}" + cmd.append(debug_flag) + if debugging_flags is not None: cmd.append(f"-D{debugging_flags}") + if execute_program: + for prog in execute_program: cmd.extend(["-e", prog]) + if execute_program_extended: + for prog in execute_program_extended: cmd.extend(["-E", prog]) + if no_sitecustomize: cmd.append("-f") + if split_pattern is not None: cmd.append(f"-F{split_pattern}") + if in_place_edit: + i_flag = "-i" + if backup_extension is not None: i_flag += backup_extension + cmd.append(i_flag) + if include_dirs: + for directory in include_dirs: cmd.extend(["-I", str(directory)]) + if line_ending_processing is not None: cmd.append(f"-l{line_ending_processing}") + if use_modules: + for module in use_modules: cmd.append(f"-M{module}") + if use_modules_no_import: + for module in use_modules_no_import: cmd.append(f"-m{module}") + if no_modules: + for module in no_modules: cmd.append(f"-M-{module}") + if loop_program: cmd.append("-n") + if loop_and_print: cmd.append("-p") + if parse_switches: cmd.append("-s") + if search_path: cmd.append("-S") + if taint_warnings: cmd.append("-t") + if taint_checks: cmd.append("-T") + if dump_core: cmd.append("-u") + if allow_unsafe: cmd.append("-U") + if print_version: cmd.append("-v") + if print_config: + v_flag = "-V" + if config_variable: v_flag += f":{config_variable}" + cmd.append(v_flag) + if enable_warnings: cmd.append("-w") + if enable_all_warnings: cmd.append("-W") + if extract_script: + x_flag = "-x" + if change_dir_before_extract: x_flag += str(change_dir_before_extract) + cmd.append(x_flag) + if disable_all_warnings: cmd.append("-X") + + if programfile: + cmd.append(str(programfile)) + if arguments: + cmd.extend(arguments) + + # --- Subprocess Execution --- + command_executed = " ".join(cmd) + logger.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, # Set to False to handle non-zero exit codes manually + ) + + if result.returncode != 0: + logger.error(f"Command failed with exit code {result.returncode}") + logger.error(f"Stderr: {result.stderr}") + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "return_code": result.returncode, + "output_files": [] + } + except FileNotFoundError: + stderr_msg = "Error: 'perl' command not found. Please ensure Perl is installed and in the system's PATH." + logger.error(stderr_msg) + return { + "command_executed": command_executed, + "stdout": "", + "stderr": stderr_msg, + "return_code": 1, + "output_files": [] + } + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return { + "command_executed": command_executed, + "stdout": "", + "stderr": str(e), + "return_code": 1, + "output_files": [] + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-libxml-perl/app/perl-libxml-perl_shim_server.py b/Biomni/mcp_generated/mcp_perl-libxml-perl/app/perl-libxml-perl_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..971d69624a8eaf7ecb313adbf79dad1092c43ef9 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-libxml-perl/app/perl-libxml-perl_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-libxml-perl/app/perl-libxml-perl_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_libxml_perl' + + +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-libxml-perl/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-libxml-perl/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-libxml-perl/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-libxml-perl/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-libxml-perl/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..2f296c50f5a1823252191716f4a5a2b092641e02 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-libxml-perl/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-libxml-perl: + build: . + image: mcp-perl-libxml-perl:latest + container_name: mcp-perl-libxml-perl + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-libxml-perl + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-libxml-perl/environment.yaml b/Biomni/mcp_generated/mcp_perl-libxml-perl/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e1c8320a14489a7a62de9bb968e7ae5ab2037e9a --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-libxml-perl/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-libxml-perl + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-libxml-perl/requirements.txt b/Biomni/mcp_generated/mcp_perl-libxml-perl/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-libxml-perl/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-list-moreutils/Dockerfile b/Biomni/mcp_generated/mcp_perl-list-moreutils/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..7fe2222d2c3b01f8391ccda2a0d690d823d472da --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-list-moreutils/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-list-moreutils via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-list-moreutils -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-list-moreutils_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-list-moreutils_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-list-moreutils_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-list-moreutils/app/perl-list-moreutils_server.py b/Biomni/mcp_generated/mcp_perl-list-moreutils/app/perl-list-moreutils_server.py new file mode 100644 index 0000000000000000000000000000000000000000..fbcce2659f127de0ca1c7158849ca1f7302ac9b1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-list-moreutils/app/perl-list-moreutils_server.py @@ -0,0 +1,352 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Union +import tempfile + +def _run_perl_command(script: str, input_data: Optional[str] = None, input_file: Optional[str] = None): + """ + Internal helper to execute Perl one-liners using List::MoreUtils. + """ + cmd = ["perl", "-MList::MoreUtils=:all", "-e", script] + + stdin_handle = None + if input_file: + input_path = Path(input_file) + if not input_path.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + stdin_handle = open(input_path, "r") + + try: + process = subprocess.run( + cmd, + input=input_data if not input_file else None, + stdin=stdin_handle, + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr, + "return_code": process.returncode + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "stdout": e.stdout, + "stderr": e.stderr, + "return_code": e.returncode, + "error": str(e) + } + finally: + if stdin_handle: + stdin_handle.close() + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_list_moreutils' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def list_moreutils_uniq( + input_file: str, + output_file: Optional[str] = None, +): + """ + Returns a list of the elements, in the order they first appear, with all duplicates removed. + + Args: + input_file: Path to the input file (one element per line). + output_file: Optional path to save the unique elements. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + # Perl script to read lines, chomp them, run uniq, and print + script = 'print uniq();' + + result = _run_perl_command(script, input_file=input_file) + + if output_file and "stdout" in result: + out_path = Path(output_file) + out_path.write_text(result["stdout"]) + result["output_files"] = [str(out_path)] + + return result + +@mcp.tool() +def list_moreutils_singleton( + input_file: str, + output_file: Optional[str] = None, +): + """ + Returns a list of the elements which appear exactly once in the input. + + Args: + input_file: Path to the input file (one element per line). + output_file: Optional path to save the singleton elements. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + script = 'print singleton();' + result = _run_perl_command(script, input_file=input_file) + + if output_file and "stdout" in result: + out_path = Path(output_file) + out_path.write_text(result["stdout"]) + result["output_files"] = [str(out_path)] + + return result + +@mcp.tool() +def list_moreutils_mesh( + file1: str, + file2: str, + output_file: Optional[str] = None, +): + """ + Interleaves the elements of two files. Also known as 'zip'. + + Args: + file1: Path to the first input file. + file2: Path to the second input file. + output_file: Optional path to save the meshed output. + """ + p1 = Path(file1) + p2 = Path(file2) + if not p1.exists() or not p2.exists(): + return {"error": "One or both input files do not exist."} + + # Read both files into arrays and mesh them + script = ( + 'open my $f1, "<", $ARGV[0] or die; my @a = <$f1>; ' + 'open my $f2, "<", $ARGV[1] or die; my @b = <$f2>; ' + 'print mesh @a, @b;' + ) + + cmd = ["perl", "-MList::MoreUtils=:all", "-e", script, str(p1), str(p2)] + + try: + process = subprocess.run(cmd, capture_output=True, text=True, check=True) + result = { + "command_executed": " ".join(cmd), + "stdout": process.stdout, + "stderr": process.stderr + } + if output_file: + out_path = Path(output_file) + out_path.write_text(process.stdout) + result["output_files"] = [str(out_path)] + return result + except subprocess.CalledProcessError as e: + return {"error": str(e), "stderr": e.stderr} + +@mcp.tool() +def list_moreutils_minmax( + input_file: str, +): + """ + Calculates the minimum and maximum values in a list of numbers. + + Args: + input_file: Path to a file containing one number per line. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + script = 'my @a = ; my ($min, $max) = minmax @a; print "Min: $min\nMax: $max\n";' + return _run_perl_command(script, input_file=input_file) + +@mcp.tool() +def list_moreutils_junction( + input_file: str, + mode: str = "any", + pattern: str = ".", +): + """ + Performs junction logic (any, all, none, notall) on a list based on a regex pattern. + + Args: + input_file: Path to the input file. + mode: Logic mode to use: 'any', 'all', 'none', or 'notall'. + pattern: Regex pattern to match against each line. + """ + if mode not in ["any", "all", "none", "notall"]: + return {"error": "Mode must be one of: any, all, none, notall"} + + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + # Escape pattern for Perl + safe_pattern = pattern.replace("'", "\\'") + script = f'my @a = ; if ({mode} {{ m/{safe_pattern}/ }} @a) {{ print "True\\n" }} else {{ print "False\\n" }}' + + return _run_perl_command(script, input_file=input_file) + +@mcp.tool() +def list_moreutils_counting( + input_file: str, + mode: str = "true", + pattern: str = ".", +): + """ + Counts how many elements match (true) or do not match (false) a pattern. + + Args: + input_file: Path to the input file. + mode: 'true' to count matches, 'false' to count non-matches. + pattern: Regex pattern to match. + """ + if mode not in ["true", "false"]: + return {"error": "Mode must be 'true' or 'false'"} + + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + safe_pattern = pattern.replace("'", "\\'") + script = f'my @a = ; print {mode} {{ m/{safe_pattern}/ }} @a;' + + return _run_perl_command(script, input_file=input_file) + +@mcp.tool() +def list_moreutils_filtering( + input_file: str, + mode: str = "before", + pattern: str = ".", + include_boundary: bool = False, + output_file: Optional[str] = None, +): + """ + Filters a list based on a boundary pattern. + + Args: + input_file: Path to the input file. + mode: 'before' (elements before match) or 'after' (elements after match). + pattern: Regex pattern defining the boundary. + include_boundary: Whether to include the element that matched the pattern. + output_file: Optional path to save results. + """ + if mode not in ["before", "after"]: + return {"error": "Mode must be 'before' or 'after'"} + + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + func = mode + if include_boundary: + func += "_incl" + + safe_pattern = pattern.replace("'", "\\'") + script = f'my @a = ; print {func} {{ m/{safe_pattern}/ }} @a;' + + result = _run_perl_command(script, input_file=input_file) + + if output_file and "stdout" in result: + out_path = Path(output_file) + out_path.write_text(result["stdout"]) + result["output_files"] = [str(out_path)] + + return result + +@mcp.tool() +def list_moreutils_apply( + input_file: str, + perl_expression: str = "s/\\s+//g", + output_file: Optional[str] = None, +): + """ + Applies a Perl transformation to every element in the list. + + Args: + input_file: Path to the input file. + perl_expression: Perl code to apply to $_ (e.g., 's/foo/bar/' or '$_ = reverse'). + output_file: Optional path to save results. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + # apply { ... } @list returns the modified list + script = f'my @a = ; print apply {{ {perl_expression} }} @a;' + + result = _run_perl_command(script, input_file=input_file) + + if output_file and "stdout" in result: + out_path = Path(output_file) + out_path.write_text(result["stdout"]) + result["output_files"] = [str(out_path)] + + return result + +@mcp.tool() +def list_moreutils_indexes( + input_file: str, + pattern: str = ".", +): + """ + Returns the 0-based indices of all elements matching the pattern. + + Args: + input_file: Path to the input file. + pattern: Regex pattern to match. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + safe_pattern = pattern.replace("'", "\\'") + script = f'my @a = ; print join("\\n", indexes {{ m/{safe_pattern}/ }} @a);' + + return _run_perl_command(script, input_file=input_file) + +@mcp.tool() +def list_moreutils_part( + input_file: str, + perl_condition: str = "$_ =~ /pattern/ ? 0 : 1", + output_prefix: str = "part", +): + """ + Partitions a list into multiple files based on a Perl condition. + The condition should return an integer index (0, 1, 2...). + + Args: + input_file: Path to the input file. + perl_condition: Perl expression returning an index for each element. + output_prefix: Prefix for the generated partition files (e.g., part_0.txt, part_1.txt). + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} does not exist."} + + # This script partitions the list and writes each part to a separate file + script = ( + f'my @a = ; ' + f'my @parts = part {{ {perl_condition} }} @a; ' + f'for my $i (0 .. $#parts) {{ ' + f' if ($parts[$i]) {{ ' + f' open my $fh, ">", "{output_prefix}_$i.txt" or die; ' + f' print $fh @{{$parts[$i]}}; ' + f' close $fh; ' + f' print "{output_prefix}_$i.txt\\n"; ' + f' }} ' + f'}}' + ) + + result = _run_perl_command(script, input_file=input_file) + + if "stdout" in result: + files = result["stdout"].strip().split("\n") + result["output_files"] = [f for f in files if f] + + return result + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-list-moreutils/app/perl-list-moreutils_shim_server.py b/Biomni/mcp_generated/mcp_perl-list-moreutils/app/perl-list-moreutils_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..212d28c4682c20a6b7690cb8b4cc22881673f69f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-list-moreutils/app/perl-list-moreutils_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-list-moreutils/app/perl-list-moreutils_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_list_moreutils' + + +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-list-moreutils/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-list-moreutils/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-list-moreutils/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-list-moreutils/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-list-moreutils/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..810993544ab20ad8894bbd61b484efed9c376fbd --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-list-moreutils/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-list-moreutils: + build: . + image: mcp-perl-list-moreutils:latest + container_name: mcp-perl-list-moreutils + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-list-moreutils + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-list-moreutils/environment.yaml b/Biomni/mcp_generated/mcp_perl-list-moreutils/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ca225013df9e37225c0933287c09b4949b1eac45 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-list-moreutils/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-list-moreutils + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-list-moreutils/requirements.txt b/Biomni/mcp_generated/mcp_perl-list-moreutils/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-list-moreutils/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-module-runtime-conflicts/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-module-runtime-conflicts/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..4821a20f946b707cfb7ad0b5900028ad713249f5 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-module-runtime-conflicts/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-module-runtime-conflicts: + build: . + image: mcp-perl-module-runtime-conflicts:latest + container_name: mcp-perl-module-runtime-conflicts + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-module-runtime-conflicts + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-module-runtime/environment.yaml b/Biomni/mcp_generated/mcp_perl-module-runtime/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9285fdc54765621f56bebc5df4dc01170f124bb3 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-module-runtime/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-module-runtime + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-params-util/Dockerfile b/Biomni/mcp_generated/mcp_perl-params-util/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..38698618f3c5bdb5d9fa879f2b25083281817959 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-params-util/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-params-util via conda (e.g., from bioconda) +RUN conda install -c bioconda perl-params-util -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-params-util_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-params-util_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-params-util_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-params-util/app/perl-params-util_server.py b/Biomni/mcp_generated/mcp_perl-params-util/app/perl-params-util_server.py new file mode 100644 index 0000000000000000000000000000000000000000..2e09e6a1ce5dad75ff70156e11a68a46fb5dc90f --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-params-util/app/perl-params-util_server.py @@ -0,0 +1,752 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Dict, Union, Any + +# Helper function to run Perl commands +def _run_perl_util_command( + perl_path: Path, + function_name: str, # The specific Params::Util function to import + perl_code: str, # The Perl code snippet to execute + additional_imports: Optional[List[str]] = None, # Other modules to import +) -> Dict[str, Any]: + """ + Executes a Perl one-liner using Params::Util and captures output. + """ + if not perl_path.is_file(): + raise FileNotFoundError(f"Perl executable not found at {perl_path}") + + # Construct the Perl module import string + # We always import the specific function_name, and optionally others + imports = [function_name] + if additional_imports: + imports.extend(additional_imports) + + # Ensure unique imports + imports = list(set(imports)) + + # The -M flag expects a comma-separated list of functions for Params::Util=qw(...) + # and a space-separated list for other modules. + # For simplicity, we'll assume Params::Util functions are passed as qw(...) + # and other modules are just 'use Module;' + + module_import_parts = [f"-MParams::Util=qw({' '.join(imports)})"] + if additional_imports: + # Add other modules as separate -M flags or within the -e block + # For simplicity, we'll add them to the -e block for now. + # If they were to be added via -M, it would be like -MScalar::Util + pass # Handled by including 'use Scalar::Util;' in perl_code if needed + + command = [ + str(perl_path), + *module_import_parts, + "-e", + perl_code, + ] + + try: + process = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + stdout = process.stdout.strip() + stderr = process.stderr.strip() + + # Params::Util functions return undef on failure, which we print as 'undef' + # Map 'undef' string output to Python None + result_value = None if stdout == "undef" else stdout + + return { + "command_executed": " ".join(command), + "stdout": stdout, + "stderr": stderr, + "result": result_value, + "output_files": [], + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(command), + "stdout": e.stdout.strip(), + "stderr": e.stderr.strip(), + "error": f"Perl command failed with exit code {e.returncode}", + "result": None, + "output_files": [], + } + except FileNotFoundError: + return { + "command_executed": " ".join(command), + "stdout": "", + "stderr": f"Error: Perl executable not found at {perl_path}", + "error": f"Perl executable not found at {perl_path}", + "result": None, + "output_files": [], + } + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_perl_params_util' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def _string( + value: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests if a value is a normal non-false string of non-zero length using Params::Util::_STRING. + Returns the string if valid, or None otherwise. + """ + if not isinstance(value, str): + raise TypeError("Value must be a string.") + if not value: + # _STRING specifically checks for non-zero length + return { + "command_executed": "", + "stdout": "", + "stderr": "Input value is an empty string, which _STRING will treat as invalid.", + "result": None, + "output_files": [], + } + + perl_code = f"my $result = _STRING('{value}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_STRING", perl_code) + +@mcp.tool() +def _identifier( + value: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests if a value is a string that is a valid Perl identifier using Params::Util::_IDENTIFIER. + Returns the string if valid, or None otherwise. + """ + if not isinstance(value, str): + raise TypeError("Value must be a string.") + + perl_code = f"my $result = _IDENTIFIER('{value}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_IDENTIFIER", perl_code) + +@mcp.tool() +def _class( + value: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests if a value is a string that is a valid Perl class name using Params::Util::_CLASS. + Checks format only, not if the class is loaded. Assumes "normalized" form. + Returns the string if valid, or None otherwise. + """ + if not isinstance(value, str): + raise TypeError("Value must be a string.") + + perl_code = f"my $result = _CLASS('{value}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_CLASS", perl_code) + +@mcp.tool() +def _classisa( + value: str, + class_name: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests if a value is a string that is a particular class, or a subclass of it, + using Params::Util::_CLASSISA. Calls the ->isa method on the class name. + Returns the string if valid, or None otherwise. + """ + if not isinstance(value, str) or not isinstance(class_name, str): + raise TypeError("Value and class_name must be strings.") + + perl_code = f"my $result = _CLASSISA('{value}', '{class_name}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_CLASSISA", perl_code) + +@mcp.tool() +def _classdoes( + value: str, + role_name: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests if a value is a string that is a particular class, or a class that DOES a role, + using Params::Util::_CLASSDOES. Calls the ->DOES method on the class name. + Recommended for Perl 5.10 or later. + Returns the string if valid, or None otherwise. + """ + if not isinstance(value, str) or not isinstance(role_name, str): + raise TypeError("Value and role_name must be strings.") + + perl_code = f"my $result = _CLASSDOES('{value}', '{role_name}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_CLASSDOES", perl_code) + +@mcp.tool() +def _subclass( + value: str, + class_name: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests if a value is a string that is a subclass of a specified class, + using Params::Util::_SUBCLASS. Calls the ->isa method on the class name. + Returns the string if valid, or None otherwise. + """ + if not isinstance(value, str) or not isinstance(class_name, str): + raise TypeError("Value and class_name must be strings.") + + perl_code = f"my $result = _SUBCLASS('{value}', '{class_name}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_SUBCLASS", perl_code) + +@mcp.tool() +def _number( + value: Union[str, int, float], + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests if a value is a number (defined and Perl thinks it's a number) + using Params::Util::_NUMBER. + Returns the value if valid, or None otherwise. + """ + # Perl's looks_like_number is quite flexible, so we pass the string representation + perl_value = str(value) + perl_code = f"use Scalar::Util qw(looks_like_number); my $result = _NUMBER('{perl_value}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_NUMBER", perl_code, additional_imports=["Scalar::Util"]) + +@mcp.tool() +def _posint( + value: Union[str, int], + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests if a value is a positive integer using Params::Util::_POSINT. + Returns the value if valid, or None otherwise. + """ + perl_value = str(value) + perl_code = f"my $result = _POSINT('{perl_value}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_POSINT", perl_code) + +@mcp.tool() +def _nonnegint( + value: Union[str, int], + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests if a value is a non-negative integer (positive integer or zero) + using Params::Util::_NONNEGINT. + Returns the value if valid, or None otherwise. + """ + perl_value = str(value) + perl_code = f"my $result = _NONNEGINT('{perl_value}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_NONNEGINT", perl_code) + +@mcp.tool() +def _scalar( + value_content: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a raw and unblessed SCALAR reference with non-zero length content + using Params::Util::_SCALAR. + The Python 'value_content' is used to create a scalar reference in Perl. + Returns the dereferenced scalar content if valid, or None otherwise. + """ + # Perl code creates a scalar variable, takes a reference to it, and passes the reference. + # Then it prints the dereferenced value if valid. + perl_code = f""" + my $val = '{value_content}'; + my $ref = \\$val; + my $result = _SCALAR($ref); + print defined $result ? $$result : 'undef'; + """ + return _run_perl_util_command(perl_path, "_SCALAR", perl_code) + +@mcp.tool() +def _scalar0( + value_content: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a raw and unblessed SCALAR reference, allowing zero-length content, + using Params::Util::_SCALAR0. + The Python 'value_content' is used to create a scalar reference in Perl. + Returns the dereferenced scalar content if valid, or None otherwise. + """ + perl_code = f""" + my $val = '{value_content}'; + my $ref = \\$val; + my $result = _SCALAR0($ref); + print defined $result ? $$result : 'undef'; + """ + return _run_perl_util_command(perl_path, "_SCALAR0", perl_code) + +@mcp.tool() +def _array( + values: List[str], + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a raw and unblessed ARRAY reference containing at least one element + using Params::Util::_ARRAY. + The Python 'values' list is used to create an array reference in Perl. + Returns a comma-separated string of dereferenced array elements if valid, or None otherwise. + """ + if not isinstance(values, list): + raise TypeError("Values must be a list of strings.") + if not values: + # _ARRAY specifically checks for at least one element + return { + "command_executed": "", + "stdout": "", + "stderr": "Input values list is empty, which _ARRAY will treat as invalid.", + "result": None, + "output_files": [], + } + + perl_array_elements = ', '.join(f"'{v}'" for v in values) + perl_code = f""" + my @arr = ({perl_array_elements}); + my $ref = \\@arr; + my $result = _ARRAY($ref); + print defined $result ? join(',', @$result) : 'undef'; + """ + return _run_perl_util_command(perl_path, "_ARRAY", perl_code) + +@mcp.tool() +def _array0( + values: List[str], + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a raw and unblessed ARRAY reference, allowing zero-length content, + using Params::Util::_ARRAY0. + The Python 'values' list is used to create an array reference in Perl. + Returns a comma-separated string of dereferenced array elements if valid, or None otherwise. + """ + if not isinstance(values, list): + raise TypeError("Values must be a list of strings.") + + perl_array_elements = ', '.join(f"'{v}'" for v in values) + perl_code = f""" + my @arr = ({perl_array_elements}); + my $ref = \\@arr; + my $result = _ARRAY0($ref); + print defined $result ? join(',', @$result) : 'undef'; + """ + return _run_perl_util_command(perl_path, "_ARRAY0", perl_code) + +@mcp.tool() +def _arraylike( + value: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests whether a given scalar value can respond to array dereferencing + using Params::Util::_ARRAYLIKE. + Note: This function is difficult to test generically via `perl -e` with simple strings, + as it typically expects an actual Perl reference or an object that overloads array dereferencing. + Passing a simple string will likely result in 'undef'. + Returns the value if it's array-like, or None otherwise. + """ + if not isinstance(value, str): + raise TypeError("Value must be a string.") + + perl_code = f""" + my $val = '{value}'; + my $result = _ARRAYLIKE($val); + print defined $result ? $result : 'undef'; + """ + return _run_perl_util_command(perl_path, "_ARRAYLIKE", perl_code) + +@mcp.tool() +def _hash( + values: Dict[str, str], + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a raw and unblessed HASH reference with at least one entry + using Params::Util::_HASH. + The Python 'values' dictionary is used to create a hash reference in Perl. + Returns a comma-separated string of key-value pairs (key=>value) if valid, or None otherwise. + """ + if not isinstance(values, dict): + raise TypeError("Values must be a dictionary of strings.") + if not values: + # _HASH specifically checks for at least one entry + return { + "command_executed": "", + "stdout": "", + "stderr": "Input values dictionary is empty, which _HASH will treat as invalid.", + "result": None, + "output_files": [], + } + + perl_hash_elements = ', '.join(f"'{k}' => '{v}'" for k, v in values.items()) + perl_code = f""" + my %hash = ({perl_hash_elements}); + my $ref = \\%hash; + my $result = _HASH($ref); + print defined $result ? join(',', map {{ "$_=>$hash{{$_}}" }} sort keys %$result) : 'undef'; + """ + return _run_perl_util_command(perl_path, "_HASH", perl_code) + +@mcp.tool() +def _hash0( + values: Dict[str, str], + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a raw and unblessed HASH reference, regardless of content, + using Params::Util::_HASH0. + The Python 'values' dictionary is used to create a hash reference in Perl. + Returns a comma-separated string of key-value pairs (key=>value) if valid, or None otherwise. + """ + if not isinstance(values, dict): + raise TypeError("Values must be a dictionary of strings.") + + perl_hash_elements = ', '.join(f"'{k}' => '{v}'" for k, v in values.items()) + perl_code = f""" + my %hash = ({perl_hash_elements}); + my $ref = \\%hash; + my $result = _HASH0($ref); + print defined $result ? join(',', map {{ "$_=>$hash{{$_}}" }} sort keys %$result) : 'undef'; + """ + return _run_perl_util_command(perl_path, "_HASH0", perl_code) + +@mcp.tool() +def _hashlike( + value: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests whether a given scalar value can respond to hash dereferencing + using Params::Util::_HASHLIKE. + Note: This function is difficult to test generically via `perl -e` with simple strings, + as it typically expects an actual Perl reference or an object that overloads hash dereferencing. + Passing a simple string will likely result in 'undef'. + Returns the value if it's hash-like, or None otherwise. + """ + if not isinstance(value, str): + raise TypeError("Value must be a string.") + + perl_code = f""" + my $val = '{value}'; + my $result = _HASHLIKE($val); + print defined $result ? $result : 'undef'; + """ + return _run_perl_util_command(perl_path, "_HASHLIKE", perl_code) + +@mcp.tool() +def _code( + code_string: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a raw and unblessed CODE reference using Params::Util::_CODE. + The 'code_string' is evaluated in Perl to create a code reference. + WARNING: Evaluating arbitrary code from user input can be a security risk. + Returns 'CODE_REF' if a valid code reference is created and validated, or None otherwise. + """ + if not isinstance(code_string, str): + raise TypeError("Code string must be a string.") + + perl_code = f""" + my $code_ref; + eval '$code_ref = sub {{ {code_string} }};'; + if ($@) {{ + print STDERR "Perl eval error: $@"; + print "undef"; + }} else {{ + my $result = _CODE($code_ref); + print defined $result ? 'CODE_REF' : 'undef'; + }} + """ + return _run_perl_util_command(perl_path, "_CODE", perl_code) + +@mcp.tool() +def _codelike( + code_string: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests whether a given scalar value can respond to code dereferencing (like _CODE, + but also includes objects overloading '&{}') using Params::Util::_CODELIKE. + The 'code_string' is evaluated in Perl to create a code reference. + WARNING: Evaluating arbitrary code from user input can be a security risk. + Returns 'CODE_REF' if a valid callable is created and validated, or None otherwise. + """ + if not isinstance(code_string, str): + raise TypeError("Code string must be a string.") + + perl_code = f""" + my $code_ref; + eval '$code_ref = sub {{ {code_string} }};'; + if ($@) {{ + print STDERR "Perl eval error: $@"; + print "undef"; + }} else {{ + my $result = _CODELIKE($code_ref); + print defined $result ? 'CODE_REF' : 'undef'; + }} + """ + return _run_perl_util_command(perl_path, "_CODELIKE", perl_code) + +@mcp.tool() +def _invocant( + value: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests whether the given value is a valid method invocant (object instance or class name) + using Params::Util::_INVOCANT. + Returns the value if it's a valid invocant, or None otherwise. + """ + if not isinstance(value, str): + raise TypeError("Value must be a string.") + + perl_code = f"my $result = _INVOCANT('{value}'); print defined $result ? $result : 'undef';" + return _run_perl_util_command(perl_path, "_INVOCANT", perl_code) + +@mcp.tool() +def _instance( + object_class_name: str, + expected_class: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for an object of a particular class in a strictly correct manner + using Params::Util::_INSTANCE. + The 'object_class_name' is used to attempt to instantiate an object in Perl. + Returns the class name of the object if valid, or None otherwise. + Note: This requires the 'object_class_name' to be a loadable Perl module with a 'new' constructor that takes no arguments. + """ + if not isinstance(object_class_name, str) or not isinstance(expected_class, str): + raise TypeError("Object class name and expected class must be strings.") + + # Attempt to instantiate the object in Perl + perl_code = f""" + my $obj; + eval "use {object_class_name};"; + if ($@) {{ + print STDERR "Perl module load error for {object_class_name}: $@"; + print "undef"; + }} else {{ + eval '$obj = {object_class_name}->new();'; + if ($@) {{ + print STDERR "Perl object instantiation error for {object_class_name}: $@"; + print "undef"; + }} else {{ + my $result = _INSTANCE($obj, '{expected_class}'); + print defined $result ? ref($result) : 'undef'; + }} + }} + """ + return _run_perl_util_command(perl_path, "_INSTANCE", perl_code) + +@mcp.tool() +def _instancedoes( + object_class_name: str, + role_name: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for an object that DOES a particular role, similar to _INSTANCE, + using Params::Util::_INSTANCEDOES. + The 'object_class_name' is used to attempt to instantiate an object in Perl. + Returns the class name of the object if valid, or None otherwise. + Note: This requires the 'object_class_name' to be a loadable Perl module with a 'new' constructor that takes no arguments. + Recommended for Perl 5.10 or later. + """ + if not isinstance(object_class_name, str) or not isinstance(role_name, str): + raise TypeError("Object class name and role name must be strings.") + + perl_code = f""" + my $obj; + eval "use {object_class_name};"; + if ($@) {{ + print STDERR "Perl module load error for {object_class_name}: $@"; + print "undef"; + }} else {{ + eval '$obj = {object_class_name}->new();'; + if ($@) {{ + print STDERR "Perl object instantiation error for {object_class_name}: $@"; + print "undef"; + }} else {{ + my $result = _INSTANCEDOES($obj, '{role_name}'); + print defined $result ? ref($result) : 'undef'; + }} + }} + """ + return _run_perl_util_command(perl_path, "_INSTANCEDOES", perl_code) + +@mcp.tool() +def _regex( + pattern: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a regular expression using Params::Util::_REGEX. + The 'pattern' string is converted to a Perl regex object (qr{}) for testing. + Returns the pattern string if it's a valid regex, or None otherwise. + """ + if not isinstance(pattern, str): + raise TypeError("Pattern must be a string.") + + # Perl code creates a regex object from the pattern string + perl_code = f""" + my $regex_obj; + eval '$regex_obj = qr{{{pattern}}};'; + if ($@) {{ + print STDERR "Perl regex compilation error: $@"; + print "undef"; + }} else {{ + my $result = _REGEX($regex_obj); + print defined $result ? '{pattern}' : 'undef'; # Return the original pattern if valid + }} + """ + return _run_perl_util_command(perl_path, "_REGEX", perl_code) + +@mcp.tool() +def _set( + num_objects: int, + class_name: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a set of at least one object of a particular class using Params::Util::_SET. + 'num_objects' specifies how many objects of 'class_name' to instantiate in Perl. + Returns a comma-separated string of dereferenced object class names if valid, or None otherwise. + Note: This requires 'class_name' to be a loadable Perl module with a 'new' constructor that takes no arguments. + """ + if not isinstance(num_objects, int) or num_objects < 0: + raise ValueError("num_objects must be a non-negative integer.") + if not isinstance(class_name, str): + raise TypeError("class_name must be a string.") + if num_objects == 0: + # _SET specifically checks for at least one object + return { + "command_executed": "", + "stdout": "", + "stderr": "num_objects is 0, which _SET will treat as invalid (requires at least one object).", + "result": None, + "output_files": [], + } + + # Construct Perl code to instantiate objects and create an array reference + perl_object_instantiation = [] + for i in range(num_objects): + perl_object_instantiation.append(f"eval '$obj_{i} = {class_name}->new();' or die $@;") + perl_object_instantiation.append(f"push @arr, $obj_{i};") + + perl_code = f""" + my @arr; + eval "use {class_name};"; + if ($@) {{ + print STDERR "Perl module load error for {class_name}: $@"; + print "undef"; + }} else {{ + {' '.join(perl_object_instantiation)} + my $ref = \\@arr; + my $result = _SET($ref, '{class_name}'); + print defined $result ? join(',', map {{ ref($_) }} @$result) : 'undef'; + }} + """ + return _run_perl_util_command(perl_path, "_SET", perl_code) + +@mcp.tool() +def _set0( + num_objects: int, + class_name: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests for a set of objects of a particular class, allowing zero objects, + using Params::Util::_SET0. + 'num_objects' specifies how many objects of 'class_name' to instantiate in Perl. + Returns a comma-separated string of dereferenced object class names if valid, or None otherwise. + Note: This requires 'class_name' to be a loadable Perl module with a 'new' constructor that takes no arguments. + """ + if not isinstance(num_objects, int) or num_objects < 0: + raise ValueError("num_objects must be a non-negative integer.") + if not isinstance(class_name, str): + raise TypeError("class_name must be a string.") + + # Construct Perl code to instantiate objects and create an array reference + perl_object_instantiation = [] + for i in range(num_objects): + perl_object_instantiation.append(f"eval '$obj_{i} = {class_name}->new();' or die $@;") + perl_object_instantiation.append(f"push @arr, $obj_{i};") + + perl_code = f""" + my @arr; + eval "use {class_name};"; + if ($@) {{ + print STDERR "Perl module load error for {class_name}: $@"; + print "undef"; + }} else {{ + {' '.join(perl_object_instantiation)} + my $ref = \\@arr; + my $result = _SET0($ref, '{class_name}'); + print defined $result ? join(',', map {{ ref($_) }} @$result) : 'undef'; + }} + """ + return _run_perl_util_command(perl_path, "_SET0", perl_code) + +@mcp.tool() +def _handle( + file_path: Optional[Path] = None, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Tests whether a single scalar value is a file handle using Params::Util::_HANDLE. + If 'file_path' is provided, Perl attempts to open it and checks the resulting handle. + If 'file_path' is None, it checks STDIN. + Returns 'HANDLE_OK' if a valid file handle is detected, or None otherwise. + """ + if file_path is not None and not isinstance(file_path, Path): + raise TypeError("file_path must be a Path object or None.") + + if file_path: + # Ensure the file exists if provided + if not file_path.is_file(): + return { + "command_executed": "", + "stdout": "", + "stderr": f"Input file not found at {file_path}", + "result": None, + "output_files": [], + } + perl_code = f""" + open my $fh, '<', '{file_path}' or die "Could not open file {file_path}: $!"; + my $result = _HANDLE($fh); + close $fh; + print defined $result ? 'HANDLE_OK' : 'undef'; + """ + else: + # Check STDIN if no file path is provided + perl_code = """ + my $result = _HANDLE(*STDIN{IO}); + print defined $result ? 'HANDLE_OK' : 'undef'; + """ + return _run_perl_util_command(perl_path, "_HANDLE", perl_code) + +@mcp.tool() +def _driver( + class_name: str, + base_driver_class: str, + perl_path: Path = Path("perl"), +) -> Dict[str, Any]: + """ + Loads and validates a driver class using Params::Util::_DRIVER. + Checks that 'class_name' is a valid class, loads it, and ensures it returns true + for the 'isa' method on 'base_driver_class'. + Returns the class name if valid, or None otherwise. + """ + if not isinstance(class_name, str) or not isinstance(base_driver_class, str): + raise TypeError("Class name and base driver class must be strings.") + + perl_code = f""" + my $result = _DRIVER('{class_name}', '{base_driver_class}'); + print defined $result ? $result : 'undef'; + """ + return _run_perl_util_command(perl_path, "_DRIVER", perl_code) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_perl-params-util/app/perl-params-util_shim_server.py b/Biomni/mcp_generated/mcp_perl-params-util/app/perl-params-util_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..aeebea28108d1e3f773c7511cfff66cba11141c8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-params-util/app/perl-params-util_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-params-util/app/perl-params-util_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_perl_params_util' + + +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-params-util/app/requirements.txt b/Biomni/mcp_generated/mcp_perl-params-util/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-params-util/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_perl-params-util/docker-compose.yml b/Biomni/mcp_generated/mcp_perl-params-util/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..9bbed5b605328a259b48223495990a2c85112ac8 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-params-util/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-perl-params-util: + build: . + image: mcp-perl-params-util:latest + container_name: mcp-perl-params-util + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=perl-params-util + volumes: + - ./workspace:/app/workspace + - ./output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-params-util/environment.yaml b/Biomni/mcp_generated/mcp_perl-params-util/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e3c65947edd91d4c4f1bb24a35720f1218527dbf --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-params-util/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - perl-params-util + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_perl-params-util/requirements.txt b/Biomni/mcp_generated/mcp_perl-params-util/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-params-util/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_perl-set-scalar/requirements.txt b/Biomni/mcp_generated/mcp_perl-set-scalar/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_perl-set-scalar/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_pysftp/Dockerfile b/Biomni/mcp_generated/mcp_pysftp/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..06d2de73bc29cb7fffb060f858d0dd136098a38a --- /dev/null +++ b/Biomni/mcp_generated/mcp_pysftp/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 pysftp via conda (e.g., from bioconda) +RUN conda install -c bioconda pysftp -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/pysftp_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/pysftp_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/pysftp_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_pysftp/app/pysftp_server.py b/Biomni/mcp_generated/mcp_pysftp/app/pysftp_server.py new file mode 100644 index 0000000000000000000000000000000000000000..39d402d0daac23322322ed462c77d38e5e7276cf --- /dev/null +++ b/Biomni/mcp_generated/mcp_pysftp/app/pysftp_server.py @@ -0,0 +1,442 @@ +import subprocess +import os +from pathlib import Path +from typing import Optional, List, Dict, Any + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_pysftp' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def pysftp_get( + host: str, + username: str, + remote_path: str, + local_path: str, + password: Optional[str] = None, + port: int = 22, + private_key: Optional[str] = None, + private_key_pass: Optional[str] = None, + no_host_key_check: bool = False +) -> Dict[str, Any]: + """ + Download a file from a remote SFTP server using pysftp. + + Args: + host: Remote hostname or IP address. + username: SSH username. + remote_path: Path to the file on the remote server. + local_path: Path where the file should be saved locally. + password: SSH password (optional if using private key). + port: SSH port (default 22). + private_key: Path to a private key file (optional). + private_key_pass: Password for the private key (optional). + no_host_key_check: If True, bypasses host key verification (insecure). + """ + # Validate local path directory exists + local_file = Path(local_path) + if not local_file.parent.exists(): + return {"error": f"Local directory {local_file.parent} does not exist."} + + # Construct Python script to execute pysftp logic + # We use a subprocess to ensure the environment's pysftp is used and to handle the connection lifecycle + py_script = f""" +import pysftp +import sys + +cnopts = pysftp.CnOpts() +if {no_host_key_check}: + cnopts.hostkeys = None + +try: + with pysftp.Connection( + host='{host}', + username='{username}', + password={repr(password)}, + port={port}, + private_key={repr(private_key)}, + private_key_pass={repr(private_key_pass)}, + cnopts=cnopts + ) as sftp: + sftp.get('{remote_path}', '{local_path}') + print("Successfully downloaded {remote_path} to {local_path}") +except Exception as e: + print(f"SFTP Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", py_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pysftp.get({remote_path})", + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(local_file.absolute())] + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to download file via SFTP", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def pysftp_put( + host: str, + username: str, + local_path: str, + remote_path: str, + password: Optional[str] = None, + port: int = 22, + private_key: Optional[str] = None, + private_key_pass: Optional[str] = None, + no_host_key_check: bool = False +) -> Dict[str, Any]: + """ + Upload a file to a remote SFTP server using pysftp. + + Args: + host: Remote hostname or IP address. + username: SSH username. + local_path: Path to the local file to upload. + remote_path: Destination path on the remote server. + password: SSH password (optional if using private key). + port: SSH port (default 22). + private_key: Path to a private key file (optional). + private_key_pass: Password for the private key (optional). + no_host_key_check: If True, bypasses host key verification (insecure). + """ + local_file = Path(local_path) + if not local_file.exists(): + return {"error": f"Local file {local_path} does not exist."} + + py_script = f""" +import pysftp +import sys + +cnopts = pysftp.CnOpts() +if {no_host_key_check}: + cnopts.hostkeys = None + +try: + with pysftp.Connection( + host='{host}', + username='{username}', + password={repr(password)}, + port={port}, + private_key={repr(private_key)}, + private_key_pass={repr(private_key_pass)}, + cnopts=cnopts + ) as sftp: + sftp.put('{local_path}', '{remote_path}') + print("Successfully uploaded {local_path} to {remote_path}") +except Exception as e: + print(f"SFTP Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", py_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pysftp.put({local_path})", + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to upload file via SFTP", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def pysftp_listdir( + host: str, + username: str, + remote_path: str = ".", + password: Optional[str] = None, + port: int = 22, + private_key: Optional[str] = None, + private_key_pass: Optional[str] = None, + no_host_key_check: bool = False +) -> Dict[str, Any]: + """ + List the contents of a remote directory using pysftp. + + Args: + host: Remote hostname or IP address. + username: SSH username. + remote_path: Remote directory to list (default is home directory "."). + password: SSH password (optional). + port: SSH port (default 22). + private_key: Path to a private key file (optional). + private_key_pass: Password for the private key (optional). + no_host_key_check: If True, bypasses host key verification. + """ + py_script = f""" +import pysftp +import sys + +cnopts = pysftp.CnOpts() +if {no_host_key_check}: + cnopts.hostkeys = None + +try: + with pysftp.Connection( + host='{host}', + username='{username}', + password={repr(password)}, + port={port}, + private_key={repr(private_key)}, + private_key_pass={repr(private_key_pass)}, + cnopts=cnopts + ) as sftp: + files = sftp.listdir('{remote_path}') + for f in files: + print(f) +except Exception as e: + print(f"SFTP Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", py_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pysftp.listdir({remote_path})", + "stdout": result.stdout, + "stderr": result.stderr, + "file_list": result.stdout.splitlines() + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to list directory via SFTP", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def pysftp_execute( + host: str, + username: str, + command: str, + password: Optional[str] = None, + port: int = 22, + private_key: Optional[str] = None, + private_key_pass: Optional[str] = None, + no_host_key_check: bool = False +) -> Dict[str, Any]: + """ + Execute a shell command on the remote server using pysftp. + + Args: + host: Remote hostname or IP address. + username: SSH username. + command: The shell command to execute. + password: SSH password (optional). + port: SSH port (default 22). + private_key: Path to a private key file (optional). + private_key_pass: Password for the private key (optional). + no_host_key_check: If True, bypasses host key verification. + """ + py_script = f""" +import pysftp +import sys + +cnopts = pysftp.CnOpts() +if {no_host_key_check}: + cnopts.hostkeys = None + +try: + with pysftp.Connection( + host='{host}', + username='{username}', + password={repr(password)}, + port={port}, + private_key={repr(private_key)}, + private_key_pass={repr(private_key_pass)}, + cnopts=cnopts + ) as sftp: + output = sftp.execute('{command}') + for line in output: + print(line.decode('utf-8'), end='') +except Exception as e: + print(f"SFTP Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", py_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pysftp.execute({command})", + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to execute command via SFTP", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def pysftp_mkdir( + host: str, + username: str, + remote_directory: str, + password: Optional[str] = None, + port: int = 22, + private_key: Optional[str] = None, + private_key_pass: Optional[str] = None, + no_host_key_check: bool = False +) -> Dict[str, Any]: + """ + Create a directory on the remote SFTP server. + + Args: + host: Remote hostname or IP address. + username: SSH username. + remote_directory: Path of the directory to create. + password: SSH password (optional). + port: SSH port (default 22). + private_key: Path to a private key file (optional). + private_key_pass: Password for the private key (optional). + no_host_key_check: If True, bypasses host key verification. + """ + py_script = f""" +import pysftp +import sys + +cnopts = pysftp.CnOpts() +if {no_host_key_check}: + cnopts.hostkeys = None + +try: + with pysftp.Connection( + host='{host}', + username='{username}', + password={repr(password)}, + port={port}, + private_key={repr(private_key)}, + private_key_pass={repr(private_key_pass)}, + cnopts=cnopts + ) as sftp: + sftp.mkdir('{remote_directory}') + print("Successfully created directory {remote_directory}") +except Exception as e: + print(f"SFTP Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", py_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pysftp.mkdir({remote_directory})", + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "error": "Failed to create directory via SFTP", + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def pysftp_remove( + host: str, + username: str, + remote_path: str, + is_directory: bool = False, + password: Optional[str] = None, + port: int = 22, + private_key: Optional[str] = None, + private_key_pass: Optional[str] = None, + no_host_key_check: bool = False +) -> Dict[str, Any]: + """ + Remove a file or directory from the remote SFTP server. + + Args: + host: Remote hostname or IP address. + username: SSH username. + remote_path: Path to the file or directory to remove. + is_directory: Set to True if the path is a directory (uses rmdir). + password: SSH password (optional). + port: SSH port (default 22). + private_key: Path to a private key file (optional). + private_key_pass: Password for the private key (optional). + no_host_key_check: If True, bypasses host key verification. + """ + method = "rmdir" if is_directory else "remove" + py_script = f""" +import pysftp +import sys + +cnopts = pysftp.CnOpts() +if {no_host_key_check}: + cnopts.hostkeys = None + +try: + with pysftp.Connection( + host='{host}', + username='{username}', + password={repr(password)}, + port={port}, + private_key={repr(private_key)}, + private_key_pass={repr(private_key_pass)}, + cnopts=cnopts + ) as sftp: + sftp.{method}('{remote_path}') + print("Successfully removed {remote_path}") +except Exception as e: + print(f"SFTP Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) +""" + + try: + result = subprocess.run( + ["python", "-c", py_script], + capture_output=True, + text=True, + check=True + ) + return { + "command_executed": f"pysftp.{method}({remote_path})", + "stdout": result.stdout, + "stderr": result.stderr + } + except subprocess.CalledProcessError as e: + return { + "error": f"Failed to remove {remote_path} via SFTP", + "stdout": e.stdout, + "stderr": e.stderr + } + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_pysftp/app/pysftp_shim_server.py b/Biomni/mcp_generated/mcp_pysftp/app/pysftp_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..ec8f3671f2a4952c7f97bc118dbf5155ef4976d0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pysftp/app/pysftp_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_pysftp/app/pysftp_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_pysftp' + + +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_pysftp/app/requirements.txt b/Biomni/mcp_generated/mcp_pysftp/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_pysftp/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_pysftp/docker-compose.yml b/Biomni/mcp_generated/mcp_pysftp/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..4745e883f4ea3014699cb84b9ce89385933f3da2 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pysftp/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-pysftp: + build: . + image: mcp-pysftp:latest + container_name: mcp-pysftp + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=pysftp + 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_pysftp/environment.yaml b/Biomni/mcp_generated/mcp_pysftp/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..70cd7be7831d779f923ea475db19fb00611b8c22 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pysftp/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - pysftp + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_pysftp/requirements.txt b/Biomni/mcp_generated/mcp_pysftp/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_pysftp/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_sage-proteomics/Dockerfile b/Biomni/mcp_generated/mcp_sage-proteomics/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8128d701bda20d39d79c4e10af88a48b50b7d62e --- /dev/null +++ b/Biomni/mcp_generated/mcp_sage-proteomics/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 sage-proteomics via conda (e.g., from bioconda) +RUN conda install -c bioconda sage-proteomics -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 sage-proteomics_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/sage-proteomics_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/sage-proteomics_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sage-proteomics/docker-compose.yml b/Biomni/mcp_generated/mcp_sage-proteomics/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..6cc0fa47608daa6e5df128751fd6f12c67328714 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sage-proteomics/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-sage-proteomics: + build: . + image: mcp-sage-proteomics:latest + container_name: mcp-sage-proteomics + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=sage-proteomics + 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_sage-proteomics/environment.yaml b/Biomni/mcp_generated/mcp_sage-proteomics/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cdbcb6dde46e0490609022353633074ec531fc92 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sage-proteomics/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - sage-proteomics + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_sage-proteomics/requirements.txt b/Biomni/mcp_generated/mcp_sage-proteomics/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_sage-proteomics/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_scepia/app/scepia_server.py b/Biomni/mcp_generated/mcp_scepia/app/scepia_server.py new file mode 100644 index 0000000000000000000000000000000000000000..fcf06aa8513a46d7b15e44afeaf442ee6b367522 --- /dev/null +++ b/Biomni/mcp_generated/mcp_scepia/app/scepia_server.py @@ -0,0 +1,168 @@ +import subprocess +import logging +from pathlib import Path +from typing import Optional, List, Dict, Any + +# This is a placeholder for the MCP framework's decorator. +# In a real MCP environment, this would be provided by the framework. +class McpToolDecorator: + def __init__(self): + pass + def __call__(self, func): + return func +mcp = type('mcp', (), {'tool': McpToolDecorator()})() + + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_scepia' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def infer_motifs( + input_file: Path, + output_dir: Path, +) -> Dict[str, Any]: + """ + Infers transcription factor motif activity from single cell RNA-seq data. + + This command works on any file that is supported by scanpy.read(). + It is recommended to process your data, including QC, filtering, normalization, + and clustering, using scanpy and saving the results to an .h5ad file. + + Args: + input_file: Path to the input single-cell data file (e.g., .h5ad, .csv). + output_dir: Path to the directory where output files will be saved. + + Returns: + A dictionary containing the command executed, stdout, stderr, and a list + of output files generated. + """ + # Input validation + if not input_file.is_file(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + try: + output_dir.mkdir(parents=True, exist_ok=True) + except OSError as e: + raise OSError(f"Could not create output directory {output_dir}: {e}") + + command = [ + "scepia", + "infer_motifs", + str(input_file), + str(output_dir), + ] + + command_executed = " ".join(command) + logging.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + encoding='utf-8' + ) + + # List all files in the output directory as they are not explicitly named + output_files = [str(p) for p in output_dir.rglob("*") if p.is_file()] + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except FileNotFoundError: + raise RuntimeError("scepia command not found. Please ensure it is installed and in your PATH.") + except subprocess.CalledProcessError as e: + logging.error(f"Command failed with exit code {e.returncode}") + logging.error(f"stdout: {e.stdout}") + logging.error(f"stderr: {e.stderr}") + raise RuntimeError( + f"scepia infer_motifs failed with exit code {e.returncode}.\n" + f"Stderr: {e.stderr}\n" + f"Stdout: {e.stdout}" + ) + +@mcp.tool() +def area27( + bamfile: Path, + outfile: Path, + threads: Optional[int] = None, +) -> Dict[str, Any]: + """ + Calculates the enhancer-based regulatory potential (ERP) score per gene. + + This approach is based on the one developed by Wang et al., 2016, but the + score is calculated based only on H3K27ac signal in enhancers. It uses + log-transformed, z-score normalized H3K27ac read counts in 2kb windows + centered at enhancer locations. + + Args: + bamfile: Path to the input H3K27ac BAM file (mapped to hg38). + outfile: Path for the output file. + threads: Number of threads to use (-N). + + Returns: + A dictionary containing the command executed, stdout, stderr, and the + path to the output file. + """ + # Input validation + if not bamfile.is_file(): + raise FileNotFoundError(f"Input BAM file not found: {bamfile}") + if threads is not None and threads <= 0: + raise ValueError("Number of threads must be a positive integer.") + + # Ensure output directory exists + try: + outfile.parent.mkdir(parents=True, exist_ok=True) + except OSError as e: + raise OSError(f"Could not create output directory {outfile.parent}: {e}") + + command = [ + "scepia", + "area27", + str(bamfile), + str(outfile), + ] + + if threads is not None: + command.extend(["-N", str(threads)]) + + command_executed = " ".join(command) + logging.info(f"Executing command: {command_executed}") + + try: + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + encoding='utf-8' + ) + + output_files = [str(outfile)] if outfile.exists() else [] + + return { + "command_executed": command_executed, + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": output_files, + } + except FileNotFoundError: + raise RuntimeError("scepia command not found. Please ensure it is installed and in your PATH.") + except subprocess.CalledProcessError as e: + logging.error(f"Command failed with exit code {e.returncode}") + logging.error(f"stdout: {e.stdout}") + logging.error(f"stderr: {e.stderr}") + raise RuntimeError( + f"scepia area27 failed with exit code {e.returncode}.\n" + f"Stderr: {e.stderr}\n" + f"Stdout: {e.stdout}" + ) + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/Biomni/mcp_generated/mcp_scrnasim-toolz/environment.yaml b/Biomni/mcp_generated/mcp_scrnasim-toolz/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ab534d34ba18b865b4fbf86a8ab201f4d81fe534 --- /dev/null +++ b/Biomni/mcp_generated/mcp_scrnasim-toolz/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - scrnasim-toolz + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_seqfu/Dockerfile b/Biomni/mcp_generated/mcp_seqfu/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2b7f099b28e9329e97ec4b74e635fbb1c44defa4 --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqfu/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 seqfu via conda (e.g., from bioconda) +RUN conda install -c bioconda seqfu -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/seqfu_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/seqfu_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/seqfu_server.py"] + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_seqfu/app/requirements.txt b/Biomni/mcp_generated/mcp_seqfu/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqfu/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_seqfu/app/seqfu_shim_server.py b/Biomni/mcp_generated/mcp_seqfu/app/seqfu_shim_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6ab4f2f832aa20d3255ae300d1e5233acd291b29 --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqfu/app/seqfu_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_seqfu/app/seqfu_server.py') +LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name) +SERVER_NAME = 'biosci_seqfu' + + +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_seqfu/docker-compose.yml b/Biomni/mcp_generated/mcp_seqfu/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..33764d11705796d29d0d7386dd242b71e0d23cd7 --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqfu/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mcp-seqfu: + build: . + image: mcp-seqfu:latest + container_name: mcp-seqfu + ports: + - "8000:8000" + environment: + - MCP_SERVER_NAME=seqfu + 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_seqfu/environment.yaml b/Biomni/mcp_generated/mcp_seqfu/environment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a2e5fe164ca7d16fa3ca96239598ed8fb717c0d1 --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqfu/environment.yaml @@ -0,0 +1,10 @@ + +name: mcp-tool +channels: + - bioconda + - conda-forge + - defaults +dependencies: + - seqfu + - python=3.10 + \ No newline at end of file diff --git a/Biomni/mcp_generated/mcp_seqfu/requirements.txt b/Biomni/mcp_generated/mcp_seqfu/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4702c254694b94825ae9800a56d8f68b1bcda0 --- /dev/null +++ b/Biomni/mcp_generated/mcp_seqfu/requirements.txt @@ -0,0 +1,2 @@ +fastmcp +mcp diff --git a/Biomni/mcp_generated/mcp_vcftools/app/requirements.txt b/Biomni/mcp_generated/mcp_vcftools/app/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/Biomni/mcp_generated/mcp_vcftools/app/requirements.txt @@ -0,0 +1 @@ + diff --git a/Biomni/mcp_generated/mcp_vcftools/app/vcftools_server.py b/Biomni/mcp_generated/mcp_vcftools/app/vcftools_server.py new file mode 100644 index 0000000000000000000000000000000000000000..07406abfef7c4c8d557132261a5bd738cc395e47 --- /dev/null +++ b/Biomni/mcp_generated/mcp_vcftools/app/vcftools_server.py @@ -0,0 +1,422 @@ +import subprocess +from pathlib import Path +from typing import Optional, List, Union +import tempfile + +def _get_input_flag(input_path: str) -> str: + """Helper to determine the correct input flag based on file extension.""" + path = Path(input_path) + if path.suffix == ".gz": + if ".vcf" in path.name: + return "--gzvcf" + if path.suffix == ".bcf": + return "--bcf" + return "--vcf" + +from mcp.server.fastmcp import FastMCP + +SERVER_NAME = 'local_vcftools' +mcp = FastMCP(SERVER_NAME) + +@mcp.tool() +def vcftools_filter_and_recode( + input_file: str, + output_prefix: str, + chromosome: Optional[str] = None, + from_bp: Optional[int] = None, + to_bp: Optional[int] = None, + min_q: float = 0.0, + min_maf: float = 0.0, + max_maf: float = 1.0, + max_missing: float = 0.0, + min_mean_dp: float = 0.0, + max_mean_dp: float = 999999.0, + min_alleles: int = 0, + max_alleles: int = 100, + remove_indels: bool = False, + recode: bool = True, + recode_info_all: bool = False, +) -> dict: + """ + Filter a VCF file based on various criteria and optionally output a new VCF file. + + Args: + input_file: Path to the input VCF/VCF.GZ/BCF file. + output_prefix: Prefix for the output files. + chromosome: Only process sites on this chromosome. + from_bp: Lower bound for range filtering (requires chromosome). + to_bp: Upper bound for range filtering (requires chromosome). + min_q: Minimum quality score (Phred scale). + min_maf: Minimum Minor Allele Frequency. + max_maf: Maximum Minor Allele Frequency. + max_missing: Exclude sites on the basis of the proportion of missing data (0 to 1). + min_mean_dp: Minimum average depth for a site. + max_mean_dp: Maximum average depth for a site. + min_alleles: Minimum number of alleles. + max_alleles: Maximum number of alleles. + remove_indels: Exclude all sites that contain an indel. + recode: If True, generate a new VCF file. + recode_info_all: If True, keep all INFO fields in the recoded VCF. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} not found."} + + input_flag = _get_input_flag(input_file) + cmd = ["vcftools", input_flag, input_file, "--out", output_prefix] + + if chromosome: + cmd.extend(["--chr", chromosome]) + if from_bp is not None: + cmd.extend(["--from-bp", str(from_bp)]) + if to_bp is not None: + cmd.extend(["--to-bp", str(to_bp)]) + + if min_q > 0: + cmd.extend(["--minQ", str(min_q)]) + if min_maf > 0: + cmd.extend(["--maf", str(min_maf)]) + if max_maf < 1.0: + cmd.extend(["--max-maf", str(max_maf)]) + if max_missing > 0: + cmd.extend(["--max-missing", str(max_missing)]) + if min_mean_dp > 0: + cmd.extend(["--min-meanDP", str(min_mean_dp)]) + if max_mean_dp < 999999.0: + cmd.extend(["--max-meanDP", str(max_mean_dp)]) + if min_alleles > 0: + cmd.extend(["--min-alleles", str(min_alleles)]) + if max_alleles < 100: + cmd.extend(["--max-alleles", str(max_alleles)]) + if remove_indels: + cmd.append("--remove-indels") + + if recode: + cmd.append("--recode") + if recode_info_all: + cmd.append("--recode-INFO-all") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + # VCFtools usually outputs to files with the prefix, but logs to stderr + output_files = list(Path(".").glob(f"{output_prefix}*")) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(f) for f in output_files] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def vcftools_calculate_statistics( + input_file: str, + output_prefix: str, + stat_type: str, + window_size: int = 0, + window_step: int = 0, +) -> dict: + """ + Calculate various population genetics statistics from a VCF file. + + Args: + input_file: Path to the input VCF/VCF.GZ/BCF file. + output_prefix: Prefix for the output files. + stat_type: Type of statistic to calculate. Options: + 'freq' (allele frequency), + 'counts' (allele counts), + 'depth' (mean depth per individual), + 'site-depth' (depth per site), + 'site-mean-depth' (mean depth per site), + 'site-pi' (nucleotide diversity per site), + 'window-pi' (nucleotide diversity in windows), + 'tajima-d' (Tajima's D), + 'het' (heterozygosity), + 'hardy' (Hardy-Weinberg Equilibrium p-value), + 'missing-indv' (missingness per individual), + 'missing-site' (missingness per site). + window_size: Size of the window for windowed statistics (e.g., window-pi, tajima-d). + window_step: Step size between windows. + """ + input_path = Path(input_file) + if not input_path.exists(): + return {"error": f"Input file {input_file} not found."} + + input_flag = _get_input_flag(input_file) + cmd = ["vcftools", input_flag, input_file, "--out", output_prefix] + + stat_map = { + "freq": "--freq", + "counts": "--counts", + "depth": "--depth", + "site-depth": "--site-depth", + "site-mean-depth": "--site-mean-depth", + "site-pi": "--site-pi", + "window-pi": "--window-pi", + "tajima-d": "--TajimaD", + "het": "--het", + "hardy": "--hardy", + "missing-indv": "--missing-indv", + "missing-site": "--missing-site" + } + + if stat_type not in stat_map: + return {"error": f"Invalid stat_type. Choose from: {list(stat_map.keys())}"} + + if stat_type == "window-pi": + if window_size <= 0: + return {"error": "window_size must be > 0 for window-pi"} + cmd.extend([stat_map[stat_type], str(window_size)]) + if window_step > 0: + cmd.extend(["--window-pi-step", str(window_step)]) + elif stat_type == "tajima-d": + if window_size <= 0: + return {"error": "window_size must be > 0 for tajima-d"} + cmd.extend([stat_map[stat_type], str(window_size)]) + else: + cmd.append(stat_map[stat_type]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = list(Path(".").glob(f"{output_prefix}*")) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(f) for f in output_files] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def vcftools_calculate_fst( + input_file: str, + output_prefix: str, + pop1_file: str, + pop2_file: str, + window_size: int = 0, + window_step: int = 0, +) -> dict: + """ + Calculate Weir and Cockerham Fst between two populations. + + Args: + input_file: Path to the input VCF/VCF.GZ/BCF file. + output_prefix: Prefix for the output files. + pop1_file: Path to a file containing a list of individuals in the first population. + pop2_file: Path to a file containing a list of individuals in the second population. + window_size: Size of the window for windowed Fst calculation. + window_step: Step size between windows. + """ + for f in [input_file, pop1_file, pop2_file]: + if not Path(f).exists(): + return {"error": f"File {f} not found."} + + input_flag = _get_input_flag(input_file) + cmd = [ + "vcftools", input_flag, input_file, + "--weir-fst-pop", pop1_file, + "--weir-fst-pop", pop2_file, + "--out", output_prefix + ] + + if window_size > 0: + cmd.extend(["--fst-window-size", str(window_size)]) + if window_step > 0: + cmd.extend(["--fst-window-step", str(window_step)]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = list(Path(".").glob(f"{output_prefix}*")) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(f) for f in output_files] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def vcftools_calculate_ld( + input_file: str, + output_prefix: str, + ld_type: str = "geno-r2", + ld_window_bp: int = 0, + ld_window_min_r2: float = 0.0, + min_maf: float = 0.0, +) -> dict: + """ + Calculate Linkage Disequilibrium (LD) statistics. + + Args: + input_file: Path to the input VCF/VCF.GZ/BCF file. + output_prefix: Prefix for the output files. + ld_type: Type of LD calculation. Options: 'geno-r2', 'hap-r2', 'geno-chisq'. + ld_window_bp: Maximum distance between SNPs to calculate LD. + ld_window_min_r2: Minimum r2 value to report. + min_maf: Minimum Minor Allele Frequency (recommended for LD). + """ + if not Path(input_file).exists(): + return {"error": f"Input file {input_file} not found."} + + input_flag = _get_input_flag(input_file) + cmd = ["vcftools", input_flag, input_file, "--out", output_prefix] + + ld_map = { + "geno-r2": "--geno-r2", + "hap-r2": "--hap-r2", + "geno-chisq": "--geno-chisq" + } + + if ld_type not in ld_map: + return {"error": f"Invalid ld_type. Choose from: {list(ld_map.keys())}"} + + cmd.append(ld_map[ld_type]) + + if ld_window_bp > 0: + cmd.extend(["--ld-window-bp", str(ld_window_bp)]) + if ld_window_min_r2 > 0: + cmd.extend(["--ld-window-min-r2", str(ld_window_min_r2)]) + if min_maf > 0: + cmd.extend(["--maf", str(min_maf)]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = list(Path(".").glob(f"{output_prefix}*")) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(f) for f in output_files] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def vcftools_convert_format( + input_file: str, + output_prefix: str, + target_format: str, +) -> dict: + """ + Convert VCF files to other formats like PLINK or BEAGLE. + + Args: + input_file: Path to the input VCF/VCF.GZ/BCF file. + output_prefix: Prefix for the output files. + target_format: Target format. Options: 'plink' (PED/MAP), 'plink-tped' (TPED/TFAM), 'beagle', 'impute'. + """ + if not Path(input_file).exists(): + return {"error": f"Input file {input_file} not found."} + + input_flag = _get_input_flag(input_file) + cmd = ["vcftools", input_flag, input_file, "--out", output_prefix] + + format_map = { + "plink": "--plink", + "plink-tped": "--plink-tped", + "beagle": "--beagle", + "impute": "--impute" + } + + if target_format not in format_map: + return {"error": f"Invalid target_format. Choose from: {list(format_map.keys())}"} + + cmd.append(format_map[target_format]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = list(Path(".").glob(f"{output_prefix}*")) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(f) for f in output_files] + } + except subprocess.CalledProcessError as e: + return { + "command_executed": " ".join(cmd), + "error": str(e), + "stdout": e.stdout, + "stderr": e.stderr + } + +@mcp.tool() +def vcftools_subset_samples( + input_file: str, + output_prefix: str, + keep_samples_file: Optional[str] = None, + remove_samples_file: Optional[str] = None, + recode: bool = True, +) -> dict: + """ + Subset a VCF file by keeping or removing specific individuals. + + Args: + input_file: Path to the input VCF/VCF.GZ/BCF file. + output_prefix: Prefix for the output files. + keep_samples_file: Path to a file containing a list of individuals to keep. + remove_samples_file: Path to a file containing a list of individuals to remove. + recode: If True, generate a new VCF file. + """ + if not Path(input_file).exists(): + return {"error": f"Input file {input_file} not found."} + + input_flag = _get_input_flag(input_file) + cmd = ["vcftools", input_flag, input_file, "--out", output_prefix] + + if keep_samples_file: + if not Path(keep_samples_file).exists(): + return {"error": f"Keep file {keep_samples_file} not found."} + cmd.extend(["--keep", keep_samples_file]) + + if remove_samples_file: + if not Path(remove_samples_file).exists(): + return {"error": f"Remove file {remove_samples_file} not found."} + cmd.extend(["--remove", remove_samples_file]) + + if recode: + cmd.append("--recode") + cmd.append("--recode-INFO-all") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + output_files = list(Path(".").glob(f"{output_prefix}*")) + return { + "command_executed": " ".join(cmd), + "stdout": result.stdout, + "stderr": result.stderr, + "output_files": [str(f) for f in output_files] + } + 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")