Spaces:
Running
Running
Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -2,22 +2,27 @@
|
|
| 2 |
|
| 3 |
Architecture (matching PDF Viewer pattern):
|
| 4 |
1. Resource at ui://render-html/viewer.html (text/html;profile=mcp-app)
|
| 5 |
-
2. Tools have _meta.ui.resourceUri
|
| 6 |
3. Tools return structuredContent + outputSchema
|
| 7 |
-
4. Viewer
|
| 8 |
"""
|
| 9 |
|
| 10 |
from mcp.server.fastmcp import FastMCP
|
| 11 |
from mcp.server.transport_security import TransportSecuritySettings
|
| 12 |
from mcp.types import CallToolResult, TextContent
|
| 13 |
-
from pydantic import BaseModel
|
| 14 |
from typing import Annotated
|
| 15 |
|
| 16 |
mcp = FastMCP("render-html", transport_security=TransportSecuritySettings(
|
| 17 |
enable_dns_rebinding_protection=False,
|
| 18 |
))
|
| 19 |
|
| 20 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
VIEWER_HTML = """<!DOCTYPE html>
|
| 23 |
<html lang="en">
|
|
@@ -27,46 +32,87 @@ VIEWER_HTML = """<!DOCTYPE html>
|
|
| 27 |
<title>Render HTML Viewer</title>
|
| 28 |
<style>
|
| 29 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 30 |
-
html, body { width: 100%; height: 100%; overflow: auto; }
|
| 31 |
#content { width: 100%; min-height: 100%; }
|
| 32 |
#loading { display: flex; align-items: center; justify-content: center;
|
| 33 |
-
height: 100vh; font-family: system-ui; color: #666; }
|
| 34 |
</style>
|
| 35 |
</head>
|
| 36 |
<body>
|
| 37 |
-
<div id="loading">Loading
|
| 38 |
<div id="content" style="display:none;"></div>
|
| 39 |
<script>
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
| 42 |
|
|
|
|
| 43 |
function renderContent(data) {
|
| 44 |
if (!data) return;
|
| 45 |
-
|
| 46 |
-
if (html)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
}
|
| 59 |
|
| 60 |
-
//
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
if (event.source !== window.parent) return;
|
| 63 |
-
|
| 64 |
if (!msg || msg.jsonrpc !== '2.0') return;
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
if (msg.method === 'ui/notifications/tool-result') {
|
| 67 |
-
renderContent(msg.params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
}, { passive: true });
|
|
|
|
| 70 |
</script>
|
| 71 |
</body>
|
| 72 |
</html>
|
|
@@ -74,14 +120,14 @@ VIEWER_HTML = """<!DOCTYPE html>
|
|
| 74 |
|
| 75 |
VIEWER_URI = "ui://render-html/viewer.html"
|
| 76 |
|
| 77 |
-
# Both forms of the meta key (PDF Viewer
|
| 78 |
TOOL_META = {
|
| 79 |
"ui": {"resourceUri": VIEWER_URI},
|
| 80 |
"ui/resourceUri": VIEWER_URI,
|
| 81 |
}
|
| 82 |
|
| 83 |
|
| 84 |
-
#
|
| 85 |
@mcp.resource(
|
| 86 |
VIEWER_URI,
|
| 87 |
name="HTML Viewer",
|
|
@@ -92,7 +138,7 @@ def viewer_resource() -> str:
|
|
| 92 |
return VIEWER_HTML
|
| 93 |
|
| 94 |
|
| 95 |
-
#
|
| 96 |
|
| 97 |
class HtmlOutput(BaseModel):
|
| 98 |
html: str
|
|
@@ -103,7 +149,7 @@ class SvgOutput(BaseModel):
|
|
| 103 |
title: str
|
| 104 |
|
| 105 |
|
| 106 |
-
#
|
| 107 |
|
| 108 |
@mcp.tool(
|
| 109 |
name="render_html",
|
|
@@ -118,10 +164,12 @@ def render_html(html: str, title: str = "Rendered Content") -> Annotated[CallToo
|
|
| 118 |
"""Render arbitrary HTML content inline in the conversation.
|
| 119 |
|
| 120 |
Use this to display images, SVG diagrams, charts, styled content,
|
| 121 |
-
or any HTML/CSS/JS directly in the chat.
|
|
|
|
| 122 |
|
| 123 |
Args:
|
| 124 |
-
html: Complete HTML content to render.
|
|
|
|
| 125 |
title: Optional title for the rendered content.
|
| 126 |
"""
|
| 127 |
return CallToolResult(
|
|
|
|
| 2 |
|
| 3 |
Architecture (matching PDF Viewer pattern):
|
| 4 |
1. Resource at ui://render-html/viewer.html (text/html;profile=mcp-app)
|
| 5 |
+
2. Tools have _meta.ui.resourceUri pointing to viewer
|
| 6 |
3. Tools return structuredContent + outputSchema
|
| 7 |
+
4. Viewer implements MCP Apps handshake: ui/initialize -> tool-result
|
| 8 |
"""
|
| 9 |
|
| 10 |
from mcp.server.fastmcp import FastMCP
|
| 11 |
from mcp.server.transport_security import TransportSecuritySettings
|
| 12 |
from mcp.types import CallToolResult, TextContent
|
| 13 |
+
from pydantic import BaseModel
|
| 14 |
from typing import Annotated
|
| 15 |
|
| 16 |
mcp = FastMCP("render-html", transport_security=TransportSecuritySettings(
|
| 17 |
enable_dns_rebinding_protection=False,
|
| 18 |
))
|
| 19 |
|
| 20 |
+
# -- The viewer HTML app ---------------------------------------------------
|
| 21 |
+
# Implements the MCP Apps lifecycle:
|
| 22 |
+
# 1. View -> Host: ui/initialize (JSON-RPC request)
|
| 23 |
+
# 2. Host -> View: response with host context
|
| 24 |
+
# 3. View -> Host: ui/notifications/initialized
|
| 25 |
+
# 4. Host -> View: ui/notifications/tool-result with structuredContent
|
| 26 |
|
| 27 |
VIEWER_HTML = """<!DOCTYPE html>
|
| 28 |
<html lang="en">
|
|
|
|
| 32 |
<title>Render HTML Viewer</title>
|
| 33 |
<style>
|
| 34 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 35 |
+
html, body { width: 100%; height: 100%; overflow: auto; background: transparent; }
|
| 36 |
#content { width: 100%; min-height: 100%; }
|
| 37 |
#loading { display: flex; align-items: center; justify-content: center;
|
| 38 |
+
height: 100vh; font-family: system-ui; color: #666; font-size: 14px; }
|
| 39 |
</style>
|
| 40 |
</head>
|
| 41 |
<body>
|
| 42 |
+
<div id="loading">Loading…</div>
|
| 43 |
<div id="content" style="display:none;"></div>
|
| 44 |
<script>
|
| 45 |
+
(function() {
|
| 46 |
+
var contentEl = document.getElementById('content');
|
| 47 |
+
var loadingEl = document.getElementById('loading');
|
| 48 |
+
var msgId = 1;
|
| 49 |
|
| 50 |
+
// -- Render HTML content from structuredContent --
|
| 51 |
function renderContent(data) {
|
| 52 |
if (!data) return;
|
| 53 |
+
var html = data.html || data.svg || '';
|
| 54 |
+
if (!html) return;
|
| 55 |
+
loadingEl.style.display = 'none';
|
| 56 |
+
contentEl.style.display = 'block';
|
| 57 |
+
contentEl.innerHTML = html;
|
| 58 |
+
// Re-execute script tags
|
| 59 |
+
var scripts = contentEl.querySelectorAll('script');
|
| 60 |
+
for (var i = 0; i < scripts.length; i++) {
|
| 61 |
+
var old = scripts[i];
|
| 62 |
+
var s = document.createElement('script');
|
| 63 |
+
if (old.src) s.src = old.src;
|
| 64 |
+
else s.textContent = old.textContent;
|
| 65 |
+
old.parentNode.replaceChild(s, old);
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// -- Send JSON-RPC message to host --
|
| 70 |
+
function sendToHost(msg) {
|
| 71 |
+
if (window.parent && window.parent !== window) {
|
| 72 |
+
window.parent.postMessage(msg, '*');
|
| 73 |
}
|
| 74 |
}
|
| 75 |
|
| 76 |
+
// -- MCP Apps initialization handshake --
|
| 77 |
+
// Step 1: Send ui/initialize request to host
|
| 78 |
+
var initId = msgId++;
|
| 79 |
+
sendToHost({
|
| 80 |
+
jsonrpc: '2.0',
|
| 81 |
+
id: initId,
|
| 82 |
+
method: 'ui/initialize',
|
| 83 |
+
params: {
|
| 84 |
+
appInfo: { name: 'render-html', version: '1.0.0' },
|
| 85 |
+
capabilities: {}
|
| 86 |
+
}
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
// -- Listen for messages from host --
|
| 90 |
+
window.addEventListener('message', function(event) {
|
| 91 |
if (event.source !== window.parent) return;
|
| 92 |
+
var msg = event.data;
|
| 93 |
if (!msg || msg.jsonrpc !== '2.0') return;
|
| 94 |
|
| 95 |
+
// Step 2: Host responds to ui/initialize with host context
|
| 96 |
+
if (msg.id === initId && msg.result) {
|
| 97 |
+
// Step 3: Send ui/notifications/initialized
|
| 98 |
+
sendToHost({
|
| 99 |
+
jsonrpc: '2.0',
|
| 100 |
+
method: 'ui/notifications/initialized',
|
| 101 |
+
params: {}
|
| 102 |
+
});
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Step 4: Receive tool-result notification
|
| 106 |
if (msg.method === 'ui/notifications/tool-result') {
|
| 107 |
+
renderContent(msg.params && msg.params.structuredContent);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// Also handle tool-input (streaming partial args)
|
| 111 |
+
if (msg.method === 'ui/notifications/tool-input') {
|
| 112 |
+
// Could show preview; for render-html we wait for final result
|
| 113 |
}
|
| 114 |
}, { passive: true });
|
| 115 |
+
})();
|
| 116 |
</script>
|
| 117 |
</body>
|
| 118 |
</html>
|
|
|
|
| 120 |
|
| 121 |
VIEWER_URI = "ui://render-html/viewer.html"
|
| 122 |
|
| 123 |
+
# Both forms of the meta key (matching PDF Viewer)
|
| 124 |
TOOL_META = {
|
| 125 |
"ui": {"resourceUri": VIEWER_URI},
|
| 126 |
"ui/resourceUri": VIEWER_URI,
|
| 127 |
}
|
| 128 |
|
| 129 |
|
| 130 |
+
# -- Register the viewer as an MCP resource --------------------------------
|
| 131 |
@mcp.resource(
|
| 132 |
VIEWER_URI,
|
| 133 |
name="HTML Viewer",
|
|
|
|
| 138 |
return VIEWER_HTML
|
| 139 |
|
| 140 |
|
| 141 |
+
# -- Output schemas --------------------------------------------------------
|
| 142 |
|
| 143 |
class HtmlOutput(BaseModel):
|
| 144 |
html: str
|
|
|
|
| 149 |
title: str
|
| 150 |
|
| 151 |
|
| 152 |
+
# -- Tools -----------------------------------------------------------------
|
| 153 |
|
| 154 |
@mcp.tool(
|
| 155 |
name="render_html",
|
|
|
|
| 164 |
"""Render arbitrary HTML content inline in the conversation.
|
| 165 |
|
| 166 |
Use this to display images, SVG diagrams, charts, styled content,
|
| 167 |
+
or any HTML/CSS/JS directly in the chat. The HTML is rendered in
|
| 168 |
+
a sandboxed iframe.
|
| 169 |
|
| 170 |
Args:
|
| 171 |
+
html: Complete HTML content to render. Can include style, script,
|
| 172 |
+
inline styles, SVG, images via img tags, etc.
|
| 173 |
title: Optional title for the rendered content.
|
| 174 |
"""
|
| 175 |
return CallToolResult(
|