| | """ |
| | QR Code MCP Server - Generates QR codes from text |
| | """ |
| | import os |
| | import sys |
| | import io |
| | import base64 |
| | from pathlib import Path |
| |
|
| | import qrcode |
| | import uvicorn |
| | from mcp.server.fastmcp import FastMCP |
| | from mcp import types |
| | from starlette.middleware.cors import CORSMiddleware |
| |
|
| | WIDGET_URI = "ui://qr-server/widget.html" |
| | HOST = os.environ.get("HOST", "0.0.0.0") |
| | PORT = int(os.environ.get("PORT", "3108")) |
| |
|
| | mcp = FastMCP("QR Server", port=PORT, stateless_http=True) |
| |
|
| |
|
| | @mcp.tool(meta={"ui/resourceUri": WIDGET_URI}) |
| | def generate_qr( |
| | text: str, |
| | 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")] |
| |
|
| |
|
| | |
| | @mcp.resource(WIDGET_URI, mime_type="text/html;profile=mcp-app") |
| | def widget() -> str: |
| | return Path(__file__).parent.joinpath("widget.html").read_text() |
| |
|
| |
|
| | |
| | |
| | _low_level_server = mcp._mcp_server |
| |
|
| |
|
| | async def _read_resource_with_meta(req: types.ReadResourceRequest): |
| | """Custom handler that injects CSP metadata for the widget resource.""" |
| | uri = str(req.params.uri) |
| | html = Path(__file__).parent.joinpath("widget.html").read_text() |
| |
|
| | if uri == WIDGET_URI: |
| | |
| | content = types.TextResourceContents.model_validate({ |
| | "uri": WIDGET_URI, |
| | "mimeType": "text/html;profile=mcp-app", |
| | "text": html, |
| | |
| | |
| | "_meta": {"ui": {"csp": {"resourceDomains": ["https://unpkg.com"]}}} |
| | }) |
| | return types.ServerResult( |
| | types.ReadResourceResult(contents=[content]) |
| | ) |
| |
|
| | |
| | return types.ServerResult( |
| | types.ReadResourceResult( |
| | contents=[ |
| | types.TextResourceContents( |
| | uri=uri, |
| | mimeType="text/plain", |
| | text="Resource not found" |
| | ) |
| | ] |
| | ) |
| | ) |
| |
|
| |
|
| | |
| | _low_level_server.request_handlers[types.ReadResourceRequest] = _read_resource_with_meta |
| |
|
| | if __name__ == "__main__": |
| | if "--stdio" in sys.argv: |
| | |
| | mcp.run(transport="stdio") |
| | else: |
| | |
| | app = mcp.streamable_http_app() |
| | app.add_middleware( |
| | CORSMiddleware, |
| | allow_origins=["*"], |
| | allow_methods=["*"], |
| | allow_headers=["*"], |
| | ) |
| | print(f"QR Server listening on http://{HOST}:{PORT}/mcp") |
| | uvicorn.run(app, host=HOST, port=PORT) |
| |
|