czty's picture
Add files using upload-large-folder tool
ea975d9 verified
Raw
History Blame Contribute Delete
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)
@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: # 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)
@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))
# 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)
@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])
# 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
@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))
# Reheader does not support -O, output type is inferred from extension
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])
# 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)
@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")