60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
171 lines
4.7 KiB
Python
171 lines
4.7 KiB
Python
"""QR Code MCP App Server — generates QR codes with an interactive view UI.
|
|
|
|
Demonstrates MCP Apps with FastMCP:
|
|
- Tool linked to a ui:// resource via AppConfig
|
|
- HTML resource with CSP metadata for CDN-loaded dependencies
|
|
- Embedded HTML using the @modelcontextprotocol/ext-apps JS SDK
|
|
- ImageContent return type for binary data
|
|
- Both stdio and HTTP transport modes
|
|
|
|
Based on https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server
|
|
|
|
Setup (from examples/apps/):
|
|
uv sync
|
|
|
|
Usage:
|
|
uv run python qr_server.py # HTTP mode (port 3001)
|
|
uv run python qr_server.py --stdio # stdio mode for MCP clients
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import io
|
|
|
|
import qrcode # type: ignore[import-untyped]
|
|
from mcp import types
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.apps import AppConfig, ResourceCSP
|
|
from fastmcp.tools import ToolResult
|
|
|
|
VIEW_URI: str = "ui://qr-server/view.html"
|
|
|
|
mcp: FastMCP = FastMCP("QR Code Server")
|
|
|
|
EMBEDDED_VIEW_HTML: str = """\
|
|
<!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(app=AppConfig(resource_uri=VIEW_URI))
|
|
def generate_qr(
|
|
text: str = "https://gofastmcp.com",
|
|
box_size: int = 10,
|
|
border: int = 4,
|
|
error_correction: str = "M",
|
|
fill_color: str = "black",
|
|
back_color: str = "white",
|
|
) -> ToolResult:
|
|
"""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,
|
|
}
|
|
|
|
if box_size <= 0:
|
|
raise ValueError("box_size must be > 0")
|
|
if border < 0:
|
|
raise ValueError("border must be >= 0")
|
|
|
|
error_key = error_correction.upper()
|
|
if error_key not in error_levels:
|
|
raise ValueError(f"error_correction must be one of: {', '.join(error_levels)}")
|
|
|
|
qr = qrcode.QRCode(
|
|
version=1,
|
|
error_correction=error_levels[error_key],
|
|
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 ToolResult(
|
|
content=[types.ImageContent(type="image", data=b64, mimeType="image/png")]
|
|
)
|
|
|
|
|
|
@mcp.resource(
|
|
VIEW_URI,
|
|
app=AppConfig(csp=ResourceCSP(resource_domains=["https://unpkg.com"])),
|
|
)
|
|
def view() -> str:
|
|
"""Interactive QR code viewer — renders tool results as images."""
|
|
return EMBEDDED_VIEW_HTML
|
|
|
|
|
|
if __name__ == "__main__":
|
|
mcp.run()
|