Skip to content

Commit 5f4099a

Browse files
antonpk1claude
andcommitted
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]>
1 parent c03e99d commit 5f4099a

File tree

5 files changed

+360
-0
lines changed

5 files changed

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

examples/qr-server/widget.html

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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>
29+
const WIDTH = 340;
30+
const HEIGHT = 340;
31+
let requestId = 1;
32+
33+
// Listen for messages from host
34+
window.addEventListener('message', e => {
35+
console.log('[Widget] Received message:', e)
36+
const msg = e.data;
37+
if (!msg || typeof msg !== 'object') return;
38+
39+
// Handle ui/notifications/tool-result
40+
if (msg.method === 'ui/notifications/tool-result') {
41+
const content = msg.params?.content;
42+
const img = content?.find(c => c.type === 'image');
43+
if (img) {
44+
document.getElementById('qr').innerHTML =
45+
`<img src="data:${img.mimeType};base64,${img.data}" alt="QR Code"/>`;
46+
47+
// Report size to host
48+
window.parent.postMessage({
49+
jsonrpc: '2.0',
50+
method: 'ui/notifications/size-changed',
51+
params: { width: WIDTH, height: HEIGHT }
52+
}, '*');
53+
}
54+
}
55+
56+
// Handle ui/initialize response - MUST send ui/notifications/initialized
57+
if (msg.id && msg.result) {
58+
console.log('[Widget] Initialize response received:', msg.result);
59+
// Send initialized notification - HOST WAITS FOR THIS!
60+
window.parent.postMessage({
61+
jsonrpc: '2.0',
62+
method: 'ui/notifications/initialized',
63+
params: {}
64+
}, '*');
65+
console.log('[Widget] Sent ui/notifications/initialized');
66+
}
67+
});
68+
69+
// Send ui/initialize request when ready
70+
window.parent.postMessage({
71+
jsonrpc: '2.0',
72+
id: requestId++,
73+
method: 'ui/initialize',
74+
params: {
75+
appInfo: { name: 'QR Widget', version: '1.0.0' },
76+
appCapabilities: {},
77+
protocolVersion: '2024-11-05'
78+
}
79+
}, '*');
80+
</script>
81+
</body>
82+
</html>

0 commit comments

Comments
 (0)