Spaces:
Running
Running
| """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 = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Render HTML Viewer</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| html, body { width: 100%; height: 100%; overflow: auto; background: transparent; } | |
| #content { width: 100%; min-height: 100%; } | |
| #loading { display: flex; align-items: center; justify-content: center; | |
| height: 100vh; font-family: system-ui; color: #666; font-size: 14px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loading">Loading…</div> | |
| <div id="content" style="display:none;"></div> | |
| <script> | |
| (function() { | |
| var contentEl = document.getElementById('content'); | |
| var loadingEl = document.getElementById('loading'); | |
| var msgId = 1; | |
| // -- Render HTML content from structuredContent -- | |
| function renderContent(data) { | |
| if (!data) return; | |
| var html = data.html || data.svg || ''; | |
| if (!html) return; | |
| loadingEl.style.display = 'none'; | |
| contentEl.style.display = 'block'; | |
| contentEl.innerHTML = html; | |
| // Re-execute script tags | |
| var scripts = contentEl.querySelectorAll('script'); | |
| for (var i = 0; i < scripts.length; i++) { | |
| var old = scripts[i]; | |
| var s = document.createElement('script'); | |
| if (old.src) s.src = old.src; | |
| else s.textContent = old.textContent; | |
| old.parentNode.replaceChild(s, old); | |
| } | |
| } | |
| // -- Send JSON-RPC message to host -- | |
| function sendToHost(msg) { | |
| if (window.parent && window.parent !== window) { | |
| window.parent.postMessage(msg, '*'); | |
| } | |
| } | |
| // -- MCP Apps initialization handshake -- | |
| // Step 1: Send ui/initialize request to host | |
| var initId = msgId++; | |
| sendToHost({ | |
| jsonrpc: '2.0', | |
| id: initId, | |
| method: 'ui/initialize', | |
| params: { | |
| appInfo: { name: 'render-html', version: '1.0.0' }, | |
| capabilities: {} | |
| } | |
| }); | |
| // -- Listen for messages from host -- | |
| window.addEventListener('message', function(event) { | |
| if (event.source !== window.parent) return; | |
| var msg = event.data; | |
| if (!msg || msg.jsonrpc !== '2.0') return; | |
| // Step 2: Host responds to ui/initialize with host context | |
| if (msg.id === initId && msg.result) { | |
| // Step 3: Send ui/notifications/initialized | |
| sendToHost({ | |
| jsonrpc: '2.0', | |
| method: 'ui/notifications/initialized', | |
| params: {} | |
| }); | |
| } | |
| // Step 4: Receive tool-result notification | |
| if (msg.method === 'ui/notifications/tool-result') { | |
| renderContent(msg.params && msg.params.structuredContent); | |
| } | |
| // Also handle tool-input (streaming partial args) | |
| if (msg.method === 'ui/notifications/tool-input') { | |
| // Could show preview; for render-html we wait for final result | |
| } | |
| }, { passive: true }); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| 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 -------------------------------- | |
| def viewer_resource() -> str: | |
| return VIEWER_HTML | |
| # -- Output schemas -------------------------------------------------------- | |
| class HtmlOutput(BaseModel): | |
| html: str | |
| title: str | |
| class SvgOutput(BaseModel): | |
| svg: str | |
| title: str | |
| # -- Tools ----------------------------------------------------------------- | |
| 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, | |
| ) | |
| 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'<div style="display:flex;justify-content:center;align-items:center;' | |
| f'min-height:200px;padding:8px;">' | |
| f'<img src="{url}" alt="{alt}" style="{style}" />' | |
| f'</div>' | |
| ) | |
| return CallToolResult( | |
| content=[TextContent(type="text", text=f"Image: {alt or url}")], | |
| structuredContent={"html": img_html, "title": alt or "Image"}, | |
| _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'<div style="display:flex;justify-content:center;align-items:center;' | |
| f'min-height:200px;padding:8px;background:#fff;">' | |
| f'{svg}</div>' | |
| ) | |
| 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) | |