- add config.yml as single source of truth for collected repos/categories - add collect.py CLI to pull lib/api/wiki site builds and mcp bundles from source repos and regenerate nginx.conf + _index/index.html - port mcp runtime (main.py/core.py/config.py): nginx + health + FastMCP servers started from config.yml - docker: python:3.13-slim + nginx, expose 8000-8006 + 9000 (html portal) - drone: publish html (6007:9000) and MCP ports 8000-8006 - move mongo-ops docs to wiki/, add auth-server (api) and hexa (lib) - refresh lib site builds (lib/ prefix) and collect mcp bundles
122 lines
3.9 KiB
Python
122 lines
3.9 KiB
Python
#!/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()
|