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:
|
* 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 (100–2000)" },
|
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: 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"]
|
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: 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"]
|
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`);
|
||||||
|
|||||||
4
mcp-server/package-lock.json
generated
4
mcp-server/package-lock.json
generated
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: 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"]
|
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: 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"]
|
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`);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user