From 546f507e490a49aae6734ad8fd02fc7e4e2eea92 Mon Sep 17 00:00:00 2001 From: Anton Pidkuiko <105124934+antonpk1@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:20:08 +0000 Subject: [PATCH] Add QR code Python MCP server example (#105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add QR code Python MCP server example A minimal Python MCP server that generates customizable QR codes with an interactive widget UI. Features: - Generate QR codes from any text or URL - Customizable colors, size, border, and error correction - Interactive widget using MCP Apps SDK protocol - Supports both HTTP (for web clients) and stdio (for Claude Desktop) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Potential fix for code scanning alert no. 10: Client-side cross-site scripting Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Add semicolon * Bind to 0.0.0.0 for Docker compatibility * Use SDK from unpkg + add CSP resourceDomains for external scripts --------- Co-authored-by: Claude Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- examples/qr-server/.gitignore | 25 +++++ examples/qr-server/README.md | 163 ++++++++++++++++++++++++++++ examples/qr-server/requirements.txt | 2 + examples/qr-server/server.py | 102 +++++++++++++++++ examples/qr-server/widget.html | 52 +++++++++ 5 files changed, 344 insertions(+) create mode 100644 examples/qr-server/.gitignore create mode 100644 examples/qr-server/README.md create mode 100644 examples/qr-server/requirements.txt create mode 100644 examples/qr-server/server.py create mode 100644 examples/qr-server/widget.html diff --git a/examples/qr-server/.gitignore b/examples/qr-server/.gitignore new file mode 100644 index 0000000..3c6b816 --- /dev/null +++ b/examples/qr-server/.gitignore @@ -0,0 +1,25 @@ +# Python virtual environment +.venv/ +venv/ +env/ + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/qr-server/README.md b/examples/qr-server/README.md new file mode 100644 index 0000000..c0ffa95 --- /dev/null +++ b/examples/qr-server/README.md @@ -0,0 +1,163 @@ +# QR Code MCP Server + +A minimal Python MCP server that generates customizable QR codes with an interactive widget UI. + +![Screenshot](https://modelcontextprotocol.github.io/ext-apps/screenshots/qr-server/screenshot.png) + +## Features + +- Generate QR codes from any text or URL +- Customizable colors, size, and error correction +- Interactive widget that displays in MCP-UI enabled clients +- Supports both HTTP (for web clients) and stdio (for Claude Desktop) + +## Quick Start + +```bash +# Create virtual environment +python3 -m venv .venv +source .venv/bin/activate + +# Install dependencies +pip install -r requirements.txt + +# Run server (HTTP mode) +python server.py +# → QR Server listening on http://localhost:3108/mcp +``` + +## Usage + +### HTTP Mode (for basic-host / web clients) + +```bash +python server.py +``` + +Connect from basic-host: + +```bash +SERVERS='["http://localhost:3108/mcp"]' bun serve.ts +``` + +### Stdio Mode (for Claude Desktop) + +```bash +python server.py --stdio +``` + +Add to Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "qr": { + "command": "/path/to/qr-server/.venv/bin/python", + "args": ["/path/to/qr-server/server.py", "--stdio"] + } + } +} +``` + +### Docker (accessing host server from container) + +``` +http://host.docker.internal:3108/mcp +``` + +## Tool: `generate_qr` + +Generate a QR code with optional customization. + +### Parameters + +| Parameter | Type | Default | Description | +| ------------------ | ------ | ---------- | ------------------------------- | +| `text` | string | (required) | The text or URL to encode | +| `box_size` | int | 10 | Size of each box in pixels | +| `border` | int | 4 | Border size in boxes | +| `error_correction` | string | "M" | Error correction level: L/M/Q/H | +| `fill_color` | string | "black" | Foreground color (hex or name) | +| `back_color` | string | "white" | Background color (hex or name) | + +### Error Correction Levels + +| Level | Recovery | Use Case | +| ----- | -------- | ------------------------- | +| L | 7% | Clean environments | +| M | 15% | General use (default) | +| Q | 25% | Industrial/outdoor | +| H | 30% | Adding logos/damage-prone | + +### Example Inputs + +**Basic:** + +```json +{ "text": "https://example.com" } +``` + +**Styled:** + +```json +{ + "text": "https://claude.ai", + "fill_color": "#CC785C", + "back_color": "#FFF8F5", + "box_size": 12, + "border": 3 +} +``` + +**Dark Mode:** + +```json +{ + "text": "Hello World", + "fill_color": "#E0E0E0", + "back_color": "#1a1a1a", + "box_size": 15, + "border": 2 +} +``` + +**WiFi QR Code:** + +```json +{ + "text": "WIFI:T:WPA;S:MyNetwork;P:MyPassword;;", + "error_correction": "H", + "box_size": 10 +} +``` + +## Architecture + +``` +qr-server/ +├── server.py # MCP server (FastMCP + uvicorn) +├── widget.html # Interactive UI widget +├── requirements.txt +└── README.md +``` + +### Protocol + +The widget uses MCP Apps SDK protocol: + +1. Widget sends `ui/initialize` request +2. Host responds with capabilities +3. Widget sends `ui/notifications/initialized` +4. Host sends `ui/notifications/tool-result` with QR image +5. Widget renders image and sends `ui/notifications/size-changed` + +## Dependencies + +- `mcp[cli]` - MCP Python SDK with FastMCP +- `qrcode[pil]` - QR code generation with Pillow +- `uvicorn` - ASGI server (included with mcp) +- `starlette` - CORS middleware (included with mcp) + +## License + +MIT diff --git a/examples/qr-server/requirements.txt b/examples/qr-server/requirements.txt new file mode 100644 index 0000000..9d87890 --- /dev/null +++ b/examples/qr-server/requirements.txt @@ -0,0 +1,2 @@ +mcp[cli]>=1.0.0 +qrcode[pil]>=7.4 diff --git a/examples/qr-server/server.py b/examples/qr-server/server.py new file mode 100644 index 0000000..e3e6d50 --- /dev/null +++ b/examples/qr-server/server.py @@ -0,0 +1,102 @@ +""" +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.types import ImageContent +from starlette.middleware.cors import CORSMiddleware + +WIDGET_URI = "ui://qr-server/widget.html" +HOST = os.environ.get("HOST", "0.0.0.0") # 0.0.0.0 for Docker compatibility +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[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 [ImageContent(type="image", data=b64, mimeType="image/png")] + + +# IMPORTANT: resourceDomains needed for CSP to allow loading SDK from unpkg.com +# Without this, hosts enforcing CSP will block the external script import +@mcp.resource(WIDGET_URI, mime_type="text/html") +def widget() -> dict: + html = Path(__file__).parent.joinpath("widget.html").read_text() + return { + "text": html, + "_meta": { + "ui": { + "csp": { + "resourceDomains": ["https://unpkg.com"] + } + } + } + } + +# HACK: Bypass SDK's restrictive mime_type validation +# The SDK pattern doesn't allow ";profile=mcp-app" but MCP spec requires it for widgets +# https://github.com/modelcontextprotocol/python-sdk/pull/1755 +for resource in mcp._resource_manager._resources.values(): + if str(resource.uri) == WIDGET_URI: + object.__setattr__(resource, 'mime_type', 'text/html;profile=mcp-app') + +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 Server listening on http://{HOST}:{PORT}/mcp") + uvicorn.run(app, host=HOST, port=PORT) diff --git a/examples/qr-server/widget.html b/examples/qr-server/widget.html new file mode 100644 index 0000000..9fe444d --- /dev/null +++ b/examples/qr-server/widget.html @@ -0,0 +1,52 @@ + + + + + + +
+ + +