#!/usr/bin/env python """aetos-docs runtime entrypoint. Starts nginx (HTML portal on html_port), a /healthz server (health_port), and one FastMCP streamable-http server per repo that declares an MCP bundle. """ from __future__ import annotations import argparse import multiprocessing import shutil import subprocess import sys from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path from config import MCPBundle, load_config from core import MCPServer APP_ROOT = Path(__file__).resolve().parent CONFIG_FILE = APP_ROOT / "config.yml" # ── nginx ──────────────────────────────────────────────────────────────────── def start_nginx() -> subprocess.Popen | None: """Validate config and start nginx in the background.""" if shutil.which("nginx") is None: print("[NGINX] binary not found; skipping web portal (local dev)", flush=True) return None result = subprocess.run( ["nginx", "-t"], capture_output=True, text=True, ) if result.returncode != 0: print(f"[NGINX] config test failed:\n{result.stderr}", file=sys.stderr, flush=True) sys.exit(1) proc = subprocess.Popen(["nginx"]) print("[NGINX] started", flush=True) return proc # ── health ─────────────────────────────────────────────────────────────────── def run_health_server(port: int) -> None: class Handler(BaseHTTPRequestHandler): def do_GET(self) -> None: if self.path == "/healthz": self.send_response(200) self.end_headers() self.wfile.write(b"ok") else: self.send_response(404) self.end_headers() def log_message(self, format: str, *args: object) -> None: # noqa: A002 return # silence logs server = HTTPServer(("0.0.0.0", port), Handler) print(f"[HEALTH] listening on :{port}", flush=True) server.serve_forever() # ── MCP server ─────────────────────────────────────────────────────────────── def run_server(name: str, bundle: MCPBundle) -> None: print(f"[START] {name} starting", flush=True) server = MCPServer(bundle) server.app.run(transport="streamable-http") print(f"[EXIT] {name} exited", flush=True) # ── main ───────────────────────────────────────────────────────────────────── def main() -> None: config = load_config(CONFIG_FILE) # nginx start_nginx() # health health = multiprocessing.Process( target=run_health_server, args=(config.health_port,), daemon=True, ) health.start() # MCP servers bundles = config.mcp_servers parser = argparse.ArgumentParser(description="aetos-docs runtime") parser.add_argument( "--servers", help=f"Comma-separated server names ({', '.join(bundles)})", ) args = parser.parse_args() names = [s.strip() for s in args.servers.split(",")] if args.servers else list(bundles) for name in names: if name not in bundles: print(f"Unknown server: {name} (available: {', '.join(bundles)})", file=sys.stderr) sys.exit(1) processes: list[multiprocessing.Process] = [] for name in names: p = multiprocessing.Process(target=run_server, args=(name, bundles[name])) p.start() processes.append(p) for p in processes: p.join() if __name__ == "__main__": main()