"""render-html MCP Apps server — renders arbitrary HTML inline in AI chat. Architecture (matching PDF Viewer pattern): 1. Resource at ui://render-html/viewer.html (text/html;profile=mcp-app) 2. Tools have _meta.ui.resourceUri pointing to viewer 3. Tools return structuredContent + outputSchema 4. Viewer implements MCP Apps handshake: ui/initialize -> tool-result """ from mcp.server.fastmcp import FastMCP from mcp.server.transport_security import TransportSecuritySettings from mcp.types import CallToolResult, TextContent from pydantic import BaseModel from typing import Annotated mcp = FastMCP("render-html", transport_security=TransportSecuritySettings( enable_dns_rebinding_protection=False, )) # -- The viewer HTML app --------------------------------------------------- # Implements the MCP Apps lifecycle: # 1. View -> Host: ui/initialize (JSON-RPC request) # 2. Host -> View: response with host context # 3. View -> Host: ui/notifications/initialized # 4. Host -> View: ui/notifications/tool-result with structuredContent VIEWER_HTML = """ Render HTML Viewer
Loading…
""" VIEWER_URI = "ui://render-html/viewer.html" # Both forms of the meta key (matching PDF Viewer) TOOL_META = { "ui": {"resourceUri": VIEWER_URI}, "ui/resourceUri": VIEWER_URI, } # -- Register the viewer as an MCP resource -------------------------------- @mcp.resource( VIEWER_URI, name="HTML Viewer", description="Viewer app that renders arbitrary HTML content", mime_type="text/html;profile=mcp-app", ) def viewer_resource() -> str: return VIEWER_HTML # -- Output schemas -------------------------------------------------------- class HtmlOutput(BaseModel): html: str title: str class SvgOutput(BaseModel): svg: str title: str # -- Tools ----------------------------------------------------------------- @mcp.tool( name="render_html", annotations={ "title": "Render HTML", "readOnlyHint": True, "openWorldHint": True, }, meta=TOOL_META, ) def render_html(html: str, title: str = "Rendered Content") -> Annotated[CallToolResult, HtmlOutput]: """Render arbitrary HTML content inline in the conversation. Use this to display images, SVG diagrams, charts, styled content, or any HTML/CSS/JS directly in the chat. The HTML is rendered in a sandboxed iframe. Args: html: Complete HTML content to render. Can include style, script, inline styles, SVG, images via img tags, etc. title: Optional title for the rendered content. """ return CallToolResult( content=[TextContent(type="text", text=f"Rendered: {title}")], structuredContent={"html": html, "title": title}, _meta=TOOL_META, ) @mcp.tool( name="render_image", annotations={ "title": "Render Image", "readOnlyHint": True, "openWorldHint": True, }, meta=TOOL_META, ) def render_image(url: str, alt: str = "", width: int = 0) -> Annotated[CallToolResult, HtmlOutput]: """Display an image from a URL inline in the conversation. Args: url: The image URL to display. alt: Alt text for the image. width: Optional width in pixels (0 = auto). """ style = 'max-width:100%;height:auto;' if width > 0: style = f'width:{width}px;max-width:100%;height:auto;' img_html = ( f'
' f'{alt}' f'
' ) return CallToolResult( content=[TextContent(type="text", text=f"Image: {alt or url}")], structuredContent={"html": img_html, "title": alt or "Image"}, _meta=TOOL_META, ) @mcp.tool( name="render_svg", annotations={ "title": "Render SVG", "readOnlyHint": True, "openWorldHint": True, }, meta=TOOL_META, ) def render_svg(svg: str, title: str = "SVG Diagram") -> Annotated[CallToolResult, SvgOutput]: """Render an SVG diagram inline in the conversation. Args: svg: SVG markup string. title: Optional title. """ wrapped = ( f'
' f'{svg}
' ) return CallToolResult( content=[TextContent(type="text", text=f"SVG: {title}")], structuredContent={"svg": wrapped, "title": title}, _meta=TOOL_META, ) if __name__ == "__main__": import uvicorn app = mcp.streamable_http_app() uvicorn.run(app, host="0.0.0.0", port=7860)