#!/usr/bin/env python3 """Generate Biomni MCP configs at approximately 100, 500, 1000, 1500, and 2000 tools. The generated YAML files use explicit tool metadata so Biomni's ``A1.add_mcp`` does not need to start every MCP server during registration. """ from __future__ import annotations import argparse import ast import json import re import sys from pathlib import Path from typing import Any try: import yaml except ImportError as exc: # pragma: no cover raise SystemExit("PyYAML is required: pip install pyyaml") from exc DEFAULT_TARGETS = (100, 500, 1000, 1500, 2000) REPO_ROOT = Path(__file__).resolve().parents[3] DEFAULT_MCP_ROOT = REPO_ROOT / "mcp_generated" DEFAULT_OUT_DIR = Path(__file__).resolve().parents[1] / "configs" DEFAULT_GOLD = Path(__file__).resolve().parents[1] / "gold_tools.json" DEFAULT_EXECUTION_PYTHON = Path("/225040511/miniconda3/envs/biomni_e1/bin/python") PRIORITY_SERVER_STEMS = [ "mcp_kallisto", "mcp_salmon", "mcp_kraken2", "mcp_bracken", "mcp_csvtk", "mcp_megahit", "mcp_kaiju", "mcp_fastp", "mcp_spades", "mcp_scanpy", "mcp_scanpy-cli", "mcp_scanpy-scripts", "mcp_seurat-scripts", "mcp_bioconductor-deseq2", "mcp_bioconductor-edger", "mcp_bioconductor-limma", "mcp_bioconductor-clusterprofiler", "mcp_star", "mcp_hisat2", "mcp_subread", "mcp_samtools", "mcp_bcftools", "mcp_bwa", "mcp_gatk4", "mcp_gatk", "mcp_snpeff", "mcp_freebayes", "mcp_orthofinder", "mcp_blast", "mcp_blast-legacy", "mcp_diamond", "mcp_gffread", "mcp_seqkit", "mcp_multiqc", "mcp_fastqc", "mcp_trimmomatic", ] def safe_server_name(dirname: str) -> str: name = dirname[4:] if dirname.startswith("mcp_") else dirname return re.sub(r"[^A-Za-z0-9_]", "_", name) def choose_server_file(server_dir: Path) -> Path | None: app_dir = server_dir / "app" if not app_dir.exists(): return None shims = sorted(app_dir.glob("*_shim_server.py")) servers = sorted(p for p in app_dir.glob("*_server.py") if not p.name.endswith("_shim_server.py")) if shims: return shims[0] if servers: return servers[0] return None def source_file_for_metadata(server_file: Path) -> Path: if not server_file.name.endswith("_shim_server.py"): return server_file local_name = server_file.name.replace("_shim_server.py", "_server.py") local = server_file.with_name(local_name) return local if local.exists() else server_file def annotation_to_json_type(node: ast.AST | None) -> str: if node is None: return "string" text = ast.unparse(node).lower() if any(x in text for x in ("int", "float", "double")): return "number" if "bool" in text: return "boolean" if any(x in text for x in ("list", "tuple", "set", "sequence")): return "array" return "string" def is_mcp_tool_decorator(dec: ast.AST) -> bool: target = dec.func if isinstance(dec, ast.Call) else dec if isinstance(target, ast.Attribute) and target.attr == "tool": return True if isinstance(target, ast.Name) and target.id in {"tool", "mcp_tool"}: return True return False def parse_tools(source_file: Path, server_stem: str) -> list[dict[str, Any]]: try: tree = ast.parse(source_file.read_text(encoding="utf-8"), filename=str(source_file)) except Exception: return [] tools: list[dict[str, Any]] = [] for node in tree.body: if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): continue if node.name.startswith("_"): continue if not any(is_mcp_tool_decorator(dec) for dec in node.decorator_list): continue doc = ast.get_docstring(node) or f"MCP tool {node.name} from {server_stem}" first_line = " ".join(doc.strip().splitlines()[0].split()) if doc.strip() else "" properties: dict[str, dict[str, Any]] = {} required: list[str] = [] defaults = [None] * (len(node.args.args) - len(node.args.defaults)) + list(node.args.defaults) for arg, default in zip(node.args.args, defaults, strict=False): if arg.arg in {"self", "cls"}: continue spec: dict[str, Any] = { "type": annotation_to_json_type(arg.annotation), "description": "", } if default is None: required.append(arg.arg) else: with contextlib_suppress(Exception): spec["default"] = ast.literal_eval(default) properties[arg.arg] = spec tools.append( { "name": node.name, "description": first_line or f"MCP tool {node.name} from {server_stem}", "inputSchema": {"properties": properties, "required": required}, } ) return tools class contextlib_suppress: def __init__(self, *exceptions: type[BaseException]): self.exceptions = exceptions or (Exception,) def __enter__(self): return None def __exit__(self, exc_type, exc, traceback): return exc_type is not None and issubclass(exc_type, self.exceptions) def discover_servers(mcp_root: Path, execution_python: Path) -> list[dict[str, Any]]: records: list[dict[str, Any]] = [] for server_dir in sorted(p for p in mcp_root.iterdir() if p.is_dir() and p.name.startswith("mcp_")): server_file = choose_server_file(server_dir) if server_file is None: continue metadata_file = source_file_for_metadata(server_file) tools = parse_tools(metadata_file, server_dir.name) if not tools: continue records.append( { "server_dir": server_dir.name, "server_name": safe_server_name(server_dir.name), "path": str(server_dir), "command": [str(execution_python), str(server_file)], "metadata_source": str(metadata_file), "tool_count": len(tools), "tools": tools, } ) return records def priority_order(records: list[dict[str, Any]], gold_path: Path) -> list[dict[str, Any]]: gold_servers: list[str] = [] if gold_path.exists(): gold = json.loads(gold_path.read_text(encoding="utf-8")) for entry in gold.values(): gold_servers.extend(entry.get("gold_servers", [])) priority = [] seen = set() for name in [*PRIORITY_SERVER_STEMS, *gold_servers]: if name not in seen: priority.append(name) seen.add(name) rank = {name: i for i, name in enumerate(priority)} return sorted(records, key=lambda r: (rank.get(r["server_dir"], 10_000), r["server_dir"])) def select_for_target(records: list[dict[str, Any]], target: int) -> list[dict[str, Any]]: selected: list[dict[str, Any]] = [] total = 0 for record in records: if total >= target: break selected.append(record) total += record["tool_count"] return selected def write_yaml(selected: list[dict[str, Any]], out_path: Path) -> None: config = {"mcp_servers": {}} for record in selected: config["mcp_servers"][record["server_name"]] = { "enabled": True, "description": f"Generated MCP server config from {record['server_dir']} ({record['tool_count']} tools)", "command": record["command"], "source_dir": record["path"], "metadata_source": record["metadata_source"], "tools": record["tools"], } out_path.write_text(yaml.safe_dump(config, sort_keys=False, width=120), encoding="utf-8") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--mcp-root", type=Path, default=DEFAULT_MCP_ROOT) parser.add_argument("--out-dir", type=Path, default=DEFAULT_OUT_DIR) parser.add_argument("--gold", type=Path, default=DEFAULT_GOLD) parser.add_argument("--execution-python", type=Path, default=DEFAULT_EXECUTION_PYTHON) parser.add_argument("--targets", type=int, nargs="+", default=list(DEFAULT_TARGETS)) args = parser.parse_args() args.out_dir.mkdir(parents=True, exist_ok=True) execution_python = args.execution_python if args.execution_python.exists() else Path(sys.executable) records = priority_order(discover_servers(args.mcp_root, execution_python), args.gold) if not records: raise SystemExit(f"No MCP servers with parseable tools found under {args.mcp_root}") manifest: dict[str, Any] = { "mcp_root": str(args.mcp_root), "execution_python": str(execution_python), "available_server_count": len(records), "available_tool_count": sum(r["tool_count"] for r in records), "targets": {}, } (args.out_dir / "mcp_server_inventory.json").write_text( json.dumps(records, indent=2, ensure_ascii=False), encoding="utf-8" ) for target in args.targets: selected = select_for_target(records, target) total = sum(r["tool_count"] for r in selected) out_path = args.out_dir / f"mcp_scale_{target}.yaml" write_yaml(selected, out_path) manifest["targets"][str(target)] = { "config": str(out_path), "server_count": len(selected), "tool_count": total, "servers": [r["server_dir"] for r in selected], } (args.out_dir / "mcp_scale_manifest.json").write_text( json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8" ) print(json.dumps(manifest, indent=2, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())