render-html / app.py
korakot's picture
Upload app.py with huggingface_hub
ee7487c verified
"""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&#8230;</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 --------------------------------
@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'<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,
)
@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'<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)