Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
commit
cb5be898ea
663 files changed
+82674
No files matched your search
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Response, status
|
||||
|
||||
from scripts.reconcile_gitea_public_surface import reconcile_gitea_public_surface
|
||||
from services.control_api.contextforge import ContextForgeClient, ContextForgeSettings
|
||||
from services.control_api.persistence import create_engine_from_url, session_factory
|
||||
|
||||
from .common import hub_database_url, run_asgi
|
||||
from .state_sync import reconcile_runtime_state
|
||||
from .synthetic_monitor import GiteaSyntheticMonitor
|
||||
|
||||
|
||||
class WorkerState:
|
||||
def __init__(self) -> None:
|
||||
self._lock = Lock()
|
||||
self.ready = False
|
||||
self.last_error = "bootstrap pending"
|
||||
|
||||
def set(self, ready: bool, detail: str) -> None:
|
||||
with self._lock:
|
||||
self.ready = ready
|
||||
self.last_error = detail
|
||||
|
||||
def snapshot(self) -> tuple[bool, str]:
|
||||
with self._lock:
|
||||
return self.ready, self.last_error
|
||||
|
||||
|
||||
def _safe_error_detail(exc: Exception) -> str:
|
||||
"""Return a bounded operator hint without exposing exception payloads."""
|
||||
message = str(exc).lower()
|
||||
if "tool inventory parity failed" in message:
|
||||
reason = "tool-inventory-parity"
|
||||
elif "input schema parity failed" in message:
|
||||
reason = "input-schema-parity"
|
||||
elif "failed connector preflight" in message:
|
||||
reason = "connector-preflight"
|
||||
elif "public surface reconciliation did not pass" in message:
|
||||
reason = "public-surface-readback"
|
||||
else:
|
||||
reason = type(exc).__name__
|
||||
return f"runtime-reconciliation:{reason}"
|
||||
|
||||
|
||||
def _sync_once(
|
||||
client: ContextForgeClient,
|
||||
sessions: Any,
|
||||
catalog_root: Path,
|
||||
monitor: GiteaSyntheticMonitor,
|
||||
) -> None:
|
||||
reconcile_runtime_state(client, sessions, catalog_root, os.environ["HUB_OIDC_ISSUER"])
|
||||
surface = reconcile_gitea_public_surface(client, catalog_root)
|
||||
if surface.get("status") != "pass":
|
||||
raise RuntimeError("Gitea public surface reconciliation did not pass")
|
||||
monitor.probe()
|
||||
|
||||
|
||||
def build_app() -> FastAPI:
|
||||
catalog_root = Path(os.environ.get("HUB_CATALOG_ROOT", "/opt/hub/catalog"))
|
||||
engine = create_engine_from_url(hub_database_url())
|
||||
sessions = session_factory(engine)
|
||||
contextforge = ContextForgeClient(ContextForgeSettings.from_env())
|
||||
monitor = GiteaSyntheticMonitor(catalog_root)
|
||||
state = WorkerState()
|
||||
|
||||
async def reconcile_forever() -> None:
|
||||
while True:
|
||||
try:
|
||||
await asyncio.to_thread(_sync_once, contextforge, sessions, catalog_root, monitor)
|
||||
state.set(True, "runtime state, gateways and Gitea synthetic checks passed")
|
||||
except Exception as exc:
|
||||
state.set(False, _safe_error_detail(exc))
|
||||
await asyncio.sleep(30)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
task = asyncio.create_task(reconcile_forever())
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
task.cancel()
|
||||
await asyncio.gather(task, return_exceptions=True)
|
||||
contextforge.close()
|
||||
engine.dispose()
|
||||
|
||||
app = FastAPI(
|
||||
title="ITWorx MCP Hub worker",
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
openapi_url=None,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
@app.get("/healthz")
|
||||
@app.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"status": "healthy", "component": "worker"}
|
||||
|
||||
@app.get("/ready")
|
||||
@app.get("/health/ready")
|
||||
def ready(response: Response) -> dict[str, object]:
|
||||
is_ready, detail = state.snapshot()
|
||||
if not is_ready:
|
||||
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
return {"status": "ready" if is_ready else "not-ready", "detail": detail}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def run() -> None:
|
||||
run_asgi(build_app())
|
||||
Reference in new issue
Block a user