Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Biomni/mcp_generated/mcp_bamtools/Dockerfile +40 -0
- Biomni/mcp_generated/mcp_bamtools/app/bamtools_server.py +712 -0
- Biomni/mcp_generated/mcp_bamtools/app/bamtools_shim_server.py +55 -0
- Biomni/mcp_generated/mcp_bamtools/docker-compose.yml +22 -0
- Biomni/mcp_generated/mcp_bamtools/environment.yaml +10 -0
- Biomni/mcp_generated/mcp_bamtools/requirements.txt +2 -0
- Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/Dockerfile +40 -0
- Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/docker-compose.yml +22 -0
- Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/environment.yaml +10 -0
- Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/requirements.txt +2 -0
- Biomni/mcp_generated/mcp_bioconductor-cardspa/Dockerfile +40 -0
- Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_server.py +189 -0
- Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_shim_server.py +55 -0
- Biomni/mcp_generated/mcp_bioconductor-cardspa/app/requirements.txt +1 -0
- Biomni/mcp_generated/mcp_bioconductor-cardspa/docker-compose.yml +22 -0
- Biomni/mcp_generated/mcp_bioconductor-cardspa/environment.yaml +10 -0
- Biomni/mcp_generated/mcp_bioconductor-cardspa/requirements.txt +2 -0
- Biomni/mcp_generated/mcp_bioconductor-cellid/Dockerfile +40 -0
- Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_server.py +288 -0
- Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_shim_server.py +55 -0
- Biomni/mcp_generated/mcp_bioconductor-cellid/docker-compose.yml +22 -0
- Biomni/mcp_generated/mcp_bioconductor-cellid/environment.yaml +10 -0
- Biomni/mcp_generated/mcp_bioconductor-cellid/requirements.txt +2 -0
- Biomni/mcp_generated/mcp_bioconductor-ggspavis/Dockerfile +40 -0
- Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_server.py +403 -0
- Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_shim_server.py +55 -0
- Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/requirements.txt +1 -0
- Biomni/mcp_generated/mcp_bioconductor-ggspavis/docker-compose.yml +22 -0
- Biomni/mcp_generated/mcp_bioconductor-ggspavis/environment.yaml +10 -0
- Biomni/mcp_generated/mcp_bioconductor-ggspavis/requirements.txt +2 -0
- Biomni/mcp_generated/mcp_bioconductor-mastr/Dockerfile +40 -0
- Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_server.py +208 -0
- Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_shim_server.py +55 -0
- Biomni/mcp_generated/mcp_bioconductor-mastr/app/requirements.txt +1 -0
- Biomni/mcp_generated/mcp_bioconductor-mastr/requirements.txt +2 -0
- Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/Dockerfile +40 -0
- Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/bioconductor-rhdf5filters_server.py +78 -0
- Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/requirements.txt +1 -0
- Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/docker-compose.yml +22 -0
- Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/environment.yaml +10 -0
- Biomni/mcp_generated/mcp_bioconductor-rsubread/Dockerfile +40 -0
- Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_server.py +322 -0
- Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_shim_server.py +55 -0
- Biomni/mcp_generated/mcp_bioconductor-rsubread/app/requirements.txt +1 -0
- Biomni/mcp_generated/mcp_bioconductor-rtracklayer/app/bioconductor-rtracklayer_server.py +340 -0
- Biomni/mcp_generated/mcp_bioconductor-scdotplot/Dockerfile +40 -0
- Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_server.py +273 -0
- Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_shim_server.py +55 -0
- Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/requirements.txt +1 -0
- Biomni/mcp_generated/mcp_bioconductor-scdotplot/docker-compose.yml +22 -0
Biomni/mcp_generated/mcp_bamtools/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install Miniconda
|
| 8 |
+
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
|
| 9 |
+
|
| 10 |
+
# Add conda to PATH
|
| 11 |
+
ENV PATH="/opt/conda/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Install bamtools via conda (e.g., from bioconda)
|
| 14 |
+
RUN conda install -c bioconda bamtools -y && conda clean -a
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
RUN pip install uv
|
| 18 |
+
RUN uv pip install --system fastmcp
|
| 19 |
+
|
| 20 |
+
# Create app directory
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Copy your MCP server
|
| 24 |
+
COPY bamtools_server.py /app/
|
| 25 |
+
|
| 26 |
+
# Create workspace and output directories
|
| 27 |
+
RUN mkdir -p /app/workspace /app/output
|
| 28 |
+
|
| 29 |
+
# Make sure the server script is executable
|
| 30 |
+
RUN chmod +x /app/bamtools_server.py
|
| 31 |
+
|
| 32 |
+
# Expose port for MCP over HTTP (optional)
|
| 33 |
+
EXPOSE 8000
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
|
| 37 |
+
|
| 38 |
+
# Default command runs the MCP server via stdio
|
| 39 |
+
CMD ["python", "/app/bamtools_server.py"]
|
| 40 |
+
|
Biomni/mcp_generated/mcp_bamtools/app/bamtools_server.py
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import shlex
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
|
| 7 |
+
# In a real MCP environment, the 'mcp' object with its decorators
|
| 8 |
+
# would be provided by the framework. This is a placeholder for standalone validation.
|
| 9 |
+
class _MCP:
|
| 10 |
+
def tool(self, *args, **kwargs):
|
| 11 |
+
def decorator(f):
|
| 12 |
+
return f
|
| 13 |
+
return decorator
|
| 14 |
+
mcp = _MCP()
|
| 15 |
+
|
| 16 |
+
### Bamtools Tool Definitions ###
|
| 17 |
+
|
| 18 |
+
from mcp.server.fastmcp import FastMCP
|
| 19 |
+
|
| 20 |
+
SERVER_NAME = 'local_bamtools'
|
| 21 |
+
mcp = FastMCP(SERVER_NAME)
|
| 22 |
+
|
| 23 |
+
@mcp.tool()
|
| 24 |
+
def convert(
|
| 25 |
+
in_bam: Path,
|
| 26 |
+
format: str,
|
| 27 |
+
out: Optional[Path] = None,
|
| 28 |
+
region: Optional[str] = None,
|
| 29 |
+
) -> dict:
|
| 30 |
+
"""
|
| 31 |
+
Converts a BAM file to various other formats.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
in_bam: The input BAM file.
|
| 35 |
+
format: The output format. Must be one of: bed, fasta, fastq, json, pileup, sam, yaml.
|
| 36 |
+
out: The output filename. If not provided, output is sent to stdout.
|
| 37 |
+
region: Only convert alignments that overlap this region (e.g., "ref:start-end").
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
A dictionary containing the command executed, stdout, stderr, and output files.
|
| 41 |
+
"""
|
| 42 |
+
if not in_bam.is_file():
|
| 43 |
+
raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
|
| 44 |
+
|
| 45 |
+
valid_formats = {"bed", "fasta", "fastq", "json", "pileup", "sam", "yaml"}
|
| 46 |
+
if format not in valid_formats:
|
| 47 |
+
raise ValueError(f"Invalid format '{format}'. Must be one of {valid_formats}")
|
| 48 |
+
|
| 49 |
+
cmd = ["bamtools", "convert", "-in", str(in_bam), "-format", format]
|
| 50 |
+
output_files = []
|
| 51 |
+
|
| 52 |
+
if out:
|
| 53 |
+
cmd.extend(["-out", str(out)])
|
| 54 |
+
output_files.append(str(out))
|
| 55 |
+
if region:
|
| 56 |
+
cmd.extend(["-region", region])
|
| 57 |
+
|
| 58 |
+
command_executed = shlex.join(cmd)
|
| 59 |
+
try:
|
| 60 |
+
result = subprocess.run(
|
| 61 |
+
cmd,
|
| 62 |
+
capture_output=True,
|
| 63 |
+
text=True,
|
| 64 |
+
check=True,
|
| 65 |
+
)
|
| 66 |
+
return {
|
| 67 |
+
"command_executed": command_executed,
|
| 68 |
+
"stdout": result.stdout,
|
| 69 |
+
"stderr": result.stderr,
|
| 70 |
+
"output_files": output_files,
|
| 71 |
+
}
|
| 72 |
+
except subprocess.CalledProcessError as e:
|
| 73 |
+
return {
|
| 74 |
+
"command_executed": command_executed,
|
| 75 |
+
"stdout": e.stdout,
|
| 76 |
+
"stderr": e.stderr,
|
| 77 |
+
"error": f"bamtools convert failed with exit code {e.returncode}",
|
| 78 |
+
"output_files": [],
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
@mcp.tool()
|
| 82 |
+
def count(in_bams: List[Path]) -> dict:
|
| 83 |
+
"""
|
| 84 |
+
Prints the number of alignments in one or more BAM files.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
in_bams: A list of input BAM files.
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
A dictionary containing the command executed, stdout (the count), and stderr.
|
| 91 |
+
"""
|
| 92 |
+
if not in_bams:
|
| 93 |
+
raise ValueError("At least one input BAM file must be provided.")
|
| 94 |
+
|
| 95 |
+
cmd = ["bamtools", "count"]
|
| 96 |
+
for bam_file in in_bams:
|
| 97 |
+
if not bam_file.is_file():
|
| 98 |
+
raise FileNotFoundError(f"Input BAM file not found: {bam_file}")
|
| 99 |
+
cmd.extend(["-in", str(bam_file)])
|
| 100 |
+
|
| 101 |
+
command_executed = shlex.join(cmd)
|
| 102 |
+
try:
|
| 103 |
+
result = subprocess.run(
|
| 104 |
+
cmd,
|
| 105 |
+
capture_output=True,
|
| 106 |
+
text=True,
|
| 107 |
+
check=True,
|
| 108 |
+
)
|
| 109 |
+
return {
|
| 110 |
+
"command_executed": command_executed,
|
| 111 |
+
"stdout": result.stdout,
|
| 112 |
+
"stderr": result.stderr,
|
| 113 |
+
"output_files": [],
|
| 114 |
+
}
|
| 115 |
+
except subprocess.CalledProcessError as e:
|
| 116 |
+
return {
|
| 117 |
+
"command_executed": command_executed,
|
| 118 |
+
"stdout": e.stdout,
|
| 119 |
+
"stderr": e.stderr,
|
| 120 |
+
"error": f"bamtools count failed with exit code {e.returncode}",
|
| 121 |
+
"output_files": [],
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
@mcp.tool()
|
| 125 |
+
def coverage(in_bams: List[Path]) -> dict:
|
| 126 |
+
"""
|
| 127 |
+
Prints coverage statistics from one or more BAM files.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
in_bams: A list of input BAM files.
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
A dictionary containing the command executed, stdout (coverage stats), and stderr.
|
| 134 |
+
"""
|
| 135 |
+
if not in_bams:
|
| 136 |
+
raise ValueError("At least one input BAM file must be provided.")
|
| 137 |
+
|
| 138 |
+
cmd = ["bamtools", "coverage"]
|
| 139 |
+
for bam_file in in_bams:
|
| 140 |
+
if not bam_file.is_file():
|
| 141 |
+
raise FileNotFoundError(f"Input BAM file not found: {bam_file}")
|
| 142 |
+
cmd.extend(["-in", str(bam_file)])
|
| 143 |
+
|
| 144 |
+
command_executed = shlex.join(cmd)
|
| 145 |
+
try:
|
| 146 |
+
result = subprocess.run(
|
| 147 |
+
cmd,
|
| 148 |
+
capture_output=True,
|
| 149 |
+
text=True,
|
| 150 |
+
check=True,
|
| 151 |
+
)
|
| 152 |
+
return {
|
| 153 |
+
"command_executed": command_executed,
|
| 154 |
+
"stdout": result.stdout,
|
| 155 |
+
"stderr": result.stderr,
|
| 156 |
+
"output_files": [],
|
| 157 |
+
}
|
| 158 |
+
except subprocess.CalledProcessError as e:
|
| 159 |
+
return {
|
| 160 |
+
"command_executed": command_executed,
|
| 161 |
+
"stdout": e.stdout,
|
| 162 |
+
"stderr": e.stderr,
|
| 163 |
+
"error": f"bamtools coverage failed with exit code {e.returncode}",
|
| 164 |
+
"output_files": [],
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
@mcp.tool()
|
| 168 |
+
def filter_bam(
|
| 169 |
+
in_bams: List[Path],
|
| 170 |
+
out_bam: Optional[Path] = None,
|
| 171 |
+
region: Optional[str] = None,
|
| 172 |
+
property_filters: Optional[List[str]] = None,
|
| 173 |
+
tag_filters: Optional[List[str]] = None,
|
| 174 |
+
script: Optional[Path] = None,
|
| 175 |
+
list_file: Optional[Path] = None,
|
| 176 |
+
force_compression: bool = False,
|
| 177 |
+
) -> dict:
|
| 178 |
+
"""
|
| 179 |
+
Filters BAM file(s) by user-specified criteria.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
in_bams: The input BAM file(s).
|
| 183 |
+
out_bam: The output BAM file. If not provided, output is sent to stdout.
|
| 184 |
+
region: Only keeps alignments that overlap this region.
|
| 185 |
+
property_filters: List of "TAG:VALUE" strings to filter by BamAlignment property.
|
| 186 |
+
tag_filters: List of "TAG:VALUE" strings to filter by BamAlignment tag.
|
| 187 |
+
script: A file containing a list of filter rules.
|
| 188 |
+
list_file: A file containing a list of read names to be kept.
|
| 189 |
+
force_compression: If True, the output BAM file will be compressed.
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
A dictionary containing the command executed, stdout, stderr, and output files.
|
| 193 |
+
"""
|
| 194 |
+
if not in_bams:
|
| 195 |
+
raise ValueError("At least one input BAM file must be provided.")
|
| 196 |
+
|
| 197 |
+
cmd = ["bamtools", "filter"]
|
| 198 |
+
output_files = []
|
| 199 |
+
|
| 200 |
+
for bam_file in in_bams:
|
| 201 |
+
if not bam_file.is_file():
|
| 202 |
+
raise FileNotFoundError(f"Input BAM file not found: {bam_file}")
|
| 203 |
+
cmd.extend(["-in", str(bam_file)])
|
| 204 |
+
|
| 205 |
+
if out_bam:
|
| 206 |
+
cmd.extend(["-out", str(out_bam)])
|
| 207 |
+
output_files.append(str(out_bam))
|
| 208 |
+
if region:
|
| 209 |
+
cmd.extend(["-region", region])
|
| 210 |
+
if property_filters:
|
| 211 |
+
for p_filter in property_filters:
|
| 212 |
+
cmd.extend(["-property", p_filter])
|
| 213 |
+
if tag_filters:
|
| 214 |
+
for t_filter in tag_filters:
|
| 215 |
+
cmd.extend(["-tag", t_filter])
|
| 216 |
+
if script:
|
| 217 |
+
if not script.is_file():
|
| 218 |
+
raise FileNotFoundError(f"Script file not found: {script}")
|
| 219 |
+
cmd.extend(["-script", str(script)])
|
| 220 |
+
if list_file:
|
| 221 |
+
if not list_file.is_file():
|
| 222 |
+
raise FileNotFoundError(f"List file not found: {list_file}")
|
| 223 |
+
cmd.extend(["-list", str(list_file)])
|
| 224 |
+
if force_compression:
|
| 225 |
+
cmd.append("-forceCompression")
|
| 226 |
+
|
| 227 |
+
command_executed = shlex.join(cmd)
|
| 228 |
+
try:
|
| 229 |
+
result = subprocess.run(
|
| 230 |
+
cmd,
|
| 231 |
+
capture_output=True,
|
| 232 |
+
text=True,
|
| 233 |
+
check=True,
|
| 234 |
+
)
|
| 235 |
+
return {
|
| 236 |
+
"command_executed": command_executed,
|
| 237 |
+
"stdout": result.stdout,
|
| 238 |
+
"stderr": result.stderr,
|
| 239 |
+
"output_files": output_files,
|
| 240 |
+
}
|
| 241 |
+
except subprocess.CalledProcessError as e:
|
| 242 |
+
return {
|
| 243 |
+
"command_executed": command_executed,
|
| 244 |
+
"stdout": e.stdout,
|
| 245 |
+
"stderr": e.stderr,
|
| 246 |
+
"error": f"bamtools filter failed with exit code {e.returncode}",
|
| 247 |
+
"output_files": [],
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
@mcp.tool()
|
| 251 |
+
def header(in_bam: Path) -> dict:
|
| 252 |
+
"""
|
| 253 |
+
Prints the header from a BAM file.
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
in_bam: The input BAM file.
|
| 257 |
+
|
| 258 |
+
Returns:
|
| 259 |
+
A dictionary containing the command executed, stdout (the header), and stderr.
|
| 260 |
+
"""
|
| 261 |
+
if not in_bam.is_file():
|
| 262 |
+
raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
|
| 263 |
+
|
| 264 |
+
cmd = ["bamtools", "header", "-in", str(in_bam)]
|
| 265 |
+
command_executed = shlex.join(cmd)
|
| 266 |
+
try:
|
| 267 |
+
result = subprocess.run(
|
| 268 |
+
cmd,
|
| 269 |
+
capture_output=True,
|
| 270 |
+
text=True,
|
| 271 |
+
check=True,
|
| 272 |
+
)
|
| 273 |
+
return {
|
| 274 |
+
"command_executed": command_executed,
|
| 275 |
+
"stdout": result.stdout,
|
| 276 |
+
"stderr": result.stderr,
|
| 277 |
+
"output_files": [],
|
| 278 |
+
}
|
| 279 |
+
except subprocess.CalledProcessError as e:
|
| 280 |
+
return {
|
| 281 |
+
"command_executed": command_executed,
|
| 282 |
+
"stdout": e.stdout,
|
| 283 |
+
"stderr": e.stderr,
|
| 284 |
+
"error": f"bamtools header failed with exit code {e.returncode}",
|
| 285 |
+
"output_files": [],
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
@mcp.tool()
|
| 289 |
+
def index(in_bam: Path) -> dict:
|
| 290 |
+
"""
|
| 291 |
+
Generates an index for a BAM file. The index file (.bai) is created in the same directory.
|
| 292 |
+
|
| 293 |
+
Args:
|
| 294 |
+
in_bam: The input BAM file to index.
|
| 295 |
+
|
| 296 |
+
Returns:
|
| 297 |
+
A dictionary containing the command executed, stdout, stderr, and the path to the generated index file.
|
| 298 |
+
"""
|
| 299 |
+
if not in_bam.is_file():
|
| 300 |
+
raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
|
| 301 |
+
|
| 302 |
+
cmd = ["bamtools", "index", "-in", str(in_bam)]
|
| 303 |
+
output_index_file = in_bam.with_suffix(in_bam.suffix + '.bai')
|
| 304 |
+
|
| 305 |
+
command_executed = shlex.join(cmd)
|
| 306 |
+
try:
|
| 307 |
+
result = subprocess.run(
|
| 308 |
+
cmd,
|
| 309 |
+
capture_output=True,
|
| 310 |
+
text=True,
|
| 311 |
+
check=True,
|
| 312 |
+
)
|
| 313 |
+
if not output_index_file.is_file():
|
| 314 |
+
raise FileNotFoundError(f"Expected index file was not created: {output_index_file}")
|
| 315 |
+
return {
|
| 316 |
+
"command_executed": command_executed,
|
| 317 |
+
"stdout": result.stdout,
|
| 318 |
+
"stderr": result.stderr,
|
| 319 |
+
"output_files": [str(output_index_file)],
|
| 320 |
+
}
|
| 321 |
+
except subprocess.CalledProcessError as e:
|
| 322 |
+
return {
|
| 323 |
+
"command_executed": command_executed,
|
| 324 |
+
"stdout": e.stdout,
|
| 325 |
+
"stderr": e.stderr,
|
| 326 |
+
"error": f"bamtools index failed with exit code {e.returncode}",
|
| 327 |
+
"output_files": [],
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
@mcp.tool()
|
| 331 |
+
def merge(
|
| 332 |
+
in_bams: List[Path],
|
| 333 |
+
out_bam: Optional[Path] = None,
|
| 334 |
+
region: Optional[str] = None,
|
| 335 |
+
force: bool = False,
|
| 336 |
+
) -> dict:
|
| 337 |
+
"""
|
| 338 |
+
Merges multiple BAM files into a single file.
|
| 339 |
+
|
| 340 |
+
Args:
|
| 341 |
+
in_bams: A list of input BAM files to merge. Must contain at least two files.
|
| 342 |
+
out_bam: The output BAM file. If not provided, output is sent to stdout.
|
| 343 |
+
region: Merges only alignments that overlap this region.
|
| 344 |
+
force: Forces merge, even if headers are not identical. Uses the first BAM file's header.
|
| 345 |
+
|
| 346 |
+
Returns:
|
| 347 |
+
A dictionary containing the command executed, stdout, stderr, and output files.
|
| 348 |
+
"""
|
| 349 |
+
if len(in_bams) < 2:
|
| 350 |
+
raise ValueError("At least two input BAM files must be provided for merging.")
|
| 351 |
+
|
| 352 |
+
cmd = ["bamtools", "merge"]
|
| 353 |
+
output_files = []
|
| 354 |
+
|
| 355 |
+
for bam_file in in_bams:
|
| 356 |
+
if not bam_file.is_file():
|
| 357 |
+
raise FileNotFoundError(f"Input BAM file not found: {bam_file}")
|
| 358 |
+
cmd.extend(["-in", str(bam_file)])
|
| 359 |
+
|
| 360 |
+
if out_bam:
|
| 361 |
+
cmd.extend(["-out", str(out_bam)])
|
| 362 |
+
output_files.append(str(out_bam))
|
| 363 |
+
if region:
|
| 364 |
+
cmd.extend(["-region", region])
|
| 365 |
+
if force:
|
| 366 |
+
cmd.append("-force")
|
| 367 |
+
|
| 368 |
+
command_executed = shlex.join(cmd)
|
| 369 |
+
try:
|
| 370 |
+
result = subprocess.run(
|
| 371 |
+
cmd,
|
| 372 |
+
capture_output=True,
|
| 373 |
+
text=True,
|
| 374 |
+
check=True,
|
| 375 |
+
)
|
| 376 |
+
return {
|
| 377 |
+
"command_executed": command_executed,
|
| 378 |
+
"stdout": result.stdout,
|
| 379 |
+
"stderr": result.stderr,
|
| 380 |
+
"output_files": output_files,
|
| 381 |
+
}
|
| 382 |
+
except subprocess.CalledProcessError as e:
|
| 383 |
+
return {
|
| 384 |
+
"command_executed": command_executed,
|
| 385 |
+
"stdout": e.stdout,
|
| 386 |
+
"stderr": e.stderr,
|
| 387 |
+
"error": f"bamtools merge failed with exit code {e.returncode}",
|
| 388 |
+
"output_files": [],
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
@mcp.tool()
|
| 392 |
+
def random(
|
| 393 |
+
in_bams: List[Path],
|
| 394 |
+
out_bam: Optional[Path] = None,
|
| 395 |
+
n: int = 100,
|
| 396 |
+
seed: int = 0,
|
| 397 |
+
) -> dict:
|
| 398 |
+
"""
|
| 399 |
+
Selects a random number of alignments from existing BAM file(s).
|
| 400 |
+
|
| 401 |
+
Args:
|
| 402 |
+
in_bams: The input BAM file(s).
|
| 403 |
+
out_bam: The output BAM file. If not provided, output is sent to stdout.
|
| 404 |
+
n: Number of alignments to select.
|
| 405 |
+
seed: Random seed.
|
| 406 |
+
|
| 407 |
+
Returns:
|
| 408 |
+
A dictionary containing the command executed, stdout, stderr, and output files.
|
| 409 |
+
"""
|
| 410 |
+
if not in_bams:
|
| 411 |
+
raise ValueError("At least one input BAM file must be provided.")
|
| 412 |
+
|
| 413 |
+
cmd = ["bamtools", "random", "-n", str(n), "-seed", str(seed)]
|
| 414 |
+
output_files = []
|
| 415 |
+
|
| 416 |
+
for bam_file in in_bams:
|
| 417 |
+
if not bam_file.is_file():
|
| 418 |
+
raise FileNotFoundError(f"Input BAM file not found: {bam_file}")
|
| 419 |
+
cmd.extend(["-in", str(bam_file)])
|
| 420 |
+
|
| 421 |
+
if out_bam:
|
| 422 |
+
cmd.extend(["-out", str(out_bam)])
|
| 423 |
+
output_files.append(str(out_bam))
|
| 424 |
+
|
| 425 |
+
command_executed = shlex.join(cmd)
|
| 426 |
+
try:
|
| 427 |
+
result = subprocess.run(
|
| 428 |
+
cmd,
|
| 429 |
+
capture_output=True,
|
| 430 |
+
text=True,
|
| 431 |
+
check=True,
|
| 432 |
+
)
|
| 433 |
+
return {
|
| 434 |
+
"command_executed": command_executed,
|
| 435 |
+
"stdout": result.stdout,
|
| 436 |
+
"stderr": result.stderr,
|
| 437 |
+
"output_files": output_files,
|
| 438 |
+
}
|
| 439 |
+
except subprocess.CalledProcessError as e:
|
| 440 |
+
return {
|
| 441 |
+
"command_executed": command_executed,
|
| 442 |
+
"stdout": e.stdout,
|
| 443 |
+
"stderr": e.stderr,
|
| 444 |
+
"error": f"bamtools random failed with exit code {e.returncode}",
|
| 445 |
+
"output_files": [],
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
@mcp.tool()
|
| 449 |
+
def resolve(in_bam: Path, out_bam: Path) -> dict:
|
| 450 |
+
"""
|
| 451 |
+
Resolves paired-end reads, marking the IsProperPair flag as needed.
|
| 452 |
+
|
| 453 |
+
Args:
|
| 454 |
+
in_bam: The input BAM file.
|
| 455 |
+
out_bam: The output BAM file.
|
| 456 |
+
|
| 457 |
+
Returns:
|
| 458 |
+
A dictionary containing the command executed, stdout, stderr, and the output file path.
|
| 459 |
+
"""
|
| 460 |
+
if not in_bam.is_file():
|
| 461 |
+
raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
|
| 462 |
+
|
| 463 |
+
cmd = ["bamtools", "resolve", "-in", str(in_bam), "-out", str(out_bam)]
|
| 464 |
+
command_executed = shlex.join(cmd)
|
| 465 |
+
try:
|
| 466 |
+
result = subprocess.run(
|
| 467 |
+
cmd,
|
| 468 |
+
capture_output=True,
|
| 469 |
+
text=True,
|
| 470 |
+
check=True,
|
| 471 |
+
)
|
| 472 |
+
return {
|
| 473 |
+
"command_executed": command_executed,
|
| 474 |
+
"stdout": result.stdout,
|
| 475 |
+
"stderr": result.stderr,
|
| 476 |
+
"output_files": [str(out_bam)],
|
| 477 |
+
}
|
| 478 |
+
except subprocess.CalledProcessError as e:
|
| 479 |
+
return {
|
| 480 |
+
"command_executed": command_executed,
|
| 481 |
+
"stdout": e.stdout,
|
| 482 |
+
"stderr": e.stderr,
|
| 483 |
+
"error": f"bamtools resolve failed with exit code {e.returncode}",
|
| 484 |
+
"output_files": [],
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
@mcp.tool()
|
| 488 |
+
def revert(in_bam: Path, out_bam: Path) -> dict:
|
| 489 |
+
"""
|
| 490 |
+
Removes duplicate marks and restores original base qualities from a BAM file.
|
| 491 |
+
|
| 492 |
+
Args:
|
| 493 |
+
in_bam: The input BAM file.
|
| 494 |
+
out_bam: The output BAM file.
|
| 495 |
+
|
| 496 |
+
Returns:
|
| 497 |
+
A dictionary containing the command executed, stdout, stderr, and the output file path.
|
| 498 |
+
"""
|
| 499 |
+
if not in_bam.is_file():
|
| 500 |
+
raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
|
| 501 |
+
|
| 502 |
+
cmd = ["bamtools", "revert", "-in", str(in_bam), "-out", str(out_bam)]
|
| 503 |
+
command_executed = shlex.join(cmd)
|
| 504 |
+
try:
|
| 505 |
+
result = subprocess.run(
|
| 506 |
+
cmd,
|
| 507 |
+
capture_output=True,
|
| 508 |
+
text=True,
|
| 509 |
+
check=True,
|
| 510 |
+
)
|
| 511 |
+
return {
|
| 512 |
+
"command_executed": command_executed,
|
| 513 |
+
"stdout": result.stdout,
|
| 514 |
+
"stderr": result.stderr,
|
| 515 |
+
"output_files": [str(out_bam)],
|
| 516 |
+
}
|
| 517 |
+
except subprocess.CalledProcessError as e:
|
| 518 |
+
return {
|
| 519 |
+
"command_executed": command_executed,
|
| 520 |
+
"stdout": e.stdout,
|
| 521 |
+
"stderr": e.stderr,
|
| 522 |
+
"error": f"bamtools revert failed with exit code {e.returncode}",
|
| 523 |
+
"output_files": [],
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
@mcp.tool()
|
| 527 |
+
def sort(
|
| 528 |
+
in_bam: Path,
|
| 529 |
+
out_bam: Optional[Path] = None,
|
| 530 |
+
by: str = "position",
|
| 531 |
+
order: str = "ascending",
|
| 532 |
+
n: int = 1000,
|
| 533 |
+
mem: int = 512,
|
| 534 |
+
) -> dict:
|
| 535 |
+
"""
|
| 536 |
+
Sorts a BAM file by position or name.
|
| 537 |
+
|
| 538 |
+
Args:
|
| 539 |
+
in_bam: The input BAM file.
|
| 540 |
+
out_bam: The output BAM file. If not provided, output is sent to stdout.
|
| 541 |
+
by: Sort by 'position' or 'name'.
|
| 542 |
+
order: Sort in 'ascending' or 'descending' order.
|
| 543 |
+
n: Max number of alignments to buffer in memory.
|
| 544 |
+
mem: Max memory to use (in megabytes).
|
| 545 |
+
|
| 546 |
+
Returns:
|
| 547 |
+
A dictionary containing the command executed, stdout, stderr, and output files.
|
| 548 |
+
"""
|
| 549 |
+
if not in_bam.is_file():
|
| 550 |
+
raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
|
| 551 |
+
if by not in ["position", "name"]:
|
| 552 |
+
raise ValueError("Sort 'by' must be 'position' or 'name'.")
|
| 553 |
+
if order not in ["ascending", "descending"]:
|
| 554 |
+
raise ValueError("Sort 'order' must be 'ascending' or 'descending'.")
|
| 555 |
+
|
| 556 |
+
cmd = ["bamtools", "sort", "-in", str(in_bam), "-by", by, "-order", order, "-n", str(n), "-mem", str(mem)]
|
| 557 |
+
output_files = []
|
| 558 |
+
|
| 559 |
+
if out_bam:
|
| 560 |
+
cmd.extend(["-out", str(out_bam)])
|
| 561 |
+
output_files.append(str(out_bam))
|
| 562 |
+
|
| 563 |
+
command_executed = shlex.join(cmd)
|
| 564 |
+
try:
|
| 565 |
+
result = subprocess.run(
|
| 566 |
+
cmd,
|
| 567 |
+
capture_output=True,
|
| 568 |
+
text=True,
|
| 569 |
+
check=True,
|
| 570 |
+
)
|
| 571 |
+
return {
|
| 572 |
+
"command_executed": command_executed,
|
| 573 |
+
"stdout": result.stdout,
|
| 574 |
+
"stderr": result.stderr,
|
| 575 |
+
"output_files": output_files,
|
| 576 |
+
}
|
| 577 |
+
except subprocess.CalledProcessError as e:
|
| 578 |
+
return {
|
| 579 |
+
"command_executed": command_executed,
|
| 580 |
+
"stdout": e.stdout,
|
| 581 |
+
"stderr": e.stderr,
|
| 582 |
+
"error": f"bamtools sort failed with exit code {e.returncode}",
|
| 583 |
+
"output_files": [],
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
@mcp.tool()
|
| 587 |
+
def split(
|
| 588 |
+
in_bam: Path,
|
| 589 |
+
output_dir: Path,
|
| 590 |
+
stub: Optional[str] = None,
|
| 591 |
+
by: Optional[str] = None,
|
| 592 |
+
reference: bool = False,
|
| 593 |
+
mapped: bool = False,
|
| 594 |
+
paired: bool = False,
|
| 595 |
+
tag: Optional[str] = None,
|
| 596 |
+
) -> dict:
|
| 597 |
+
"""
|
| 598 |
+
Splits a BAM file based on a specified property, creating multiple output BAM files.
|
| 599 |
+
|
| 600 |
+
Args:
|
| 601 |
+
in_bam: The input BAM file.
|
| 602 |
+
output_dir: The directory where output files will be created.
|
| 603 |
+
stub: The prefix for output file names. Defaults to the input file name.
|
| 604 |
+
by: Split file by a specific property.
|
| 605 |
+
reference: Split by reference sequence.
|
| 606 |
+
mapped: Split into mapped and unmapped files.
|
| 607 |
+
paired: Split into paired and single-end files.
|
| 608 |
+
tag: Split by the value of a specific tag.
|
| 609 |
+
|
| 610 |
+
Returns:
|
| 611 |
+
A dictionary containing the command executed, stdout, stderr, and a list of created files.
|
| 612 |
+
"""
|
| 613 |
+
if not in_bam.is_file():
|
| 614 |
+
raise FileNotFoundError(f"Input BAM file not found: {in_bam}")
|
| 615 |
+
|
| 616 |
+
split_options = [by is not None, reference, mapped, paired, tag is not None]
|
| 617 |
+
if sum(split_options) != 1:
|
| 618 |
+
raise ValueError("Exactly one split criterion (by, reference, mapped, paired, or tag) must be specified.")
|
| 619 |
+
|
| 620 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 621 |
+
|
| 622 |
+
cmd = ["bamtools", "split", "-in", str(in_bam)]
|
| 623 |
+
|
| 624 |
+
# Use the stub to direct output to the specified directory
|
| 625 |
+
output_stub = stub if stub else in_bam.stem
|
| 626 |
+
cmd.extend(["-stub", str(output_dir / output_stub)])
|
| 627 |
+
|
| 628 |
+
if by:
|
| 629 |
+
cmd.extend(["-by", by])
|
| 630 |
+
if reference:
|
| 631 |
+
cmd.append("-reference")
|
| 632 |
+
if mapped:
|
| 633 |
+
cmd.append("-mapped")
|
| 634 |
+
if paired:
|
| 635 |
+
cmd.append("-paired")
|
| 636 |
+
if tag:
|
| 637 |
+
cmd.extend(["-tag", tag])
|
| 638 |
+
|
| 639 |
+
command_executed = shlex.join(cmd)
|
| 640 |
+
try:
|
| 641 |
+
result = subprocess.run(
|
| 642 |
+
cmd,
|
| 643 |
+
capture_output=True,
|
| 644 |
+
text=True,
|
| 645 |
+
check=True,
|
| 646 |
+
)
|
| 647 |
+
# Find all files created in the output directory
|
| 648 |
+
created_files = [str(p) for p in output_dir.glob(f"{output_stub}*")]
|
| 649 |
+
return {
|
| 650 |
+
"command_executed": command_executed,
|
| 651 |
+
"stdout": result.stdout,
|
| 652 |
+
"stderr": result.stderr,
|
| 653 |
+
"output_files": created_files,
|
| 654 |
+
}
|
| 655 |
+
except subprocess.CalledProcessError as e:
|
| 656 |
+
return {
|
| 657 |
+
"command_executed": command_executed,
|
| 658 |
+
"stdout": e.stdout,
|
| 659 |
+
"stderr": e.stderr,
|
| 660 |
+
"error": f"bamtools split failed with exit code {e.returncode}",
|
| 661 |
+
"output_files": [],
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
@mcp.tool()
|
| 665 |
+
def stats(in_bams: List[Path], insert: bool = False) -> dict:
|
| 666 |
+
"""
|
| 667 |
+
Prints basic statistics from one or more BAM files.
|
| 668 |
+
|
| 669 |
+
Args:
|
| 670 |
+
in_bams: A list of input BAM files.
|
| 671 |
+
insert: If True, print insert size summary.
|
| 672 |
+
|
| 673 |
+
Returns:
|
| 674 |
+
A dictionary containing the command executed, stdout (the stats), and stderr.
|
| 675 |
+
"""
|
| 676 |
+
if not in_bams:
|
| 677 |
+
raise ValueError("At least one input BAM file must be provided.")
|
| 678 |
+
|
| 679 |
+
cmd = ["bamtools", "stats"]
|
| 680 |
+
for bam_file in in_bams:
|
| 681 |
+
if not bam_file.is_file():
|
| 682 |
+
raise FileNotFoundError(f"Input BAM file not found: {bam_file}")
|
| 683 |
+
cmd.extend(["-in", str(bam_file)])
|
| 684 |
+
|
| 685 |
+
if insert:
|
| 686 |
+
cmd.append("-insert")
|
| 687 |
+
|
| 688 |
+
command_executed = shlex.join(cmd)
|
| 689 |
+
try:
|
| 690 |
+
result = subprocess.run(
|
| 691 |
+
cmd,
|
| 692 |
+
capture_output=True,
|
| 693 |
+
text=True,
|
| 694 |
+
check=True,
|
| 695 |
+
)
|
| 696 |
+
return {
|
| 697 |
+
"command_executed": command_executed,
|
| 698 |
+
"stdout": result.stdout,
|
| 699 |
+
"stderr": result.stderr,
|
| 700 |
+
"output_files": [],
|
| 701 |
+
}
|
| 702 |
+
except subprocess.CalledProcessError as e:
|
| 703 |
+
return {
|
| 704 |
+
"command_executed": command_executed,
|
| 705 |
+
"stdout": e.stdout,
|
| 706 |
+
"stderr": e.stderr,
|
| 707 |
+
"error": f"bamtools stats failed with exit code {e.returncode}",
|
| 708 |
+
"output_files": [],
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
if __name__ == "__main__":
|
| 712 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bamtools/app/bamtools_shim_server.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import ast
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from mcp.server.fastmcp import FastMCP
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_help_txt/mcp_bamtools/app/bamtools_server.py')
|
| 11 |
+
LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name)
|
| 12 |
+
SERVER_NAME = 'biosci_bamtools'
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class _ShimMCP:
|
| 16 |
+
@staticmethod
|
| 17 |
+
def tool(*args, **kwargs):
|
| 18 |
+
if args and callable(args[0]) and len(args) == 1 and not kwargs:
|
| 19 |
+
return args[0]
|
| 20 |
+
def _decorator(fn):
|
| 21 |
+
return fn
|
| 22 |
+
return _decorator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _resolve_source_server():
|
| 26 |
+
if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name:
|
| 27 |
+
return LOCAL_SERVER
|
| 28 |
+
return SOURCE_SERVER
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _load_functions():
|
| 32 |
+
source_server = _resolve_source_server()
|
| 33 |
+
code = source_server.read_text(encoding="utf-8")
|
| 34 |
+
tree = ast.parse(code, filename=str(source_server))
|
| 35 |
+
function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
|
| 36 |
+
namespace = {
|
| 37 |
+
"__name__": "__mcp_source__",
|
| 38 |
+
"mcp": _ShimMCP(),
|
| 39 |
+
}
|
| 40 |
+
exec(compile(code, str(source_server), "exec"), namespace, namespace)
|
| 41 |
+
loaded = []
|
| 42 |
+
for name in function_names:
|
| 43 |
+
fn = namespace.get(name)
|
| 44 |
+
if callable(fn):
|
| 45 |
+
loaded.append(fn)
|
| 46 |
+
return loaded
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
mcp = FastMCP(SERVER_NAME)
|
| 50 |
+
for _fn in _load_functions():
|
| 51 |
+
mcp.tool()(_fn)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bamtools/docker-compose.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
mcp-bamtools:
|
| 5 |
+
build: .
|
| 6 |
+
image: mcp-bamtools:latest
|
| 7 |
+
container_name: mcp-bamtools
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
environment:
|
| 11 |
+
- MCP_SERVER_NAME=bamtools
|
| 12 |
+
volumes:
|
| 13 |
+
- ./workspace:/app/workspace
|
| 14 |
+
- ./output:/app/output
|
| 15 |
+
restart: unless-stopped
|
| 16 |
+
healthcheck:
|
| 17 |
+
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
| 18 |
+
interval: 30s
|
| 19 |
+
timeout: 10s
|
| 20 |
+
retries: 3
|
| 21 |
+
start_period: 5s
|
| 22 |
+
|
Biomni/mcp_generated/mcp_bamtools/environment.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
name: mcp-tool
|
| 3 |
+
channels:
|
| 4 |
+
- bioconda
|
| 5 |
+
- conda-forge
|
| 6 |
+
- defaults
|
| 7 |
+
dependencies:
|
| 8 |
+
- bamtools
|
| 9 |
+
- python=3.10
|
| 10 |
+
|
Biomni/mcp_generated/mcp_bamtools/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
mcp
|
Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install Miniconda
|
| 8 |
+
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
|
| 9 |
+
|
| 10 |
+
# Add conda to PATH
|
| 11 |
+
ENV PATH="/opt/conda/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Install bioconductor-alabaster.spatial via conda (e.g., from bioconda)
|
| 14 |
+
RUN conda install -c bioconda bioconductor-alabaster.spatial -y && conda clean -a
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
RUN pip install uv
|
| 18 |
+
RUN uv pip install --system fastmcp
|
| 19 |
+
|
| 20 |
+
# Create app directory
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Copy your MCP server
|
| 24 |
+
COPY app/bioconductor-alabaster.spatial_server.py /app/
|
| 25 |
+
|
| 26 |
+
# Create workspace and output directories
|
| 27 |
+
RUN mkdir -p /app/workspace /app/output
|
| 28 |
+
|
| 29 |
+
# Make sure the server script is executable
|
| 30 |
+
RUN chmod +x /app/bioconductor-alabaster.spatial_server.py
|
| 31 |
+
|
| 32 |
+
# Expose port for MCP over HTTP (optional)
|
| 33 |
+
EXPOSE 8000
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
|
| 37 |
+
|
| 38 |
+
# Default command runs the MCP server via stdio
|
| 39 |
+
CMD ["python", "/app/bioconductor-alabaster.spatial_server.py"]
|
| 40 |
+
|
Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/docker-compose.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
mcp-bioconductor-alabaster.spatial:
|
| 5 |
+
build: .
|
| 6 |
+
image: mcp-bioconductor-alabaster.spatial:latest
|
| 7 |
+
container_name: mcp-bioconductor-alabaster.spatial
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
environment:
|
| 11 |
+
- MCP_SERVER_NAME=bioconductor-alabaster.spatial
|
| 12 |
+
volumes:
|
| 13 |
+
- ./workspace:/app/workspace
|
| 14 |
+
- ./output:/app/output
|
| 15 |
+
restart: unless-stopped
|
| 16 |
+
healthcheck:
|
| 17 |
+
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
| 18 |
+
interval: 30s
|
| 19 |
+
timeout: 10s
|
| 20 |
+
retries: 3
|
| 21 |
+
start_period: 5s
|
| 22 |
+
|
Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/environment.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
name: mcp-tool
|
| 3 |
+
channels:
|
| 4 |
+
- bioconda
|
| 5 |
+
- conda-forge
|
| 6 |
+
- defaults
|
| 7 |
+
dependencies:
|
| 8 |
+
- bioconductor-alabaster.spatial
|
| 9 |
+
- python=3.10
|
| 10 |
+
|
Biomni/mcp_generated/mcp_bioconductor-alabaster.spatial/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
mcp
|
Biomni/mcp_generated/mcp_bioconductor-cardspa/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install Miniconda
|
| 8 |
+
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
|
| 9 |
+
|
| 10 |
+
# Add conda to PATH
|
| 11 |
+
ENV PATH="/opt/conda/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Install bioconductor-cardspa via conda (e.g., from bioconda)
|
| 14 |
+
RUN conda install -c bioconda bioconductor-cardspa -y && conda clean -a
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
RUN pip install uv
|
| 18 |
+
RUN uv pip install --system fastmcp
|
| 19 |
+
|
| 20 |
+
# Create app directory
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Copy your MCP server
|
| 24 |
+
COPY app/bioconductor-cardspa_server.py /app/
|
| 25 |
+
|
| 26 |
+
# Create workspace and output directories
|
| 27 |
+
RUN mkdir -p /app/workspace /app/output
|
| 28 |
+
|
| 29 |
+
# Make sure the server script is executable
|
| 30 |
+
RUN chmod +x /app/bioconductor-cardspa_server.py
|
| 31 |
+
|
| 32 |
+
# Expose port for MCP over HTTP (optional)
|
| 33 |
+
EXPOSE 8000
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
|
| 37 |
+
|
| 38 |
+
# Default command runs the MCP server via stdio
|
| 39 |
+
CMD ["python", "/app/bioconductor-cardspa_server.py"]
|
| 40 |
+
|
Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_server.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import tempfile
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Dict, List, Literal, Union
|
| 5 |
+
|
| 6 |
+
# This is a placeholder for the MCP framework's decorator.
|
| 7 |
+
# The user's environment will provide the actual mcp.tool implementation.
|
| 8 |
+
class Mcp:
|
| 9 |
+
def tool(self):
|
| 10 |
+
def decorator(f):
|
| 11 |
+
return f
|
| 12 |
+
return decorator
|
| 13 |
+
mcp = Mcp()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from mcp.server.fastmcp import FastMCP
|
| 17 |
+
|
| 18 |
+
SERVER_NAME = 'local_bioconductor_cardspa'
|
| 19 |
+
mcp = FastMCP(SERVER_NAME)
|
| 20 |
+
|
| 21 |
+
@mcp.tool()
|
| 22 |
+
def cardspa(
|
| 23 |
+
sce_object: Path,
|
| 24 |
+
output_path: Path,
|
| 25 |
+
phenotype_col: str,
|
| 26 |
+
location_col_1: str = "row",
|
| 27 |
+
location_col_2: str = "col",
|
| 28 |
+
method: Literal["Giotto", "SpaGCN", "BayesSpace"] = "Giotto",
|
| 29 |
+
num_results: int = 10,
|
| 30 |
+
num_core: int = 1,
|
| 31 |
+
min_cells: int = 10,
|
| 32 |
+
min_pairs: int = 10,
|
| 33 |
+
) -> Dict[str, Union[str, List[str], int]]:
|
| 34 |
+
"""
|
| 35 |
+
Performs spatial analysis on single-cell data using the cardspa R package.
|
| 36 |
+
|
| 37 |
+
This tool wraps the main `cardspa` function, which can utilize different
|
| 38 |
+
backend methods like Giotto, SpaGCN, or BayesSpace to identify significant
|
| 39 |
+
co-enrichment of cell types in spatial transcriptomics data.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
sce_object: Path to the input SingleCellExperiment object in RDS format.
|
| 43 |
+
output_path: Path to save the output results object in RDS format.
|
| 44 |
+
phenotype_col: The column name in the SCE object's colData that contains cell type annotations.
|
| 45 |
+
location_col_1: The column name for the first spatial coordinate (e.g., 'row', 'x').
|
| 46 |
+
location_col_2: The column name for the second spatial coordinate (e.g., 'col', 'y').
|
| 47 |
+
method: The computational method to use for the analysis.
|
| 48 |
+
num_results: The number of top co-enriched cell type pairs to return.
|
| 49 |
+
num_core: The number of CPU cores to use for parallel processing.
|
| 50 |
+
min_cells: The minimum number of cells required for a cell type to be included in the analysis.
|
| 51 |
+
min_pairs: The minimum number of interacting pairs for a result to be considered significant.
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
A dictionary containing the executed command, stdout, stderr, and a list of output files.
|
| 55 |
+
In case of an error, it includes an error message and return code.
|
| 56 |
+
"""
|
| 57 |
+
# --- Input Validation ---
|
| 58 |
+
if not sce_object.is_file():
|
| 59 |
+
raise FileNotFoundError(f"Input file not found: {sce_object}")
|
| 60 |
+
|
| 61 |
+
if not output_path.parent.is_dir():
|
| 62 |
+
try:
|
| 63 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 64 |
+
except OSError as e:
|
| 65 |
+
raise NotADirectoryError(
|
| 66 |
+
f"Output directory does not exist and could not be created: {output_path.parent}"
|
| 67 |
+
) from e
|
| 68 |
+
|
| 69 |
+
if num_results <= 0:
|
| 70 |
+
raise ValueError("num_results must be a positive integer.")
|
| 71 |
+
if num_core <= 0:
|
| 72 |
+
raise ValueError("num_core must be a positive integer.")
|
| 73 |
+
if min_cells <= 0:
|
| 74 |
+
raise ValueError("min_cells must be a positive integer.")
|
| 75 |
+
if min_pairs <= 0:
|
| 76 |
+
raise ValueError("min_pairs must be a positive integer.")
|
| 77 |
+
|
| 78 |
+
# R script to be executed. It includes package installation for portability.
|
| 79 |
+
r_script_content = """
|
| 80 |
+
# run_cardspa.R
|
| 81 |
+
options(warn=-1)
|
| 82 |
+
suppressPackageStartupMessages({
|
| 83 |
+
if (!requireNamespace("argparser", quietly = TRUE)) {
|
| 84 |
+
install.packages("argparser", repos = "http://cran.us.r-project.org", quiet = TRUE)
|
| 85 |
+
}
|
| 86 |
+
if (!requireNamespace("BiocManager", quietly = TRUE)) {
|
| 87 |
+
install.packages("BiocManager", repos = "http://cran.us.r-project.org", quiet = TRUE)
|
| 88 |
+
}
|
| 89 |
+
if (!requireNamespace("cardspa", quietly = TRUE)) {
|
| 90 |
+
BiocManager::install("cardspa", update=FALSE, ask=FALSE, quiet = TRUE)
|
| 91 |
+
}
|
| 92 |
+
if (!requireNamespace("SingleCellExperiment", quietly = TRUE)) {
|
| 93 |
+
BiocManager::install("SingleCellExperiment", update=FALSE, ask=FALSE, quiet = TRUE)
|
| 94 |
+
}
|
| 95 |
+
library(argparser)
|
| 96 |
+
library(cardspa)
|
| 97 |
+
library(SingleCellExperiment)
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
p <- arg_parser("Run cardspa analysis")
|
| 101 |
+
p <- add_argument(p, "--sce_object", help="Path to the input SingleCellExperiment RDS file", type="character")
|
| 102 |
+
p <- add_argument(p, "--output_path", help="Path to save the output RDS file", type="character")
|
| 103 |
+
p <- add_argument(p, "--phenotype_col", help="Column name for cell types", type="character")
|
| 104 |
+
p <- add_argument(p, "--location_col_1", help="First column name for spatial coordinates", default="row", type="character")
|
| 105 |
+
p <- add_argument(p, "--location_col_2", help="Second column name for spatial coordinates", default="col", type="character")
|
| 106 |
+
p <- add_argument(p, "--method", help="Method for spatial analysis (Giotto, SpaGCN, BayesSpace)", default="Giotto", type="character")
|
| 107 |
+
p <- add_argument(p, "--num_results", help="Number of top results to return", default=10, type="integer")
|
| 108 |
+
p <- add_argument(p, "--num_core", help="Number of cores for parallel processing", default=1, type="integer")
|
| 109 |
+
p <- add_argument(p, "--min_cells", help="Minimum number of cells for a cell type", default=10, type="integer")
|
| 110 |
+
p <- add_argument(p, "--min_pairs", help="Minimum number of interacting pairs", default=10, type="integer")
|
| 111 |
+
|
| 112 |
+
argv <- parse_args(p)
|
| 113 |
+
|
| 114 |
+
if (is.na(argv$sce_object) || is.na(argv$output_path) || is.na(argv$phenotype_col)) {
|
| 115 |
+
stop("Missing required arguments: --sce_object, --output_path, or --phenotype_col", call. = FALSE)
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
cat("Reading input SCE object from:", argv$sce_object, "\\n")
|
| 119 |
+
sce <- readRDS(argv$sce_object)
|
| 120 |
+
|
| 121 |
+
cat("Running cardspa analysis with method:", argv$method, "\\n")
|
| 122 |
+
results <- cardspa(
|
| 123 |
+
sce_object = sce,
|
| 124 |
+
location_col = c(argv$location_col_1, argv$location_col_2),
|
| 125 |
+
phenotype_col = argv$phenotype_col,
|
| 126 |
+
method = argv$method,
|
| 127 |
+
num_results = argv$num_results,
|
| 128 |
+
num_core = argv$num_core,
|
| 129 |
+
min_cells = argv$min_cells,
|
| 130 |
+
min_pairs = argv$min_pairs
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
cat("Saving results to:", argv$output_path, "\\n")
|
| 134 |
+
saveRDS(results, file = argv$output_path)
|
| 135 |
+
cat("cardspa analysis complete. Results saved to:", argv$output_path, "\\n")
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
# --- Subprocess Execution ---
|
| 139 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file:
|
| 140 |
+
r_script_file.write(r_script_content)
|
| 141 |
+
r_script_path = Path(r_script_file.name)
|
| 142 |
+
|
| 143 |
+
cmd = [
|
| 144 |
+
"Rscript", str(r_script_path),
|
| 145 |
+
"--sce_object", str(sce_object.resolve()),
|
| 146 |
+
"--output_path", str(output_path.resolve()),
|
| 147 |
+
"--phenotype_col", phenotype_col,
|
| 148 |
+
"--location_col_1", location_col_1,
|
| 149 |
+
"--location_col_2", location_col_2,
|
| 150 |
+
"--method", method,
|
| 151 |
+
"--num_results", str(num_results),
|
| 152 |
+
"--num_core", str(num_core),
|
| 153 |
+
"--min_cells", str(min_cells),
|
| 154 |
+
"--min_pairs", str(min_pairs),
|
| 155 |
+
]
|
| 156 |
+
command_executed = " ".join(cmd)
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
result = subprocess.run(
|
| 160 |
+
cmd,
|
| 161 |
+
check=True,
|
| 162 |
+
capture_output=True,
|
| 163 |
+
text=True,
|
| 164 |
+
encoding='utf-8'
|
| 165 |
+
)
|
| 166 |
+
return {
|
| 167 |
+
"command_executed": command_executed,
|
| 168 |
+
"stdout": result.stdout,
|
| 169 |
+
"stderr": result.stderr,
|
| 170 |
+
"output_files": [str(output_path.resolve())]
|
| 171 |
+
}
|
| 172 |
+
except FileNotFoundError:
|
| 173 |
+
raise RuntimeError("Rscript command not found. Please ensure R is installed and in your system's PATH.")
|
| 174 |
+
except subprocess.CalledProcessError as e:
|
| 175 |
+
return {
|
| 176 |
+
"command_executed": command_executed,
|
| 177 |
+
"stdout": e.stdout,
|
| 178 |
+
"stderr": e.stderr,
|
| 179 |
+
"error": "cardspa execution failed.",
|
| 180 |
+
"return_code": e.returncode,
|
| 181 |
+
"output_files": []
|
| 182 |
+
}
|
| 183 |
+
finally:
|
| 184 |
+
# Ensure temporary script is always removed
|
| 185 |
+
if r_script_path.exists():
|
| 186 |
+
r_script_path.unlink()
|
| 187 |
+
|
| 188 |
+
if __name__ == "__main__":
|
| 189 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-cardspa/app/bioconductor-cardspa_shim_server.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import ast
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from mcp.server.fastmcp import FastMCP
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-cardspa/app/bioconductor-cardspa_server.py')
|
| 11 |
+
LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name)
|
| 12 |
+
SERVER_NAME = 'biosci_bioconductor_cardspa'
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class _ShimMCP:
|
| 16 |
+
@staticmethod
|
| 17 |
+
def tool(*args, **kwargs):
|
| 18 |
+
if args and callable(args[0]) and len(args) == 1 and not kwargs:
|
| 19 |
+
return args[0]
|
| 20 |
+
def _decorator(fn):
|
| 21 |
+
return fn
|
| 22 |
+
return _decorator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _resolve_source_server():
|
| 26 |
+
if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name:
|
| 27 |
+
return LOCAL_SERVER
|
| 28 |
+
return SOURCE_SERVER
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _load_functions():
|
| 32 |
+
source_server = _resolve_source_server()
|
| 33 |
+
code = source_server.read_text(encoding="utf-8")
|
| 34 |
+
tree = ast.parse(code, filename=str(source_server))
|
| 35 |
+
function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
|
| 36 |
+
namespace = {
|
| 37 |
+
"__name__": "__mcp_source__",
|
| 38 |
+
"mcp": _ShimMCP(),
|
| 39 |
+
}
|
| 40 |
+
exec(compile(code, str(source_server), "exec"), namespace, namespace)
|
| 41 |
+
loaded = []
|
| 42 |
+
for name in function_names:
|
| 43 |
+
fn = namespace.get(name)
|
| 44 |
+
if callable(fn):
|
| 45 |
+
loaded.append(fn)
|
| 46 |
+
return loaded
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
mcp = FastMCP(SERVER_NAME)
|
| 50 |
+
for _fn in _load_functions():
|
| 51 |
+
mcp.tool()(_fn)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-cardspa/app/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
Biomni/mcp_generated/mcp_bioconductor-cardspa/docker-compose.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
mcp-bioconductor-cardspa:
|
| 5 |
+
build: .
|
| 6 |
+
image: mcp-bioconductor-cardspa:latest
|
| 7 |
+
container_name: mcp-bioconductor-cardspa
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
environment:
|
| 11 |
+
- MCP_SERVER_NAME=bioconductor-cardspa
|
| 12 |
+
volumes:
|
| 13 |
+
- ./workspace:/app/workspace
|
| 14 |
+
- ./output:/app/output
|
| 15 |
+
restart: unless-stopped
|
| 16 |
+
healthcheck:
|
| 17 |
+
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
| 18 |
+
interval: 30s
|
| 19 |
+
timeout: 10s
|
| 20 |
+
retries: 3
|
| 21 |
+
start_period: 5s
|
| 22 |
+
|
Biomni/mcp_generated/mcp_bioconductor-cardspa/environment.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
name: mcp-tool
|
| 3 |
+
channels:
|
| 4 |
+
- bioconda
|
| 5 |
+
- conda-forge
|
| 6 |
+
- defaults
|
| 7 |
+
dependencies:
|
| 8 |
+
- bioconductor-cardspa
|
| 9 |
+
- python=3.10
|
| 10 |
+
|
Biomni/mcp_generated/mcp_bioconductor-cardspa/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
mcp
|
Biomni/mcp_generated/mcp_bioconductor-cellid/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install Miniconda
|
| 8 |
+
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
|
| 9 |
+
|
| 10 |
+
# Add conda to PATH
|
| 11 |
+
ENV PATH="/opt/conda/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Install bioconductor-cellid via conda (e.g., from bioconda)
|
| 14 |
+
RUN conda install -c bioconda bioconductor-cellid -y && conda clean -a
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
RUN pip install uv
|
| 18 |
+
RUN uv pip install --system fastmcp
|
| 19 |
+
|
| 20 |
+
# Create app directory
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Copy your MCP server
|
| 24 |
+
COPY app/bioconductor-cellid_server.py /app/
|
| 25 |
+
|
| 26 |
+
# Create workspace and output directories
|
| 27 |
+
RUN mkdir -p /app/workspace /app/output
|
| 28 |
+
|
| 29 |
+
# Make sure the server script is executable
|
| 30 |
+
RUN chmod +x /app/bioconductor-cellid_server.py
|
| 31 |
+
|
| 32 |
+
# Expose port for MCP over HTTP (optional)
|
| 33 |
+
EXPOSE 8000
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
|
| 37 |
+
|
| 38 |
+
# Default command runs the MCP server via stdio
|
| 39 |
+
CMD ["python", "/app/bioconductor-cellid_server.py"]
|
| 40 |
+
|
Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_server.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Optional, List, Union
|
| 5 |
+
import tempfile
|
| 6 |
+
|
| 7 |
+
def run_r_command(script_content: str):
|
| 8 |
+
"""Helper function to execute R code via Rscript"""
|
| 9 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp:
|
| 10 |
+
tmp.write(script_content)
|
| 11 |
+
tmp_path = tmp.name
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
result = subprocess.run(
|
| 15 |
+
["Rscript", tmp_path],
|
| 16 |
+
capture_output=True,
|
| 17 |
+
text=True,
|
| 18 |
+
check=True
|
| 19 |
+
)
|
| 20 |
+
return result.stdout, result.stderr
|
| 21 |
+
except subprocess.CalledProcessError as e:
|
| 22 |
+
raise RuntimeError(f"R execution failed: {e.stderr}\nStdout: {e.stdout}")
|
| 23 |
+
finally:
|
| 24 |
+
if os.path.exists(tmp_path):
|
| 25 |
+
os.remove(tmp_path)
|
| 26 |
+
|
| 27 |
+
from mcp.server.fastmcp import FastMCP
|
| 28 |
+
|
| 29 |
+
SERVER_NAME = 'local_bioconductor_cellid'
|
| 30 |
+
mcp = FastMCP(SERVER_NAME)
|
| 31 |
+
|
| 32 |
+
@mcp.tool()
|
| 33 |
+
def cellid_run_mca(
|
| 34 |
+
input_rds: str,
|
| 35 |
+
output_rds: str,
|
| 36 |
+
nmcs: int = 50,
|
| 37 |
+
assay: str = "RNA",
|
| 38 |
+
slot: str = "counts",
|
| 39 |
+
features: Optional[List[str]] = None
|
| 40 |
+
):
|
| 41 |
+
"""
|
| 42 |
+
Performs Multiple Correspondence Analysis (MCA) on a SingleCellExperiment or Seurat object.
|
| 43 |
+
MCA allows for the simultaneous representation of cells and genes in a common low-dimensional space.
|
| 44 |
+
"""
|
| 45 |
+
input_path = Path(input_rds)
|
| 46 |
+
output_path = Path(output_rds)
|
| 47 |
+
|
| 48 |
+
if not input_path.exists():
|
| 49 |
+
return {"error": f"Input file {input_rds} not found."}
|
| 50 |
+
|
| 51 |
+
if nmcs <= 0:
|
| 52 |
+
return {"error": "nmcs must be a positive integer."}
|
| 53 |
+
|
| 54 |
+
features_r = "NULL"
|
| 55 |
+
if features:
|
| 56 |
+
features_formatted = ", ".join([f"'{f}'" for f in features])
|
| 57 |
+
features_r = f"c({features_formatted})"
|
| 58 |
+
|
| 59 |
+
r_script = f"""
|
| 60 |
+
library(CelliD)
|
| 61 |
+
library(Seurat)
|
| 62 |
+
library(SingleCellExperiment)
|
| 63 |
+
|
| 64 |
+
obj <- readRDS("{input_path}")
|
| 65 |
+
|
| 66 |
+
# Run MCA
|
| 67 |
+
obj <- RunMCA(obj, nmcs = {nmcs}, assay = "{assay}", slot = "{slot}", features = {features_r})
|
| 68 |
+
|
| 69 |
+
saveRDS(obj, "{output_path}")
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
stdout, stderr = run_r_command(r_script)
|
| 74 |
+
return {
|
| 75 |
+
"command_executed": "RunMCA",
|
| 76 |
+
"stdout": stdout,
|
| 77 |
+
"stderr": stderr,
|
| 78 |
+
"output_files": [str(output_path)]
|
| 79 |
+
}
|
| 80 |
+
except Exception as e:
|
| 81 |
+
return {"error": str(e)}
|
| 82 |
+
|
| 83 |
+
@mcp.tool()
|
| 84 |
+
def cellid_get_cell_gene_set(
|
| 85 |
+
input_rds: str,
|
| 86 |
+
output_csv: str,
|
| 87 |
+
dims: int = 50,
|
| 88 |
+
n_features: int = 200
|
| 89 |
+
):
|
| 90 |
+
"""
|
| 91 |
+
Extracts the top gene signatures for each individual cell based on MCA coordinates.
|
| 92 |
+
"""
|
| 93 |
+
input_path = Path(input_rds)
|
| 94 |
+
output_path = Path(output_csv)
|
| 95 |
+
|
| 96 |
+
if not input_path.exists():
|
| 97 |
+
return {"error": f"Input file {input_rds} not found."}
|
| 98 |
+
|
| 99 |
+
if dims <= 0 or n_features <= 0:
|
| 100 |
+
return {"error": "dims and n_features must be positive integers."}
|
| 101 |
+
|
| 102 |
+
r_script = f"""
|
| 103 |
+
library(CelliD)
|
| 104 |
+
library(Seurat)
|
| 105 |
+
library(SingleCellExperiment)
|
| 106 |
+
library(data.table)
|
| 107 |
+
|
| 108 |
+
obj <- readRDS("{input_path}")
|
| 109 |
+
|
| 110 |
+
# Extract gene sets per cell
|
| 111 |
+
# dims is treated as 1:dims
|
| 112 |
+
gene_sets <- GetCellGeneSet(obj, dims = 1:{dims}, n.features = {n_features})
|
| 113 |
+
|
| 114 |
+
# Convert list to a data table for export
|
| 115 |
+
# Each column is a cell, rows are top genes
|
| 116 |
+
max_len <- max(sapply(gene_sets, length))
|
| 117 |
+
dt <- as.data.frame(lapply(gene_sets, function(x) c(x, rep(NA, max_len - length(x)))))
|
| 118 |
+
|
| 119 |
+
write.csv(dt, "{output_path}", row.names = FALSE)
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
stdout, stderr = run_r_command(r_script)
|
| 124 |
+
return {
|
| 125 |
+
"command_executed": "GetCellGeneSet",
|
| 126 |
+
"stdout": stdout,
|
| 127 |
+
"stderr": stderr,
|
| 128 |
+
"output_files": [str(output_path)]
|
| 129 |
+
}
|
| 130 |
+
except Exception as e:
|
| 131 |
+
return {"error": str(e)}
|
| 132 |
+
|
| 133 |
+
@mcp.tool()
|
| 134 |
+
def cellid_run_cell_hgt(
|
| 135 |
+
input_rds: str,
|
| 136 |
+
pathways_gmt: str,
|
| 137 |
+
output_rds: str,
|
| 138 |
+
dims: int = 50,
|
| 139 |
+
n_features: int = 200,
|
| 140 |
+
min_size: int = 5
|
| 141 |
+
):
|
| 142 |
+
"""
|
| 143 |
+
Performs cell-type assignment or functional enrichment using a Hypergeometric Test (HGT).
|
| 144 |
+
Matches per-cell gene signatures against a reference database (GMT format).
|
| 145 |
+
"""
|
| 146 |
+
input_path = Path(input_rds)
|
| 147 |
+
gmt_path = Path(pathways_gmt)
|
| 148 |
+
output_path = Path(output_rds)
|
| 149 |
+
|
| 150 |
+
if not input_path.exists():
|
| 151 |
+
return {"error": f"Input RDS {input_rds} not found."}
|
| 152 |
+
if not gmt_path.exists():
|
| 153 |
+
return {"error": f"GMT file {pathways_gmt} not found."}
|
| 154 |
+
|
| 155 |
+
r_script = f"""
|
| 156 |
+
library(CelliD)
|
| 157 |
+
library(Seurat)
|
| 158 |
+
library(SingleCellExperiment)
|
| 159 |
+
library(fgsea)
|
| 160 |
+
|
| 161 |
+
obj <- readRDS("{input_path}")
|
| 162 |
+
pathways <- gmtPathways("{gmt_path}")
|
| 163 |
+
|
| 164 |
+
# Run Hypergeometric Test
|
| 165 |
+
# Returns a matrix of -log10(p-value)
|
| 166 |
+
hgt_matrix <- RunCellHGT(obj, pathways = pathways, dims = 1:{dims}, n.features = {n_features}, minSize = {min_size})
|
| 167 |
+
|
| 168 |
+
# If Seurat, we can add it as an assay or metadata
|
| 169 |
+
if(inherits(obj, "Seurat")) {{
|
| 170 |
+
obj[["HGT"]] <- CreateAssayObject(counts = hgt_matrix)
|
| 171 |
+
}} else {{
|
| 172 |
+
# For SCE, add to altExp
|
| 173 |
+
altExp(obj, "HGT") <- SummarizedExperiment(assays = list(counts = hgt_matrix))
|
| 174 |
+
}}
|
| 175 |
+
|
| 176 |
+
saveRDS(obj, "{output_path}")
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
stdout, stderr = run_r_command(r_script)
|
| 181 |
+
return {
|
| 182 |
+
"command_executed": "RunCellHGT",
|
| 183 |
+
"stdout": stdout,
|
| 184 |
+
"stderr": stderr,
|
| 185 |
+
"output_files": [str(output_path)]
|
| 186 |
+
}
|
| 187 |
+
except Exception as e:
|
| 188 |
+
return {"error": str(e)}
|
| 189 |
+
|
| 190 |
+
@mcp.tool()
|
| 191 |
+
def cellid_run_gsea(
|
| 192 |
+
input_rds: str,
|
| 193 |
+
pathways_gmt: str,
|
| 194 |
+
output_rds: str,
|
| 195 |
+
dims: int = 50
|
| 196 |
+
):
|
| 197 |
+
"""
|
| 198 |
+
Performs Gene Set Enrichment Analysis (GSEA) on cells using the MCA gene rankings.
|
| 199 |
+
"""
|
| 200 |
+
input_path = Path(input_rds)
|
| 201 |
+
gmt_path = Path(pathways_gmt)
|
| 202 |
+
output_path = Path(output_rds)
|
| 203 |
+
|
| 204 |
+
if not input_path.exists():
|
| 205 |
+
return {"error": f"Input RDS {input_rds} not found."}
|
| 206 |
+
if not gmt_path.exists():
|
| 207 |
+
return {"error": f"GMT file {pathways_gmt} not found."}
|
| 208 |
+
|
| 209 |
+
r_script = f"""
|
| 210 |
+
library(CelliD)
|
| 211 |
+
library(Seurat)
|
| 212 |
+
library(SingleCellExperiment)
|
| 213 |
+
library(fgsea)
|
| 214 |
+
|
| 215 |
+
obj <- readRDS("{input_path}")
|
| 216 |
+
pathways <- gmtPathways("{gmt_path}")
|
| 217 |
+
|
| 218 |
+
# Run GSEA
|
| 219 |
+
# This calculates enrichment scores for each cell and each pathway
|
| 220 |
+
gsea_res <- RunGSEA(obj, pathways = pathways, dims = 1:{dims})
|
| 221 |
+
|
| 222 |
+
# Save results (usually returns a matrix of NES)
|
| 223 |
+
if(inherits(obj, "Seurat")) {{
|
| 224 |
+
obj[["GSEA"]] <- CreateAssayObject(counts = gsea_res)
|
| 225 |
+
}} else {{
|
| 226 |
+
altExp(obj, "GSEA") <- SummarizedExperiment(assays = list(counts = gsea_res))
|
| 227 |
+
}}
|
| 228 |
+
|
| 229 |
+
saveRDS(obj, "{output_path}")
|
| 230 |
+
"""
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
stdout, stderr = run_r_command(r_script)
|
| 234 |
+
return {
|
| 235 |
+
"command_executed": "RunGSEA",
|
| 236 |
+
"stdout": stdout,
|
| 237 |
+
"stderr": stderr,
|
| 238 |
+
"output_files": [str(output_path)]
|
| 239 |
+
}
|
| 240 |
+
except Exception as e:
|
| 241 |
+
return {"error": str(e)}
|
| 242 |
+
|
| 243 |
+
@mcp.tool()
|
| 244 |
+
def cellid_run_group_mca(
|
| 245 |
+
input_rds: str,
|
| 246 |
+
group_by: str,
|
| 247 |
+
output_rds: str,
|
| 248 |
+
nmcs: int = 50,
|
| 249 |
+
assay: str = "RNA",
|
| 250 |
+
slot: str = "counts"
|
| 251 |
+
):
|
| 252 |
+
"""
|
| 253 |
+
Performs MCA on grouped cells (e.g., by cluster or cell type) rather than individual cells.
|
| 254 |
+
Useful for extracting group-specific gene signatures.
|
| 255 |
+
"""
|
| 256 |
+
input_path = Path(input_rds)
|
| 257 |
+
output_path = Path(output_rds)
|
| 258 |
+
|
| 259 |
+
if not input_path.exists():
|
| 260 |
+
return {"error": f"Input file {input_rds} not found."}
|
| 261 |
+
|
| 262 |
+
r_script = f"""
|
| 263 |
+
library(CelliD)
|
| 264 |
+
library(Seurat)
|
| 265 |
+
library(SingleCellExperiment)
|
| 266 |
+
|
| 267 |
+
obj <- readRDS("{input_path}")
|
| 268 |
+
|
| 269 |
+
# Run Group MCA
|
| 270 |
+
# This aggregates cells by the specified metadata column before MCA
|
| 271 |
+
group_mca <- RunGroupMCA(obj, group.by = "{group_by}", nmcs = {nmcs}, assay = "{assay}", slot = "{slot}")
|
| 272 |
+
|
| 273 |
+
saveRDS(group_mca, "{output_path}")
|
| 274 |
+
"""
|
| 275 |
+
|
| 276 |
+
try:
|
| 277 |
+
stdout, stderr = run_r_command(r_script)
|
| 278 |
+
return {
|
| 279 |
+
"command_executed": "RunGroupMCA",
|
| 280 |
+
"stdout": stdout,
|
| 281 |
+
"stderr": stderr,
|
| 282 |
+
"output_files": [str(output_path)]
|
| 283 |
+
}
|
| 284 |
+
except Exception as e:
|
| 285 |
+
return {"error": str(e)}
|
| 286 |
+
|
| 287 |
+
if __name__ == "__main__":
|
| 288 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-cellid/app/bioconductor-cellid_shim_server.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import ast
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from mcp.server.fastmcp import FastMCP
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-cellid/app/bioconductor-cellid_server.py')
|
| 11 |
+
LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name)
|
| 12 |
+
SERVER_NAME = 'biosci_bioconductor_cellid'
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class _ShimMCP:
|
| 16 |
+
@staticmethod
|
| 17 |
+
def tool(*args, **kwargs):
|
| 18 |
+
if args and callable(args[0]) and len(args) == 1 and not kwargs:
|
| 19 |
+
return args[0]
|
| 20 |
+
def _decorator(fn):
|
| 21 |
+
return fn
|
| 22 |
+
return _decorator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _resolve_source_server():
|
| 26 |
+
if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name:
|
| 27 |
+
return LOCAL_SERVER
|
| 28 |
+
return SOURCE_SERVER
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _load_functions():
|
| 32 |
+
source_server = _resolve_source_server()
|
| 33 |
+
code = source_server.read_text(encoding="utf-8")
|
| 34 |
+
tree = ast.parse(code, filename=str(source_server))
|
| 35 |
+
function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
|
| 36 |
+
namespace = {
|
| 37 |
+
"__name__": "__mcp_source__",
|
| 38 |
+
"mcp": _ShimMCP(),
|
| 39 |
+
}
|
| 40 |
+
exec(compile(code, str(source_server), "exec"), namespace, namespace)
|
| 41 |
+
loaded = []
|
| 42 |
+
for name in function_names:
|
| 43 |
+
fn = namespace.get(name)
|
| 44 |
+
if callable(fn):
|
| 45 |
+
loaded.append(fn)
|
| 46 |
+
return loaded
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
mcp = FastMCP(SERVER_NAME)
|
| 50 |
+
for _fn in _load_functions():
|
| 51 |
+
mcp.tool()(_fn)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-cellid/docker-compose.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
mcp-bioconductor-cellid:
|
| 5 |
+
build: .
|
| 6 |
+
image: mcp-bioconductor-cellid:latest
|
| 7 |
+
container_name: mcp-bioconductor-cellid
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
environment:
|
| 11 |
+
- MCP_SERVER_NAME=bioconductor-cellid
|
| 12 |
+
volumes:
|
| 13 |
+
- ./workspace:/app/workspace
|
| 14 |
+
- ./output:/app/output
|
| 15 |
+
restart: unless-stopped
|
| 16 |
+
healthcheck:
|
| 17 |
+
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
| 18 |
+
interval: 30s
|
| 19 |
+
timeout: 10s
|
| 20 |
+
retries: 3
|
| 21 |
+
start_period: 5s
|
| 22 |
+
|
Biomni/mcp_generated/mcp_bioconductor-cellid/environment.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
name: mcp-tool
|
| 3 |
+
channels:
|
| 4 |
+
- bioconda
|
| 5 |
+
- conda-forge
|
| 6 |
+
- defaults
|
| 7 |
+
dependencies:
|
| 8 |
+
- bioconductor-cellid
|
| 9 |
+
- python=3.10
|
| 10 |
+
|
Biomni/mcp_generated/mcp_bioconductor-cellid/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
mcp
|
Biomni/mcp_generated/mcp_bioconductor-ggspavis/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install Miniconda
|
| 8 |
+
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
|
| 9 |
+
|
| 10 |
+
# Add conda to PATH
|
| 11 |
+
ENV PATH="/opt/conda/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Install bioconductor-ggspavis via conda (e.g., from bioconda)
|
| 14 |
+
RUN conda install -c bioconda bioconductor-ggspavis -y && conda clean -a
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
RUN pip install uv
|
| 18 |
+
RUN uv pip install --system fastmcp
|
| 19 |
+
|
| 20 |
+
# Create app directory
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Copy your MCP server
|
| 24 |
+
COPY app/bioconductor-ggspavis_server.py /app/
|
| 25 |
+
|
| 26 |
+
# Create workspace and output directories
|
| 27 |
+
RUN mkdir -p /app/workspace /app/output
|
| 28 |
+
|
| 29 |
+
# Make sure the server script is executable
|
| 30 |
+
RUN chmod +x /app/bioconductor-ggspavis_server.py
|
| 31 |
+
|
| 32 |
+
# Expose port for MCP over HTTP (optional)
|
| 33 |
+
EXPOSE 8000
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
|
| 37 |
+
|
| 38 |
+
# Default command runs the MCP server via stdio
|
| 39 |
+
CMD ["python", "/app/bioconductor-ggspavis_server.py"]
|
| 40 |
+
|
Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_server.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import tempfile
|
| 3 |
+
import json
|
| 4 |
+
import shlex
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Optional, List, Literal
|
| 7 |
+
|
| 8 |
+
# This R script acts as a generic command-line wrapper for ggspavis functions.
|
| 9 |
+
# It is written to a temporary file and executed by the Python functions below.
|
| 10 |
+
# It dynamically installs required R packages, making the tool self-contained.
|
| 11 |
+
R_SCRIPT = """
|
| 12 |
+
# ggspavis_runner.R
|
| 13 |
+
# A generic runner for ggspavis functions
|
| 14 |
+
|
| 15 |
+
# Suppress startup messages for cleaner output
|
| 16 |
+
suppressPackageStartupMessages({
|
| 17 |
+
# Check if packages are installed, install if not
|
| 18 |
+
if (!requireNamespace("argparse", quietly = TRUE)) install.packages("argparse", repos = "http://cran.us.r-project.org")
|
| 19 |
+
if (!requireNamespace("jsonlite", quietly = TRUE)) install.packages("jsonlite", repos = "http://cran.us.r-project.org")
|
| 20 |
+
if (!requireNamespace("ggplot2", quietly = TRUE)) install.packages("ggplot2", repos = "http://cran.us.r-project.org")
|
| 21 |
+
if (!requireNamespace("BiocManager", quietly = TRUE)) install.packages("BiocManager", repos = "http://cran.us.r-project.org")
|
| 22 |
+
|
| 23 |
+
# Use BiocManager to install Bioconductor packages
|
| 24 |
+
if (!requireNamespace("SpatialExperiment", quietly = TRUE)) BiocManager::install("SpatialExperiment", update = FALSE, ask = FALSE)
|
| 25 |
+
if (!requireNamespace("ggspavis", quietly = TRUE)) BiocManager::install("ggspavis", update = FALSE, ask = FALSE)
|
| 26 |
+
})
|
| 27 |
+
|
| 28 |
+
library(argparse)
|
| 29 |
+
library(jsonlite)
|
| 30 |
+
library(ggplot2)
|
| 31 |
+
library(SpatialExperiment)
|
| 32 |
+
library(ggspavis)
|
| 33 |
+
|
| 34 |
+
# Main function to parse arguments and call the appropriate ggspavis function
|
| 35 |
+
main <- function() {
|
| 36 |
+
parser <- ArgumentParser(description = "A command-line wrapper for ggspavis functions.")
|
| 37 |
+
|
| 38 |
+
parser$add_argument("func", help = "The ggspavis function to call (e.g., plotSpots, plotFeature).")
|
| 39 |
+
parser$add_argument("--spe_path", required = TRUE, help = "Path to the SpatialExperiment object (.rds file).")
|
| 40 |
+
parser$add_argument("--output_path", required = TRUE, help = "Path to save the output plot.")
|
| 41 |
+
parser$add_argument("--width", type = "double", default = 7, help = "Width of the output plot in inches.")
|
| 42 |
+
parser$add_argument("--height", type = "double", default = 7, help = "Height of the output plot in inches.")
|
| 43 |
+
parser$add_argument("--json_args", type = "character", default = "{}", help = "A JSON string of arguments for the ggspavis function.")
|
| 44 |
+
|
| 45 |
+
args <- parser$parse_args()
|
| 46 |
+
|
| 47 |
+
# Load SpatialExperiment object
|
| 48 |
+
if (!file.exists(args$spe_path)) {
|
| 49 |
+
stop("Input SPE file not found: ", args$spe_path)
|
| 50 |
+
}
|
| 51 |
+
spe <- readRDS(args$spe_path)
|
| 52 |
+
|
| 53 |
+
# Parse JSON arguments for the target function
|
| 54 |
+
func_args <- fromJSON(args$json_args)
|
| 55 |
+
|
| 56 |
+
# Add the main 'spe' object to the arguments list
|
| 57 |
+
func_args <- c(list(spe = spe), func_args)
|
| 58 |
+
|
| 59 |
+
# Check if the requested function exists in the ggspavis package
|
| 60 |
+
if (!exists(args$func, where = "package:ggspavis", mode = "function")) {
|
| 61 |
+
stop("Function '", args$func, "' not found in ggspavis package.")
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# Dynamically call the ggspavis function with the parsed arguments
|
| 65 |
+
plot_obj <- do.call(args$func, func_args)
|
| 66 |
+
|
| 67 |
+
# Save the generated plot to the specified output file
|
| 68 |
+
ggsave(
|
| 69 |
+
filename = args$output_path,
|
| 70 |
+
plot = plot_obj,
|
| 71 |
+
width = args$width,
|
| 72 |
+
height = args$height,
|
| 73 |
+
dpi = 300
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
cat("Plot successfully saved to", args$output_path, "\\n")
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
# Execute the main function and handle potential errors
|
| 80 |
+
tryCatch({
|
| 81 |
+
main()
|
| 82 |
+
}, error = function(e) {
|
| 83 |
+
message("Error: ", e$message)
|
| 84 |
+
quit(status = 1)
|
| 85 |
+
})
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
def _run_ggspavis(
|
| 89 |
+
function_name: str,
|
| 90 |
+
spe_path: Path,
|
| 91 |
+
output_path: Path,
|
| 92 |
+
plot_width: float,
|
| 93 |
+
plot_height: float,
|
| 94 |
+
json_args: dict,
|
| 95 |
+
) -> dict:
|
| 96 |
+
"""
|
| 97 |
+
Helper function to execute the ggspavis R script wrapper.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
function_name: The ggspavis function to execute (e.g., 'plotSpots').
|
| 101 |
+
spe_path: Path to the input SpatialExperiment RDS file.
|
| 102 |
+
output_path: Path to save the output plot image.
|
| 103 |
+
plot_width: Width of the output plot in inches.
|
| 104 |
+
plot_height: Height of the output plot in inches.
|
| 105 |
+
json_args: A dictionary of arguments for the ggspavis function.
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
A dictionary containing the command executed, stdout, stderr, and output file path.
|
| 109 |
+
"""
|
| 110 |
+
# Ensure output directory exists
|
| 111 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 112 |
+
|
| 113 |
+
# Use a temporary file to store and execute the R script
|
| 114 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False) as r_script_file:
|
| 115 |
+
r_script_path = Path(r_script_file.name)
|
| 116 |
+
r_script_file.write(R_SCRIPT)
|
| 117 |
+
|
| 118 |
+
command = [
|
| 119 |
+
"Rscript",
|
| 120 |
+
str(r_script_path),
|
| 121 |
+
function_name,
|
| 122 |
+
"--spe_path", str(spe_path),
|
| 123 |
+
"--output_path", str(output_path),
|
| 124 |
+
"--width", str(plot_width),
|
| 125 |
+
"--height", str(plot_height),
|
| 126 |
+
"--json_args", json.dumps(json_args),
|
| 127 |
+
]
|
| 128 |
+
|
| 129 |
+
command_executed = " ".join(shlex.quote(c) for c in command)
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
process = subprocess.run(
|
| 133 |
+
command,
|
| 134 |
+
capture_output=True,
|
| 135 |
+
text=True,
|
| 136 |
+
check=True,
|
| 137 |
+
)
|
| 138 |
+
stdout = process.stdout
|
| 139 |
+
stderr = process.stderr
|
| 140 |
+
output_files = {"plot": str(output_path)} if output_path.exists() else {}
|
| 141 |
+
|
| 142 |
+
except FileNotFoundError:
|
| 143 |
+
# This error occurs if 'Rscript' is not in the system's PATH
|
| 144 |
+
return {
|
| 145 |
+
"command_executed": command_executed,
|
| 146 |
+
"stdout": "",
|
| 147 |
+
"stderr": "Error: 'Rscript' command not found. Please ensure R is installed and in your system's PATH.",
|
| 148 |
+
"output_files": {},
|
| 149 |
+
}
|
| 150 |
+
except subprocess.CalledProcessError as e:
|
| 151 |
+
# This error occurs if the R script returns a non-zero exit code
|
| 152 |
+
return {
|
| 153 |
+
"command_executed": command_executed,
|
| 154 |
+
"stdout": e.stdout,
|
| 155 |
+
"stderr": e.stderr,
|
| 156 |
+
"output_files": {},
|
| 157 |
+
}
|
| 158 |
+
finally:
|
| 159 |
+
# Clean up the temporary R script file
|
| 160 |
+
r_script_path.unlink()
|
| 161 |
+
|
| 162 |
+
return {
|
| 163 |
+
"command_executed": command_executed,
|
| 164 |
+
"stdout": stdout,
|
| 165 |
+
"stderr": stderr,
|
| 166 |
+
"output_files": output_files,
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
from mcp.server.fastmcp import FastMCP
|
| 171 |
+
|
| 172 |
+
SERVER_NAME = 'local_bioconductor_ggspavis'
|
| 173 |
+
mcp = FastMCP(SERVER_NAME)
|
| 174 |
+
|
| 175 |
+
@mcp.tool()
|
| 176 |
+
def ggspavis_plot_spots(
|
| 177 |
+
spe_path: Path,
|
| 178 |
+
output_filename: str,
|
| 179 |
+
fill: Optional[str] = None,
|
| 180 |
+
highlight: Optional[str] = None,
|
| 181 |
+
facet_by: Optional[str] = None,
|
| 182 |
+
assay: str = "logcounts",
|
| 183 |
+
palette: str = "viridis",
|
| 184 |
+
point_size: float = 1.5,
|
| 185 |
+
point_alpha: float = 1.0,
|
| 186 |
+
plot_width: float = 7.0,
|
| 187 |
+
plot_height: float = 7.0,
|
| 188 |
+
):
|
| 189 |
+
"""
|
| 190 |
+
Generates a spatial plot of spots/capture locations using ggspavis::plotSpots.
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
spe_path: Path to the input SpatialExperiment object (.rds file).
|
| 194 |
+
output_filename: Name of the output plot file (e.g., 'plot.png').
|
| 195 |
+
fill: Column name in colData or a feature name to use for spot fill color.
|
| 196 |
+
highlight: Column name in colData to use for highlighting spots.
|
| 197 |
+
facet_by: Column name in colData to use for faceting the plot.
|
| 198 |
+
assay: Name of the assay to use if 'fill' is a feature name.
|
| 199 |
+
palette: Color palette for continuous values (e.g., 'viridis', 'magma').
|
| 200 |
+
point_size: Size of the points representing spots.
|
| 201 |
+
point_alpha: Alpha transparency of the points.
|
| 202 |
+
plot_width: Width of the output plot in inches.
|
| 203 |
+
plot_height: Height of the output plot in inches.
|
| 204 |
+
"""
|
| 205 |
+
# Input validation
|
| 206 |
+
if not spe_path.is_file():
|
| 207 |
+
raise FileNotFoundError(f"Input file not found: {spe_path}")
|
| 208 |
+
if point_size <= 0:
|
| 209 |
+
raise ValueError("point_size must be positive.")
|
| 210 |
+
if not (0.0 <= point_alpha <= 1.0):
|
| 211 |
+
raise ValueError("point_alpha must be between 0.0 and 1.0.")
|
| 212 |
+
|
| 213 |
+
# Prepare arguments for the R function
|
| 214 |
+
json_args = {
|
| 215 |
+
"fill": fill,
|
| 216 |
+
"highlight": highlight,
|
| 217 |
+
"facet_by": facet_by,
|
| 218 |
+
"assay": assay,
|
| 219 |
+
"palette": palette,
|
| 220 |
+
"point_size": point_size,
|
| 221 |
+
"point_alpha": point_alpha,
|
| 222 |
+
}
|
| 223 |
+
# Filter out None values so they are not passed in the JSON
|
| 224 |
+
json_args = {k: v for k, v in json_args.items() if v is not None}
|
| 225 |
+
|
| 226 |
+
output_path = Path(output_filename)
|
| 227 |
+
|
| 228 |
+
return _run_ggspavis(
|
| 229 |
+
function_name="plotSpots",
|
| 230 |
+
spe_path=spe_path,
|
| 231 |
+
output_path=output_path,
|
| 232 |
+
plot_width=plot_width,
|
| 233 |
+
plot_height=plot_height,
|
| 234 |
+
json_args=json_args,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
@mcp.tool()
|
| 239 |
+
def ggspavis_plot_feature(
|
| 240 |
+
spe_path: Path,
|
| 241 |
+
output_filename: str,
|
| 242 |
+
feature: str,
|
| 243 |
+
assay: str = "logcounts",
|
| 244 |
+
platform: Literal["Visium", "ST", "VisiumHD", "mIF", "seqFISH", "MERFISH", "Xenium"] = "Visium",
|
| 245 |
+
palette: str = "viridis",
|
| 246 |
+
point_size: float = 1.5,
|
| 247 |
+
plot_width: float = 7.0,
|
| 248 |
+
plot_height: float = 7.0,
|
| 249 |
+
):
|
| 250 |
+
"""
|
| 251 |
+
Visualizes the expression of a single feature (e.g., gene) in spatial context using ggspavis::plotFeature.
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
spe_path: Path to the input SpatialExperiment object (.rds file).
|
| 255 |
+
output_filename: Name of the output plot file (e.g., 'feature_plot.png').
|
| 256 |
+
feature: Name of the feature (gene) to plot from the specified assay.
|
| 257 |
+
assay: Name of the assay containing the feature.
|
| 258 |
+
platform: The spatial transcriptomics platform.
|
| 259 |
+
palette: Color palette for continuous expression values.
|
| 260 |
+
point_size: Size of the points.
|
| 261 |
+
plot_width: Width of the output plot in inches.
|
| 262 |
+
plot_height: Height of the output plot in inches.
|
| 263 |
+
"""
|
| 264 |
+
# Input validation
|
| 265 |
+
if not spe_path.is_file():
|
| 266 |
+
raise FileNotFoundError(f"Input file not found: {spe_path}")
|
| 267 |
+
if not feature:
|
| 268 |
+
raise ValueError("A feature name must be provided.")
|
| 269 |
+
|
| 270 |
+
# Prepare arguments for the R function
|
| 271 |
+
json_args = {
|
| 272 |
+
"feature": feature,
|
| 273 |
+
"assay": assay,
|
| 274 |
+
"platform": platform,
|
| 275 |
+
"palette": palette,
|
| 276 |
+
"point_size": point_size,
|
| 277 |
+
}
|
| 278 |
+
json_args = {k: v for k, v in json_args.items() if v is not None}
|
| 279 |
+
|
| 280 |
+
output_path = Path(output_filename)
|
| 281 |
+
|
| 282 |
+
return _run_ggspavis(
|
| 283 |
+
function_name="plotFeature",
|
| 284 |
+
spe_path=spe_path,
|
| 285 |
+
output_path=output_path,
|
| 286 |
+
plot_width=plot_width,
|
| 287 |
+
plot_height=plot_height,
|
| 288 |
+
json_args=json_args,
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
@mcp.tool()
|
| 293 |
+
def ggspavis_plot_dim_red(
|
| 294 |
+
spe_path: Path,
|
| 295 |
+
output_filename: str,
|
| 296 |
+
reduction_type: str,
|
| 297 |
+
x_axis: Optional[str] = None,
|
| 298 |
+
y_axis: Optional[str] = None,
|
| 299 |
+
color_by: Optional[str] = None,
|
| 300 |
+
shape_by: Optional[str] = None,
|
| 301 |
+
assay: str = "logcounts",
|
| 302 |
+
point_size: float = 1.5,
|
| 303 |
+
plot_width: float = 7.0,
|
| 304 |
+
plot_height: float = 7.0,
|
| 305 |
+
):
|
| 306 |
+
"""
|
| 307 |
+
Plots dimensionality reduction results (e.g., UMAP, PCA) using ggspavis::plotDimRed.
|
| 308 |
+
|
| 309 |
+
Args:
|
| 310 |
+
spe_path: Path to the input SpatialExperiment object (.rds file).
|
| 311 |
+
output_filename: Name of the output plot file (e.g., 'umap.png').
|
| 312 |
+
reduction_type: Name of the dimensionality reduction result in reducedDims(spe).
|
| 313 |
+
x_axis: Column name for the x-axis (e.g., 'UMAP_1'). Defaults to type_1.
|
| 314 |
+
y_axis: Column name for the y-axis (e.g., 'UMAP_2'). Defaults to type_2.
|
| 315 |
+
color_by: Column name in colData or feature name to use for point color.
|
| 316 |
+
shape_by: Column name in colData to use for point shape.
|
| 317 |
+
assay: Name of assay to use if 'color_by' is a feature name.
|
| 318 |
+
point_size: Size of the points.
|
| 319 |
+
plot_width: Width of the output plot in inches.
|
| 320 |
+
plot_height: Height of the output plot in inches.
|
| 321 |
+
"""
|
| 322 |
+
# Input validation
|
| 323 |
+
if not spe_path.is_file():
|
| 324 |
+
raise FileNotFoundError(f"Input file not found: {spe_path}")
|
| 325 |
+
if not reduction_type:
|
| 326 |
+
raise ValueError("A reduction_type must be provided.")
|
| 327 |
+
|
| 328 |
+
# Prepare arguments for the R function
|
| 329 |
+
json_args = {
|
| 330 |
+
"type": reduction_type,
|
| 331 |
+
"x_axis": x_axis,
|
| 332 |
+
"y_axis": y_axis,
|
| 333 |
+
"color_by": color_by,
|
| 334 |
+
"shape_by": shape_by,
|
| 335 |
+
"assay": assay,
|
| 336 |
+
"point_size": point_size,
|
| 337 |
+
}
|
| 338 |
+
json_args = {k: v for k, v in json_args.items() if v is not None}
|
| 339 |
+
|
| 340 |
+
output_path = Path(output_filename)
|
| 341 |
+
|
| 342 |
+
return _run_ggspavis(
|
| 343 |
+
function_name="plotDimRed",
|
| 344 |
+
spe_path=spe_path,
|
| 345 |
+
output_path=output_path,
|
| 346 |
+
plot_width=plot_width,
|
| 347 |
+
plot_height=plot_height,
|
| 348 |
+
json_args=json_args,
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
@mcp.tool()
|
| 353 |
+
def ggspavis_plot_label(
|
| 354 |
+
spe_path: Path,
|
| 355 |
+
output_filename: str,
|
| 356 |
+
label: str,
|
| 357 |
+
platform: Literal["Visium", "ST", "VisiumHD", "mIF", "seqFISH", "MERFISH", "Xenium"] = "Visium",
|
| 358 |
+
palette: Optional[str] = None,
|
| 359 |
+
point_size: float = 1.5,
|
| 360 |
+
plot_width: float = 7.0,
|
| 361 |
+
plot_height: float = 7.0,
|
| 362 |
+
):
|
| 363 |
+
"""
|
| 364 |
+
Visualizes spatial clusters or other categorical labels using ggspavis::plotLabel.
|
| 365 |
+
|
| 366 |
+
Args:
|
| 367 |
+
spe_path: Path to the input SpatialExperiment object (.rds file).
|
| 368 |
+
output_filename: Name of the output plot file (e.g., 'clusters.png').
|
| 369 |
+
label: Column name in colData containing the labels to plot.
|
| 370 |
+
platform: The spatial transcriptomics platform.
|
| 371 |
+
palette: Optional color palette for labels. If None, ggplot2 default is used.
|
| 372 |
+
point_size: Size of the points.
|
| 373 |
+
plot_width: Width of the output plot in inches.
|
| 374 |
+
plot_height: Height of the output plot in inches.
|
| 375 |
+
"""
|
| 376 |
+
# Input validation
|
| 377 |
+
if not spe_path.is_file():
|
| 378 |
+
raise FileNotFoundError(f"Input file not found: {spe_path}")
|
| 379 |
+
if not label:
|
| 380 |
+
raise ValueError("A label column name must be provided.")
|
| 381 |
+
|
| 382 |
+
# Prepare arguments for the R function
|
| 383 |
+
json_args = {
|
| 384 |
+
"label": label,
|
| 385 |
+
"platform": platform,
|
| 386 |
+
"palette": palette,
|
| 387 |
+
"point_size": point_size,
|
| 388 |
+
}
|
| 389 |
+
json_args = {k: v for k, v in json_args.items() if v is not None}
|
| 390 |
+
|
| 391 |
+
output_path = Path(output_filename)
|
| 392 |
+
|
| 393 |
+
return _run_ggspavis(
|
| 394 |
+
function_name="plotLabel",
|
| 395 |
+
spe_path=spe_path,
|
| 396 |
+
output_path=output_path,
|
| 397 |
+
plot_width=plot_width,
|
| 398 |
+
plot_height=plot_height,
|
| 399 |
+
json_args=json_args,
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
if __name__ == "__main__":
|
| 403 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_shim_server.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import ast
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from mcp.server.fastmcp import FastMCP
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-ggspavis/app/bioconductor-ggspavis_server.py')
|
| 11 |
+
LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name)
|
| 12 |
+
SERVER_NAME = 'biosci_bioconductor_ggspavis'
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class _ShimMCP:
|
| 16 |
+
@staticmethod
|
| 17 |
+
def tool(*args, **kwargs):
|
| 18 |
+
if args and callable(args[0]) and len(args) == 1 and not kwargs:
|
| 19 |
+
return args[0]
|
| 20 |
+
def _decorator(fn):
|
| 21 |
+
return fn
|
| 22 |
+
return _decorator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _resolve_source_server():
|
| 26 |
+
if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name:
|
| 27 |
+
return LOCAL_SERVER
|
| 28 |
+
return SOURCE_SERVER
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _load_functions():
|
| 32 |
+
source_server = _resolve_source_server()
|
| 33 |
+
code = source_server.read_text(encoding="utf-8")
|
| 34 |
+
tree = ast.parse(code, filename=str(source_server))
|
| 35 |
+
function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
|
| 36 |
+
namespace = {
|
| 37 |
+
"__name__": "__mcp_source__",
|
| 38 |
+
"mcp": _ShimMCP(),
|
| 39 |
+
}
|
| 40 |
+
exec(compile(code, str(source_server), "exec"), namespace, namespace)
|
| 41 |
+
loaded = []
|
| 42 |
+
for name in function_names:
|
| 43 |
+
fn = namespace.get(name)
|
| 44 |
+
if callable(fn):
|
| 45 |
+
loaded.append(fn)
|
| 46 |
+
return loaded
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
mcp = FastMCP(SERVER_NAME)
|
| 50 |
+
for _fn in _load_functions():
|
| 51 |
+
mcp.tool()(_fn)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-ggspavis/app/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
Biomni/mcp_generated/mcp_bioconductor-ggspavis/docker-compose.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
mcp-bioconductor-ggspavis:
|
| 5 |
+
build: .
|
| 6 |
+
image: mcp-bioconductor-ggspavis:latest
|
| 7 |
+
container_name: mcp-bioconductor-ggspavis
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
environment:
|
| 11 |
+
- MCP_SERVER_NAME=bioconductor-ggspavis
|
| 12 |
+
volumes:
|
| 13 |
+
- ./workspace:/app/workspace
|
| 14 |
+
- ./output:/app/output
|
| 15 |
+
restart: unless-stopped
|
| 16 |
+
healthcheck:
|
| 17 |
+
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
| 18 |
+
interval: 30s
|
| 19 |
+
timeout: 10s
|
| 20 |
+
retries: 3
|
| 21 |
+
start_period: 5s
|
| 22 |
+
|
Biomni/mcp_generated/mcp_bioconductor-ggspavis/environment.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
name: mcp-tool
|
| 3 |
+
channels:
|
| 4 |
+
- bioconda
|
| 5 |
+
- conda-forge
|
| 6 |
+
- defaults
|
| 7 |
+
dependencies:
|
| 8 |
+
- bioconductor-ggspavis
|
| 9 |
+
- python=3.10
|
| 10 |
+
|
Biomni/mcp_generated/mcp_bioconductor-ggspavis/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
mcp
|
Biomni/mcp_generated/mcp_bioconductor-mastr/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install Miniconda
|
| 8 |
+
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
|
| 9 |
+
|
| 10 |
+
# Add conda to PATH
|
| 11 |
+
ENV PATH="/opt/conda/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Install bioconductor-mastr via conda (e.g., from bioconda)
|
| 14 |
+
RUN conda install -c bioconda bioconductor-mastr -y && conda clean -a
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
RUN pip install uv
|
| 18 |
+
RUN uv pip install --system fastmcp
|
| 19 |
+
|
| 20 |
+
# Create app directory
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Copy your MCP server
|
| 24 |
+
COPY app/bioconductor-mastr_server.py /app/
|
| 25 |
+
|
| 26 |
+
# Create workspace and output directories
|
| 27 |
+
RUN mkdir -p /app/workspace /app/output
|
| 28 |
+
|
| 29 |
+
# Make sure the server script is executable
|
| 30 |
+
RUN chmod +x /app/bioconductor-mastr_server.py
|
| 31 |
+
|
| 32 |
+
# Expose port for MCP over HTTP (optional)
|
| 33 |
+
EXPOSE 8000
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
|
| 37 |
+
|
| 38 |
+
# Default command runs the MCP server via stdio
|
| 39 |
+
CMD ["python", "/app/bioconductor-mastr_server.py"]
|
| 40 |
+
|
Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_server.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import tempfile
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Optional, List, Union
|
| 5 |
+
|
| 6 |
+
from mcp.server.fastmcp import FastMCP
|
| 7 |
+
|
| 8 |
+
SERVER_NAME = 'local_bioconductor_mastr'
|
| 9 |
+
mcp = FastMCP(SERVER_NAME)
|
| 10 |
+
|
| 11 |
+
@mcp.tool()
|
| 12 |
+
def mastr_align(
|
| 13 |
+
fasta_input: str,
|
| 14 |
+
output_alignment: str,
|
| 15 |
+
iterations: int = 100,
|
| 16 |
+
temperature: float = 1.0,
|
| 17 |
+
cooling_rate: float = 0.9,
|
| 18 |
+
gap_open: float = -10.0,
|
| 19 |
+
gap_extend: float = -1.0,
|
| 20 |
+
use_structure: bool = True,
|
| 21 |
+
r_path: str = "Rscript"
|
| 22 |
+
):
|
| 23 |
+
"""
|
| 24 |
+
Perform Multiple Alignment of Structural RNAs (MASTR) using the Bioconductor mastr package.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
fasta_input: Path to the input FASTA file containing RNA sequences.
|
| 28 |
+
output_alignment: Path where the resulting alignment (FASTA format) will be saved.
|
| 29 |
+
iterations: Number of iterations for the simulated annealing (default: 100).
|
| 30 |
+
temperature: Initial temperature for simulated annealing (default: 1.0).
|
| 31 |
+
cooling_rate: Cooling rate for simulated annealing (default: 0.9).
|
| 32 |
+
gap_open: Penalty for opening a gap (default: -10.0).
|
| 33 |
+
gap_extend: Penalty for extending a gap (default: -1.0).
|
| 34 |
+
use_structure: Whether to incorporate secondary structure prediction (default: True).
|
| 35 |
+
r_path: Path to the Rscript executable (default: 'Rscript').
|
| 36 |
+
"""
|
| 37 |
+
# Input validation
|
| 38 |
+
input_path = Path(fasta_input)
|
| 39 |
+
if not input_path.exists():
|
| 40 |
+
return {"error": f"Input file not found: {fasta_input}"}
|
| 41 |
+
|
| 42 |
+
output_path = Path(output_alignment)
|
| 43 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 44 |
+
|
| 45 |
+
if iterations <= 0:
|
| 46 |
+
return {"error": "Iterations must be a positive integer."}
|
| 47 |
+
|
| 48 |
+
if not (0 < cooling_rate < 1):
|
| 49 |
+
return {"error": "Cooling rate must be between 0 and 1."}
|
| 50 |
+
|
| 51 |
+
# Create an R script to execute the mastr package
|
| 52 |
+
# Note: mastr is a Bioconductor package, so we use Rscript to interface with it.
|
| 53 |
+
r_script_content = f"""
|
| 54 |
+
library(mastr)
|
| 55 |
+
library(Biostrings)
|
| 56 |
+
|
| 57 |
+
# Load sequences
|
| 58 |
+
seqs <- readRNAStringSet("{input_path.absolute()}")
|
| 59 |
+
|
| 60 |
+
# Define parameters
|
| 61 |
+
params <- list(
|
| 62 |
+
iterations = {iterations},
|
| 63 |
+
temp = {temperature},
|
| 64 |
+
cooling = {cooling_rate},
|
| 65 |
+
gapOpen = {gap_open},
|
| 66 |
+
gapExtend = {gap_extend},
|
| 67 |
+
useStructure = {"TRUE" if use_structure else "FALSE"}
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Run MASTR alignment
|
| 71 |
+
# Note: mastr() is the primary function in the Bioconductor package
|
| 72 |
+
alignment_result <- mastr(seqs,
|
| 73 |
+
iterations = params$iterations,
|
| 74 |
+
temp = params$temp,
|
| 75 |
+
cooling = params$cooling,
|
| 76 |
+
gapOpen = params$gapOpen,
|
| 77 |
+
gapExtend = params$gapExtend)
|
| 78 |
+
|
| 79 |
+
# Write output
|
| 80 |
+
writeXStringSet(alignment_result, "{output_path.absolute()}")
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp:
|
| 84 |
+
tmp.write(r_script_content)
|
| 85 |
+
tmp_path = tmp.name
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
# Execute the R script
|
| 89 |
+
process = subprocess.run(
|
| 90 |
+
[r_path, tmp_path],
|
| 91 |
+
capture_output=True,
|
| 92 |
+
text=True,
|
| 93 |
+
check=True
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
return {
|
| 97 |
+
"command_executed": f"{r_path} {tmp_path}",
|
| 98 |
+
"stdout": process.stdout,
|
| 99 |
+
"stderr": process.stderr,
|
| 100 |
+
"output_files": [str(output_path.absolute())],
|
| 101 |
+
"status": "Success"
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
except subprocess.CalledProcessError as e:
|
| 105 |
+
return {
|
| 106 |
+
"command_executed": f"{r_path} {tmp_path}",
|
| 107 |
+
"stdout": e.stdout,
|
| 108 |
+
"stderr": e.stderr,
|
| 109 |
+
"error": "R execution failed. Ensure 'mastr' and 'Biostrings' packages are installed in R.",
|
| 110 |
+
"status": "Error"
|
| 111 |
+
}
|
| 112 |
+
finally:
|
| 113 |
+
# Cleanup temporary R script
|
| 114 |
+
if Path(tmp_path).exists():
|
| 115 |
+
Path(tmp_path).unlink()
|
| 116 |
+
|
| 117 |
+
@mcp.tool()
|
| 118 |
+
def mastr_score_alignment(
|
| 119 |
+
alignment_file: str,
|
| 120 |
+
r_path: str = "Rscript"
|
| 121 |
+
):
|
| 122 |
+
"""
|
| 123 |
+
Calculate the structural conservation score for an existing RNA alignment using mastr metrics.
|
| 124 |
+
|
| 125 |
+
Args:
|
| 126 |
+
alignment_file: Path to the aligned FASTA/Stockholm file.
|
| 127 |
+
r_path: Path to the Rscript executable.
|
| 128 |
+
"""
|
| 129 |
+
input_path = Path(alignment_file)
|
| 130 |
+
if not input_path.exists():
|
| 131 |
+
return {"error": f"Alignment file not found: {alignment_file}"}
|
| 132 |
+
|
| 133 |
+
r_script_content = f"""
|
| 134 |
+
library(mastr)
|
| 135 |
+
library(Biostrings)
|
| 136 |
+
|
| 137 |
+
aln <- readRNAStringSet("{input_path.absolute()}")
|
| 138 |
+
# Calculate conservation score based on mastr's internal scoring
|
| 139 |
+
# This typically involves base-pairing probabilities and covariance
|
| 140 |
+
score <- mastr:::calculateScore(aln) # Accessing internal scoring if exported or via wrapper
|
| 141 |
+
cat("Alignment_Score:", score, "\\n")
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp:
|
| 145 |
+
tmp.write(r_script_content)
|
| 146 |
+
tmp_path = tmp.name
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
process = subprocess.run(
|
| 150 |
+
[r_path, tmp_path],
|
| 151 |
+
capture_output=True,
|
| 152 |
+
text=True,
|
| 153 |
+
check=True
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
return {
|
| 157 |
+
"command_executed": f"{r_path} {tmp_path}",
|
| 158 |
+
"stdout": process.stdout,
|
| 159 |
+
"stderr": process.stderr,
|
| 160 |
+
"status": "Success"
|
| 161 |
+
}
|
| 162 |
+
except subprocess.CalledProcessError as e:
|
| 163 |
+
return {
|
| 164 |
+
"error": str(e),
|
| 165 |
+
"stderr": e.stderr,
|
| 166 |
+
"status": "Error"
|
| 167 |
+
}
|
| 168 |
+
finally:
|
| 169 |
+
if Path(tmp_path).exists():
|
| 170 |
+
Path(tmp_path).unlink()
|
| 171 |
+
|
| 172 |
+
@mcp.tool()
|
| 173 |
+
def mastr_check_dependencies(r_path: str = "Rscript"):
|
| 174 |
+
"""
|
| 175 |
+
Verify if R and the required Bioconductor mastr package are installed and accessible.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
r_path: Path to the Rscript executable.
|
| 179 |
+
"""
|
| 180 |
+
r_script_content = """
|
| 181 |
+
if (!requireNamespace("mastr", quietly = TRUE)) {
|
| 182 |
+
stop("Package 'mastr' not found. Please install it via BiocManager::install('mastr')")
|
| 183 |
+
}
|
| 184 |
+
if (!requireNamespace("Biostrings", quietly = TRUE)) {
|
| 185 |
+
stop("Package 'Biostrings' not found.")
|
| 186 |
+
}
|
| 187 |
+
cat("All dependencies are satisfied.\\n")
|
| 188 |
+
"""
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
process = subprocess.run(
|
| 192 |
+
[r_path, "-e", r_script_content],
|
| 193 |
+
capture_output=True,
|
| 194 |
+
text=True,
|
| 195 |
+
check=True
|
| 196 |
+
)
|
| 197 |
+
return {
|
| 198 |
+
"status": "Ready",
|
| 199 |
+
"stdout": process.stdout
|
| 200 |
+
}
|
| 201 |
+
except Exception as e:
|
| 202 |
+
return {
|
| 203 |
+
"status": "Missing Dependencies",
|
| 204 |
+
"error": str(e)
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
if __name__ == "__main__":
|
| 208 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-mastr/app/bioconductor-mastr_shim_server.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import ast
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from mcp.server.fastmcp import FastMCP
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-mastr/app/bioconductor-mastr_server.py')
|
| 11 |
+
LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name)
|
| 12 |
+
SERVER_NAME = 'biosci_bioconductor_mastr'
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class _ShimMCP:
|
| 16 |
+
@staticmethod
|
| 17 |
+
def tool(*args, **kwargs):
|
| 18 |
+
if args and callable(args[0]) and len(args) == 1 and not kwargs:
|
| 19 |
+
return args[0]
|
| 20 |
+
def _decorator(fn):
|
| 21 |
+
return fn
|
| 22 |
+
return _decorator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _resolve_source_server():
|
| 26 |
+
if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name:
|
| 27 |
+
return LOCAL_SERVER
|
| 28 |
+
return SOURCE_SERVER
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _load_functions():
|
| 32 |
+
source_server = _resolve_source_server()
|
| 33 |
+
code = source_server.read_text(encoding="utf-8")
|
| 34 |
+
tree = ast.parse(code, filename=str(source_server))
|
| 35 |
+
function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
|
| 36 |
+
namespace = {
|
| 37 |
+
"__name__": "__mcp_source__",
|
| 38 |
+
"mcp": _ShimMCP(),
|
| 39 |
+
}
|
| 40 |
+
exec(compile(code, str(source_server), "exec"), namespace, namespace)
|
| 41 |
+
loaded = []
|
| 42 |
+
for name in function_names:
|
| 43 |
+
fn = namespace.get(name)
|
| 44 |
+
if callable(fn):
|
| 45 |
+
loaded.append(fn)
|
| 46 |
+
return loaded
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
mcp = FastMCP(SERVER_NAME)
|
| 50 |
+
for _fn in _load_functions():
|
| 51 |
+
mcp.tool()(_fn)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-mastr/app/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
Biomni/mcp_generated/mcp_bioconductor-mastr/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
mcp
|
Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install Miniconda
|
| 8 |
+
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
|
| 9 |
+
|
| 10 |
+
# Add conda to PATH
|
| 11 |
+
ENV PATH="/opt/conda/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Install bioconductor-rhdf5filters via conda (e.g., from bioconda)
|
| 14 |
+
RUN conda install -c bioconda bioconductor-rhdf5filters -y && conda clean -a
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
RUN pip install uv
|
| 18 |
+
RUN uv pip install --system fastmcp
|
| 19 |
+
|
| 20 |
+
# Create app directory
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Copy your MCP server
|
| 24 |
+
COPY app/bioconductor-rhdf5filters_server.py /app/
|
| 25 |
+
|
| 26 |
+
# Create workspace and output directories
|
| 27 |
+
RUN mkdir -p /app/workspace /app/output
|
| 28 |
+
|
| 29 |
+
# Make sure the server script is executable
|
| 30 |
+
RUN chmod +x /app/bioconductor-rhdf5filters_server.py
|
| 31 |
+
|
| 32 |
+
# Expose port for MCP over HTTP (optional)
|
| 33 |
+
EXPOSE 8000
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
|
| 37 |
+
|
| 38 |
+
# Default command runs the MCP server via stdio
|
| 39 |
+
CMD ["python", "/app/bioconductor-rhdf5filters_server.py"]
|
| 40 |
+
|
Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/bioconductor-rhdf5filters_server.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import tempfile
|
| 3 |
+
import shutil
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, Any, List
|
| 6 |
+
|
| 7 |
+
# MCP decorator is commented out as per instructions
|
| 8 |
+
from mcp.server.fastmcp import FastMCP
|
| 9 |
+
|
| 10 |
+
SERVER_NAME = 'local_bioconductor_rhdf5filters'
|
| 11 |
+
mcp = FastMCP(SERVER_NAME)
|
| 12 |
+
|
| 13 |
+
@mcp.tool()
|
| 14 |
+
def check_installation() -> Dict[str, Any]:
|
| 15 |
+
"""
|
| 16 |
+
Verifies the installation of the bioconductor-rhdf5filters R library.
|
| 17 |
+
|
| 18 |
+
bioconductor-rhdf5filters is an R library that provides HDF5 compression
|
| 19 |
+
filters and does not have a direct command-line interface. This function
|
| 20 |
+
executes a simple R script to confirm that the library is installed and
|
| 21 |
+
can be loaded in the R environment, and it reports the installed version.
|
| 22 |
+
"""
|
| 23 |
+
# Pre-flight check for Rscript executable
|
| 24 |
+
if not shutil.which("Rscript"):
|
| 25 |
+
raise FileNotFoundError("Rscript executable not found in PATH. R is required to run this tool.")
|
| 26 |
+
|
| 27 |
+
r_script_content = """
|
| 28 |
+
if (!requireNamespace("rhdf5filters", quietly = TRUE)) {
|
| 29 |
+
cat("Error: The 'rhdf5filters' package is not installed.\\n", file = stderr())
|
| 30 |
+
quit(status = 1)
|
| 31 |
+
}
|
| 32 |
+
ver <- as.character(packageVersion("rhdf5filters"))
|
| 33 |
+
cat(paste0("Successfully loaded 'rhdf5filters' version: ", ver, "\\n"))
|
| 34 |
+
quit(status = 0)
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
command_executed = ""
|
| 38 |
+
script_path = ""
|
| 39 |
+
try:
|
| 40 |
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".R", encoding='utf-8') as tmp_r_script:
|
| 41 |
+
script_path = tmp_r_script.name
|
| 42 |
+
tmp_r_script.write(r_script_content)
|
| 43 |
+
|
| 44 |
+
cmd: List[str] = ["Rscript", script_path]
|
| 45 |
+
command_executed = " ".join(cmd)
|
| 46 |
+
|
| 47 |
+
process = subprocess.run(
|
| 48 |
+
cmd,
|
| 49 |
+
capture_output=True,
|
| 50 |
+
text=True,
|
| 51 |
+
check=True,
|
| 52 |
+
encoding='utf-8'
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
"command_executed": command_executed,
|
| 57 |
+
"stdout": process.stdout,
|
| 58 |
+
"stderr": process.stderr,
|
| 59 |
+
"output_files": []
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
except subprocess.CalledProcessError as e:
|
| 63 |
+
# This will catch the case where the R script exits with a non-zero status
|
| 64 |
+
# (e.g., package not installed) and return a structured error.
|
| 65 |
+
return {
|
| 66 |
+
"command_executed": command_executed,
|
| 67 |
+
"stdout": e.stdout,
|
| 68 |
+
"stderr": e.stderr,
|
| 69 |
+
"return_code": e.returncode,
|
| 70 |
+
"error": "R script execution failed. The 'rhdf5filters' package may not be installed or accessible."
|
| 71 |
+
}
|
| 72 |
+
finally:
|
| 73 |
+
# Ensure the temporary file is cleaned up
|
| 74 |
+
if script_path and Path(script_path).exists():
|
| 75 |
+
Path(script_path).unlink()
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/app/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/docker-compose.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
mcp-bioconductor-rhdf5filters:
|
| 5 |
+
build: .
|
| 6 |
+
image: mcp-bioconductor-rhdf5filters:latest
|
| 7 |
+
container_name: mcp-bioconductor-rhdf5filters
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
environment:
|
| 11 |
+
- MCP_SERVER_NAME=bioconductor-rhdf5filters
|
| 12 |
+
volumes:
|
| 13 |
+
- ./workspace:/app/workspace
|
| 14 |
+
- ./output:/app/output
|
| 15 |
+
restart: unless-stopped
|
| 16 |
+
healthcheck:
|
| 17 |
+
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
| 18 |
+
interval: 30s
|
| 19 |
+
timeout: 10s
|
| 20 |
+
retries: 3
|
| 21 |
+
start_period: 5s
|
| 22 |
+
|
Biomni/mcp_generated/mcp_bioconductor-rhdf5filters/environment.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
name: mcp-tool
|
| 3 |
+
channels:
|
| 4 |
+
- bioconda
|
| 5 |
+
- conda-forge
|
| 6 |
+
- defaults
|
| 7 |
+
dependencies:
|
| 8 |
+
- bioconductor-rhdf5filters
|
| 9 |
+
- python=3.10
|
| 10 |
+
|
Biomni/mcp_generated/mcp_bioconductor-rsubread/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install Miniconda
|
| 8 |
+
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
|
| 9 |
+
|
| 10 |
+
# Add conda to PATH
|
| 11 |
+
ENV PATH="/opt/conda/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Install bioconductor-rsubread via conda (e.g., from bioconda)
|
| 14 |
+
RUN conda install -c bioconda bioconductor-rsubread -y && conda clean -a
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
RUN pip install uv
|
| 18 |
+
RUN uv pip install --system fastmcp
|
| 19 |
+
|
| 20 |
+
# Create app directory
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Copy your MCP server
|
| 24 |
+
COPY app/bioconductor-rsubread_server.py /app/
|
| 25 |
+
|
| 26 |
+
# Create workspace and output directories
|
| 27 |
+
RUN mkdir -p /app/workspace /app/output
|
| 28 |
+
|
| 29 |
+
# Make sure the server script is executable
|
| 30 |
+
RUN chmod +x /app/bioconductor-rsubread_server.py
|
| 31 |
+
|
| 32 |
+
# Expose port for MCP over HTTP (optional)
|
| 33 |
+
EXPOSE 8000
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
|
| 37 |
+
|
| 38 |
+
# Default command runs the MCP server via stdio
|
| 39 |
+
CMD ["python", "/app/bioconductor-rsubread_server.py"]
|
| 40 |
+
|
Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_server.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Optional, List, Union
|
| 5 |
+
import tempfile
|
| 6 |
+
|
| 7 |
+
def run_r_command(command: str):
|
| 8 |
+
"""Helper to execute R commands via Rscript"""
|
| 9 |
+
try:
|
| 10 |
+
# We use -e to execute the expression directly
|
| 11 |
+
full_cmd = ["Rscript", "-e", f"library(Rsubread); {command}"]
|
| 12 |
+
result = subprocess.run(full_cmd, capture_output=True, text=True, check=True)
|
| 13 |
+
return result.stdout, result.stderr
|
| 14 |
+
except subprocess.CalledProcessError as e:
|
| 15 |
+
raise RuntimeError(f"Rsubread execution failed: {e.stderr}")
|
| 16 |
+
|
| 17 |
+
from mcp.server.fastmcp import FastMCP
|
| 18 |
+
|
| 19 |
+
SERVER_NAME = 'local_bioconductor_rsubread'
|
| 20 |
+
mcp = FastMCP(SERVER_NAME)
|
| 21 |
+
|
| 22 |
+
@mcp.tool()
|
| 23 |
+
def rsubread_buildindex(
|
| 24 |
+
basename: str,
|
| 25 |
+
reference: str,
|
| 26 |
+
gapped_index: bool = False,
|
| 27 |
+
index_memory: int = 8000,
|
| 28 |
+
colorspace: bool = False,
|
| 29 |
+
split_index: bool = False
|
| 30 |
+
):
|
| 31 |
+
"""
|
| 32 |
+
Builds an index for a reference genome.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
basename: Base name of the index files to be generated.
|
| 36 |
+
reference: Path to the reference genome file (FASTA format).
|
| 37 |
+
gapped_index: If True, a gapped index will be built (saves memory).
|
| 38 |
+
index_memory: Memory usage in MB.
|
| 39 |
+
colorspace: If True, index is built for color-space reads (SOLiD).
|
| 40 |
+
split_index: If True, the index will be split into multiple blocks.
|
| 41 |
+
"""
|
| 42 |
+
ref_path = Path(reference)
|
| 43 |
+
if not ref_path.exists():
|
| 44 |
+
return {"error": f"Reference file {reference} not found."}
|
| 45 |
+
|
| 46 |
+
# Construct R command
|
| 47 |
+
r_cmd = (
|
| 48 |
+
f"buildindex(basename='{basename}', reference='{reference}', "
|
| 49 |
+
f"gappedIndex={'TRUE' if gapped_index else 'FALSE'}, "
|
| 50 |
+
f"indexMemory={index_memory}, colorspace={'TRUE' if colorspace else 'FALSE'}, "
|
| 51 |
+
f"splitIndex={'TRUE' if split_index else 'FALSE'})"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
stdout, stderr = run_r_command(r_cmd)
|
| 56 |
+
return {
|
| 57 |
+
"command_executed": f"Rsubread::buildindex({basename})",
|
| 58 |
+
"stdout": stdout,
|
| 59 |
+
"stderr": stderr,
|
| 60 |
+
"output_files": [str(Path(f"{basename}.{ext}")) for ext in ["files"]] # Rsubread creates multiple files
|
| 61 |
+
}
|
| 62 |
+
except Exception as e:
|
| 63 |
+
return {"error": str(e)}
|
| 64 |
+
|
| 65 |
+
@mcp.tool()
|
| 66 |
+
def rsubread_align(
|
| 67 |
+
index: str,
|
| 68 |
+
readfile1: str,
|
| 69 |
+
readfile2: Optional[str] = None,
|
| 70 |
+
output_file: str = "output.bam",
|
| 71 |
+
input_format: str = "gzFASTQ",
|
| 72 |
+
output_format: str = "BAM",
|
| 73 |
+
nthreads: int = 1,
|
| 74 |
+
type: str = "dna",
|
| 75 |
+
phred_offset: int = 33,
|
| 76 |
+
unique: bool = False,
|
| 77 |
+
n_best_locations: int = 1,
|
| 78 |
+
max_mismatches: int = 3
|
| 79 |
+
):
|
| 80 |
+
"""
|
| 81 |
+
Aligns reads to a reference genome using the Subread aligner.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
index: Base name of the index.
|
| 85 |
+
readfile1: Path to the first read file.
|
| 86 |
+
readfile2: Path to the second read file (for paired-end).
|
| 87 |
+
output_file: Name of the output file.
|
| 88 |
+
input_format: Format of input reads ('FASTQ', 'gzFASTQ', 'SAM', 'BAM').
|
| 89 |
+
output_format: Format of output ('BAM', 'SAM').
|
| 90 |
+
nthreads: Number of threads to use.
|
| 91 |
+
type: Type of sequencing ('dna' or 'rna').
|
| 92 |
+
phred_offset: Phred score offset (33 or 64).
|
| 93 |
+
unique: If True, only uniquely mapped reads are reported.
|
| 94 |
+
n_best_locations: Number of best mapping locations to report.
|
| 95 |
+
max_mismatches: Maximum number of mismatches allowed.
|
| 96 |
+
"""
|
| 97 |
+
if not Path(readfile1).exists():
|
| 98 |
+
return {"error": f"Read file {readfile1} not found."}
|
| 99 |
+
|
| 100 |
+
read2_param = f"'{readfile2}'" if readfile2 else "NULL"
|
| 101 |
+
|
| 102 |
+
r_cmd = (
|
| 103 |
+
f"align(index='{index}', readfile1='{readfile1}', readfile2={read2_param}, "
|
| 104 |
+
f"output_file='{output_file}', input_format='{input_format}', output_format='{output_format}', "
|
| 105 |
+
f"nthreads={nthreads}, type='{type}', phredOffset={phred_offset}, "
|
| 106 |
+
f"unique={'TRUE' if unique else 'FALSE'}, nBestLocations={n_best_locations}, "
|
| 107 |
+
f"maxMismatches={max_mismatches})"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
stdout, stderr = run_r_command(r_cmd)
|
| 112 |
+
return {
|
| 113 |
+
"command_executed": "Rsubread::align",
|
| 114 |
+
"stdout": stdout,
|
| 115 |
+
"stderr": stderr,
|
| 116 |
+
"output_files": [output_file]
|
| 117 |
+
}
|
| 118 |
+
except Exception as e:
|
| 119 |
+
return {"error": str(e)}
|
| 120 |
+
|
| 121 |
+
@mcp.tool()
|
| 122 |
+
def rsubread_subjunc(
|
| 123 |
+
index: str,
|
| 124 |
+
readfile1: str,
|
| 125 |
+
readfile2: Optional[str] = None,
|
| 126 |
+
output_file: str = "output_subjunc.bam",
|
| 127 |
+
input_format: str = "gzFASTQ",
|
| 128 |
+
output_format: str = "BAM",
|
| 129 |
+
nthreads: int = 1,
|
| 130 |
+
phred_offset: int = 33,
|
| 131 |
+
unique: bool = False
|
| 132 |
+
):
|
| 133 |
+
"""
|
| 134 |
+
Aligns RNA-seq reads and detects exon-exon junctions using the Subjunc aligner.
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
index: Base name of the index.
|
| 138 |
+
readfile1: Path to the first read file.
|
| 139 |
+
readfile2: Path to the second read file (for paired-end).
|
| 140 |
+
output_file: Name of the output file.
|
| 141 |
+
input_format: Format of input reads.
|
| 142 |
+
output_format: Format of output.
|
| 143 |
+
nthreads: Number of threads.
|
| 144 |
+
phred_offset: Phred score offset.
|
| 145 |
+
unique: If True, only uniquely mapped reads are reported.
|
| 146 |
+
"""
|
| 147 |
+
read2_param = f"'{readfile2}'" if readfile2 else "NULL"
|
| 148 |
+
|
| 149 |
+
r_cmd = (
|
| 150 |
+
f"subjunc(index='{index}', readfile1='{readfile1}', readfile2={read2_param}, "
|
| 151 |
+
f"output_file='{output_file}', input_format='{input_format}', output_format='{output_format}', "
|
| 152 |
+
f"nthreads={nthreads}, phredOffset={phred_offset}, unique={'TRUE' if unique else 'FALSE'})"
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
try:
|
| 156 |
+
stdout, stderr = run_r_command(r_cmd)
|
| 157 |
+
return {
|
| 158 |
+
"command_executed": "Rsubread::subjunc",
|
| 159 |
+
"stdout": stdout,
|
| 160 |
+
"stderr": stderr,
|
| 161 |
+
"output_files": [output_file]
|
| 162 |
+
}
|
| 163 |
+
except Exception as e:
|
| 164 |
+
return {"error": str(e)}
|
| 165 |
+
|
| 166 |
+
@mcp.tool()
|
| 167 |
+
def rsubread_featurecounts(
|
| 168 |
+
files: List[str],
|
| 169 |
+
annot_ext: str,
|
| 170 |
+
is_gtf_annotation_file: bool = True,
|
| 171 |
+
gtf_feature_type: str = "exon",
|
| 172 |
+
gtf_attr_type: str = "gene_id",
|
| 173 |
+
use_meta_features: bool = True,
|
| 174 |
+
allow_multi_overlap: bool = False,
|
| 175 |
+
is_paired_end: bool = False,
|
| 176 |
+
nthreads: int = 1,
|
| 177 |
+
strand_specific: int = 0,
|
| 178 |
+
count_multi_mapping_reads: bool = False
|
| 179 |
+
):
|
| 180 |
+
"""
|
| 181 |
+
Assigns mapped reads to genomic features (e.g. genes, exons).
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
files: List of paths to alignment files (BAM/SAM).
|
| 185 |
+
annot_ext: Path to the annotation file (GTF/GFF/SAF).
|
| 186 |
+
is_gtf_annotation_file: Whether the annotation file is in GTF format.
|
| 187 |
+
gtf_feature_type: Feature type in GTF to be used (e.g. 'exon').
|
| 188 |
+
gtf_attr_type: Attribute type in GTF to be used (e.g. 'gene_id').
|
| 189 |
+
use_meta_features: If True, reads are counted for meta-features (e.g. genes).
|
| 190 |
+
allow_multi_overlap: If True, a read is allowed to be assigned to multiple features.
|
| 191 |
+
is_paired_end: If True, reads are treated as paired-end.
|
| 192 |
+
nthreads: Number of threads.
|
| 193 |
+
strand_specific: Strand specificity (0: unstranded, 1: stranded, 2: reversely stranded).
|
| 194 |
+
count_multi_mapping_reads: If True, multi-mapping reads will be counted.
|
| 195 |
+
"""
|
| 196 |
+
# Convert Python list to R vector
|
| 197 |
+
files_r = "c(" + ",".join([f"'{f}'" for f in files]) + ")"
|
| 198 |
+
|
| 199 |
+
r_cmd = (
|
| 200 |
+
f"fc <- featureCounts(files={files_r}, annot.ext='{annot_ext}', "
|
| 201 |
+
f"isGTFAnnotationFile={'TRUE' if is_gtf_annotation_file else 'FALSE'}, "
|
| 202 |
+
f"GTF.featureType='{gtf_feature_type}', GTF.attrType='{gtf_attr_type}', "
|
| 203 |
+
f"useMetaFeatures={'TRUE' if use_meta_features else 'FALSE'}, "
|
| 204 |
+
f"allowMultiOverlap={'TRUE' if allow_multi_overlap else 'FALSE'}, "
|
| 205 |
+
f"isPairedEnd={'TRUE' if is_paired_end else 'FALSE'}, "
|
| 206 |
+
f"nthreads={nthreads}, strandSpecific={strand_specific}, "
|
| 207 |
+
f"countMultiMappingReads={'TRUE' if count_multi_mapping_reads else 'FALSE'}); "
|
| 208 |
+
f"write.table(fc$counts, file='counts.txt', sep='\\t', quote=FALSE); "
|
| 209 |
+
f"write.table(fc$stat, file='stats.txt', sep='\\t', quote=FALSE)"
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
try:
|
| 213 |
+
stdout, stderr = run_r_command(r_cmd)
|
| 214 |
+
return {
|
| 215 |
+
"command_executed": "Rsubread::featureCounts",
|
| 216 |
+
"stdout": stdout,
|
| 217 |
+
"stderr": stderr,
|
| 218 |
+
"output_files": ["counts.txt", "stats.txt"]
|
| 219 |
+
}
|
| 220 |
+
except Exception as e:
|
| 221 |
+
return {"error": str(e)}
|
| 222 |
+
|
| 223 |
+
@mcp.tool()
|
| 224 |
+
def rsubread_qualityscores(
|
| 225 |
+
filename: str,
|
| 226 |
+
input_format: str = "gzFASTQ",
|
| 227 |
+
offset: int = 33,
|
| 228 |
+
nreads: int = 10000
|
| 229 |
+
):
|
| 230 |
+
"""
|
| 231 |
+
Extracts quality scores from a read file.
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
filename: Path to the read file.
|
| 235 |
+
input_format: Format of the input file.
|
| 236 |
+
offset: Phred score offset.
|
| 237 |
+
nreads: Number of reads to extract quality scores from.
|
| 238 |
+
"""
|
| 239 |
+
if not Path(filename).exists():
|
| 240 |
+
return {"error": f"File {filename} not found."}
|
| 241 |
+
|
| 242 |
+
r_cmd = (
|
| 243 |
+
f"qs <- qualityScores(filename='{filename}', input_format='{input_format}', "
|
| 244 |
+
f"offset={offset}, nreads={nreads}); "
|
| 245 |
+
f"write.table(qs, file='quality_scores.txt', sep='\\t', quote=FALSE)"
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
try:
|
| 249 |
+
stdout, stderr = run_r_command(r_cmd)
|
| 250 |
+
return {
|
| 251 |
+
"command_executed": "Rsubread::qualityScores",
|
| 252 |
+
"stdout": stdout,
|
| 253 |
+
"stderr": stderr,
|
| 254 |
+
"output_files": ["quality_scores.txt"]
|
| 255 |
+
}
|
| 256 |
+
except Exception as e:
|
| 257 |
+
return {"error": str(e)}
|
| 258 |
+
|
| 259 |
+
@mcp.tool()
|
| 260 |
+
def rsubread_atgccontent(
|
| 261 |
+
filename: str,
|
| 262 |
+
input_format: str = "gzFASTQ",
|
| 263 |
+
basewise: bool = True
|
| 264 |
+
):
|
| 265 |
+
"""
|
| 266 |
+
Calculates ATGC content of reads.
|
| 267 |
+
|
| 268 |
+
Args:
|
| 269 |
+
filename: Path to the read file.
|
| 270 |
+
input_format: Format of the input file.
|
| 271 |
+
basewise: If True, calculate content for each base position.
|
| 272 |
+
"""
|
| 273 |
+
r_cmd = (
|
| 274 |
+
f"atgc <- atgcContent(filename='{filename}', input_format='{input_format}', "
|
| 275 |
+
f"basewise={'TRUE' if basewise else 'FALSE'}); "
|
| 276 |
+
f"write.table(atgc, file='atgc_content.txt', sep='\\t', quote=FALSE)"
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
try:
|
| 280 |
+
stdout, stderr = run_r_command(r_cmd)
|
| 281 |
+
return {
|
| 282 |
+
"command_executed": "Rsubread::atgcContent",
|
| 283 |
+
"stdout": stdout,
|
| 284 |
+
"stderr": stderr,
|
| 285 |
+
"output_files": ["atgc_content.txt"]
|
| 286 |
+
}
|
| 287 |
+
except Exception as e:
|
| 288 |
+
return {"error": str(e)}
|
| 289 |
+
|
| 290 |
+
@mcp.tool()
|
| 291 |
+
def rsubread_propmapped(
|
| 292 |
+
files: List[str],
|
| 293 |
+
count_multi_mapping_reads: bool = False
|
| 294 |
+
):
|
| 295 |
+
"""
|
| 296 |
+
Calculates the proportion of mapped reads in BAM/SAM files.
|
| 297 |
+
|
| 298 |
+
Args:
|
| 299 |
+
files: List of paths to alignment files.
|
| 300 |
+
count_multi_mapping_reads: Whether to include multi-mapping reads.
|
| 301 |
+
"""
|
| 302 |
+
files_r = "c(" + ",".join([f"'{f}'" for f in files]) + ")"
|
| 303 |
+
|
| 304 |
+
r_cmd = (
|
| 305 |
+
f"pm <- propMapped(files={files_r}, "
|
| 306 |
+
f"countMultiMappingReads={'TRUE' if count_multi_mapping_reads else 'FALSE'}); "
|
| 307 |
+
f"write.table(pm, file='prop_mapped.txt', sep='\\t', quote=FALSE, row.names=FALSE)"
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
try:
|
| 311 |
+
stdout, stderr = run_r_command(r_cmd)
|
| 312 |
+
return {
|
| 313 |
+
"command_executed": "Rsubread::propMapped",
|
| 314 |
+
"stdout": stdout,
|
| 315 |
+
"stderr": stderr,
|
| 316 |
+
"output_files": ["prop_mapped.txt"]
|
| 317 |
+
}
|
| 318 |
+
except Exception as e:
|
| 319 |
+
return {"error": str(e)}
|
| 320 |
+
|
| 321 |
+
if __name__ == "__main__":
|
| 322 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-rsubread/app/bioconductor-rsubread_shim_server.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import ast
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from mcp.server.fastmcp import FastMCP
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_help_txt/mcp_bioconductor-rsubread/app/bioconductor-rsubread_server.py')
|
| 11 |
+
LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name)
|
| 12 |
+
SERVER_NAME = 'biosci_bioconductor_rsubread'
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class _ShimMCP:
|
| 16 |
+
@staticmethod
|
| 17 |
+
def tool(*args, **kwargs):
|
| 18 |
+
if args and callable(args[0]) and len(args) == 1 and not kwargs:
|
| 19 |
+
return args[0]
|
| 20 |
+
def _decorator(fn):
|
| 21 |
+
return fn
|
| 22 |
+
return _decorator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _resolve_source_server():
|
| 26 |
+
if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name:
|
| 27 |
+
return LOCAL_SERVER
|
| 28 |
+
return SOURCE_SERVER
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _load_functions():
|
| 32 |
+
source_server = _resolve_source_server()
|
| 33 |
+
code = source_server.read_text(encoding="utf-8")
|
| 34 |
+
tree = ast.parse(code, filename=str(source_server))
|
| 35 |
+
function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
|
| 36 |
+
namespace = {
|
| 37 |
+
"__name__": "__mcp_source__",
|
| 38 |
+
"mcp": _ShimMCP(),
|
| 39 |
+
}
|
| 40 |
+
exec(compile(code, str(source_server), "exec"), namespace, namespace)
|
| 41 |
+
loaded = []
|
| 42 |
+
for name in function_names:
|
| 43 |
+
fn = namespace.get(name)
|
| 44 |
+
if callable(fn):
|
| 45 |
+
loaded.append(fn)
|
| 46 |
+
return loaded
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
mcp = FastMCP(SERVER_NAME)
|
| 50 |
+
for _fn in _load_functions():
|
| 51 |
+
mcp.tool()(_fn)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-rsubread/app/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
Biomni/mcp_generated/mcp_bioconductor-rtracklayer/app/bioconductor-rtracklayer_server.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import tempfile
|
| 3 |
+
import re
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Optional, List
|
| 6 |
+
|
| 7 |
+
# @mcp.tool() is a placeholder for the actual decorator.
|
| 8 |
+
# The code is written to be compatible with the MCP framework.
|
| 9 |
+
|
| 10 |
+
def mcp_tool_placeholder():
|
| 11 |
+
def decorator(func):
|
| 12 |
+
return func
|
| 13 |
+
return decorator
|
| 14 |
+
|
| 15 |
+
@mcp_tool_placeholder()
|
| 16 |
+
def rtracklayer_convert(
|
| 17 |
+
input_file: Path,
|
| 18 |
+
output_file: Path,
|
| 19 |
+
input_format: Optional[str] = None,
|
| 20 |
+
output_format: Optional[str] = None,
|
| 21 |
+
which: Optional[str] = None,
|
| 22 |
+
genome: Optional[str] = None,
|
| 23 |
+
gff_feature_type_filter: Optional[str] = None,
|
| 24 |
+
gff_version: Optional[str] = None,
|
| 25 |
+
export_source: Optional[str] = None,
|
| 26 |
+
index_output: bool = False,
|
| 27 |
+
):
|
| 28 |
+
"""
|
| 29 |
+
Converts genomic data files from one format to another using rtracklayer.
|
| 30 |
+
|
| 31 |
+
This tool wraps the core import() and export() functionality of the rtracklayer
|
| 32 |
+
R package, allowing for conversion between formats like BED, GFF, GTF, WIG,
|
| 33 |
+
and BigWig. It also supports filtering by genomic region.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
input_file: Path to the input genomic data file.
|
| 37 |
+
output_file: Path to write the converted output file.
|
| 38 |
+
input_format: The format of the input file (e.g., 'bed', 'gff', 'bigWig').
|
| 39 |
+
If not provided, rtracklayer will guess from the file extension.
|
| 40 |
+
output_format: The format for the output file. If not provided, rtracklayer
|
| 41 |
+
will guess from the file extension.
|
| 42 |
+
which: A specific genomic region to process, in the format 'chr:start-end'
|
| 43 |
+
(e.g., 'chr1:1000-2000'). This is highly effective for indexed files.
|
| 44 |
+
genome: The genome assembly name (e.g., 'hg19', 'mm10').
|
| 45 |
+
gff_feature_type_filter: For GFF/GTF import, a comma-separated list of
|
| 46 |
+
feature types to keep (e.g., 'exon,gene').
|
| 47 |
+
gff_version: Specify the GFF version ('1', '2', or '3') for import/export.
|
| 48 |
+
export_source: The 'source' field to use for GFF/GTF export.
|
| 49 |
+
index_output: If True, create a tabix index (.tbi) for the output file.
|
| 50 |
+
Applicable to formats like BED and GFF.
|
| 51 |
+
"""
|
| 52 |
+
# 1. Input Validation
|
| 53 |
+
if not input_file.exists():
|
| 54 |
+
raise FileNotFoundError(f"Input file not found: {input_file}")
|
| 55 |
+
|
| 56 |
+
ALLOWED_FORMATS = ["bed", "gff", "gff1", "gff2", "gff3", "gtf", "wig", "bigWig", "bedGraph", "2bit", "fasta"]
|
| 57 |
+
if input_format and input_format not in ALLOWED_FORMATS:
|
| 58 |
+
raise ValueError(f"Invalid input_format '{input_format}'. Allowed formats: {ALLOWED_FORMATS}")
|
| 59 |
+
if output_format and output_format not in ALLOWED_FORMATS:
|
| 60 |
+
raise ValueError(f"Invalid output_format '{output_format}'. Allowed formats: {ALLOWED_FORMATS}")
|
| 61 |
+
|
| 62 |
+
if gff_version and gff_version not in ["1", "2", "3"]:
|
| 63 |
+
raise ValueError("gff_version must be one of '1', '2', or '3'.")
|
| 64 |
+
|
| 65 |
+
if which and not re.match(r"^\w+:\d+-\d+$", which):
|
| 66 |
+
raise ValueError("Invalid 'which' format. Expected 'chr:start-end' (e.g., 'chr1:1000-5000').")
|
| 67 |
+
|
| 68 |
+
# 2. Build R Script
|
| 69 |
+
r_script_lines = [
|
| 70 |
+
"suppressPackageStartupMessages(library(rtracklayer))",
|
| 71 |
+
"suppressPackageStartupMessages(library(GenomicRanges))",
|
| 72 |
+
"",
|
| 73 |
+
"# --- Import Arguments ---",
|
| 74 |
+
f"import_args <- list(con = '{input_file.resolve()}')",
|
| 75 |
+
]
|
| 76 |
+
if input_format:
|
| 77 |
+
r_script_lines.append(f"import_args$format <- '{input_format}'")
|
| 78 |
+
if which:
|
| 79 |
+
r_script_lines.append(f"import_args$which <- GRanges('{which}')")
|
| 80 |
+
if genome:
|
| 81 |
+
r_script_lines.append(f"import_args$genome <- '{genome}'")
|
| 82 |
+
if gff_feature_type_filter:
|
| 83 |
+
r_vector = 'c(' + ', '.join([f'"{f.strip()}"' for f in gff_feature_type_filter.split(',')]) + ')'
|
| 84 |
+
r_script_lines.append(f"import_args$feature.type <- {r_vector}")
|
| 85 |
+
if gff_version:
|
| 86 |
+
r_script_lines.append(f"import_args$version <- '{gff_version}'")
|
| 87 |
+
|
| 88 |
+
r_script_lines.extend([
|
| 89 |
+
"",
|
| 90 |
+
"# --- Import Data ---",
|
| 91 |
+
"gr_object <- do.call(rtracklayer::import, import_args)",
|
| 92 |
+
"",
|
| 93 |
+
"# --- Export Arguments ---",
|
| 94 |
+
f"export_args <- list(object = gr_object, con = '{output_file.resolve()}')",
|
| 95 |
+
])
|
| 96 |
+
|
| 97 |
+
if output_format:
|
| 98 |
+
r_script_lines.append(f"export_args$format <- '{output_format}'")
|
| 99 |
+
if export_source:
|
| 100 |
+
r_script_lines.append(f"export_args$source <- '{export_source}'")
|
| 101 |
+
if gff_version:
|
| 102 |
+
r_script_lines.append(f"export_args$version <- '{gff_version}'")
|
| 103 |
+
if index_output:
|
| 104 |
+
r_script_lines.append("export_args$index <- TRUE")
|
| 105 |
+
|
| 106 |
+
r_script_lines.extend([
|
| 107 |
+
"",
|
| 108 |
+
"# --- Export Data ---",
|
| 109 |
+
"do.call(rtracklayer::export, export_args)",
|
| 110 |
+
"cat('Conversion successful.\\n')"
|
| 111 |
+
])
|
| 112 |
+
|
| 113 |
+
r_script = "\n".join(r_script_lines)
|
| 114 |
+
|
| 115 |
+
# 3. Execute R Script
|
| 116 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False, dir=".") as tmp_script:
|
| 117 |
+
script_path = Path(tmp_script.name)
|
| 118 |
+
tmp_script.write(r_script)
|
| 119 |
+
|
| 120 |
+
command = ["Rscript", str(script_path.name)]
|
| 121 |
+
command_str = " ".join(command)
|
| 122 |
+
|
| 123 |
+
try:
|
| 124 |
+
result = subprocess.run(
|
| 125 |
+
command,
|
| 126 |
+
capture_output=True,
|
| 127 |
+
text=True,
|
| 128 |
+
check=True,
|
| 129 |
+
cwd=Path.cwd()
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
output_files = [str(output_file)]
|
| 133 |
+
index_file = Path(f"{output_file}.tbi")
|
| 134 |
+
if index_output and index_file.exists():
|
| 135 |
+
output_files.append(str(index_file))
|
| 136 |
+
|
| 137 |
+
return {
|
| 138 |
+
"command_executed": command_str,
|
| 139 |
+
"stdout": result.stdout,
|
| 140 |
+
"stderr": result.stderr,
|
| 141 |
+
"output_files": output_files
|
| 142 |
+
}
|
| 143 |
+
except subprocess.CalledProcessError as e:
|
| 144 |
+
raise RuntimeError(
|
| 145 |
+
f"Rscript execution failed with return code {e.returncode}.\n"
|
| 146 |
+
f"Command: {command_str}\n"
|
| 147 |
+
f"Stderr: {e.stderr}\n"
|
| 148 |
+
f"Stdout: {e.stdout}"
|
| 149 |
+
) from e
|
| 150 |
+
finally:
|
| 151 |
+
script_path.unlink()
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@mcp_tool_placeholder()
|
| 155 |
+
def rtracklayer_liftover(
|
| 156 |
+
input_file: Path,
|
| 157 |
+
output_file: Path,
|
| 158 |
+
chain_file: Path,
|
| 159 |
+
input_format: Optional[str] = None,
|
| 160 |
+
unmapped_output_file: Optional[Path] = None,
|
| 161 |
+
):
|
| 162 |
+
"""
|
| 163 |
+
Lifts over genomic coordinates from one assembly to another using a chain file.
|
| 164 |
+
|
| 165 |
+
This tool wraps the liftOver() function from rtracklayer, which is essential
|
| 166 |
+
for translating genomic features between different reference genome versions
|
| 167 |
+
(e.g., hg18 to hg19).
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
input_file: Path to the input genomic data file (e.g., BED, GFF) with
|
| 171 |
+
coordinates to be lifted over.
|
| 172 |
+
output_file: Path to write the output file with lifted coordinates.
|
| 173 |
+
chain_file: Path to the liftOver chain file (.chain or .chain.gz).
|
| 174 |
+
input_format: The format of the input file (e.g., 'bed', 'gff'). If not
|
| 175 |
+
provided, rtracklayer will guess from the file extension.
|
| 176 |
+
unmapped_output_file: Optional path to write features that could not be
|
| 177 |
+
mapped to the new assembly.
|
| 178 |
+
"""
|
| 179 |
+
# 1. Input Validation
|
| 180 |
+
if not input_file.exists():
|
| 181 |
+
raise FileNotFoundError(f"Input file not found: {input_file}")
|
| 182 |
+
if not chain_file.exists():
|
| 183 |
+
raise FileNotFoundError(f"Chain file not found: {chain_file}")
|
| 184 |
+
|
| 185 |
+
# 2. Build R Script
|
| 186 |
+
r_script_lines = [
|
| 187 |
+
"suppressPackageStartupMessages(library(rtracklayer))",
|
| 188 |
+
"",
|
| 189 |
+
"# --- Import Data ---",
|
| 190 |
+
f"ch <- rtracklayer::import.chain('{chain_file.resolve()}')",
|
| 191 |
+
f"gr <- rtracklayer::import('{input_file.resolve()}'" + (f", format = '{input_format}'" if input_format else "") + ")",
|
| 192 |
+
"",
|
| 193 |
+
"# --- Perform LiftOver ---",
|
| 194 |
+
"lifted_grl <- rtracklayer::liftOver(gr, ch)",
|
| 195 |
+
"lifted_gr <- unlist(lifted_grl)",
|
| 196 |
+
"",
|
| 197 |
+
"# --- Export Lifted Data ---",
|
| 198 |
+
f"rtracklayer::export(lifted_gr, '{output_file.resolve()}')",
|
| 199 |
+
]
|
| 200 |
+
|
| 201 |
+
if unmapped_output_file:
|
| 202 |
+
r_script_lines.extend([
|
| 203 |
+
"",
|
| 204 |
+
"# --- Handle Unmapped Regions ---",
|
| 205 |
+
"unmapped_indices <- elementNROWS(lifted_grl) == 0",
|
| 206 |
+
"unmapped_gr <- gr[unmapped_indices]",
|
| 207 |
+
f"if (length(unmapped_gr) > 0) {{",
|
| 208 |
+
f" rtracklayer::export(unmapped_gr, '{unmapped_output_file.resolve()}')",
|
| 209 |
+
f"}}",
|
| 210 |
+
])
|
| 211 |
+
|
| 212 |
+
r_script_lines.append("cat('LiftOver successful.\\n')")
|
| 213 |
+
r_script = "\n".join(r_script_lines)
|
| 214 |
+
|
| 215 |
+
# 3. Execute R Script
|
| 216 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False, dir=".") as tmp_script:
|
| 217 |
+
script_path = Path(tmp_script.name)
|
| 218 |
+
tmp_script.write(r_script)
|
| 219 |
+
|
| 220 |
+
command = ["Rscript", str(script_path.name)]
|
| 221 |
+
command_str = " ".join(command)
|
| 222 |
+
|
| 223 |
+
try:
|
| 224 |
+
result = subprocess.run(
|
| 225 |
+
command,
|
| 226 |
+
capture_output=True,
|
| 227 |
+
text=True,
|
| 228 |
+
check=True,
|
| 229 |
+
cwd=Path.cwd()
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
output_files = [str(output_file)]
|
| 233 |
+
if unmapped_output_file and unmapped_output_file.exists():
|
| 234 |
+
output_files.append(str(unmapped_output_file))
|
| 235 |
+
|
| 236 |
+
return {
|
| 237 |
+
"command_executed": command_str,
|
| 238 |
+
"stdout": result.stdout,
|
| 239 |
+
"stderr": result.stderr,
|
| 240 |
+
"output_files": output_files
|
| 241 |
+
}
|
| 242 |
+
except subprocess.CalledProcessError as e:
|
| 243 |
+
raise RuntimeError(
|
| 244 |
+
f"Rscript execution failed with return code {e.returncode}.\n"
|
| 245 |
+
f"Command: {command_str}\n"
|
| 246 |
+
f"Stderr: {e.stderr}\n"
|
| 247 |
+
f"Stdout: {e.stdout}"
|
| 248 |
+
) from e
|
| 249 |
+
finally:
|
| 250 |
+
script_path.unlink()
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
@mcp_tool_placeholder()
|
| 254 |
+
def rtracklayer_ucsc_query(
|
| 255 |
+
output_file: Path,
|
| 256 |
+
genome: str,
|
| 257 |
+
track: str,
|
| 258 |
+
which: Optional[str] = None,
|
| 259 |
+
output_format: Optional[str] = None,
|
| 260 |
+
):
|
| 261 |
+
"""
|
| 262 |
+
Downloads data directly from the UCSC Table Browser.
|
| 263 |
+
|
| 264 |
+
This tool provides an interface to query and download genomic tracks from the
|
| 265 |
+
UCSC database, saving them to a local file in a specified format.
|
| 266 |
+
|
| 267 |
+
Args:
|
| 268 |
+
output_file: Path to write the downloaded data.
|
| 269 |
+
genome: The UCSC genome assembly to query (e.g., 'hg19', 'mm10').
|
| 270 |
+
track: The name of the track/table to download (e.g., 'knownGene', 'cpgIslandExt').
|
| 271 |
+
which: A specific genomic region to query, in the format 'chr:start-end'
|
| 272 |
+
(e.g., 'chr1:1000-2000'). If not provided, the whole genome is queried.
|
| 273 |
+
output_format: The format for the output file (e.g., 'bed', 'gff'). If not
|
| 274 |
+
provided, rtracklayer will guess from the file extension.
|
| 275 |
+
"""
|
| 276 |
+
# 1. Input Validation
|
| 277 |
+
if which and not re.match(r"^\w+:\d+-\d+$", which):
|
| 278 |
+
raise ValueError("Invalid 'which' format. Expected 'chr:start-end' (e.g., 'chr1:1000-5000').")
|
| 279 |
+
|
| 280 |
+
# 2. Build R Script
|
| 281 |
+
r_script_lines = [
|
| 282 |
+
"suppressPackageStartupMessages(library(rtracklayer))",
|
| 283 |
+
"suppressPackageStartupMessages(library(GenomicRanges))",
|
| 284 |
+
"",
|
| 285 |
+
"# --- Build UCSC Data Source ---",
|
| 286 |
+
f"ucsc_args <- list(genome = '{genome}', tablename = '{track}')",
|
| 287 |
+
]
|
| 288 |
+
if which:
|
| 289 |
+
r_script_lines.append(f"ucsc_args$range <- GRanges('{which}')")
|
| 290 |
+
|
| 291 |
+
r_script_lines.extend([
|
| 292 |
+
"ucsc_data_source <- do.call(UCSCData, ucsc_args)",
|
| 293 |
+
"",
|
| 294 |
+
"# --- Import Data from UCSC ---",
|
| 295 |
+
"gr_object <- rtracklayer::import(ucsc_data_source)",
|
| 296 |
+
"",
|
| 297 |
+
"# --- Export Data to File ---",
|
| 298 |
+
f"export_args <- list(object = gr_object, con = '{output_file.resolve()}')",
|
| 299 |
+
])
|
| 300 |
+
if output_format:
|
| 301 |
+
r_script_lines.append(f"export_args$format <- '{output_format}'")
|
| 302 |
+
|
| 303 |
+
r_script_lines.extend([
|
| 304 |
+
"do.call(rtracklayer::export, export_args)",
|
| 305 |
+
"cat('UCSC data downloaded successfully.\\n')"
|
| 306 |
+
])
|
| 307 |
+
r_script = "\n".join(r_script_lines)
|
| 308 |
+
|
| 309 |
+
# 3. Execute R Script
|
| 310 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix=".R", delete=False, dir=".") as tmp_script:
|
| 311 |
+
script_path = Path(tmp_script.name)
|
| 312 |
+
tmp_script.write(r_script)
|
| 313 |
+
|
| 314 |
+
command = ["Rscript", str(script_path.name)]
|
| 315 |
+
command_str = " ".join(command)
|
| 316 |
+
|
| 317 |
+
try:
|
| 318 |
+
result = subprocess.run(
|
| 319 |
+
command,
|
| 320 |
+
capture_output=True,
|
| 321 |
+
text=True,
|
| 322 |
+
check=True,
|
| 323 |
+
cwd=Path.cwd()
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
return {
|
| 327 |
+
"command_executed": command_str,
|
| 328 |
+
"stdout": result.stdout,
|
| 329 |
+
"stderr": result.stderr,
|
| 330 |
+
"output_files": [str(output_file)]
|
| 331 |
+
}
|
| 332 |
+
except subprocess.CalledProcessError as e:
|
| 333 |
+
raise RuntimeError(
|
| 334 |
+
f"Rscript execution failed with return code {e.returncode}.\n"
|
| 335 |
+
f"Command: {command_str}\n"
|
| 336 |
+
f"Stderr: {e.stderr}\n"
|
| 337 |
+
f"Stdout: {e.stdout}"
|
| 338 |
+
) from e
|
| 339 |
+
finally:
|
| 340 |
+
script_path.unlink()
|
Biomni/mcp_generated/mcp_bioconductor-scdotplot/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y default-jre wget curl && apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install Miniconda
|
| 8 |
+
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
|
| 9 |
+
|
| 10 |
+
# Add conda to PATH
|
| 11 |
+
ENV PATH="/opt/conda/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Install bioconductor-scdotplot via conda (e.g., from bioconda)
|
| 14 |
+
RUN conda install -c bioconda bioconductor-scdotplot -y && conda clean -a
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
RUN pip install uv
|
| 18 |
+
RUN uv pip install --system fastmcp
|
| 19 |
+
|
| 20 |
+
# Create app directory
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Copy your MCP server
|
| 24 |
+
COPY app/bioconductor-scdotplot_server.py /app/
|
| 25 |
+
|
| 26 |
+
# Create workspace and output directories
|
| 27 |
+
RUN mkdir -p /app/workspace /app/output
|
| 28 |
+
|
| 29 |
+
# Make sure the server script is executable
|
| 30 |
+
RUN chmod +x /app/bioconductor-scdotplot_server.py
|
| 31 |
+
|
| 32 |
+
# Expose port for MCP over HTTP (optional)
|
| 33 |
+
EXPOSE 8000
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD python -c "import sys; sys.exit(0)"
|
| 37 |
+
|
| 38 |
+
# Default command runs the MCP server via stdio
|
| 39 |
+
CMD ["python", "/app/bioconductor-scdotplot_server.py"]
|
| 40 |
+
|
Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_server.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import tempfile
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import List, Optional, Union
|
| 6 |
+
|
| 7 |
+
from mcp.server.fastmcp import FastMCP
|
| 8 |
+
|
| 9 |
+
SERVER_NAME = 'local_bioconductor_scdotplot'
|
| 10 |
+
mcp = FastMCP(SERVER_NAME)
|
| 11 |
+
|
| 12 |
+
@mcp.tool()
|
| 13 |
+
def sc_dot_plot(
|
| 14 |
+
input_path: str,
|
| 15 |
+
features: List[str],
|
| 16 |
+
group_by: str,
|
| 17 |
+
output_path: str = "dotplot.pdf",
|
| 18 |
+
cluster_cols: bool = True,
|
| 19 |
+
cluster_rows: bool = True,
|
| 20 |
+
assay: str = "RNA",
|
| 21 |
+
slot: str = "data",
|
| 22 |
+
scale: bool = True,
|
| 23 |
+
cap: float = 2.5,
|
| 24 |
+
annotation_cols: Optional[List[str]] = None,
|
| 25 |
+
annotation_rows: Optional[List[str]] = None,
|
| 26 |
+
) -> dict:
|
| 27 |
+
"""
|
| 28 |
+
Generate a clustered single-cell RNA-seq dot plot using the scDotPlot package.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
input_path: Path to the input file (RDS format containing Seurat or SingleCellExperiment object).
|
| 32 |
+
features: List of gene names or features to plot.
|
| 33 |
+
group_by: Metadata column name to group cells by (e.g., 'ident' or 'cluster').
|
| 34 |
+
output_path: Path where the resulting plot (PDF, PNG, etc.) will be saved.
|
| 35 |
+
cluster_cols: Whether to perform hierarchical clustering on the columns (features).
|
| 36 |
+
cluster_rows: Whether to perform hierarchical clustering on the rows (cell groups).
|
| 37 |
+
assay: Name of the assay to use (for Seurat objects).
|
| 38 |
+
slot: Name of the slot to use (e.g., 'data', 'counts').
|
| 39 |
+
scale: Whether to scale the expression data.
|
| 40 |
+
cap: Value at which to cap the scaled expression data.
|
| 41 |
+
annotation_cols: Optional list of metadata columns to use for annotating the columns.
|
| 42 |
+
annotation_rows: Optional list of metadata columns to use for annotating the rows.
|
| 43 |
+
"""
|
| 44 |
+
# Input validation
|
| 45 |
+
input_file = Path(input_path)
|
| 46 |
+
if not input_file.exists():
|
| 47 |
+
return {"error": f"Input file not found: {input_path}"}
|
| 48 |
+
|
| 49 |
+
output_file = Path(output_path)
|
| 50 |
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
| 51 |
+
|
| 52 |
+
# Prepare R script
|
| 53 |
+
features_r = ", ".join([f"'{f}'" for f in features])
|
| 54 |
+
|
| 55 |
+
# Handle optional annotations
|
| 56 |
+
ann_cols_r = "NULL"
|
| 57 |
+
if annotation_cols:
|
| 58 |
+
ann_cols_r = "c(" + ", ".join([f"'{a}'" for a in annotation_cols]) + ")"
|
| 59 |
+
|
| 60 |
+
ann_rows_r = "NULL"
|
| 61 |
+
if annotation_rows:
|
| 62 |
+
ann_rows_r = "c(" + ", ".join([f"'{a}'" for a in annotation_rows]) + ")"
|
| 63 |
+
|
| 64 |
+
r_script = f"""
|
| 65 |
+
library(scDotPlot)
|
| 66 |
+
library(Seurat)
|
| 67 |
+
library(SingleCellExperiment)
|
| 68 |
+
library(ggplot2)
|
| 69 |
+
|
| 70 |
+
# Load object
|
| 71 |
+
obj <- readRDS('{input_path}')
|
| 72 |
+
|
| 73 |
+
# Generate plot
|
| 74 |
+
p <- scDotPlot(
|
| 75 |
+
object = obj,
|
| 76 |
+
features = c({features_r}),
|
| 77 |
+
group.by = '{group_by}',
|
| 78 |
+
cluster.cols = {str(cluster_cols).upper()},
|
| 79 |
+
cluster.rows = {str(cluster_rows).upper()},
|
| 80 |
+
assay = '{assay}',
|
| 81 |
+
slot = '{slot}',
|
| 82 |
+
scale = {str(scale).upper()},
|
| 83 |
+
cap = {cap},
|
| 84 |
+
annotation_cols = {ann_cols_r},
|
| 85 |
+
annotation_rows = {ann_rows_r}
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# Save plot
|
| 89 |
+
ggsave('{output_path}', plot = p)
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp:
|
| 93 |
+
tmp.write(r_script)
|
| 94 |
+
tmp_path = tmp.name
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
result = subprocess.run(
|
| 98 |
+
["Rscript", tmp_path],
|
| 99 |
+
capture_output=True,
|
| 100 |
+
text=True,
|
| 101 |
+
check=True
|
| 102 |
+
)
|
| 103 |
+
return {
|
| 104 |
+
"command_executed": f"scDotPlot on {input_path}",
|
| 105 |
+
"stdout": result.stdout,
|
| 106 |
+
"stderr": result.stderr,
|
| 107 |
+
"output_files": [str(output_file.absolute())]
|
| 108 |
+
}
|
| 109 |
+
except subprocess.CalledProcessError as e:
|
| 110 |
+
return {
|
| 111 |
+
"error": "R execution failed",
|
| 112 |
+
"stdout": e.stdout,
|
| 113 |
+
"stderr": e.stderr,
|
| 114 |
+
"command_executed": f"Rscript {tmp_path}"
|
| 115 |
+
}
|
| 116 |
+
finally:
|
| 117 |
+
Path(tmp_path).unlink(missing_ok=True)
|
| 118 |
+
|
| 119 |
+
@mcp.tool()
|
| 120 |
+
def sc_dot_plot_data(
|
| 121 |
+
input_path: str,
|
| 122 |
+
features: List[str],
|
| 123 |
+
group_by: str,
|
| 124 |
+
output_csv: str = "dotplot_data.csv",
|
| 125 |
+
assay: str = "RNA",
|
| 126 |
+
slot: str = "data",
|
| 127 |
+
scale: bool = True,
|
| 128 |
+
cap: float = 2.5,
|
| 129 |
+
) -> dict:
|
| 130 |
+
"""
|
| 131 |
+
Extract the underlying data frame used for generating a scDotPlot.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
input_path: Path to the input file (RDS format).
|
| 135 |
+
features: List of gene names or features.
|
| 136 |
+
group_by: Metadata column name to group cells by.
|
| 137 |
+
output_csv: Path to save the extracted data as a CSV file.
|
| 138 |
+
assay: Name of the assay to use.
|
| 139 |
+
slot: Name of the slot to use.
|
| 140 |
+
scale: Whether to scale the expression data.
|
| 141 |
+
cap: Value at which to cap the scaled expression data.
|
| 142 |
+
"""
|
| 143 |
+
input_file = Path(input_path)
|
| 144 |
+
if not input_file.exists():
|
| 145 |
+
return {"error": f"Input file not found: {input_path}"}
|
| 146 |
+
|
| 147 |
+
features_r = ", ".join([f"'{f}'" for f in features])
|
| 148 |
+
|
| 149 |
+
r_script = f"""
|
| 150 |
+
library(scDotPlot)
|
| 151 |
+
library(Seurat)
|
| 152 |
+
library(SingleCellExperiment)
|
| 153 |
+
|
| 154 |
+
obj <- readRDS('{input_path}')
|
| 155 |
+
|
| 156 |
+
# Extract data using the internal data preparation logic
|
| 157 |
+
plot_data <- scDotPlotData(
|
| 158 |
+
object = obj,
|
| 159 |
+
features = c({features_r}),
|
| 160 |
+
group.by = '{group_by}',
|
| 161 |
+
assay = '{assay}',
|
| 162 |
+
slot = '{slot}',
|
| 163 |
+
scale = {str(scale).upper()},
|
| 164 |
+
cap = {cap}
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
write.csv(plot_data, '{output_csv}', row.names = FALSE)
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp:
|
| 171 |
+
tmp.write(r_script)
|
| 172 |
+
tmp_path = tmp.name
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
result = subprocess.run(
|
| 176 |
+
["Rscript", tmp_path],
|
| 177 |
+
capture_output=True,
|
| 178 |
+
text=True,
|
| 179 |
+
check=True
|
| 180 |
+
)
|
| 181 |
+
return {
|
| 182 |
+
"command_executed": f"scDotPlotData on {input_path}",
|
| 183 |
+
"stdout": result.stdout,
|
| 184 |
+
"stderr": result.stderr,
|
| 185 |
+
"output_files": [str(Path(output_csv).absolute())]
|
| 186 |
+
}
|
| 187 |
+
except subprocess.CalledProcessError as e:
|
| 188 |
+
return {
|
| 189 |
+
"error": "R execution failed",
|
| 190 |
+
"stdout": e.stdout,
|
| 191 |
+
"stderr": e.stderr
|
| 192 |
+
}
|
| 193 |
+
finally:
|
| 194 |
+
Path(tmp_path).unlink(missing_ok=True)
|
| 195 |
+
|
| 196 |
+
@mcp.tool()
|
| 197 |
+
def sc_dot_plot_custom_df(
|
| 198 |
+
data_path: str,
|
| 199 |
+
features_col: str,
|
| 200 |
+
groups_col: str,
|
| 201 |
+
avg_exp_col: str,
|
| 202 |
+
pct_exp_col: str,
|
| 203 |
+
output_path: str = "dotplot_from_df.pdf",
|
| 204 |
+
cluster_cols: bool = True,
|
| 205 |
+
cluster_rows: bool = True,
|
| 206 |
+
) -> dict:
|
| 207 |
+
"""
|
| 208 |
+
Generate a clustered dot plot from a pre-calculated data frame instead of a Seurat/SCE object.
|
| 209 |
+
|
| 210 |
+
Args:
|
| 211 |
+
data_path: Path to a CSV file containing the summarized expression data.
|
| 212 |
+
features_col: Column name in the CSV containing gene/feature names.
|
| 213 |
+
groups_col: Column name in the CSV containing cell group/cluster names.
|
| 214 |
+
avg_exp_col: Column name in the CSV containing average expression values.
|
| 215 |
+
pct_exp_col: Column name in the CSV containing percent of cells expressing the gene.
|
| 216 |
+
output_path: Path where the resulting plot will be saved.
|
| 217 |
+
cluster_cols: Whether to perform hierarchical clustering on columns.
|
| 218 |
+
cluster_rows: Whether to perform hierarchical clustering on rows.
|
| 219 |
+
"""
|
| 220 |
+
data_file = Path(data_path)
|
| 221 |
+
if not data_file.exists():
|
| 222 |
+
return {"error": f"Data file not found: {data_path}"}
|
| 223 |
+
|
| 224 |
+
r_script = f"""
|
| 225 |
+
library(scDotPlot)
|
| 226 |
+
library(ggplot2)
|
| 227 |
+
|
| 228 |
+
# Load data frame
|
| 229 |
+
df <- read.csv('{data_path}')
|
| 230 |
+
|
| 231 |
+
# Generate plot using the data frame method
|
| 232 |
+
# Note: scDotPlot handles data frames by mapping columns
|
| 233 |
+
p <- scDotPlot(
|
| 234 |
+
object = df,
|
| 235 |
+
features = '{features_col}',
|
| 236 |
+
group.by = '{groups_col}',
|
| 237 |
+
avg.exp = '{avg_exp_col}',
|
| 238 |
+
pct.exp = '{pct_exp_col}',
|
| 239 |
+
cluster.cols = {str(cluster_cols).upper()},
|
| 240 |
+
cluster.rows = {str(cluster_rows).upper()}
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
ggsave('{output_path}', plot = p)
|
| 244 |
+
"""
|
| 245 |
+
|
| 246 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.R', delete=False) as tmp:
|
| 247 |
+
tmp.write(r_script)
|
| 248 |
+
tmp_path = tmp.name
|
| 249 |
+
|
| 250 |
+
try:
|
| 251 |
+
result = subprocess.run(
|
| 252 |
+
["Rscript", tmp_path],
|
| 253 |
+
capture_output=True,
|
| 254 |
+
text=True,
|
| 255 |
+
check=True
|
| 256 |
+
)
|
| 257 |
+
return {
|
| 258 |
+
"command_executed": f"scDotPlot on data frame {data_path}",
|
| 259 |
+
"stdout": result.stdout,
|
| 260 |
+
"stderr": result.stderr,
|
| 261 |
+
"output_files": [str(Path(output_path).absolute())]
|
| 262 |
+
}
|
| 263 |
+
except subprocess.CalledProcessError as e:
|
| 264 |
+
return {
|
| 265 |
+
"error": "R execution failed",
|
| 266 |
+
"stdout": e.stdout,
|
| 267 |
+
"stderr": e.stderr
|
| 268 |
+
}
|
| 269 |
+
finally:
|
| 270 |
+
Path(tmp_path).unlink(missing_ok=True)
|
| 271 |
+
|
| 272 |
+
if __name__ == "__main__":
|
| 273 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_shim_server.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import ast
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from mcp.server.fastmcp import FastMCP
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
SOURCE_SERVER = Path('/225040511/project/BioScientist/agent_system/toolbase/mcp_batch_from_manual_txt/mcp_bioconductor-scdotplot/app/bioconductor-scdotplot_server.py')
|
| 11 |
+
LOCAL_SERVER = Path(__file__).with_name(SOURCE_SERVER.name)
|
| 12 |
+
SERVER_NAME = 'biosci_bioconductor_scdotplot'
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class _ShimMCP:
|
| 16 |
+
@staticmethod
|
| 17 |
+
def tool(*args, **kwargs):
|
| 18 |
+
if args and callable(args[0]) and len(args) == 1 and not kwargs:
|
| 19 |
+
return args[0]
|
| 20 |
+
def _decorator(fn):
|
| 21 |
+
return fn
|
| 22 |
+
return _decorator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _resolve_source_server():
|
| 26 |
+
if LOCAL_SERVER.exists() and LOCAL_SERVER.name != Path(__file__).name:
|
| 27 |
+
return LOCAL_SERVER
|
| 28 |
+
return SOURCE_SERVER
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _load_functions():
|
| 32 |
+
source_server = _resolve_source_server()
|
| 33 |
+
code = source_server.read_text(encoding="utf-8")
|
| 34 |
+
tree = ast.parse(code, filename=str(source_server))
|
| 35 |
+
function_names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
|
| 36 |
+
namespace = {
|
| 37 |
+
"__name__": "__mcp_source__",
|
| 38 |
+
"mcp": _ShimMCP(),
|
| 39 |
+
}
|
| 40 |
+
exec(compile(code, str(source_server), "exec"), namespace, namespace)
|
| 41 |
+
loaded = []
|
| 42 |
+
for name in function_names:
|
| 43 |
+
fn = namespace.get(name)
|
| 44 |
+
if callable(fn):
|
| 45 |
+
loaded.append(fn)
|
| 46 |
+
return loaded
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
mcp = FastMCP(SERVER_NAME)
|
| 50 |
+
for _fn in _load_functions():
|
| 51 |
+
mcp.tool()(_fn)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
mcp.run(transport="stdio")
|
Biomni/mcp_generated/mcp_bioconductor-scdotplot/app/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
Biomni/mcp_generated/mcp_bioconductor-scdotplot/docker-compose.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
mcp-bioconductor-scdotplot:
|
| 5 |
+
build: .
|
| 6 |
+
image: mcp-bioconductor-scdotplot:latest
|
| 7 |
+
container_name: mcp-bioconductor-scdotplot
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
environment:
|
| 11 |
+
- MCP_SERVER_NAME=bioconductor-scdotplot
|
| 12 |
+
volumes:
|
| 13 |
+
- ./workspace:/app/workspace
|
| 14 |
+
- ./output:/app/output
|
| 15 |
+
restart: unless-stopped
|
| 16 |
+
healthcheck:
|
| 17 |
+
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
| 18 |
+
interval: 30s
|
| 19 |
+
timeout: 10s
|
| 20 |
+
retries: 3
|
| 21 |
+
start_period: 5s
|
| 22 |
+
|