ext-apps/examples/qr-server/server.py
Jonathan Hefner 6eb82b5562
Refactor server startup functions
Rename `startServer` to `startStreamableHTTPServer` and add a new
`startStdioServer` helper to make the transport type explicit. Remove
the `ServerOptions` interface in favor of reading `PORT` directly from
the environment inside the function (defaulting to `3001`).

Also update Python examples to default to port `3001` for consistency.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 09:27:54 -06:00

177 lines
5.3 KiB
Python
Executable file

#!/usr/bin/env uv run
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "mcp @ git+https://github.com/modelcontextprotocol/python-sdk@main",
# "qrcode[pil]>=8.0",
# "uvicorn>=0.34.0",
# "starlette>=0.46.0",
# ]
# ///
"""
QR Code MCP Server - Generates QR codes from text
"""
import os
import sys
import io
import base64
import qrcode
import uvicorn
from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
from mcp import types
from starlette.middleware.cors import CORSMiddleware
VIEW_URI = "ui://qr-server/view.html"
HOST = os.environ.get("HOST", "0.0.0.0") # 0.0.0.0 for Docker compatibility
PORT = int(os.environ.get("PORT", "3001"))
mcp = FastMCP("QR Code Server")
# Embedded View HTML for self-contained usage (uv run <url> or unbundled)
EMBEDDED_VIEW_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 View", 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={
"ui":{"resourceUri": VIEW_URI},
"ui/resourceUri": VIEW_URI, # legacy support
})
def generate_qr(
text: str = "https://modelcontextprotocol.io",
box_size: int = 10,
border: int = 4,
error_correction: str = "M",
fill_color: str = "black",
back_color: str = "white",
) -> list[types.ImageContent]:
"""Generate a QR code from text.
Args:
text: The text/URL to encode
box_size: Size of each box in pixels (default: 10)
border: Border size in boxes (default: 4)
error_correction: Error correction level - L(7%), M(15%), Q(25%), H(30%)
fill_color: Foreground color (hex like #FF0000 or name like red)
back_color: Background color (hex like #FFFFFF or name like white)
"""
error_levels = {
"L": qrcode.constants.ERROR_CORRECT_L,
"M": qrcode.constants.ERROR_CORRECT_M,
"Q": qrcode.constants.ERROR_CORRECT_Q,
"H": qrcode.constants.ERROR_CORRECT_H,
}
qr = qrcode.QRCode(
version=1,
error_correction=error_levels.get(error_correction.upper(), qrcode.constants.ERROR_CORRECT_M),
box_size=box_size,
border=border,
)
qr.add_data(text)
qr.make(fit=True)
img = qr.make_image(fill_color=fill_color, back_color=back_color)
buffer = io.BytesIO()
img.save(buffer, format="PNG")
b64 = base64.b64encode(buffer.getvalue()).decode()
return [types.ImageContent(type="image", data=b64, mimeType="image/png")]
# IMPORTANT: all the external domains used by app must be listed
# in the meta.ui.csp.resourceDomains - otherwise they will be blocked by CSP policy
@mcp.resource(
VIEW_URI,
mime_type="text/html;profile=mcp-app",
meta={"ui": {"csp": {"resourceDomains": ["https://unpkg.com"]}}},
)
def view() -> str:
"""View HTML resource with CSP metadata for external dependencies."""
return EMBEDDED_VIEW_HTML
if __name__ == "__main__":
if "--stdio" in sys.argv:
# Claude Desktop mode
mcp.run(transport="stdio")
else:
# HTTP mode for basic-host (default) - with CORS
# Allow Docker bridge IP for container-to-host communication
security = TransportSecuritySettings(
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*", "172.17.0.1:*"]
)
app = mcp.streamable_http_app(stateless_http=True, transport_security=security)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
print(f"QR Code Server listening on http://{HOST}:{PORT}/mcp")
uvicorn.run(app, host=HOST, port=PORT)