"""Versioned Prefab renderer resource for reliable host cache invalidation.""" from __future__ import annotations import hashlib import json import os import re from collections.abc import Sequence from pathlib import Path from urllib.parse import urlsplit 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 _RENDERER_MODULE = re.compile( r'' ) _COMPATIBLE_RENDERER_DIGESTS = ( "0e57015a15d4", "3217f2d981ea", "7cfb45fbe220", "aa2fea14c7e0", ) PREFAB_OUTPUT_SCHEMA = { "type": "object", "required": ["$prefab", "view", "state"], "properties": { "$prefab": { "type": "object", "required": ["version"], "properties": {"version": {"type": "string"}}, "additionalProperties": True, }, "view": {"type": "object"}, "state": {"type": "object"}, "css": {"type": "array", "items": {"type": "string"}}, "stylesheets": { "type": "array", "items": {"type": "string"}, }, }, "additionalProperties": True, } def _with_chatgpt_remount_recovery( html: str, widget_domain: str | None = None, ) -> str: """Recover a remounted ChatGPT widget from its scoped tool output.""" match = _RENDERER_MODULE.search(html) if match is None: return html renderer_url = json.dumps(match.group(1)) allowed_origin = json.dumps(widget_domain) script = f"""""" return _RENDERER_MODULE.sub(script, html, count=1) 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 meta["ui/resourceUri"] = self.resource_uri return tool.model_copy( update={ "meta": meta, "output_schema": PREFAB_OUTPUT_SCHEMA, } ) def install_versioned_renderer( mcp: FastMCP, *, app_name: str, tool_name: str, build_id: str, resource_domains: Sequence[str] = (), widget_domain: str | None = None, ) -> str: """Register the Prefab renderer at a URI derived from its exact content.""" widget_domain = widget_domain or default_widget_domain() html = _with_chatgpt_remount_recovery( get_renderer_html(), widget_domain, ).replace( "", '', ) 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_data = dict(get_renderer_csp() or {}) domains = list(csp_data.get("resource_domains") or ()) domains.extend(domain for domain in resource_domains if domain not in domains) csp_data["resource_domains"] = domains connect_domains = list(csp_data.get("connect_domains") or ()) if widget_domain and widget_domain not in connect_domains: connect_domains.append(widget_domain) csp_data["connect_domains"] = connect_domains csp = ResourceCSP(**csp_data) app_config = AppConfig(csp=csp, prefers_border=True) def register_renderer(uri: str, name: str) -> None: @mcp.resource( uri, name=name, mime_type=UI_MIME_TYPE, app=app_config, ) def prefab_renderer() -> str: return html register_renderer(resource_uri, "Versioned Prefab Renderer") for compatible_digest in _COMPATIBLE_RENDERER_DIGESTS: if compatible_digest == digest: continue register_renderer( ( f"ui://prefab/tool/{tool_digest}/" f"renderer-{compatible_digest}.html" ), f"Compatible Prefab Renderer {compatible_digest}", ) mcp.add_transform(RendererUriTransform(tool_name, resource_uri)) return digest def default_widget_domain() -> str | None: value = os.getenv("FAST_AGENT_OAUTH_RESOURCE_URL", "").strip().rstrip("/") if not value: host = os.getenv("SPACE_HOST", "").strip().strip("/") value = host if "://" in host else f"https://{host}" if host else "" if not value: return None parsed = urlsplit(value) if parsed.scheme not in {"http", "https"} or not parsed.netloc: raise ValueError(f"Invalid MCP App widget domain: {value!r}") return f"{parsed.scheme}://{parsed.netloc}" 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]