/** * QR Rápido MCP Server (Node.js — test/dev) * * Variáveis de ambiente: * QR_API_KEY — sua API key (ex: qr_xxxx) * QR_BASE_URL — base URL da API (default: https://qrrapido.site) * NODE_TLS_REJECT_UNAUTHORIZED=0 — necessário para localhost com cert self-signed */ 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] AVISO: QR_API_KEY não definida\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: `❌ ${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] Não abriu imagem: ${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 gerado`, file ? `🖼 Aberto em: ${file}` : "", `⏱ ${data.generationTimeMs}ms`, `💾 Cache: ${data.fromCache ? "hit" : "miss"}`, `📊 Créditos restantes: ${data.remainingCredits ?? "N/A"}` ].filter(Boolean).join("\n") } ] }; } // ── Server ──────────────────────────────────────────────────────────────────── const server = new Server( { name: "qrrapido", version: "0.2.0" }, { capabilities: { tools: {} } } ); // tools/list server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "generate_qr", description: "Gera QR code para URL, Wi-Fi, vCard, WhatsApp, Email, SMS ou texto livre. " + "Para PIX use generate_pix_qr.", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["url", "wifi", "vcard", "whatsapp", "email", "sms", "texto"], description: "Tipo do QR code" }, content: { type: "string", description: "Conteúdo do QR code" }, format: { type: "string", enum: ["png", "webp", "svg"], default: "png" }, size: { type: "number", default: 400, description: "Tamanho em pixels (100–2000)" }, primaryColor: { type: "string", default: "#000000", description: "Cor hex do QR" }, backgroundColor: { type: "string", default: "#FFFFFF", description: "Cor hex de fundo" } }, required: ["type", "content"] } }, { name: "generate_pix_qr", description: "Gera QR code PIX (BRCode/EMV) para recebimento de pagamento. " + "Monte o payload automaticamente com chave, valor, nome e cidade.", inputSchema: { type: "object", properties: { pixKey: { type: "string", description: "Chave PIX: CPF (somente dígitos), email, telefone (+5511...) ou chave aleatória" }, amount: { type: "number", description: "Valor em reais (ex: 50.00). Omita para QR de valor aberto." }, merchantName: { type: "string", description: "Nome do recebedor (max 25 chars, ex: RICARDO CARNEIRO)" }, merchantCity: { type: "string", description: "Cidade do recebedor (max 15 chars, ex: SAO PAULO)" }, txId: { type: "string", default: "***", description: "ID da transação (opcional, max 25 chars)" }, size: { type: "number", default: 400, description: "Tamanho em pixels" }, primaryColor: { type: "string", default: "#000000" }, backgroundColor: { type: "string", default: "#FFFFFF" } }, required: ["pixKey", "merchantName", "merchantCity"] } } ] })); // 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 e merchantCity são obrigatórios."); let payload; try { payload = buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId }); } catch (err) { return errorContent(`Erro ao montar payload PIX: ${err.message}`); } let data; try { data = await callApi({ type: "texto", content: payload, size, primaryColor, backgroundColor }); } catch (err) { return errorContent(`Falha na requisição: ${err.message}`); } if (!data.success) return errorContent(data.message || data.error || "Erro desconhecido"); const pixFile = openImage(data.qrCodeBase64, "pix"); return { content: [ { type: "image", data: data.qrCodeBase64, mimeType: data.mimeType || "image/png" }, { type: "text", text: [ `✅ QR code PIX gerado`, pixFile ? `🖼 Aberto em: ${pixFile}` : "", `🔑 Chave: ${pixKey}`, `💰 Valor: ${amount ? `R$ ${amount.toFixed(2)}` : "aberto"}`, `👤 Recebedor: ${merchantName} — ${merchantCity}`, `⏱ ${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' e 'content' são obrigatórios."); let data; try { data = await callApi({ type, content, outputFormat: format, size, primaryColor, backgroundColor }); } catch (err) { return errorContent(`Falha na requisição: ${err.message}`); } if (!data.success) return errorContent(data.message || data.error || "Erro desconhecido"); return successContent(data); } return errorContent(`Tool desconhecida: ${name}`); }); // ── Start ───────────────────────────────────────────────────────────────────── const transport = new StdioServerTransport(); await server.connect(transport); process.stderr.write(`[qrrapido-mcp] Servidor iniciado. Base URL: ${BASE_URL}\n`);