QrRapido/mcp-server/index.mjs
Ricardo Carneiro 4570e9b70b
All checks were successful
Deploy QR Rapido / test (push) Successful in 51s
Deploy QR Rapido / build-and-push (push) Successful in 15m14s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 3m9s
feat: add monthly quota to API response body
Add monthlyQuotaRemaining/monthlyQuotaLimit fields to QRResponseDto
populated from X-Quota-* headers set by rate limiter. MCP server
now displays quota in tool result text instead of web credits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:01:20 -03:00

266 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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"}`,
data.monthlyQuotaLimit >= 0
? `📊 Quota mensal: ${data.monthlyQuotaRemaining}/${data.monthlyQuotaLimit}`
: `📊 Quota mensal: ilimitada`
].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 (1002000)" },
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`);