#!/usr/bin/env node /** * QR Rápido MCP Server (stdio transport) * * Environment variables: * QR_API_KEY — your API key (e.g. qr_xxxx). Get one free at https://qrrapido.site/Developer * QR_BASE_URL — API base URL (default: https://qrrapido.site) * NODE_TLS_REJECT_UNAUTHORIZED=0 — set only for local dev with self-signed cert */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { fetch } from "undici"; import { writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { exec } from "child_process"; const API_KEY = process.env.QR_API_KEY || ""; const BASE_URL = process.env.QR_BASE_URL || "https://qrrapido.site"; if (!API_KEY) process.stderr.write("[qrrapido-mcp] WARNING: QR_API_KEY not set\n"); // ── PIX EMV/BRCode builder (port do C# em PagamentoController) ──────────────── function pixField(id, value) { const len = String(value.length).padStart(2, "0"); return `${id}${len}${value}`; } function removeAccents(text) { return text.toUpperCase() .normalize("NFD").replace(/[\u0300-\u036f]/g, "") .replace(/[^A-Z0-9 ]/g, " "); } function crc16(data) { let crc = 0xFFFF; for (const ch of data) { crc ^= ch.charCodeAt(0) << 8; for (let i = 0; i < 8; i++) { crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xFFFF : (crc << 1) & 0xFFFF; } } return crc.toString(16).toUpperCase().padStart(4, "0"); } function buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId = "***" }) { const merchantInfo = pixField("00", "br.gov.bcb.pix") + pixField("01", pixKey); const addData = pixField("05", txId.substring(0, 25)); const name = removeAccents(merchantName).substring(0, 25); const city = removeAccents(merchantCity).substring(0, 15); let payload = pixField("00", "01") + pixField("26", merchantInfo) + pixField("52", "0000") + pixField("53", "986"); if (amount && amount > 0) { payload += pixField("54", amount.toFixed(2)); } payload += pixField("58", "BR") + pixField("59", name) + pixField("60", city) + pixField("62", addData) + "6304"; return payload + crc16(payload); } // ── API helper ──────────────────────────────────────────────────────────────── async function callApi(body) { const res = await fetch(`${BASE_URL}/api/v1/QRManager/generate`, { method: "POST", headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" }, body: JSON.stringify(body) }); return res.json(); } function errorContent(msg) { return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; } function openImage(base64, label = "qr") { try { const ext = "png"; const file = join(tmpdir(), `${label}_${Date.now()}.${ext}`); writeFileSync(file, Buffer.from(base64, "base64")); exec(`start "" "${file}"`); return file; } catch (e) { process.stderr.write(`[qrrapido-mcp] Could not open image: ${e.message}\n`); return null; } } function successContent(data, label = "qr") { const file = openImage(data.qrCodeBase64, label); return { content: [ { type: "image", data: data.qrCodeBase64, mimeType: data.mimeType || "image/png" }, { type: "text", text: [ `QR code generated successfully`, file ? `Opened at: ${file}` : "", `Generation time: ${data.generationTimeMs}ms`, `Cache: ${data.fromCache ? "hit" : "miss"}`, data.monthlyQuotaLimit >= 0 ? `Monthly quota: ${data.monthlyQuotaRemaining}/${data.monthlyQuotaLimit}` : `Monthly quota: unlimited` ].filter(Boolean).join("\n") } ] }; } // ── Server ──────────────────────────────────────────────────────────────────── const server = new Server( { name: "qrrapido", version: "1.0.3", }, { capabilities: { tools: {} } } ); const QR_OUTPUT_SCHEMA = { type: "object", properties: { content: { type: "array", description: "Array with the QR code image (base64 PNG) and a text summary.", items: { oneOf: [ { type: "object", description: "Base64-encoded QR code image.", properties: { type: { type: "string", const: "image" }, data: { type: "string", description: "Base64-encoded image data." }, mimeType: { type: "string", description: "MIME type, e.g. image/png." } }, required: ["type", "data", "mimeType"] }, { type: "object", description: "Text summary with timing, cache status, and quota info.", properties: { type: { type: "string", const: "text" }, text: { type: "string" } }, required: ["type", "text"] } ] } } }, required: ["content"] }; // tools/list server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "generate_qr", description: "Generate a QR code for a URL, Wi-Fi network, vCard contact, WhatsApp message, " + "email, SMS, or free text. Returns an inline base64 PNG — no file hosting needed. " + "For Brazilian PIX payments use generate_pix_qr instead.", annotations: { title: "Generate QR Code", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, inputSchema: { type: "object", properties: { type: { type: "string", enum: ["url", "wifi", "vcard", "whatsapp", "email", "sms", "texto"], description: "QR code type. Use 'url' for websites, 'wifi' for network credentials, " + "'vcard' for contacts, 'whatsapp' to open a WhatsApp chat, " + "'email' for pre-filled emails, 'sms' for text messages, 'texto' for free text." }, content: { type: "string", description: "Content to encode. Examples by type — " + "url: 'https://example.com'; " + "wifi: 'WIFI:T:WPA;S:MyNet;P:pass123;;'; " + "vcard: 'BEGIN:VCARD\\nFN:John\\nTEL:+1234\\nEND:VCARD'; " + "whatsapp: '+5511999998888'; " + "email: 'user@example.com'; " + "sms: '+5511999998888'; " + "texto: any plain text." }, format: { type: "string", enum: ["png", "webp", "svg"], default: "png", description: "Output image format. PNG is recommended for broadest compatibility." }, size: { type: "number", default: 400, description: "Image size in pixels (width = height). Range: 100–2000. Default: 400." }, primaryColor: { type: "string", default: "#000000", description: "QR module color as hex, e.g. '#1a1a1a'. Default: black (#000000)." }, backgroundColor: { type: "string", default: "#FFFFFF", description: "Background color as hex, e.g. '#FFFFFF'. Default: white (#FFFFFF)." } }, required: ["type", "content"] }, outputSchema: QR_OUTPUT_SCHEMA }, { name: "generate_pix_qr", description: "Generate a Brazilian PIX payment QR code (BRCode/EMV standard). " + "Builds the payload automatically from the PIX key, amount, recipient name, and city. " + "Returns an inline base64 PNG ready to display or share.", annotations: { title: "Generate PIX QR Code", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, inputSchema: { type: "object", properties: { pixKey: { type: "string", description: "PIX key of the recipient. Accepted formats: " + "CPF (digits only, e.g. '12345678901'), " + "email (e.g. 'user@example.com'), " + "phone with country code (e.g. '+5511987654321'), " + "or random key (UUID, e.g. '123e4567-e89b-12d3-a456-426614174000')." }, amount: { type: "number", description: "Payment amount in BRL (e.g. 49.90). " + "Omit or set to 0 to create an open-value QR code where the payer enters the amount." }, merchantName: { type: "string", description: "Recipient name as it appears in the payer's banking app. " + "Max 25 characters. Accents are stripped automatically. Example: 'RICARDO CARNEIRO'." }, merchantCity: { type: "string", description: "Recipient city. Max 15 characters. Accents are stripped automatically. " + "Example: 'SAO PAULO'." }, txId: { type: "string", default: "***", description: "Optional transaction identifier for reconciliation. Max 25 characters. " + "Use '***' (default) to omit. Example: 'PEDIDO123'." }, size: { type: "number", default: 400, description: "Image size in pixels (width = height). Range: 100–2000. Default: 400." }, primaryColor: { type: "string", default: "#000000", description: "QR module color as hex. Default: black (#000000)." }, backgroundColor: { type: "string", default: "#FFFFFF", description: "Background color as hex. Default: white (#FFFFFF)." } }, required: ["pixKey", "merchantName", "merchantCity"] }, outputSchema: QR_OUTPUT_SCHEMA } ] })); // tools/call server.setRequestHandler(CallToolRequestSchema, async (req) => { const { name } = req.params; const args = req.params.arguments ?? {}; // ── generate_pix_qr ────────────────────────────────────────────────────── if (name === "generate_pix_qr") { const { pixKey, amount, merchantName, merchantCity, txId = "***", size = 400, primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args; if (!pixKey || !merchantName || !merchantCity) return errorContent("pixKey, merchantName and merchantCity are required."); let payload; try { payload = buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId }); } catch (err) { return errorContent(`Failed to build PIX payload: ${err.message}`); } let data; try { data = await callApi({ type: "texto", content: payload, size, primaryColor, backgroundColor }); } catch (err) { return errorContent(`API request failed: ${err.message}`); } if (!data.success) return errorContent(data.message || data.error || "Unknown error"); const pixFile = openImage(data.qrCodeBase64, "pix"); return { content: [ { type: "image", data: data.qrCodeBase64, mimeType: data.mimeType || "image/png" }, { type: "text", text: [ `PIX QR code generated successfully`, pixFile ? `Opened at: ${pixFile}` : "", `PIX key: ${pixKey}`, `Amount: ${amount ? `BRL ${amount.toFixed(2)}` : "open (payer chooses)"}`, `Recipient: ${merchantName} — ${merchantCity}`, `Generation time: ${data.generationTimeMs}ms` ].filter(Boolean).join("\n") } ] }; } // ── generate_qr ────────────────────────────────────────────────────────── if (name === "generate_qr") { const { type, content, format = "png", size = 400, primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args; if (!type || !content) return errorContent("'type' and 'content' are required."); let data; try { data = await callApi({ type, content, outputFormat: format, size, primaryColor, backgroundColor }); } catch (err) { return errorContent(`API request failed: ${err.message}`); } if (!data.success) return errorContent(data.message || data.error || "Unknown error"); return successContent(data); } return errorContent(`Unknown tool: ${name}`); }); // ── Start ───────────────────────────────────────────────────────────────────── const transport = new StdioServerTransport(); await server.connect(transport); process.stderr.write(`[qrrapido-mcp] Server started. Base URL: ${BASE_URL}\n`);