""" LRU Bicep module catalog — fetched from DevOpsMCP at startup. Provides completion items for LRU-internal Bicep modules with higher sort priority than standard Azure modules. """ import asyncio import logging import os import time from typing import Any import aiohttp logger = logging.getLogger(__name__) DEVOPS_MCP_URL = os.getenv("DEVOPS_MCP_URL", "https://devops-mcp.i80.dk") REFRESH_INTERVAL = 3600 # 1 hour class BicepModuleCatalog: """Fetches and caches LRU Bicep modules from DevOpsMCP.""" _modules: list[dict[str, Any]] = [] _last_refresh: float = 0 _lock = asyncio.Lock() @classmethod async def get_modules(cls) -> list[dict[str, Any]]: if not cls._modules or time.time() - cls._last_refresh > REFRESH_INTERVAL: await cls._refresh() return cls._modules @classmethod async def start_background_refresh(cls) -> None: asyncio.create_task(cls._refresh_loop()) @classmethod async def _refresh_loop(cls) -> None: while True: await cls._refresh() await asyncio.sleep(REFRESH_INTERVAL) @classmethod async def _refresh(cls) -> None: async with cls._lock: try: modules = await cls._fetch_from_devops_mcp() cls._modules = modules cls._last_refresh = time.time() logger.info("Bicep module catalog refreshed: %d modules", len(modules)) except Exception: logger.exception("Failed to refresh Bicep module catalog — using stale cache") @classmethod async def _fetch_from_devops_mcp(cls) -> list[dict[str, Any]]: """Call DevOpsMCP /api/bicep-modules REST endpoint.""" url = f"{DEVOPS_MCP_URL}/api/bicep-modules" async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as session: async with session.get(url) as resp: if resp.status != 200: raise RuntimeError(f"DevOpsMCP returned {resp.status}") data = await resp.json() modules = [] for m in data.get("modules", []): path = m.get("path", "") versions = m.get("versions", ["latest"]) name = path.split("/")[-1] if "/" in path else path modules.append({ "name": name, "path": path, "versions": versions, "latest": versions[-1] if versions else "latest", "registry": data.get("registry", "iactemplatereg.azurecr.io"), }) return modules @classmethod def as_completion_items(cls) -> list[dict[str, Any]]: items = [] for mod in cls._modules: ref = f"br/modules:{mod['path']}:{mod['latest']}" items.append({ "label": mod["name"], "kind": 9, # Module "detail": f"LRU Bicep module — {mod['registry']}", "insertText": ref, "sortText": f"0_lru_{mod['name']}", # sorts above standard az modules "documentation": { "kind": "markdown", "value": ( f"**{mod['name']}** (LRU internal)\n\n" f"Registry: `{mod['registry']}`\n" f"Versions: {', '.join(mod['versions'])}\n\n" f"```bicep\nmodule {mod['name'].lower()} '{ref}' = {{\n" f" name: '{mod['name'].lower()}'\n params: {{}}\n}}\n```" ), }, }) return items