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
This commit is contained in:
121
main.py
Normal file
121
main.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user