File size: 2,488 Bytes
5891bca 9c370dc 5891bca 9c370dc 5891bca | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | """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(
"</head>",
f'<meta name="research-app-build" content="{build_id}"></head>',
)
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]
|