Files
docs/main.py
Vishesh 'ironeagle' Bangotra 0c25cbb19f feat: unify docs portal and MCP servers into one config-driven service
- 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
2026-09-11 15:14:51 +05:30

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()