File size: 4,259 Bytes
e1cc3bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""
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")  # 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[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")]


# Register widget resource using FastMCP decorator (returns HTML string)
@mcp.resource(WIDGET_URI, mime_type="text/html;profile=mcp-app")
def widget() -> str:
    return Path(__file__).parent.joinpath("widget.html").read_text()


# Override the read_resource handler to inject _meta into the response
# This is needed because FastMCP doesn't support custom _meta on resources
_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:
        # NOTE: Must use model_validate with '_meta' key (not 'meta') due to Pydantic alias behavior
        content = types.TextResourceContents.model_validate({
            "uri": WIDGET_URI,
            "mimeType": "text/html;profile=mcp-app",
            "text": html,
            # IMPORTANT: all the external domains used by app must be listed
            # in the _meta.ui.csp.resourceDomains - otherwise they will be blocked by CSP policy
            "_meta": {"ui": {"csp": {"resourceDomains": ["https://unpkg.com"]}}}
        })
        return types.ServerResult(
            types.ReadResourceResult(contents=[content])
        )

    # Fallback for other resources (shouldn't happen for this server)
    return types.ServerResult(
        types.ReadResourceResult(
            contents=[
                types.TextResourceContents(
                    uri=uri,
                    mimeType="text/plain",
                    text="Resource not found"
                )
            ]
        )
    )


# Replace the handler after FastMCP has registered its own
_low_level_server.request_handlers[types.ReadResourceRequest] = _read_resource_with_meta

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)