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>
This commit is contained in:
parent
9161977f85
commit
40cdc51e70
@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* QR Rápido MCP Server (Node.js — test/dev)
|
||||
* QR Rápido MCP Server (stdio transport)
|
||||
*
|
||||
* 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
|
||||
* 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";
|
||||
@ -19,7 +20,7 @@ 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");
|
||||
if (!API_KEY) process.stderr.write("[qrrapido-mcp] WARNING: QR_API_KEY not set\n");
|
||||
|
||||
// ── PIX EMV/BRCode builder (port do C# em PagamentoController) ────────────────
|
||||
|
||||
@ -81,7 +82,7 @@ async function callApi(body) {
|
||||
}
|
||||
|
||||
function errorContent(msg) {
|
||||
return { content: [{ type: "text", text: `❌ ${msg}` }], isError: true };
|
||||
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
||||
}
|
||||
|
||||
function openImage(base64, label = "qr") {
|
||||
@ -92,7 +93,7 @@ function openImage(base64, label = "qr") {
|
||||
exec(`start "" "${file}"`);
|
||||
return file;
|
||||
} catch (e) {
|
||||
process.stderr.write(`[qrrapido-mcp] Não abriu imagem: ${e.message}\n`);
|
||||
process.stderr.write(`[qrrapido-mcp] Could not open image: ${e.message}\n`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -105,13 +106,13 @@ function successContent(data, label = "qr") {
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
`✅ QR code gerado`,
|
||||
file ? `🖼 Aberto em: ${file}` : "",
|
||||
`⏱ ${data.generationTimeMs}ms`,
|
||||
`💾 Cache: ${data.fromCache ? "hit" : "miss"}`,
|
||||
`QR code generated successfully`,
|
||||
file ? `Opened at: ${file}` : "",
|
||||
`Generation time: ${data.generationTimeMs}ms`,
|
||||
`Cache: ${data.fromCache ? "hit" : "miss"}`,
|
||||
data.monthlyQuotaLimit >= 0
|
||||
? `📊 Quota mensal: ${data.monthlyQuotaRemaining}/${data.monthlyQuotaLimit}`
|
||||
: `📊 Quota mensal: ilimitada`
|
||||
? `Monthly quota: ${data.monthlyQuotaRemaining}/${data.monthlyQuotaLimit}`
|
||||
: `Monthly quota: unlimited`
|
||||
].filter(Boolean).join("\n")
|
||||
}
|
||||
]
|
||||
@ -121,70 +122,181 @@ function successContent(data, label = "qr") {
|
||||
// ── Server ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const server = new Server(
|
||||
{ name: "qrrapido", version: "0.2.0" },
|
||||
{
|
||||
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:
|
||||
"Gera QR code para URL, Wi-Fi, vCard, WhatsApp, Email, SMS ou texto livre. " +
|
||||
"Para PIX use generate_pix_qr.",
|
||||
"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: "Tipo do QR code"
|
||||
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: "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" }
|
||||
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:
|
||||
"Gera QR code PIX (BRCode/EMV) para recebimento de pagamento. " +
|
||||
"Monte o payload automaticamente com chave, valor, nome e cidade.",
|
||||
"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: "Chave PIX: CPF (somente dígitos), email, telefone (+5511...) ou chave aleatória"
|
||||
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: "Valor em reais (ex: 50.00). Omita para QR de valor aberto."
|
||||
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: "Nome do recebedor (max 25 chars, ex: RICARDO CARNEIRO)"
|
||||
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: "Cidade do recebedor (max 15 chars, ex: SAO PAULO)"
|
||||
description:
|
||||
"Recipient city. Max 15 characters. Accents are stripped automatically. " +
|
||||
"Example: 'SAO PAULO'."
|
||||
},
|
||||
txId: {
|
||||
type: "string",
|
||||
default: "***",
|
||||
description: "ID da transação (opcional, max 25 chars)"
|
||||
description:
|
||||
"Optional transaction identifier for reconciliation. Max 25 characters. " +
|
||||
"Use '***' (default) to omit. Example: 'PEDIDO123'."
|
||||
},
|
||||
size: { type: "number", default: 400, description: "Tamanho em pixels" },
|
||||
primaryColor: { type: "string", default: "#000000" },
|
||||
backgroundColor: { type: "string", default: "#FFFFFF" }
|
||||
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
|
||||
}
|
||||
]
|
||||
}));
|
||||
@ -200,23 +312,23 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
||||
size = 400, primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args;
|
||||
|
||||
if (!pixKey || !merchantName || !merchantCity)
|
||||
return errorContent("pixKey, merchantName e merchantCity são obrigatórios.");
|
||||
return errorContent("pixKey, merchantName and merchantCity are required.");
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId });
|
||||
} catch (err) {
|
||||
return errorContent(`Erro ao montar payload PIX: ${err.message}`);
|
||||
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(`Falha na requisição: ${err.message}`);
|
||||
return errorContent(`API request failed: ${err.message}`);
|
||||
}
|
||||
|
||||
if (!data.success) return errorContent(data.message || data.error || "Erro desconhecido");
|
||||
if (!data.success) return errorContent(data.message || data.error || "Unknown error");
|
||||
|
||||
const pixFile = openImage(data.qrCodeBase64, "pix");
|
||||
return {
|
||||
@ -225,12 +337,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
||||
{
|
||||
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`
|
||||
`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")
|
||||
}
|
||||
]
|
||||
@ -242,24 +354,24 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
||||
const { type, content, format = "png", size = 400,
|
||||
primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args;
|
||||
|
||||
if (!type || !content) return errorContent("'type' e 'content' são obrigatórios.");
|
||||
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(`Falha na requisição: ${err.message}`);
|
||||
return errorContent(`API request failed: ${err.message}`);
|
||||
}
|
||||
|
||||
if (!data.success) return errorContent(data.message || data.error || "Erro desconhecido");
|
||||
if (!data.success) return errorContent(data.message || data.error || "Unknown error");
|
||||
return successContent(data);
|
||||
}
|
||||
|
||||
return errorContent(`Tool desconhecida: ${name}`);
|
||||
return errorContent(`Unknown tool: ${name}`);
|
||||
});
|
||||
|
||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
process.stderr.write(`[qrrapido-mcp] Servidor iniciado. Base URL: ${BASE_URL}\n`);
|
||||
process.stderr.write(`[qrrapido-mcp] Server started. Base URL: ${BASE_URL}\n`);
|
||||
|
||||
4
mcp-server/package-lock.json
generated
4
mcp-server/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "qrrapido-mcp-server",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "qrrapido-mcp-server",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.1",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"undici": "^6.0.0"
|
||||
|
||||
@ -1,17 +1,38 @@
|
||||
{
|
||||
"name": "qrrapido-mcp-server",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"name": "qrrapido-mcp",
|
||||
"mcpName": "site.qrrapido/qr-generator",
|
||||
"version": "1.0.3",
|
||||
"description": "MCP server for QR Rápido — generate QR codes for URLs, PIX, Wi-Fi, vCards, WhatsApp and more",
|
||||
"type": "module",
|
||||
"main": "index.mjs",
|
||||
"bin": {
|
||||
"qrrapido-mcp": "index.mjs"
|
||||
},
|
||||
"files": [
|
||||
"index.mjs"
|
||||
],
|
||||
"scripts": {
|
||||
"start:stdio": "node index.mjs",
|
||||
"start:http": "node server-http.mjs",
|
||||
"start": "node server-http.mjs"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"qrcode",
|
||||
"qr-code",
|
||||
"pix",
|
||||
"ai-agent",
|
||||
"model-context-protocol"
|
||||
],
|
||||
"author": "Ricardo Carneiro",
|
||||
"license": "MIT",
|
||||
"homepage": "https://qrrapido.site",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/modelcontextprotocol/registry"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"express": "^4.18.0",
|
||||
"undici": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,9 +84,43 @@ function errorContent(msg) {
|
||||
|
||||
// ── MCP server factory (uma instância por sessão/request) ─────────────────────
|
||||
|
||||
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"]
|
||||
};
|
||||
|
||||
function createMcpServer(apiKey) {
|
||||
const server = new Server(
|
||||
{ name: "qrrapido", version: "0.2.0" },
|
||||
{ name: "qrrapido", version: "1.0.3" },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
@ -94,37 +128,135 @@ function createMcpServer(apiKey) {
|
||||
tools: [
|
||||
{
|
||||
name: "generate_qr",
|
||||
description: "Gera QR code para URL, Wi-Fi, vCard, WhatsApp, Email, SMS ou texto. Para PIX use generate_pix_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"] },
|
||||
content: { type: "string" },
|
||||
format: { type: "string", enum: ["png", "webp", "svg"], default: "png" },
|
||||
size: { type: "number", default: 400 },
|
||||
primaryColor: { type: "string", default: "#000000" },
|
||||
backgroundColor: { type: "string", default: "#FFFFFF" }
|
||||
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: "Gera QR code PIX (BRCode/EMV) com payload completo para recebimento.",
|
||||
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: "Chave PIX: CPF, email, telefone ou aleatória" },
|
||||
amount: { type: "number", description: "Valor em reais (omita para valor aberto)" },
|
||||
merchantName: { type: "string", description: "Nome do recebedor (max 25 chars)" },
|
||||
merchantCity: { type: "string", description: "Cidade (max 15 chars)" },
|
||||
txId: { type: "string", default: "***" },
|
||||
size: { type: "number", default: 400 },
|
||||
primaryColor: { type: "string", default: "#000000" },
|
||||
backgroundColor: { type: "string", default: "#FFFFFF" }
|
||||
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
|
||||
}
|
||||
]
|
||||
}));
|
||||
@ -137,18 +269,27 @@ function createMcpServer(apiKey) {
|
||||
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.");
|
||||
return errorContent("pixKey, merchantName and merchantCity are required.");
|
||||
let payload;
|
||||
try { payload = buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId }); }
|
||||
catch (e) { return errorContent(`Erro payload PIX: ${e.message}`); }
|
||||
catch (e) { return errorContent(`Failed to build PIX payload: ${e.message}`); }
|
||||
let data;
|
||||
try { data = await callApi(apiKey, { type: "texto", content: payload, size, primaryColor, backgroundColor }); }
|
||||
catch (e) { return errorContent(`Falha na requisição: ${e.message}`); }
|
||||
if (!data.success) return errorContent(data.message || "Erro desconhecido");
|
||||
catch (e) { return errorContent(`API request failed: ${e.message}`); }
|
||||
if (!data.success) return errorContent(data.message || "Unknown error");
|
||||
return {
|
||||
content: [
|
||||
{ type: "image", data: data.qrCodeBase64, mimeType: data.mimeType || "image/png" },
|
||||
{ type: "text", text: `✅ PIX gerado\n🔑 ${pixKey}\n💰 ${amount ? `R$ ${amount.toFixed(2)}` : "aberto"}\n👤 ${merchantName} — ${merchantCity}\n⏱ ${data.generationTimeMs}ms` }
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
`PIX QR code generated successfully`,
|
||||
`PIX key: ${pixKey}`,
|
||||
`Amount: ${amount ? `BRL ${amount.toFixed(2)}` : "open (payer chooses)"}`,
|
||||
`Recipient: ${merchantName} — ${merchantCity}`,
|
||||
`Generation time: ${data.generationTimeMs}ms`
|
||||
].join("\n")
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
@ -156,20 +297,30 @@ function createMcpServer(apiKey) {
|
||||
if (name === "generate_qr") {
|
||||
const { type, content, format = "png", size = 400,
|
||||
primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args;
|
||||
if (!type || !content) return errorContent("'type' e 'content' obrigatórios.");
|
||||
if (!type || !content) return errorContent("'type' and 'content' are required.");
|
||||
let data;
|
||||
try { data = await callApi(apiKey, { type, content, outputFormat: format, size, primaryColor, backgroundColor }); }
|
||||
catch (e) { return errorContent(`Falha: ${e.message}`); }
|
||||
if (!data.success) return errorContent(data.message || "Erro desconhecido");
|
||||
catch (e) { return errorContent(`API request failed: ${e.message}`); }
|
||||
if (!data.success) return errorContent(data.message || "Unknown error");
|
||||
return {
|
||||
content: [
|
||||
{ type: "image", data: data.qrCodeBase64, mimeType: data.mimeType || "image/png" },
|
||||
{ type: "text", text: `✅ QR gerado (${type})\n⏱ ${data.generationTimeMs}ms\n💾 cache: ${data.fromCache ? "hit" : "miss"}\n📊 quota: ${data.monthlyQuotaRemaining >= 0 ? `${data.monthlyQuotaRemaining}/${data.monthlyQuotaLimit}` : "ilimitada"}` }
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
`QR code generated successfully`,
|
||||
`Generation time: ${data.generationTimeMs}ms`,
|
||||
`Cache: ${data.fromCache ? "hit" : "miss"}`,
|
||||
data.monthlyQuotaLimit >= 0
|
||||
? `Monthly quota: ${data.monthlyQuotaRemaining}/${data.monthlyQuotaLimit}`
|
||||
: `Monthly quota: unlimited`
|
||||
].join("\n")
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return errorContent(`Tool desconhecida: ${name}`);
|
||||
return errorContent(`Unknown tool: ${name}`);
|
||||
});
|
||||
|
||||
return server;
|
||||
@ -180,8 +331,8 @@ function createMcpServer(apiKey) {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Health check (sem auth)
|
||||
app.get("/health", (_, res) => res.json({ status: "ok", service: "qrrapido-mcp", baseUrl: BASE_URL }));
|
||||
// Health check (no auth required)
|
||||
app.get("/health", (_, res) => res.json({ status: "ok", service: "qrrapido-mcp", version: "1.0.3", baseUrl: BASE_URL }));
|
||||
|
||||
// MCP endpoint — cada POST cria sessão stateless
|
||||
app.all("/mcp", async (req, res) => {
|
||||
@ -213,7 +364,7 @@ app.all("/mcp", async (req, res) => {
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`[qrrapido-mcp-http] Servidor HTTP na porta ${PORT}`);
|
||||
console.log(`[qrrapido-mcp-http] HTTP server on port ${PORT}`);
|
||||
console.log(`[qrrapido-mcp-http] Base URL: ${BASE_URL}`);
|
||||
console.log(`[qrrapido-mcp-http] Endpoint n8n: http://localhost:${PORT}/mcp`);
|
||||
console.log(`[qrrapido-mcp-http] MCP endpoint: http://localhost:${PORT}/mcp`);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user