Download Biomni/mcp_generated/mcp_bcftools/app/bcftools_server.py from czty/Beyond_Prompt-based_Retrieval: direct link, hf CLI and curl.
- Browser
- Download file 26.8 kB
-
https://huggingface.co/czty/Beyond_Prompt-based_Retrieval/resolve/main/Biomni/mcp_generated/mcp_bcftools/app/bcftools_server.py
- Command line
-
hf download hf://czty/Beyond_Prompt-based_Retrieval/Biomni/mcp_generated/mcp_bcftools/app/bcftools_server.py
-
curl -L -o bcftools_server.py https://huggingface.co/czty/Beyond_Prompt-based_Retrieval/resolve/main/Biomni/mcp_generated/mcp_bcftools/app/bcftools_server.py
26.8 kB
| import subprocess | |
| import tempfile | |
| from pathlib import Path | |
| from typing import List, Optional | |
| # Note: The @mcp.tool decorator is not defined here. | |
| # It is assumed to be provided by the Model Context Protocol (MCP) framework. | |
| # This code is designed to be used within that framework. | |
| # Helper function to handle subprocess execution and error reporting | |
| def _run_bcftools_command(cmd: List[str], output_file: Optional[Path] = None): | |
| """ | |
| A helper function to execute a bcftools command, handle errors, | |
| and return a structured dictionary. | |
| """ | |
| command_str = " ".join(cmd) | |
| try: | |
| result = subprocess.run( | |
| cmd, | |
| capture_output=True, | |
| text=True, | |
| check=True, | |
| ) | |
| output_files = [str(output_file)] if output_file and output_file.exists() else [] | |
| # For commands that create index files automatically | |
| if output_file and str(output_file).endswith((".bcf", ".vcf.gz")): | |
| index_extensions = [".csi", ".tbi"] | |
| for ext in index_extensions: | |
| index_file = Path(str(output_file) + ext) | |
| if index_file.exists(): | |
| output_files.append(str(index_file)) | |
| return { | |
| "command_executed": command_str, | |
| "stdout": result.stdout, | |
| "stderr": result.stderr, | |
| "output_files": output_files, | |
| "return_code": 0, | |
| } | |
| except FileNotFoundError: | |
| return { | |
| "command_executed": command_str, | |
| "stdout": "", | |
| "stderr": "Error: 'bcftools' command not found. Please ensure it is in your PATH.", | |
| "output_files": [], | |
| "return_code": 1, | |
| "error": "FileNotFoundError" | |
| } | |
| except subprocess.CalledProcessError as e: | |
| return { | |
| "command_executed": command_str, | |
| "stdout": e.stdout, | |
| "stderr": e.stderr, | |
| "output_files": [], | |
| "return_code": e.returncode, | |
| "error": "CalledProcessError" | |
| } | |
| from mcp.server.fastmcp import FastMCP | |
| SERVER_NAME = 'local_bcftools' | |
| mcp = FastMCP(SERVER_NAME) | |
| def bcftools_annotate( | |
| input_file: Path, | |
| annotations: Optional[Path] = None, | |
| columns: Optional[str] = None, | |
| header_lines: Optional[Path] = None, | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| remove: Optional[str] = None, | |
| set_id: Optional[str] = None, | |
| regions: Optional[str] = None, | |
| regions_file: Optional[Path] = None, | |
| targets: Optional[str] = None, | |
| targets_file: Optional[Path] = None, | |
| include: Optional[str] = None, | |
| exclude: Optional[str] = None, | |
| samples: Optional[str] = None, | |
| samples_file: Optional[Path] = None, | |
| threads: int = 1, | |
| rename_chrs: Optional[Path] = None, | |
| no_version: bool = False, | |
| single_overlaps: bool = False, | |
| mark_sites: Optional[str] = None, | |
| ): | |
| """ | |
| Annotate VCF/BCF files with information from other files. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input file not found: {input_file}") | |
| if annotations and not annotations.exists(): | |
| raise FileNotFoundError(f"Annotations file not found: {annotations}") | |
| if header_lines and not header_lines.exists(): | |
| raise FileNotFoundError(f"Header lines file not found: {header_lines}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| cmd = ["bcftools", "annotate", str(input_file)] | |
| if annotations: | |
| cmd.extend(["-a", str(annotations)]) | |
| if columns: | |
| cmd.extend(["-c", columns]) | |
| if header_lines: | |
| cmd.extend(["-h", str(header_lines)]) | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if output_type: | |
| cmd.extend(["-O", output_type]) | |
| if remove: | |
| cmd.extend(["-x", remove]) | |
| if set_id: | |
| cmd.extend(["--set-id", set_id]) | |
| if regions: | |
| cmd.extend(["-r", regions]) | |
| if regions_file: | |
| cmd.extend(["-R", str(regions_file)]) | |
| if targets: | |
| cmd.extend(["-t", targets]) | |
| if targets_file: | |
| cmd.extend(["-T", str(targets_file)]) | |
| if include: | |
| cmd.extend(["-i", include]) | |
| if exclude: | |
| cmd.extend(["-e", exclude]) | |
| if samples: | |
| cmd.extend(["-s", samples]) | |
| if samples_file: | |
| cmd.extend(["-S", str(samples_file)]) | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| if rename_chrs: | |
| cmd.extend(["--rename-chrs", str(rename_chrs)]) | |
| if no_version: | |
| cmd.append("--no-version") | |
| if single_overlaps: | |
| cmd.append("--single-overlaps") | |
| if mark_sites: | |
| cmd.extend(["--mark-sites", mark_sites]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_call( | |
| input_file: Path, | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| ploidy: Optional[str] = None, | |
| ploidy_file: Optional[Path] = None, | |
| call_method: str = "multiallelic", | |
| constrain_alleles: bool = False, | |
| group_samples: Optional[str] = None, | |
| samples: Optional[str] = None, | |
| samples_file: Optional[Path] = None, | |
| format_fields: Optional[str] = None, | |
| keep_alts: bool = False, | |
| keep_masked_ref: bool = False, | |
| skip_variants: bool = False, | |
| threads: int = 1, | |
| ): | |
| """ | |
| Call genotypes from likelihoods. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input file not found: {input_file}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| if call_method not in ["consensus", "multiallelic"]: | |
| raise ValueError("call_method must be 'consensus' or 'multiallelic'.") | |
| cmd = ["bcftools", "call", str(input_file)] | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if output_type: | |
| cmd.extend(["-O", output_type]) | |
| if ploidy: | |
| cmd.extend(["-p", ploidy]) | |
| if ploidy_file: | |
| cmd.extend(["-P", str(ploidy_file)]) | |
| if call_method == "consensus": | |
| cmd.append("-c") | |
| else: # multiallelic is default, but -m is explicit | |
| cmd.append("-m") | |
| if constrain_alleles: | |
| cmd.append("-C alleles") | |
| if group_samples: | |
| cmd.extend(["-G", group_samples]) | |
| if samples: | |
| cmd.extend(["-s", samples]) | |
| if samples_file: | |
| cmd.extend(["-S", str(samples_file)]) | |
| if format_fields: | |
| cmd.extend(["-f", format_fields]) | |
| if keep_alts: | |
| cmd.append("-A") | |
| if keep_masked_ref: | |
| cmd.append("-M") | |
| if skip_variants: | |
| cmd.append("-V") | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_concat( | |
| input_files: List[Path], | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| allow_overlaps: bool = False, | |
| remove_duplicates: bool = False, | |
| naive: bool = False, | |
| threads: int = 1, | |
| ): | |
| """ | |
| Concatenate VCF/BCF files from non-overlapping regions. | |
| """ | |
| if not input_files: | |
| raise ValueError("At least one input file must be provided.") | |
| for f in input_files: | |
| if not f.exists(): | |
| raise FileNotFoundError(f"Input file not found: {f}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| cmd = ["bcftools", "concat"] | |
| if allow_overlaps: | |
| cmd.append("-a") | |
| if remove_duplicates: | |
| cmd.append("-d") | |
| if naive: | |
| cmd.append("-n") | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if output_type: | |
| cmd.extend(["-O", output_type]) | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| cmd.extend([str(f) for f in input_files]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_consensus( | |
| input_file: Path, | |
| fasta_ref: Path, | |
| output: Optional[Path] = None, | |
| chain: Optional[Path] = None, | |
| haplotype: Optional[str] = None, | |
| iupac_codes: bool = False, | |
| mark_del: Optional[str] = None, | |
| mark_ins: Optional[str] = None, | |
| mark_snv: Optional[str] = None, | |
| missing: Optional[str] = None, | |
| sample: Optional[str] = None, | |
| include: Optional[str] = None, | |
| exclude: Optional[str] = None, | |
| ): | |
| """ | |
| Create a consensus sequence by applying VCF variants to a reference FASTA file. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input VCF/BCF file not found: {input_file}") | |
| if not fasta_ref.exists(): | |
| raise FileNotFoundError(f"Reference FASTA file not found: {fasta_ref}") | |
| if haplotype and haplotype not in ["1", "2", "R", "A", "IUPAC", "all"]: | |
| raise ValueError("haplotype must be one of '1', '2', 'R', 'A', 'IUPAC', 'all'.") | |
| cmd = ["bcftools", "consensus", str(input_file)] | |
| cmd.extend(["-f", str(fasta_ref)]) | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if chain: | |
| cmd.extend(["-c", str(chain)]) | |
| if haplotype: | |
| cmd.extend(["-H", haplotype]) | |
| if iupac_codes: | |
| cmd.append("-i") | |
| if mark_del: | |
| cmd.extend(["--mark-del", mark_del]) | |
| if mark_ins: | |
| cmd.extend(["--mark-ins", mark_ins]) | |
| if mark_snv: | |
| cmd.extend(["--mark-snv", mark_snv]) | |
| if missing: | |
| cmd.extend(["-m", missing]) | |
| if sample: | |
| cmd.extend(["-s", sample]) | |
| if include: | |
| cmd.extend(["--include", include]) | |
| if exclude: | |
| cmd.extend(["--exclude", exclude]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_filter( | |
| input_file: Path, | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| include: Optional[str] = None, | |
| exclude: Optional[str] = None, | |
| soft_filter: Optional[str] = None, | |
| set_gt: Optional[str] = None, | |
| mode: str = "+", | |
| threads: int = 1, | |
| ): | |
| """ | |
| Apply filters to VCF/BCF files. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input file not found: {input_file}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| if mode not in ["+", "x", "X"]: | |
| raise ValueError("mode must be one of '+', 'x', 'X'.") | |
| cmd = ["bcftools", "filter", str(input_file)] | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if output_type: | |
| cmd.extend(["-O", output_type]) | |
| if include: | |
| cmd.extend(["-i", include]) | |
| if exclude: | |
| cmd.extend(["-e", exclude]) | |
| if soft_filter: | |
| cmd.extend(["-s", soft_filter]) | |
| if set_gt: | |
| cmd.extend(["-S", set_gt]) | |
| if mode != "+": | |
| cmd.extend(["-m", mode]) | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_index( | |
| input_file: Path, | |
| force: bool = False, | |
| stats: bool = False, | |
| threads: int = 1, | |
| csi: bool = False, | |
| tbi: bool = False, | |
| min_csi_shift: int = 14, | |
| ): | |
| """ | |
| Index VCF/BCF files for fast random access. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input file not found: {input_file}") | |
| if csi and tbi: | |
| raise ValueError("Cannot specify both --csi (-c) and --tbi (-t).") | |
| cmd = ["bcftools", "index"] | |
| if force: | |
| cmd.append("-f") | |
| if stats: | |
| cmd.append("-n") | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| if csi: | |
| cmd.append("-c") | |
| if tbi: | |
| cmd.append("-t") | |
| if csi and min_csi_shift != 14: | |
| cmd.extend(["-m", str(min_csi_shift)]) | |
| cmd.append(str(input_file)) | |
| # The output file is the index file itself | |
| output_idx_file = None | |
| if csi: | |
| output_idx_file = Path(str(input_file) + ".csi") | |
| else: # default is tbi for vcf.gz, csi for bcf | |
| if str(input_file).endswith(".bcf"): | |
| output_idx_file = Path(str(input_file) + ".csi") | |
| else: | |
| output_idx_file = Path(str(input_file) + ".tbi") | |
| return _run_bcftools_command(cmd, output_idx_file) | |
| def bcftools_isec( | |
| input_files: List[Path], | |
| output_dir: Path, | |
| output_type: str = "v", | |
| complement: bool = False, | |
| collapse: Optional[str] = None, | |
| exclude: Optional[str] = None, | |
| include: Optional[str] = None, | |
| n_sites: Optional[str] = None, | |
| regions: Optional[str] = None, | |
| regions_file: Optional[Path] = None, | |
| targets: Optional[str] = None, | |
| targets_file: Optional[Path] = None, | |
| threads: int = 1, | |
| ): | |
| """ | |
| Create intersections, unions, and complements of VCF/BCF files. | |
| """ | |
| if len(input_files) < 2: | |
| raise ValueError("At least two input files must be provided for isec.") | |
| for f in input_files: | |
| if not f.exists(): | |
| raise FileNotFoundError(f"Input file not found: {f}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| if collapse and collapse not in ["none", "all", "snps", "indels", "both", "any"]: | |
| raise ValueError("Invalid value for collapse.") | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| cmd = ["bcftools", "isec"] | |
| cmd.extend(["-p", str(output_dir)]) | |
| cmd.extend(["-O", output_type]) | |
| if complement: | |
| cmd.append("-c") | |
| if collapse: | |
| cmd.extend(["--collapse", collapse]) | |
| if exclude: | |
| cmd.extend(["-e", exclude]) | |
| if include: | |
| cmd.extend(["-i", include]) | |
| if n_sites: | |
| cmd.extend(["-n", n_sites]) | |
| if regions: | |
| cmd.extend(["-r", regions]) | |
| if regions_file: | |
| cmd.extend(["-R", str(regions_file)]) | |
| if targets: | |
| cmd.extend(["-t", targets]) | |
| if targets_file: | |
| cmd.extend(["-T", str(targets_file)]) | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| cmd.extend([str(f) for f in input_files]) | |
| # isec doesn't have a single output file, it creates a directory | |
| # We will return the directory path in a structured way if needed, | |
| # but for now, we'll rely on the user knowing the output is a directory. | |
| # The _run_bcftools_command helper won't find a single output file. | |
| result = _run_bcftools_command(cmd) | |
| # Manually list created files | |
| if result["return_code"] == 0: | |
| created_files = [str(p) for p in output_dir.glob('*')] | |
| result["output_files"] = created_files | |
| return result | |
| def bcftools_merge( | |
| input_files: List[Path], | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| info_rules: Optional[str] = None, | |
| merge_logic: Optional[str] = None, | |
| threads: int = 1, | |
| ): | |
| """ | |
| Merge multiple VCF/BCF files from different samples into a single file. | |
| """ | |
| if not input_files: | |
| raise ValueError("At least one input file must be provided.") | |
| for f in input_files: | |
| if not f.exists(): | |
| raise FileNotFoundError(f"Input file not found: {f}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| cmd = ["bcftools", "merge"] | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if output_type: | |
| cmd.extend(["-O", output_type]) | |
| if info_rules: | |
| cmd.extend(["--info-rules", info_rules]) | |
| if merge_logic: | |
| cmd.extend(["-m", merge_logic]) | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| cmd.extend([str(f) for f in input_files]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_mpileup( | |
| input_bams: List[Path], | |
| fasta_ref: Path, | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| regions: Optional[str] = None, | |
| regions_file: Optional[Path] = None, | |
| annotate: Optional[str] = None, | |
| max_depth: int = 250, | |
| min_base_quality: int = 13, | |
| min_mapping_quality: int = 0, | |
| adjust_mq: int = 50, | |
| no_baq: bool = False, | |
| threads: int = 1, | |
| ): | |
| """ | |
| Generate VCF/BCF from alignment files (BAM/CRAM). | |
| """ | |
| if not input_bams: | |
| raise ValueError("At least one input BAM/CRAM file must be provided.") | |
| for f in input_bams: | |
| if not f.exists(): | |
| raise FileNotFoundError(f"Input alignment file not found: {f}") | |
| if not fasta_ref.exists(): | |
| raise FileNotFoundError(f"Reference FASTA file not found: {fasta_ref}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| cmd = ["bcftools", "mpileup"] | |
| cmd.extend(["-f", str(fasta_ref)]) | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if output_type: | |
| cmd.extend(["-O", output_type]) | |
| if regions: | |
| cmd.extend(["-r", regions]) | |
| if regions_file: | |
| cmd.extend(["-R", str(regions_file)]) | |
| if annotate: | |
| cmd.extend(["-a", annotate]) | |
| if max_depth != 250: | |
| cmd.extend(["-d", str(max_depth)]) | |
| if min_base_quality != 13: | |
| cmd.extend(["-q", str(min_base_quality)]) | |
| if min_mapping_quality != 0: | |
| cmd.extend(["-Q", str(min_mapping_quality)]) | |
| if adjust_mq != 50: | |
| cmd.extend(["-C", str(adjust_mq)]) | |
| if no_baq: | |
| cmd.append("-B") | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| cmd.extend([str(f) for f in input_bams]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_norm( | |
| input_file: Path, | |
| fasta_ref: Path, | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| check_ref: str = "w", | |
| multiallelics: Optional[str] = None, | |
| atomize: bool = False, | |
| rm_dup: Optional[str] = None, | |
| threads: int = 1, | |
| ): | |
| """ | |
| Left-align and normalize indels, check REF, split multiallelic sites. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input file not found: {input_file}") | |
| if not fasta_ref.exists(): | |
| raise FileNotFoundError(f"Reference FASTA file not found: {fasta_ref}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| if check_ref not in ["w", "e", "x", "s"]: | |
| raise ValueError("check_ref must be one of 'w', 'e', 'x', 's'.") | |
| cmd = ["bcftools", "norm", str(input_file)] | |
| cmd.extend(["-f", str(fasta_ref)]) | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if output_type: | |
| cmd.extend(["-O", output_type]) | |
| if check_ref != "w": | |
| cmd.extend(["-c", check_ref]) | |
| if multiallelics: | |
| cmd.extend(["-m", multiallelics]) | |
| if atomize: | |
| cmd.append("-a") | |
| if rm_dup: | |
| cmd.extend(["-d", rm_dup]) | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_query( | |
| input_file: Path, | |
| format_string: str, | |
| output: Optional[Path] = None, | |
| include: Optional[str] = None, | |
| exclude: Optional[str] = None, | |
| regions: Optional[str] = None, | |
| regions_file: Optional[Path] = None, | |
| samples: Optional[str] = None, | |
| samples_file: Optional[Path] = None, | |
| list_samples: bool = False, | |
| ): | |
| """ | |
| Extracts fields from VCF/BCF files and prints them in a user-defined format. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input file not found: {input_file}") | |
| cmd = ["bcftools", "query", str(input_file)] | |
| if list_samples: | |
| cmd.append("-l") | |
| else: | |
| cmd.extend(["-f", format_string]) | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if include: | |
| cmd.extend(["-i", include]) | |
| if exclude: | |
| cmd.extend(["-e", exclude]) | |
| if regions: | |
| cmd.extend(["-r", regions]) | |
| if regions_file: | |
| cmd.extend(["-R", str(regions_file)]) | |
| if samples: | |
| cmd.extend(["-s", samples]) | |
| if samples_file: | |
| cmd.extend(["-S", str(samples_file)]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_reheader( | |
| input_file: Path, | |
| header: Optional[Path] = None, | |
| samples: Optional[Path] = None, | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| ): | |
| """ | |
| Modify VCF/BCF header, e.g., rename samples. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input file not found: {input_file}") | |
| if header and not header.exists(): | |
| raise FileNotFoundError(f"Header file not found: {header}") | |
| if samples and not samples.exists(): | |
| raise FileNotFoundError(f"Samples file not found: {samples}") | |
| if not header and not samples: | |
| raise ValueError("Either 'header' or 'samples' file must be provided.") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| cmd = ["bcftools", "reheader"] | |
| if header: | |
| cmd.extend(["-h", str(header)]) | |
| if samples: | |
| cmd.extend(["-s", str(samples)]) | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| cmd.append(str(input_file)) | |
| # Reheader does not support -O, output type is inferred from extension | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_sort( | |
| input_file: Path, | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| max_mem: str = "768M", | |
| temp_dir: Optional[Path] = None, | |
| ): | |
| """ | |
| Sort VCF/BCF file by chromosome and position. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input file not found: {input_file}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| cmd = ["bcftools", "sort", str(input_file)] | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if output_type: | |
| cmd.extend(["-O", output_type]) | |
| if max_mem: | |
| cmd.extend(["-m", max_mem]) | |
| if temp_dir: | |
| temp_dir.mkdir(exist_ok=True, parents=True) | |
| cmd.extend(["-T", str(temp_dir)]) | |
| return _run_bcftools_command(cmd, output) | |
| def bcftools_stats( | |
| input_files: List[Path], | |
| fasta_ref: Optional[Path] = None, | |
| output_dir: Optional[Path] = None, | |
| regions: Optional[str] = None, | |
| regions_file: Optional[Path] = None, | |
| samples: Optional[str] = None, | |
| samples_file: Optional[Path] = None, | |
| threads: int = 1, | |
| ): | |
| """ | |
| Produce VCF/BCF stats, create plots with plot-vcfstats. | |
| """ | |
| if not input_files: | |
| raise ValueError("At least one input file must be provided.") | |
| for f in input_files: | |
| if not f.exists(): | |
| raise FileNotFoundError(f"Input file not found: {f}") | |
| if fasta_ref and not fasta_ref.exists(): | |
| raise FileNotFoundError(f"Reference FASTA file not found: {fasta_ref}") | |
| cmd = ["bcftools", "stats"] | |
| if fasta_ref: | |
| cmd.extend(["-f", str(fasta_ref)]) | |
| if regions: | |
| cmd.extend(["-r", regions]) | |
| if regions_file: | |
| cmd.extend(["-R", str(regions_file)]) | |
| if samples: | |
| cmd.extend(["-s", samples]) | |
| if samples_file: | |
| cmd.extend(["-S", str(samples_file)]) | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| cmd.extend([str(f) for f in input_files]) | |
| # stats command outputs to stdout by default. | |
| # If output_dir is provided, we redirect stdout to a file. | |
| output_file = None | |
| if output_dir: | |
| output_dir.mkdir(exist_ok=True, parents=True) | |
| output_file = output_dir / "stats.txt" | |
| with open(output_file, "w") as f_out: | |
| command_str = " ".join(cmd) | |
| try: | |
| result = subprocess.run( | |
| cmd, | |
| stdout=f_out, | |
| stderr=subprocess.PIPE, | |
| text=True, | |
| check=True, | |
| ) | |
| return { | |
| "command_executed": command_str, | |
| "stdout": f"Stats written to {output_file}", | |
| "stderr": result.stderr, | |
| "output_files": [str(output_file)], | |
| "return_code": 0, | |
| } | |
| except subprocess.CalledProcessError as e: | |
| return { | |
| "command_executed": command_str, | |
| "stdout": e.stdout or "", | |
| "stderr": e.stderr, | |
| "output_files": [], | |
| "return_code": e.returncode, | |
| "error": "CalledProcessError" | |
| } | |
| else: | |
| # No output file, capture stdout directly | |
| return _run_bcftools_command(cmd) | |
| def bcftools_view( | |
| input_file: Path, | |
| output: Optional[Path] = None, | |
| output_type: str = "v", | |
| header_only: bool = False, | |
| no_header: bool = False, | |
| drop_genotypes: bool = False, | |
| trim_alt_alleles: bool = False, | |
| min_alleles: Optional[int] = None, | |
| max_alleles: Optional[int] = None, | |
| min_ac: Optional[int] = None, | |
| max_ac: Optional[int] = None, | |
| min_af: Optional[float] = None, | |
| max_af: Optional[float] = None, | |
| types: Optional[str] = None, | |
| include: Optional[str] = None, | |
| exclude: Optional[str] = None, | |
| regions: Optional[str] = None, | |
| regions_file: Optional[Path] = None, | |
| samples: Optional[str] = None, | |
| samples_file: Optional[Path] = None, | |
| threads: int = 1, | |
| ): | |
| """ | |
| Subset, filter, and convert VCF and BCF files. | |
| """ | |
| if not input_file.exists(): | |
| raise FileNotFoundError(f"Input file not found: {input_file}") | |
| if output_type not in ["b", "u", "z", "v"]: | |
| raise ValueError("output_type must be one of 'b', 'u', 'z', 'v'.") | |
| cmd = ["bcftools", "view", str(input_file)] | |
| if output: | |
| cmd.extend(["-o", str(output)]) | |
| if output_type: | |
| cmd.extend(["-O", output_type]) | |
| if header_only: | |
| cmd.append("-h") | |
| if no_header: | |
| cmd.append("-H") | |
| if drop_genotypes: | |
| cmd.append("-G") | |
| if trim_alt_alleles: | |
| cmd.append("-a") | |
| if min_alleles is not None: | |
| cmd.extend(["-m", str(min_alleles)]) | |
| if max_alleles is not None: | |
| cmd.extend(["-M", str(max_alleles)]) | |
| if min_ac is not None: | |
| cmd.extend(["-c", str(min_ac)]) | |
| if max_ac is not None: | |
| cmd.extend(["-C", str(max_ac)]) | |
| if min_af is not None: | |
| cmd.extend(["-q", str(min_af)]) | |
| if max_af is not None: | |
| cmd.extend(["-Q", str(max_af)]) | |
| if types: | |
| cmd.extend(["-v", types]) | |
| if include: | |
| cmd.extend(["-i", include]) | |
| if exclude: | |
| cmd.extend(["-e", exclude]) | |
| if regions: | |
| cmd.extend(["-r", regions]) | |
| if regions_file: | |
| cmd.extend(["-R", str(regions_file)]) | |
| if samples: | |
| cmd.extend(["-s", samples]) | |
| if samples_file: | |
| cmd.extend(["-S", str(samples_file)]) | |
| if threads > 1: | |
| cmd.extend(["--threads", str(threads)]) | |
| return _run_bcftools_command(cmd, output) | |
| if __name__ == "__main__": | |
| mcp.run(transport="stdio") | |