| import subprocess |
| import tempfile |
| from pathlib import Path |
| from typing import List, Optional |
|
|
| |
| |
| |
|
|
| |
| 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 [] |
| |
| |
| 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) |
|
|
| @mcp.tool() |
| 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) |
|
|
| @mcp.tool() |
| 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: |
| 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) |
|
|
| @mcp.tool() |
| 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) |
|
|
| @mcp.tool() |
| 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) |
|
|
| @mcp.tool() |
| 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) |
|
|
| @mcp.tool() |
| 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)) |
| |
| |
| output_idx_file = None |
| if csi: |
| output_idx_file = Path(str(input_file) + ".csi") |
| else: |
| 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) |
|
|
| @mcp.tool() |
| 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]) |
|
|
| |
| |
| |
| |
| result = _run_bcftools_command(cmd) |
| |
| |
| if result["return_code"] == 0: |
| created_files = [str(p) for p in output_dir.glob('*')] |
| result["output_files"] = created_files |
|
|
| return result |
|
|
| @mcp.tool() |
| 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) |
|
|
| @mcp.tool() |
| 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) |
|
|
| @mcp.tool() |
| 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) |
|
|
| @mcp.tool() |
| 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) |
|
|
| @mcp.tool() |
| 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)) |
|
|
| |
| return _run_bcftools_command(cmd, output) |
|
|
| @mcp.tool() |
| 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) |
|
|
| @mcp.tool() |
| 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]) |
|
|
| |
| |
| 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: |
| |
| return _run_bcftools_command(cmd) |
|
|
| @mcp.tool() |
| 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") |
|
|