| import subprocess |
| from pathlib import Path |
| from typing import Optional, List, Union |
| import os |
|
|
| from mcp.server.fastmcp import FastMCP |
|
|
| SERVER_NAME = 'local_bactopia' |
| mcp = FastMCP(SERVER_NAME) |
|
|
| @mcp.tool() |
| def bactopia_run( |
| sample: str, |
| outdir: str, |
| datasets: str, |
| r1: Optional[str] = None, |
| r2: Optional[str] = None, |
| se: Optional[str] = None, |
| fastqs: Optional[str] = None, |
| accession: Optional[str] = None, |
| accessions: Optional[str] = None, |
| profile: str = "conda", |
| cpus: int = 2, |
| max_memory: int = 8, |
| resume: bool = False, |
| ) -> dict: |
| """ |
| Run the main Bactopia analysis pipeline for a bacterial genome. |
| |
| Args: |
| sample: Name of the sample. |
| outdir: Directory to store output results. |
| datasets: Path to the Bactopia datasets directory. |
| r1: Path to forward reads (Paired-end). |
| r2: Path to reverse reads (Paired-end). |
| se: Path to single-end reads. |
| fastqs: Path to a FOFN (file-of-filenames) for multiple samples. |
| accession: A single ENA/SRA accession (e.g., SRX000000). |
| accessions: A file containing a list of ENA/SRA accessions. |
| profile: Nextflow profile to use (e.g., conda, docker, singularity). |
| cpus: Number of CPUs to allocate. |
| max_memory: Maximum memory in GB to allocate. |
| resume: Whether to resume a previous run. |
| """ |
| |
| out_path = Path(outdir) |
| ds_path = Path(datasets) |
| |
| if not ds_path.exists(): |
| return {"error": f"Datasets directory not found at {datasets}"} |
|
|
| cmd = ["bactopia", "--sample", sample, "--outdir", str(out_path), "--datasets", str(ds_path)] |
| |
| |
| if r1 and r2: |
| if not Path(r1).exists() or not Path(r2).exists(): |
| return {"error": "R1 or R2 file does not exist"} |
| cmd += ["--R1", r1, "--R2", r2] |
| elif se: |
| if not Path(se).exists(): |
| return {"error": "Single-end file does not exist"} |
| cmd += ["--SE", se] |
| elif fastqs: |
| if not Path(fastqs).exists(): |
| return {"error": "Fastqs list file does not exist"} |
| cmd += ["--fastqs", fastqs] |
| elif accession: |
| cmd += ["--accession", accession] |
| elif accessions: |
| if not Path(accessions).exists(): |
| return {"error": "Accessions list file does not exist"} |
| cmd += ["--accessions", accessions] |
| else: |
| return {"error": "No input source provided (R1/R2, SE, fastqs, accession, or accessions required)"} |
|
|
| |
| cmd += ["-profile", profile] |
| cmd += ["--max_cpus", str(cpus)] |
| cmd += ["--max_memory", f"{max_memory}.GB"] |
| |
| if resume: |
| cmd.append("-resume") |
|
|
| try: |
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) |
| return { |
| "command_executed": " ".join(cmd), |
| "stdout": result.stdout, |
| "stderr": result.stderr, |
| "output_directory": str(out_path.absolute()) |
| } |
| except subprocess.CalledProcessError as e: |
| return { |
| "command_executed": " ".join(cmd), |
| "error": str(e), |
| "stdout": e.stdout, |
| "stderr": e.stderr |
| } |
|
|
| @mcp.tool() |
| def bactopia_prepare( |
| path: str, |
| recursive: bool = False, |
| extension: str = ".fastq.gz", |
| output_file: Optional[str] = None |
| ) -> dict: |
| """ |
| Prepare a FASTQ list (FOFN) for Bactopia from a directory of sequencing files. |
| |
| Args: |
| path: Directory containing FASTQ files. |
| recursive: Search for FASTQs in subdirectories. |
| extension: File extension to look for. |
| output_file: Optional path to save the generated list. |
| """ |
| input_path = Path(path) |
| if not input_path.is_dir(): |
| return {"error": f"Path {path} is not a directory"} |
|
|
| cmd = ["bactopia", "prepare", str(input_path), "--extension", extension] |
| if recursive: |
| cmd.append("--recursive") |
|
|
| try: |
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) |
| |
| if output_file: |
| out_p = Path(output_file) |
| out_p.write_text(result.stdout) |
| return { |
| "command_executed": " ".join(cmd), |
| "stdout": "File list generated and saved.", |
| "output_file": str(out_p.absolute()) |
| } |
| |
| return { |
| "command_executed": " ".join(cmd), |
| "stdout": result.stdout, |
| "stderr": result.stderr |
| } |
| except subprocess.CalledProcessError as e: |
| return { |
| "command_executed": " ".join(cmd), |
| "error": str(e), |
| "stdout": e.stdout, |
| "stderr": e.stderr |
| } |
|
|
| @mcp.tool() |
| def bactopia_search( |
| query: str, |
| limit: int = 10, |
| output_file: Optional[str] = None |
| ) -> dict: |
| """ |
| Search for bacterial genome accessions in ENA/SRA. |
| |
| Args: |
| query: Search query (e.g., "Staphylococcus aureus"). |
| limit: Maximum number of results to return. |
| output_file: Optional path to save the accession list. |
| """ |
| cmd = ["bactopia", "search", query, "--limit", str(limit)] |
|
|
| try: |
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) |
| |
| if output_file: |
| out_p = Path(output_file) |
| out_p.write_text(result.stdout) |
| return { |
| "command_executed": " ".join(cmd), |
| "stdout": f"Search results saved to {output_file}", |
| "output_file": str(out_p.absolute()) |
| } |
|
|
| return { |
| "command_executed": " ".join(cmd), |
| "stdout": result.stdout, |
| "stderr": result.stderr |
| } |
| except subprocess.CalledProcessError as e: |
| return { |
| "command_executed": " ".join(cmd), |
| "error": str(e), |
| "stdout": e.stdout, |
| "stderr": e.stderr |
| } |
|
|
| @mcp.tool() |
| def bactopia_datasets( |
| outdir: str, |
| species: Optional[str] = None, |
| include_genus: bool = False, |
| limit: int = 100 |
| ) -> dict: |
| """ |
| Download and setup Bactopia datasets/databases. |
| |
| Args: |
| outdir: Directory to save the datasets. |
| species: Optional species name to download specific datasets (e.g., "Salmonella enterica"). |
| include_genus: Include genus-level datasets. |
| limit: Limit the number of genomes used for building datasets. |
| """ |
| out_path = Path(outdir) |
| cmd = ["bactopia", "datasets", "--outdir", str(out_path)] |
| |
| if species: |
| cmd += ["--species", species] |
| if include_genus: |
| cmd.append("--include_genus") |
| cmd += ["--limit", str(limit)] |
|
|
| try: |
| |
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) |
| return { |
| "command_executed": " ".join(cmd), |
| "stdout": result.stdout, |
| "stderr": result.stderr, |
| "datasets_path": str(out_path.absolute()) |
| } |
| except subprocess.CalledProcessError as e: |
| return { |
| "command_executed": " ".join(cmd), |
| "error": str(e), |
| "stdout": e.stdout, |
| "stderr": e.stderr |
| } |
|
|
| @mcp.tool() |
| def bactopia_tool( |
| tool_name: str, |
| bactopia_dir: str, |
| outdir: str, |
| profile: str = "conda", |
| cpus: int = 2, |
| extra_params: Optional[List[str]] = None |
| ) -> dict: |
| """ |
| Run a Bactopia Tool (comparative analysis) on existing Bactopia outputs. |
| Common tools include: pangenome, roary, iqtree, pirate, summary, etc. |
| |
| Args: |
| tool_name: Name of the Bactopia Tool to run. |
| bactopia_dir: Directory containing previous Bactopia results. |
| outdir: Directory to store tool results. |
| profile: Nextflow profile to use. |
| cpus: Number of CPUs to allocate. |
| extra_params: List of additional command line arguments for the specific tool. |
| """ |
| b_path = Path(bactopia_dir) |
| o_path = Path(outdir) |
| |
| if not b_path.exists(): |
| return {"error": f"Bactopia results directory not found at {bactopia_dir}"} |
|
|
| cmd = [ |
| "bactopia", tool_name, |
| "--bactopia", str(b_path), |
| "--outdir", str(o_path), |
| "-profile", profile, |
| "--max_cpus", str(cpus) |
| ] |
| |
| if extra_params: |
| cmd.extend(extra_params) |
|
|
| try: |
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) |
| return { |
| "command_executed": " ".join(cmd), |
| "stdout": result.stdout, |
| "stderr": result.stderr, |
| "output_directory": str(o_path.absolute()) |
| } |
| except subprocess.CalledProcessError as e: |
| return { |
| "command_executed": " ".join(cmd), |
| "error": str(e), |
| "stdout": e.stdout, |
| "stderr": e.stderr |
| } |
|
|
| if __name__ == "__main__": |
| mcp.run(transport="stdio") |
|
|