Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions examples/qr-server/.gitignore
Original file line number Diff line number Diff line change
@@ -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
163 changes: 163 additions & 0 deletions examples/qr-server/README.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions examples/qr-server/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mcp[cli]>=1.0.0
qrcode[pil]>=7.4
102 changes: 102 additions & 0 deletions examples/qr-server/server.py
Original file line number Diff line number Diff line change
@@ -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)
52 changes: 52 additions & 0 deletions examples/qr-server/widget.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<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, PostMessageTransport } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.0.4";

const app = new App({ name: "QR Widget", 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);
}
};

await app.connect(new PostMessageTransport(window.parent));
</script>
</body>
</html>
Loading