Skip to content

Commit 546f507

Browse files
antonpk1claudegithub-advanced-security[bot]
authored
Add QR code Python MCP server example (#105)
* 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 <[email protected]> * 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 <[email protected]> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 3ac7c79 commit 546f507

File tree

5 files changed

+344
-0
lines changed

5 files changed

+344
-0
lines changed

examples/qr-server/.gitignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Python virtual environment
2+
.venv/
3+
venv/
4+
env/
5+
6+
# Python cache
7+
__pycache__/
8+
*.py[cod]
9+
*$py.class
10+
*.so
11+
12+
# Distribution / packaging
13+
dist/
14+
build/
15+
*.egg-info/
16+
17+
# IDE
18+
.idea/
19+
.vscode/
20+
*.swp
21+
*.swo
22+
23+
# OS
24+
.DS_Store
25+
Thumbs.db

examples/qr-server/README.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# QR Code MCP Server
2+
3+
A minimal Python MCP server that generates customizable QR codes with an interactive widget UI.
4+
5+
![Screenshot](https://modelcontextprotocol.github.io/ext-apps/screenshots/qr-server/screenshot.png)
6+
7+
## Features
8+
9+
- Generate QR codes from any text or URL
10+
- Customizable colors, size, and error correction
11+
- Interactive widget that displays in MCP-UI enabled clients
12+
- Supports both HTTP (for web clients) and stdio (for Claude Desktop)
13+
14+
## Quick Start
15+
16+
```bash
17+
# Create virtual environment
18+
python3 -m venv .venv
19+
source .venv/bin/activate
20+
21+
# Install dependencies
22+
pip install -r requirements.txt
23+
24+
# Run server (HTTP mode)
25+
python server.py
26+
# → QR Server listening on http://localhost:3108/mcp
27+
```
28+
29+
## Usage
30+
31+
### HTTP Mode (for basic-host / web clients)
32+
33+
```bash
34+
python server.py
35+
```
36+
37+
Connect from basic-host:
38+
39+
```bash
40+
SERVERS='["http://localhost:3108/mcp"]' bun serve.ts
41+
```
42+
43+
### Stdio Mode (for Claude Desktop)
44+
45+
```bash
46+
python server.py --stdio
47+
```
48+
49+
Add to Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
50+
51+
```json
52+
{
53+
"mcpServers": {
54+
"qr": {
55+
"command": "/path/to/qr-server/.venv/bin/python",
56+
"args": ["/path/to/qr-server/server.py", "--stdio"]
57+
}
58+
}
59+
}
60+
```
61+
62+
### Docker (accessing host server from container)
63+
64+
```
65+
http://host.docker.internal:3108/mcp
66+
```
67+
68+
## Tool: `generate_qr`
69+
70+
Generate a QR code with optional customization.
71+
72+
### Parameters
73+
74+
| Parameter | Type | Default | Description |
75+
| ------------------ | ------ | ---------- | ------------------------------- |
76+
| `text` | string | (required) | The text or URL to encode |
77+
| `box_size` | int | 10 | Size of each box in pixels |
78+
| `border` | int | 4 | Border size in boxes |
79+
| `error_correction` | string | "M" | Error correction level: L/M/Q/H |
80+
| `fill_color` | string | "black" | Foreground color (hex or name) |
81+
| `back_color` | string | "white" | Background color (hex or name) |
82+
83+
### Error Correction Levels
84+
85+
| Level | Recovery | Use Case |
86+
| ----- | -------- | ------------------------- |
87+
| L | 7% | Clean environments |
88+
| M | 15% | General use (default) |
89+
| Q | 25% | Industrial/outdoor |
90+
| H | 30% | Adding logos/damage-prone |
91+
92+
### Example Inputs
93+
94+
**Basic:**
95+
96+
```json
97+
{ "text": "https://example.com" }
98+
```
99+
100+
**Styled:**
101+
102+
```json
103+
{
104+
"text": "https://claude.ai",
105+
"fill_color": "#CC785C",
106+
"back_color": "#FFF8F5",
107+
"box_size": 12,
108+
"border": 3
109+
}
110+
```
111+
112+
**Dark Mode:**
113+
114+
```json
115+
{
116+
"text": "Hello World",
117+
"fill_color": "#E0E0E0",
118+
"back_color": "#1a1a1a",
119+
"box_size": 15,
120+
"border": 2
121+
}
122+
```
123+
124+
**WiFi QR Code:**
125+
126+
```json
127+
{
128+
"text": "WIFI:T:WPA;S:MyNetwork;P:MyPassword;;",
129+
"error_correction": "H",
130+
"box_size": 10
131+
}
132+
```
133+
134+
## Architecture
135+
136+
```
137+
qr-server/
138+
├── server.py # MCP server (FastMCP + uvicorn)
139+
├── widget.html # Interactive UI widget
140+
├── requirements.txt
141+
└── README.md
142+
```
143+
144+
### Protocol
145+
146+
The widget uses MCP Apps SDK protocol:
147+
148+
1. Widget sends `ui/initialize` request
149+
2. Host responds with capabilities
150+
3. Widget sends `ui/notifications/initialized`
151+
4. Host sends `ui/notifications/tool-result` with QR image
152+
5. Widget renders image and sends `ui/notifications/size-changed`
153+
154+
## Dependencies
155+
156+
- `mcp[cli]` - MCP Python SDK with FastMCP
157+
- `qrcode[pil]` - QR code generation with Pillow
158+
- `uvicorn` - ASGI server (included with mcp)
159+
- `starlette` - CORS middleware (included with mcp)
160+
161+
## License
162+
163+
MIT
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mcp[cli]>=1.0.0
2+
qrcode[pil]>=7.4

examples/qr-server/server.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
QR Code MCP Server - Generates QR codes from text
3+
"""
4+
import os
5+
import sys
6+
import io
7+
import base64
8+
from pathlib import Path
9+
10+
import qrcode
11+
import uvicorn
12+
from mcp.server.fastmcp import FastMCP
13+
from mcp.types import ImageContent
14+
from starlette.middleware.cors import CORSMiddleware
15+
16+
WIDGET_URI = "ui://qr-server/widget.html"
17+
HOST = os.environ.get("HOST", "0.0.0.0") # 0.0.0.0 for Docker compatibility
18+
PORT = int(os.environ.get("PORT", "3108"))
19+
20+
mcp = FastMCP("QR Server", port=PORT, stateless_http=True)
21+
22+
23+
@mcp.tool(meta={"ui/resourceUri": WIDGET_URI})
24+
def generate_qr(
25+
text: str,
26+
box_size: int = 10,
27+
border: int = 4,
28+
error_correction: str = "M",
29+
fill_color: str = "black",
30+
back_color: str = "white",
31+
) -> list[ImageContent]:
32+
"""Generate a QR code from text.
33+
34+
Args:
35+
text: The text/URL to encode
36+
box_size: Size of each box in pixels (default: 10)
37+
border: Border size in boxes (default: 4)
38+
error_correction: Error correction level - L(7%), M(15%), Q(25%), H(30%)
39+
fill_color: Foreground color (hex like #FF0000 or name like red)
40+
back_color: Background color (hex like #FFFFFF or name like white)
41+
"""
42+
error_levels = {
43+
"L": qrcode.constants.ERROR_CORRECT_L,
44+
"M": qrcode.constants.ERROR_CORRECT_M,
45+
"Q": qrcode.constants.ERROR_CORRECT_Q,
46+
"H": qrcode.constants.ERROR_CORRECT_H,
47+
}
48+
49+
qr = qrcode.QRCode(
50+
version=1,
51+
error_correction=error_levels.get(error_correction.upper(), qrcode.constants.ERROR_CORRECT_M),
52+
box_size=box_size,
53+
border=border,
54+
)
55+
qr.add_data(text)
56+
qr.make(fit=True)
57+
58+
img = qr.make_image(fill_color=fill_color, back_color=back_color)
59+
buffer = io.BytesIO()
60+
img.save(buffer, format="PNG")
61+
b64 = base64.b64encode(buffer.getvalue()).decode()
62+
return [ImageContent(type="image", data=b64, mimeType="image/png")]
63+
64+
65+
# IMPORTANT: resourceDomains needed for CSP to allow loading SDK from unpkg.com
66+
# Without this, hosts enforcing CSP will block the external script import
67+
@mcp.resource(WIDGET_URI, mime_type="text/html")
68+
def widget() -> dict:
69+
html = Path(__file__).parent.joinpath("widget.html").read_text()
70+
return {
71+
"text": html,
72+
"_meta": {
73+
"ui": {
74+
"csp": {
75+
"resourceDomains": ["https://unpkg.com"]
76+
}
77+
}
78+
}
79+
}
80+
81+
# HACK: Bypass SDK's restrictive mime_type validation
82+
# The SDK pattern doesn't allow ";profile=mcp-app" but MCP spec requires it for widgets
83+
# https://github.com/modelcontextprotocol/python-sdk/pull/1755
84+
for resource in mcp._resource_manager._resources.values():
85+
if str(resource.uri) == WIDGET_URI:
86+
object.__setattr__(resource, 'mime_type', 'text/html;profile=mcp-app')
87+
88+
if __name__ == "__main__":
89+
if "--stdio" in sys.argv:
90+
# Claude Desktop mode
91+
mcp.run(transport="stdio")
92+
else:
93+
# HTTP mode for basic-host (default) - with CORS
94+
app = mcp.streamable_http_app()
95+
app.add_middleware(
96+
CORSMiddleware,
97+
allow_origins=["*"],
98+
allow_methods=["*"],
99+
allow_headers=["*"],
100+
)
101+
print(f"QR Server listening on http://{HOST}:{PORT}/mcp")
102+
uvicorn.run(app, host=HOST, port=PORT)

examples/qr-server/widget.html

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<style>
5+
html, body {
6+
margin: 0;
7+
padding: 0;
8+
overflow: hidden;
9+
background: transparent;
10+
}
11+
body {
12+
display: flex;
13+
justify-content: center;
14+
align-items: center;
15+
height: 340px;
16+
width: 340px;
17+
}
18+
img {
19+
width: 300px;
20+
height: 300px;
21+
border-radius: 8px;
22+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
23+
}
24+
</style>
25+
</head>
26+
<body>
27+
<div id="qr"></div>
28+
<script type="module">
29+
import { App, PostMessageTransport } from "https://unpkg.com/@modelcontextprotocol/[email protected]";
30+
31+
const app = new App({ name: "QR Widget", version: "1.0.0" });
32+
33+
app.ontoolresult = ({ content }) => {
34+
const img = content?.find(c => c.type === 'image');
35+
if (img) {
36+
const qrDiv = document.getElementById('qr');
37+
qrDiv.innerHTML = '';
38+
39+
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];
40+
const mimeType = allowedTypes.includes(img.mimeType) ? img.mimeType : 'image/png';
41+
42+
const image = document.createElement('img');
43+
image.src = `data:${mimeType};base64,${img.data}`;
44+
image.alt = "QR Code";
45+
qrDiv.appendChild(image);
46+
}
47+
};
48+
49+
await app.connect(new PostMessageTransport(window.parent));
50+
</script>
51+
</body>
52+
</html>

0 commit comments

Comments
 (0)