QrRapido/mcp-server/index.mjs
Ricardo Carneiro 40cdc51e70
All checks were successful
Deploy QR Rapido / test (push) Successful in 1m4s
Deploy QR Rapido / build-and-push (push) Successful in 18m56s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 3m28s
feat(mcp): upgrade tools to v1.0.3 with outputSchema, annotations and EN descriptions
- Add outputSchema to both generate_qr and generate_pix_qr tools
- Add MCP annotations (title, readOnlyHint, idempotentHint, etc.)
- Full English parameter descriptions for all inputs
- Sync server-http.mjs (HTTP/Smithery endpoint) with index.mjs (stdio/npm)
- Bump server version to 1.0.3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:58:33 -03:00

378 lines
13 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.

#!/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: 1002000. 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: 1002000. 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`);