feat(mcp): upgrade tools to v1.0.3 with outputSchema, annotations and EN descriptions
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

- 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:
Ricardo Carneiro 2026-05-08 22:58:33 -03:00
parent 9161977f85
commit 40cdc51e70
4 changed files with 373 additions and 89 deletions

View File

@ -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: * Environment variables:
* QR_API_KEY sua API key (ex: qr_xxxx) * QR_API_KEY your API key (e.g. qr_xxxx). Get one free at https://qrrapido.site/Developer
* QR_BASE_URL base URL da API (default: https://qrrapido.site) * QR_BASE_URL API base URL (default: https://qrrapido.site)
* NODE_TLS_REJECT_UNAUTHORIZED=0 necessário para localhost com cert self-signed * NODE_TLS_REJECT_UNAUTHORIZED=0 set only for local dev with self-signed cert
*/ */
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 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 API_KEY = process.env.QR_API_KEY || "";
const BASE_URL = process.env.QR_BASE_URL || "https://qrrapido.site"; 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) ──────────────── // ── PIX EMV/BRCode builder (port do C# em PagamentoController) ────────────────
@ -81,7 +82,7 @@ async function callApi(body) {
} }
function errorContent(msg) { 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") { function openImage(base64, label = "qr") {
@ -92,7 +93,7 @@ function openImage(base64, label = "qr") {
exec(`start "" "${file}"`); exec(`start "" "${file}"`);
return file; return file;
} catch (e) { } 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; return null;
} }
} }
@ -105,13 +106,13 @@ function successContent(data, label = "qr") {
{ {
type: "text", type: "text",
text: [ text: [
`✅ QR code gerado`, `QR code generated successfully`,
file ? `🖼 Aberto em: ${file}` : "", file ? `Opened at: ${file}` : "",
` ${data.generationTimeMs}ms`, `Generation time: ${data.generationTimeMs}ms`,
`💾 Cache: ${data.fromCache ? "hit" : "miss"}`, `Cache: ${data.fromCache ? "hit" : "miss"}`,
data.monthlyQuotaLimit >= 0 data.monthlyQuotaLimit >= 0
? `📊 Quota mensal: ${data.monthlyQuotaRemaining}/${data.monthlyQuotaLimit}` ? `Monthly quota: ${data.monthlyQuotaRemaining}/${data.monthlyQuotaLimit}`
: `📊 Quota mensal: ilimitada` : `Monthly quota: unlimited`
].filter(Boolean).join("\n") ].filter(Boolean).join("\n")
} }
] ]
@ -121,70 +122,181 @@ function successContent(data, label = "qr") {
// ── Server ──────────────────────────────────────────────────────────────────── // ── Server ────────────────────────────────────────────────────────────────────
const server = new Server( const server = new Server(
{ name: "qrrapido", version: "0.2.0" }, {
name: "qrrapido",
version: "1.0.3",
},
{ capabilities: { tools: {} } } { 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 // tools/list
server.setRequestHandler(ListToolsRequestSchema, async () => ({ server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [ tools: [
{ {
name: "generate_qr", name: "generate_qr",
description: description:
"Gera QR code para URL, Wi-Fi, vCard, WhatsApp, Email, SMS ou texto livre. " + "Generate a QR code for a URL, Wi-Fi network, vCard contact, WhatsApp message, " +
"Para PIX use generate_pix_qr.", "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: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
type: { type: {
type: "string", type: "string",
enum: ["url", "wifi", "vcard", "whatsapp", "email", "sms", "texto"], 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" }, content: {
format: { type: "string", enum: ["png", "webp", "svg"], default: "png" }, type: "string",
size: { type: "number", default: 400, description: "Tamanho em pixels (1002000)" }, description:
primaryColor: { type: "string", default: "#000000", description: "Cor hex do QR" }, "Content to encode. Examples by type — " +
backgroundColor: { type: "string", default: "#FFFFFF", description: "Cor hex de fundo" } "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"] required: ["type", "content"]
} },
outputSchema: QR_OUTPUT_SCHEMA
}, },
{ {
name: "generate_pix_qr", name: "generate_pix_qr",
description: description:
"Gera QR code PIX (BRCode/EMV) para recebimento de pagamento. " + "Generate a Brazilian PIX payment QR code (BRCode/EMV standard). " +
"Monte o payload automaticamente com chave, valor, nome e cidade.", "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: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
pixKey: { pixKey: {
type: "string", 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: { amount: {
type: "number", 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: { merchantName: {
type: "string", 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: { merchantCity: {
type: "string", 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: { txId: {
type: "string", type: "string",
default: "***", 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" }, size: {
primaryColor: { type: "string", default: "#000000" }, type: "number",
backgroundColor: { type: "string", default: "#FFFFFF" } 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"] required: ["pixKey", "merchantName", "merchantCity"]
} },
outputSchema: QR_OUTPUT_SCHEMA
} }
] ]
})); }));
@ -200,23 +312,23 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
size = 400, primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args; size = 400, primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args;
if (!pixKey || !merchantName || !merchantCity) if (!pixKey || !merchantName || !merchantCity)
return errorContent("pixKey, merchantName e merchantCity são obrigatórios."); return errorContent("pixKey, merchantName and merchantCity are required.");
let payload; let payload;
try { try {
payload = buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId }); payload = buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId });
} catch (err) { } catch (err) {
return errorContent(`Erro ao montar payload PIX: ${err.message}`); return errorContent(`Failed to build PIX payload: ${err.message}`);
} }
let data; let data;
try { try {
data = await callApi({ type: "texto", content: payload, size, primaryColor, backgroundColor }); data = await callApi({ type: "texto", content: payload, size, primaryColor, backgroundColor });
} catch (err) { } 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"); const pixFile = openImage(data.qrCodeBase64, "pix");
return { return {
@ -225,12 +337,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
{ {
type: "text", type: "text",
text: [ text: [
`✅ QR code PIX gerado`, `PIX QR code generated successfully`,
pixFile ? `🖼 Aberto em: ${pixFile}` : "", pixFile ? `Opened at: ${pixFile}` : "",
`🔑 Chave: ${pixKey}`, `PIX key: ${pixKey}`,
`💰 Valor: ${amount ? `R$ ${amount.toFixed(2)}` : "aberto"}`, `Amount: ${amount ? `BRL ${amount.toFixed(2)}` : "open (payer chooses)"}`,
`👤 Recebedor: ${merchantName}${merchantCity}`, `Recipient: ${merchantName}${merchantCity}`,
` ${data.generationTimeMs}ms` `Generation time: ${data.generationTimeMs}ms`
].filter(Boolean).join("\n") ].filter(Boolean).join("\n")
} }
] ]
@ -242,24 +354,24 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { type, content, format = "png", size = 400, const { type, content, format = "png", size = 400,
primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args; 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; let data;
try { try {
data = await callApi({ type, content, outputFormat: format, size, primaryColor, backgroundColor }); data = await callApi({ type, content, outputFormat: format, size, primaryColor, backgroundColor });
} catch (err) { } 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 successContent(data);
} }
return errorContent(`Tool desconhecida: ${name}`); return errorContent(`Unknown tool: ${name}`);
}); });
// ── Start ───────────────────────────────────────────────────────────────────── // ── Start ─────────────────────────────────────────────────────────────────────
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
await server.connect(transport); 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`);

View File

@ -1,12 +1,12 @@
{ {
"name": "qrrapido-mcp-server", "name": "qrrapido-mcp-server",
"version": "0.1.0", "version": "1.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "qrrapido-mcp-server", "name": "qrrapido-mcp-server",
"version": "0.1.0", "version": "1.0.1",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0", "@modelcontextprotocol/sdk": "^1.0.0",
"undici": "^6.0.0" "undici": "^6.0.0"

View File

@ -1,17 +1,38 @@
{ {
"name": "qrrapido-mcp-server", "name": "qrrapido-mcp",
"version": "0.2.0", "mcpName": "site.qrrapido/qr-generator",
"private": true, "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", "type": "module",
"main": "index.mjs", "main": "index.mjs",
"bin": {
"qrrapido-mcp": "index.mjs"
},
"files": [
"index.mjs"
],
"scripts": { "scripts": {
"start:stdio": "node index.mjs", "start:stdio": "node index.mjs",
"start:http": "node server-http.mjs", "start:http": "node server-http.mjs",
"start": "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": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0", "@modelcontextprotocol/sdk": "^1.0.0",
"express": "^4.18.0",
"undici": "^6.0.0" "undici": "^6.0.0"
} }
} }

View File

@ -84,9 +84,43 @@ function errorContent(msg) {
// ── MCP server factory (uma instância por sessão/request) ───────────────────── // ── 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) { function createMcpServer(apiKey) {
const server = new Server( const server = new Server(
{ name: "qrrapido", version: "0.2.0" }, { name: "qrrapido", version: "1.0.3" },
{ capabilities: { tools: {} } } { capabilities: { tools: {} } }
); );
@ -94,37 +128,135 @@ function createMcpServer(apiKey) {
tools: [ tools: [
{ {
name: "generate_qr", 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: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
type: { type: "string", enum: ["url", "wifi", "vcard", "whatsapp", "email", "sms", "texto"] }, type: {
content: { type: "string" }, type: "string",
format: { type: "string", enum: ["png", "webp", "svg"], default: "png" }, enum: ["url", "wifi", "vcard", "whatsapp", "email", "sms", "texto"],
size: { type: "number", default: 400 }, description:
primaryColor: { type: "string", default: "#000000" }, "QR code type. Use 'url' for websites, 'wifi' for network credentials, " +
backgroundColor: { type: "string", default: "#FFFFFF" } "'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"] required: ["type", "content"]
} },
outputSchema: QR_OUTPUT_SCHEMA
}, },
{ {
name: "generate_pix_qr", 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: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
pixKey: { type: "string", description: "Chave PIX: CPF, email, telefone ou aleatória" }, pixKey: {
amount: { type: "number", description: "Valor em reais (omita para valor aberto)" }, type: "string",
merchantName: { type: "string", description: "Nome do recebedor (max 25 chars)" }, description:
merchantCity: { type: "string", description: "Cidade (max 15 chars)" }, "PIX key of the recipient. Accepted formats: " +
txId: { type: "string", default: "***" }, "CPF (digits only, e.g. '12345678901'), " +
size: { type: "number", default: 400 }, "email (e.g. 'user@example.com'), " +
primaryColor: { type: "string", default: "#000000" }, "phone with country code (e.g. '+5511987654321'), " +
backgroundColor: { type: "string", default: "#FFFFFF" } "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"] required: ["pixKey", "merchantName", "merchantCity"]
} },
outputSchema: QR_OUTPUT_SCHEMA
} }
] ]
})); }));
@ -137,18 +269,27 @@ function createMcpServer(apiKey) {
const { pixKey, amount, merchantName, merchantCity, txId = "***", const { pixKey, amount, merchantName, merchantCity, txId = "***",
size = 400, primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args; size = 400, primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args;
if (!pixKey || !merchantName || !merchantCity) if (!pixKey || !merchantName || !merchantCity)
return errorContent("pixKey, merchantName e merchantCity são obrigatórios."); return errorContent("pixKey, merchantName and merchantCity are required.");
let payload; let payload;
try { payload = buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId }); } 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; let data;
try { data = await callApi(apiKey, { type: "texto", content: payload, size, primaryColor, backgroundColor }); } try { data = await callApi(apiKey, { type: "texto", content: payload, size, primaryColor, backgroundColor }); }
catch (e) { return errorContent(`Falha na requisição: ${e.message}`); } catch (e) { return errorContent(`API request failed: ${e.message}`); }
if (!data.success) return errorContent(data.message || "Erro desconhecido"); if (!data.success) return errorContent(data.message || "Unknown error");
return { return {
content: [ content: [
{ type: "image", data: data.qrCodeBase64, mimeType: data.mimeType || "image/png" }, { 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") { if (name === "generate_qr") {
const { type, content, format = "png", size = 400, const { type, content, format = "png", size = 400,
primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args; 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; let data;
try { data = await callApi(apiKey, { type, content, outputFormat: format, size, primaryColor, backgroundColor }); } try { data = await callApi(apiKey, { type, content, outputFormat: format, size, primaryColor, backgroundColor }); }
catch (e) { return errorContent(`Falha: ${e.message}`); } catch (e) { return errorContent(`API request failed: ${e.message}`); }
if (!data.success) return errorContent(data.message || "Erro desconhecido"); if (!data.success) return errorContent(data.message || "Unknown error");
return { return {
content: [ content: [
{ type: "image", data: data.qrCodeBase64, mimeType: data.mimeType || "image/png" }, { 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; return server;
@ -180,8 +331,8 @@ function createMcpServer(apiKey) {
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
// Health check (sem auth) // Health check (no auth required)
app.get("/health", (_, res) => res.json({ status: "ok", service: "qrrapido-mcp", baseUrl: BASE_URL })); 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 // MCP endpoint — cada POST cria sessão stateless
app.all("/mcp", async (req, res) => { app.all("/mcp", async (req, res) => {
@ -213,7 +364,7 @@ app.all("/mcp", async (req, res) => {
}); });
app.listen(PORT, () => { 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] 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`);
}); });