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]