fix(example qr server): Embed widget HTML in QR server (#317)
* feat(qr-server): embed widget HTML for self-contained usage Embed the widget HTML directly in server.py following the same pattern as the say-server. This allows the QR server to be run without any local file dependencies using `uv run <url>`. The get_widget_html() function prefers the built version from dist/ for local development but falls back to the embedded HTML for unbundled usage. * refactor(qr-server): remove widget.html and simplify embedded HTML Address PR review feedback: - Remove separate widget.html file (no longer needed) - Remove get_widget_html() function and dist/mcp-app.html reference - Inline EMBEDDED_WIDGET_HTML directly in widget() resource - Remove unused Path import The server is now fully self-contained in a single file. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
17813898c0
commit
c76f827368
2 changed files with 71 additions and 70 deletions
|
|
@ -15,7 +15,6 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import qrcode
|
import qrcode
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
@ -29,6 +28,76 @@ PORT = int(os.environ.get("PORT", "3108"))
|
||||||
|
|
||||||
mcp = FastMCP("QR Code Server")
|
mcp = FastMCP("QR Code Server")
|
||||||
|
|
||||||
|
# Embedded widget HTML for self-contained usage (uv run <url> or unbundled)
|
||||||
|
EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 340px;
|
||||||
|
width: 340px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="qr"></div>
|
||||||
|
<script type="module">
|
||||||
|
import { App } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps";
|
||||||
|
|
||||||
|
const app = new App({ name: "QR Widget", version: "1.0.0" });
|
||||||
|
|
||||||
|
app.ontoolresult = ({ content }) => {
|
||||||
|
const img = content?.find(c => c.type === 'image');
|
||||||
|
if (img) {
|
||||||
|
const qrDiv = document.getElementById('qr');
|
||||||
|
qrDiv.innerHTML = '';
|
||||||
|
|
||||||
|
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];
|
||||||
|
const mimeType = allowedTypes.includes(img.mimeType) ? img.mimeType : 'image/png';
|
||||||
|
|
||||||
|
const image = document.createElement('img');
|
||||||
|
image.src = `data:${mimeType};base64,${img.data}`;
|
||||||
|
image.alt = "QR Code";
|
||||||
|
qrDiv.appendChild(image);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleHostContextChanged(ctx) {
|
||||||
|
if (ctx.safeAreaInsets) {
|
||||||
|
document.body.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
|
||||||
|
document.body.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
|
||||||
|
document.body.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
|
||||||
|
document.body.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.onhostcontextchanged = handleHostContextChanged;
|
||||||
|
|
||||||
|
await app.connect();
|
||||||
|
const ctx = app.getHostContext();
|
||||||
|
if (ctx) {
|
||||||
|
handleHostContextChanged(ctx);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool(meta={
|
@mcp.tool(meta={
|
||||||
"ui":{"resourceUri": WIDGET_URI},
|
"ui":{"resourceUri": WIDGET_URI},
|
||||||
|
|
@ -84,7 +153,7 @@ def generate_qr(
|
||||||
)
|
)
|
||||||
def widget() -> str:
|
def widget() -> str:
|
||||||
"""Widget HTML resource with CSP metadata for external dependencies."""
|
"""Widget HTML resource with CSP metadata for external dependencies."""
|
||||||
return Path(__file__).parent.joinpath("widget.html").read_text()
|
return EMBEDDED_WIDGET_HTML
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if "--stdio" in sys.argv:
|
if "--stdio" in sys.argv:
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="color-scheme" content="light dark">
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 340px;
|
|
||||||
width: 340px;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="qr"></div>
|
|
||||||
<script type="module">
|
|
||||||
import { App } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps";
|
|
||||||
|
|
||||||
const app = new App({ name: "QR Widget", version: "1.0.0" });
|
|
||||||
|
|
||||||
app.ontoolresult = ({ content }) => {
|
|
||||||
const img = content?.find(c => c.type === 'image');
|
|
||||||
if (img) {
|
|
||||||
const qrDiv = document.getElementById('qr');
|
|
||||||
qrDiv.innerHTML = '';
|
|
||||||
|
|
||||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];
|
|
||||||
const mimeType = allowedTypes.includes(img.mimeType) ? img.mimeType : 'image/png';
|
|
||||||
|
|
||||||
const image = document.createElement('img');
|
|
||||||
image.src = `data:${mimeType};base64,${img.data}`;
|
|
||||||
image.alt = "QR Code";
|
|
||||||
qrDiv.appendChild(image);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleHostContextChanged(ctx) {
|
|
||||||
if (ctx.safeAreaInsets) {
|
|
||||||
document.body.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
|
|
||||||
document.body.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
|
|
||||||
document.body.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
|
|
||||||
document.body.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.onhostcontextchanged = handleHostContextChanged;
|
|
||||||
|
|
||||||
await app.connect();
|
|
||||||
const ctx = app.getHostContext();
|
|
||||||
if (ctx) {
|
|
||||||
handleHostContextChanged(ctx);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in a new issue