"""Versioned Prefab renderer resource for reliable host cache invalidation.""" from __future__ import annotations import hashlib from collections.abc import Sequence from pathlib import Path from fastmcp import FastMCP from fastmcp.apps.config import UI_MIME_TYPE, AppConfig, ResourceCSP from fastmcp.server.providers.addressing import hash_tool from fastmcp.server.transforms import Transform from fastmcp.tools.base import Tool from prefab_ui.renderer import get_renderer_csp, get_renderer_html class RendererUriTransform(Transform): """Point one UI tool at a content-addressed renderer resource.""" def __init__(self, tool_name: str, resource_uri: str) -> None: self.tool_name = tool_name self.resource_uri = resource_uri async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]: return [ self._with_renderer(tool) if tool.name == self.tool_name else tool for tool in tools ] def _with_renderer(self, tool: Tool) -> Tool: meta = dict(tool.meta or {}) ui = dict(meta.get("ui") or {}) ui["resourceUri"] = self.resource_uri meta["ui"] = ui return tool.model_copy(update={"meta": meta}) def install_versioned_renderer( mcp: FastMCP, *, app_name: str, tool_name: str, build_id: str, ) -> str: """Register the Prefab renderer at a URI derived from its exact content.""" html = get_renderer_html().replace( "", f'', ) digest = hashlib.sha256(html.encode()).hexdigest()[:12] tool_digest = hash_tool(app_name, tool_name) resource_uri = f"ui://prefab/tool/{tool_digest}/renderer-{digest}.html" csp = ResourceCSP(**(get_renderer_csp() or {})) @mcp.resource( resource_uri, name="Versioned Prefab Renderer", mime_type=UI_MIME_TYPE, app=AppConfig(csp=csp), ) def prefab_renderer() -> str: return html mcp.add_transform(RendererUriTransform(tool_name, resource_uri)) return digest def app_build_id(home: Path) -> str: """Hash the app code and prompts that shape a rendered research view.""" digest = hashlib.sha256() paths = sorted(home.glob("*.py")) + sorted((home / "agent-cards").glob("*.md")) for path in paths: digest.update(path.relative_to(home).as_posix().encode()) digest.update(path.read_bytes()) return digest.hexdigest()[:8]