ext-apps/examples/qr-server/server.py
Maurizio Albari f9abb74fc0
fix(qr-server): use stable mcp SDK from PyPI (#360)
* fix(qr-server): use stable mcp SDK from PyPI

- Replace git-based mcp dependency with mcp>=1.26.0 from PyPI for stability
- Remove TransportSecuritySettings that was blocking browser connections
- Keep simple FastMCP initialization like other Python examples

Fixes ModuleNotFoundError and 403 Forbidden errors when connecting from basic-host

* fix(qr-server): use stable mcp SDK from PyPI

- Replace git-based mcp dependency with mcp>=1.26.0 from PyPI
- Add stateless_http=True for proper HTTP transport handling
- Remove TransportSecuritySettings that blocked browser connections
2026-01-26 18:52:10 +00:00

172 lines
4.9 KiB
Python
Executable file

#!/usr/bin/env uv run
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "mcp>=1.26.0",
# "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 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", stateless_http=True)
# 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
app = mcp.streamable_http_app()
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)