"""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'

'
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)