From a3238ca6c5fc9b2510f6f786a00011557776cef2 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Thu, 7 May 2026 21:23:50 -0300 Subject: [PATCH] feat: MCP server + landing page + OAuth returnUrl fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Node.js MCP server (stdio + HTTP/SSE) with generate_qr and generate_pix_qr tools - Add landing pages PT/EN at /mcp and /mcp/en with hreflang SEO - Fix OAuth returnUrl via RedirectUri query param (state was always null in callback) - Fix API key requests bypassing web credit check (use rate limiter instead) - Add /api/mcp nginx route + Docker Swarm service for n8n cloud integration - Auto-create API key on first OAuth login with TempData display - Add UseDefaultFiles() for /mcp → /mcp/index.html serving - Fix Serilog console log level in Development (was Error, now Info for app logs) - Add /api/v1/QRManager/me endpoint for API key validation - Update CI/CD to build and deploy qrrapido-mcp image alongside .NET app Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.local.json | 13 +- .github/workflows/deploy.yml | 33 +- Controllers/AccountController.cs | 127 +-- Controllers/DeveloperController.cs | 45 +- Controllers/HomeController.cs | 4 + Controllers/QRManagerController.cs | 18 +- Models/DTOs/UserRequesterContext.cs | 2 + Program.cs | 1 + Services/QuotaValidator.cs | 3 + Views/Account/Login.cshtml | 6 +- Views/Developer/Index.cshtml | 8 +- Views/Developer/Mcp.cshtml | 193 ++++ appsettings.Development.json | 12 +- docker-compose.yml | 15 + mcp-server/Dockerfile | 8 + mcp-server/index.mjs | 263 +++++ mcp-server/package-lock.json | 1148 +++++++++++++++++++ mcp-server/package.json | 17 + mcp-server/server-http.mjs | 219 ++++ wwwroot/mcp/en/index.html | 738 +++++++++++++ wwwroot/mcp/index.html | 1576 +++++++++++++++++++++++++++ 21 files changed, 4340 insertions(+), 109 deletions(-) create mode 100644 Views/Developer/Mcp.cshtml create mode 100644 mcp-server/Dockerfile create mode 100644 mcp-server/index.mjs create mode 100644 mcp-server/package-lock.json create mode 100644 mcp-server/package.json create mode 100644 mcp-server/server-http.mjs create mode 100644 wwwroot/mcp/en/index.html create mode 100644 wwwroot/mcp/index.html diff --git a/.claude/settings.local.json b/.claude/settings.local.json index cb80799..00fe6c5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -36,7 +36,18 @@ "Bash(npm install:*)", "Bash(git add:*)", "Bash(git commit:*)", - "Bash(git push:*)" + "Bash(git push:*)", + "Bash(sed -i 's|returnUrl=/Developer/mcp|returnUrl=/Developer%23criar-chave|g' C:/vscode/qrrapido/wwwroot/mcp/index.html)", + "Bash(sed -i 's|returnUrl=/Developer/mcp|returnUrl=/Developer%23criar-chave|g' C:/vscode/qrrapido/wwwroot/mcp/en/index.html)", + "Bash(sed -i 's|https://qrrapido.site/Account/Login|/Account/Login|g' C:/vscode/qrrapido/wwwroot/mcp/index.html)", + "Bash(sed -i 's|https://qrrapido.site/Account/Login|/Account/Login|g' C:/vscode/qrrapido/wwwroot/mcp/en/index.html)", + "Bash(sed -i 's|returnUrl=/Developer%23criar-chave|returnUrl=/Developer?new=1|g' C:/vscode/qrrapido/wwwroot/mcp/index.html)", + "Bash(sed -i 's|returnUrl=/Developer%23criar-chave|returnUrl=/Developer?new=1|g' C:/vscode/qrrapido/wwwroot/mcp/en/index.html)", + "Bash(python -m json.tool)", + "Bash(QR_BASE_URL=https://localhost:52428 NODE_TLS_REJECT_UNAUTHORIZED=0 PORT=3001 node server-http.mjs)", + "Bash(kill %1)", + "Bash(rsync:*)", + "Bash(scp:*)" ], "deny": [] } diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 116308b..fb7d66c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -63,14 +63,21 @@ jobs: else TAG="develop" fi - - # Build da imagem para ARM64 (servidores Ampere OCI) + + # Build da imagem .NET para ARM64 (servidores Ampere OCI) docker buildx build \ --platform linux/arm64 \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$TAG \ --push \ . - + + # Build da imagem MCP server (Node.js) + docker buildx build \ + --platform linux/arm64 \ + --tag ${{ env.REGISTRY }}/qrrapido-mcp:$TAG \ + --push \ + ./mcp-server + echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV deploy-staging: @@ -246,6 +253,26 @@ jobs: # Verifica se o service está funcionando docker service ps qrrapido-prod + # Atualiza MCP server + docker pull ${{ env.REGISTRY }}/qrrapido-mcp:latest + if docker service inspect qrrapido-mcp > /dev/null 2>&1; then + docker service update \ + --image ${{ env.REGISTRY }}/qrrapido-mcp:latest \ + --with-registry-auth \ + qrrapido-mcp + else + docker service create \ + --name qrrapido-mcp \ + --replicas 1 \ + --network qrrapido-network \ + --publish published=3000,target=3000 \ + --env QR_BASE_URL=https://qrrapido.site \ + --env PORT=3000 \ + --restart-condition on-failure \ + --with-registry-auth \ + ${{ env.REGISTRY }}/qrrapido-mcp:latest + fi + # Recarrega NGINX para garantir que está apontando para o novo container sudo nginx -t && sudo systemctl reload nginx EOF diff --git a/Controllers/AccountController.cs b/Controllers/AccountController.cs index e44d94e..9b34cdb 100644 --- a/Controllers/AccountController.cs +++ b/Controllers/AccountController.cs @@ -7,9 +7,6 @@ using Microsoft.AspNetCore.Mvc; using QRRapidoApp.Models.ViewModels; using QRRapidoApp.Services; using System.Security.Claims; -using System.Text.Json; -using System.Text; -using Microsoft.AspNetCore.DataProtection; namespace QRRapidoApp.Controllers { @@ -19,17 +16,14 @@ namespace QRRapidoApp.Controllers private readonly AdDisplayService _adDisplayService; private readonly ILogger _logger; private readonly IConfiguration _configuration; - private readonly IDataProtector _protector; public AccountController(IUserService userService, AdDisplayService adDisplayService, - ILogger logger, IConfiguration configuration, - IDataProtectionProvider dataProtection) + ILogger logger, IConfiguration configuration) { _userService = userService; _adDisplayService = adDisplayService; _logger = logger; _configuration = configuration; - _protector = dataProtection.CreateProtector("OAuth.StateProtection"); } [HttpGet] @@ -44,25 +38,11 @@ namespace QRRapidoApp.Controllers public IActionResult LoginGoogle(string returnUrl = "/") { var baseUrl = _configuration.GetSection("App:BaseUrl").Value; - - // Criar state com dados criptografados em vez de sessão - var stateData = new OAuthStateData - { - ReturnUrl = returnUrl, - Nonce = Guid.NewGuid().ToString(), - Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - }; - - var stateJson = JsonSerializer.Serialize(stateData); - var protectedState = _protector.Protect(stateJson); - var encodedState = Convert.ToBase64String(Encoding.UTF8.GetBytes(protectedState)); - + var safeReturn = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"; var properties = new AuthenticationProperties { - RedirectUri = $"{baseUrl}{Url.Action("GoogleCallback")}", - Items = { { "state", encodedState } } + RedirectUri = $"{baseUrl}/Account/GoogleCallback?returnUrl={Uri.EscapeDataString(safeReturn)}" }; - return Challenge(properties, GoogleDefaults.AuthenticationScheme); } @@ -70,89 +50,48 @@ namespace QRRapidoApp.Controllers public IActionResult LoginMicrosoft(string returnUrl = "/") { var baseUrl = _configuration.GetSection("App:BaseUrl").Value; - - // Mesmo processo para Microsoft - var stateData = new OAuthStateData - { - ReturnUrl = returnUrl, - Nonce = Guid.NewGuid().ToString(), - Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - }; - - var stateJson = JsonSerializer.Serialize(stateData); - var protectedState = _protector.Protect(stateJson); - var encodedState = Convert.ToBase64String(Encoding.UTF8.GetBytes(protectedState)); - - var redirectUrl = returnUrl == "/" - ? $"{baseUrl}/Account/MicrosoftCallback" - : $"{baseUrl}/Account/MicrosoftCallback"; - + var safeReturn = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"; var properties = new AuthenticationProperties { - RedirectUri = redirectUrl, - Items = { { "state", encodedState } } + RedirectUri = $"{baseUrl}/Account/MicrosoftCallback?returnUrl={Uri.EscapeDataString(safeReturn)}" }; - return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme); } [HttpGet] - public async Task GoogleCallback(string state = null) + public async Task GoogleCallback(string returnUrl = "/") { - var returnUrl = await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme, state); - return Redirect(returnUrl ?? "/"); + var destination = await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme, returnUrl); + return Redirect(destination ?? "/"); } [HttpGet] - public async Task MicrosoftCallback(string state = null) + public async Task MicrosoftCallback(string returnUrl = "/") { - var returnUrl = await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme, state); - return Redirect(returnUrl ?? "/"); + var destination = await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme, returnUrl); + return Redirect(destination ?? "/"); } - private async Task HandleExternalLoginCallbackAsync(string scheme, string state = null) + private async Task HandleExternalLoginCallbackAsync(string scheme, string returnUrl = "/") { try { _adDisplayService.SetViewBagAds(ViewBag); - // Recuperar returnUrl do state em vez da sessão - string returnUrl = "/"; - if (!string.IsNullOrEmpty(state)) - { - try - { - var decodedState = Encoding.UTF8.GetString(Convert.FromBase64String(state)); - var unprotectedState = _protector.Unprotect(decodedState); - var stateData = JsonSerializer.Deserialize(unprotectedState); + // Validate returnUrl to prevent open redirect + if (!Url.IsLocalUrl(returnUrl)) + returnUrl = "/"; - // Validar timestamp (não mais que 10 minutos) - var maxAge = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - (10 * 60); - if (stateData.Timestamp > maxAge) - { - returnUrl = stateData.ReturnUrl ?? "/"; - - // Prevent redirect loop to login page - if (returnUrl.Contains("/Account/Login", StringComparison.OrdinalIgnoreCase)) - { - returnUrl = "/"; - } - } - else - { - _logger.LogWarning($"OAuth state expired for scheme {scheme}"); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Failed to decode OAuth state for scheme {scheme}"); - } - } + // Prevent redirect loop + if (returnUrl.Contains("/Account/Login", StringComparison.OrdinalIgnoreCase)) + returnUrl = "/"; - var result = await HttpContext.AuthenticateAsync(scheme); + // OAuth middleware already signed in via cookie at /signin-google or /signin-microsoft + // with the provider's claims. Authenticate from that cookie to get the provider identity. + var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); if (!result.Succeeded) { - _logger.LogWarning($"External authentication failed for scheme {scheme}"); + _logger.LogWarning("Cookie authentication failed after {Scheme} OAuth callback", scheme); return "/Account/Login"; } @@ -168,11 +107,24 @@ namespace QRRapidoApp.Controllers // Find or create user var user = await _userService.GetUserByProviderAsync(scheme, providerId); - if (user == null) + bool isNewUser = user == null; + if (isNewUser) { // Fix CS8625: Ensure name is not null var safeName = !string.IsNullOrEmpty(name) ? name : (email ?? "User"); user = await _userService.CreateUserAsync(email, safeName, scheme, providerId); + // Auto-create first API key so user can start immediately + try + { + var (rawKey, _) = await _userService.GenerateApiKeyAsync(user.Id, "Minha primeira key"); + TempData["NewKey"] = rawKey; + TempData["NewKeyName"] = "Minha primeira key"; + _logger.LogInformation("Auto-created API key for new user {UserId}", user.Id); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to auto-create API key for new user {UserId}", user.Id); + } } else { @@ -325,11 +277,4 @@ namespace QRRapidoApp.Controllers } } - // Classe para dados do state - public class OAuthStateData - { - public string ReturnUrl { get; set; } = "/"; - public string Nonce { get; set; } = ""; - public long Timestamp { get; set; } - } } \ No newline at end of file diff --git a/Controllers/DeveloperController.cs b/Controllers/DeveloperController.cs index f58e5cd..69c9a95 100644 --- a/Controllers/DeveloperController.cs +++ b/Controllers/DeveloperController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Distributed; using QRRapidoApp.Models; using QRRapidoApp.Services; using System.Security.Claims; @@ -15,15 +16,21 @@ namespace QRRapidoApp.Controllers private readonly IUserService _userService; private readonly StripeService _stripeService; private readonly ILogger _logger; + private readonly IDistributedCache _cache; + private readonly IWebHostEnvironment _env; public DeveloperController( IUserService userService, StripeService stripeService, - ILogger logger) + ILogger logger, + IDistributedCache cache, + IWebHostEnvironment env) { _userService = userService; _stripeService = stripeService; _logger = logger; + _cache = cache; + _env = env; } [HttpGet("")] @@ -112,6 +119,19 @@ namespace QRRapidoApp.Controllers } } + [HttpGet("Mcp")] + public async Task Mcp() + { + var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userId)) return Unauthorized(); + + var user = await _userService.GetUserAsync(userId); + if (user == null) return NotFound(); + + ViewBag.Culture = GetCulture(); + return View(user); + } + [HttpPost("RevokeKey")] [ValidateAntiForgeryToken] public async Task RevokeKey(string prefix) @@ -139,6 +159,29 @@ namespace QRRapidoApp.Controllers return RedirectToAction(nameof(Index)); } + [HttpPost("ResetRateLimit")] + public async Task ResetRateLimit(string prefix) + { + if (!_env.IsDevelopment()) + return Forbid(); + + if (string.IsNullOrWhiteSpace(prefix)) + return BadRequest("prefix required"); + + var now = DateTime.UtcNow; + var monthKey = $"rl:mo:{prefix}:{now:yyyyMM}"; + var minuteKey = $"rl:min:{prefix}:{now:yyyyMMddHHmm}"; + + await Task.WhenAll( + _cache.RemoveAsync(monthKey), + _cache.RemoveAsync(minuteKey) + ); + + _logger.LogWarning("Rate limit reset for prefix {Prefix} (dev only)", prefix); + TempData["Success"] = $"Rate limit resetado para '{prefix}'."; + return RedirectToAction(nameof(Index)); + } + private string GetCulture() { var path = Request.Path.Value ?? ""; diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs index 8470468..a79e133 100644 --- a/Controllers/HomeController.cs +++ b/Controllers/HomeController.cs @@ -365,6 +365,10 @@ namespace QRRapidoApp.Controllers AppendUrl("/es/developers", "monthly", "0.7"); AppendUrl("/en/developers", "monthly", "0.7"); + // MCP landing pages + AppendUrl("/mcp", "monthly", "0.9"); + AppendUrl("/mcp/en", "monthly", "0.8"); + // Tools (Landing Pages) var tools = new[] { "pix", "wifi", "vcard", "whatsapp", "email", "sms", "texto", "url" }; diff --git a/Controllers/QRManagerController.cs b/Controllers/QRManagerController.cs index 630a185..96dd03b 100644 --- a/Controllers/QRManagerController.cs +++ b/Controllers/QRManagerController.cs @@ -36,13 +36,24 @@ namespace QRRapidoApp.Controllers } /// Health check — no API key required. - /// API is running. [HttpGet("ping")] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] public IActionResult Ping() => Ok(new { status = "QRRapido API v1 is up", timestamp = DateTime.UtcNow }); + /// Validate API key and return plan info. + [HttpGet("me")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public IActionResult Me() + { + var userId = HttpContext.Items["ApiKeyUserId"] as string; + var prefix = HttpContext.Items["ApiKeyPrefix"] as string; + var tier = HttpContext.Items["ApiPlanTier"]; + return Ok(new { userId, prefix, plan = tier?.ToString(), valid = true }); + } + /// /// Generate a QR code and receive it as a base64-encoded image. /// @@ -87,8 +98,9 @@ namespace QRRapidoApp.Controllers var userId = HttpContext.Items["ApiKeyUserId"] as string; var context = new UserRequesterContext { - UserId = userId, - IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "api" + UserId = userId, + IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "api", + IsApiKeyRequest = true }; var result = await _qrBusinessManager.ProcessGenerationAsync(request, context); diff --git a/Models/DTOs/UserRequesterContext.cs b/Models/DTOs/UserRequesterContext.cs index 4c784e4..7f8f6de 100644 --- a/Models/DTOs/UserRequesterContext.cs +++ b/Models/DTOs/UserRequesterContext.cs @@ -6,5 +6,7 @@ namespace QRRapidoApp.Models.DTOs public string? IpAddress { get; set; } public string? DeviceId { get; set; } public bool IsAuthenticated => !string.IsNullOrEmpty(UserId); + /// True when request authenticated via X-API-Key (has own rate limiting — skip credit check). + public bool IsApiKeyRequest { get; set; } } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index 6edfa19..aee731d 100644 --- a/Program.cs +++ b/Program.cs @@ -427,6 +427,7 @@ if (!app.Environment.IsDevelopment()) // Security headers + JSON exception handler for /api/v1/* routes app.UseMiddleware(); +app.UseDefaultFiles(); // serve index.html para requisições de diretório (ex: /mcp → /mcp/index.html) app.UseStaticFiles(); // Language redirection middleware (before routing) diff --git a/Services/QuotaValidator.cs b/Services/QuotaValidator.cs index c9d279e..54e5aba 100644 --- a/Services/QuotaValidator.cs +++ b/Services/QuotaValidator.cs @@ -34,6 +34,9 @@ namespace QRRapidoApp.Services return (true, null, null); } + // API key requests have their own monthly rate limiting — skip credit check + if (context.IsApiKeyRequest) return (true, null, null); + var user = await _userService.GetUserAsync(context.UserId!); if (user == null) return (false, "UNAUTHORIZED", "Usuário não encontrado."); diff --git a/Views/Account/Login.cshtml b/Views/Account/Login.cshtml index c9df6a7..872bfdf 100644 --- a/Views/Account/Login.cshtml +++ b/Views/Account/Login.cshtml @@ -29,11 +29,11 @@ diff --git a/Views/Developer/Index.cshtml b/Views/Developer/Index.cshtml index 1ac69e5..8fcf0a9 100644 --- a/Views/Developer/Index.cshtml +++ b/Views/Developer/Index.cshtml @@ -159,7 +159,7 @@ @if (activeKeys.Count < 5) { -
+
@T("Criar Nova Chave", "Crear Nueva Clave") @@ -352,6 +352,12 @@ @section Scripts { +} diff --git a/appsettings.Development.json b/appsettings.Development.json index 9555263..0e91964 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -17,13 +17,13 @@ "SeqUrl": "http://192.168.0.75:5341", "ApiKey": "", "MinimumLevel": { - "Default": "Error", + "Default": "Warning", "Override": { - "Microsoft": "Error", - "Microsoft.AspNetCore": "Error", - "Microsoft.Hosting.Lifetime": "Error", - "System": "Error", - "QRRapidoApp": "Error" + "Microsoft": "Warning", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "System": "Warning", + "QRRapidoApp": "Information" } } }, diff --git a/docker-compose.yml b/docker-compose.yml index 75e90ce..b9f39e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,6 +56,21 @@ services: - qrrapido-network restart: unless-stopped + mcp-server: + build: + context: ./mcp-server + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + - QR_BASE_URL=https://qrrapido.site + - PORT=3000 + networks: + - qrrapido-network + restart: unless-stopped + depends_on: + - qrrapido + nginx: image: nginx:alpine container_name: qrrapido-nginx diff --git a/mcp-server/Dockerfile b/mcp-server/Dockerfile new file mode 100644 index 0000000..2e7d1f5 --- /dev/null +++ b/mcp-server/Dockerfile @@ -0,0 +1,8 @@ +FROM node:22-alpine +WORKDIR /app +COPY package.json ./ +RUN npm install --production +COPY index.mjs server-http.mjs ./ +EXPOSE 3000 +ENV NODE_ENV=production +CMD ["node", "server-http.mjs"] diff --git a/mcp-server/index.mjs b/mcp-server/index.mjs new file mode 100644 index 0000000..d70d571 --- /dev/null +++ b/mcp-server/index.mjs @@ -0,0 +1,263 @@ +/** + * QR Rápido MCP Server (Node.js — test/dev) + * + * Variáveis de ambiente: + * QR_API_KEY — sua API key (ex: qr_xxxx) + * QR_BASE_URL — base URL da API (default: https://qrrapido.site) + * NODE_TLS_REJECT_UNAUTHORIZED=0 — necessário para localhost com cert self-signed + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import { fetch } from "undici"; +import { writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { exec } from "child_process"; + +const API_KEY = process.env.QR_API_KEY || ""; +const BASE_URL = process.env.QR_BASE_URL || "https://qrrapido.site"; + +if (!API_KEY) process.stderr.write("[qrrapido-mcp] AVISO: QR_API_KEY não definida\n"); + +// ── PIX EMV/BRCode builder (port do C# em PagamentoController) ──────────────── + +function pixField(id, value) { + const len = String(value.length).padStart(2, "0"); + return `${id}${len}${value}`; +} + +function removeAccents(text) { + return text.toUpperCase() + .normalize("NFD").replace(/[\u0300-\u036f]/g, "") + .replace(/[^A-Z0-9 ]/g, " "); +} + +function crc16(data) { + let crc = 0xFFFF; + for (const ch of data) { + crc ^= ch.charCodeAt(0) << 8; + for (let i = 0; i < 8; i++) { + crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xFFFF : (crc << 1) & 0xFFFF; + } + } + return crc.toString(16).toUpperCase().padStart(4, "0"); +} + +function buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId = "***" }) { + const merchantInfo = pixField("00", "br.gov.bcb.pix") + pixField("01", pixKey); + const addData = pixField("05", txId.substring(0, 25)); + const name = removeAccents(merchantName).substring(0, 25); + const city = removeAccents(merchantCity).substring(0, 15); + + let payload = pixField("00", "01") + + pixField("26", merchantInfo) + + pixField("52", "0000") + + pixField("53", "986"); + + if (amount && amount > 0) { + payload += pixField("54", amount.toFixed(2)); + } + + payload += pixField("58", "BR") + + pixField("59", name) + + pixField("60", city) + + pixField("62", addData) + + "6304"; + + return payload + crc16(payload); +} + +// ── API helper ──────────────────────────────────────────────────────────────── + +async function callApi(body) { + const res = await fetch(`${BASE_URL}/api/v1/QRManager/generate`, { + method: "POST", + headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" }, + body: JSON.stringify(body) + }); + return res.json(); +} + +function errorContent(msg) { + return { content: [{ type: "text", text: `❌ ${msg}` }], isError: true }; +} + +function openImage(base64, label = "qr") { + try { + const ext = "png"; + const file = join(tmpdir(), `${label}_${Date.now()}.${ext}`); + writeFileSync(file, Buffer.from(base64, "base64")); + exec(`start "" "${file}"`); + return file; + } catch (e) { + process.stderr.write(`[qrrapido-mcp] Não abriu imagem: ${e.message}\n`); + return null; + } +} + +function successContent(data, label = "qr") { + const file = openImage(data.qrCodeBase64, label); + return { + content: [ + { type: "image", data: data.qrCodeBase64, mimeType: data.mimeType || "image/png" }, + { + type: "text", + text: [ + `✅ QR code gerado`, + file ? `🖼 Aberto em: ${file}` : "", + `⏱ ${data.generationTimeMs}ms`, + `💾 Cache: ${data.fromCache ? "hit" : "miss"}`, + `📊 Créditos restantes: ${data.remainingCredits ?? "N/A"}` + ].filter(Boolean).join("\n") + } + ] + }; +} + +// ── Server ──────────────────────────────────────────────────────────────────── + +const server = new Server( + { name: "qrrapido", version: "0.2.0" }, + { capabilities: { tools: {} } } +); + +// tools/list +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "generate_qr", + description: + "Gera QR code para URL, Wi-Fi, vCard, WhatsApp, Email, SMS ou texto livre. " + + "Para PIX use generate_pix_qr.", + inputSchema: { + type: "object", + properties: { + type: { + type: "string", + enum: ["url", "wifi", "vcard", "whatsapp", "email", "sms", "texto"], + description: "Tipo do QR code" + }, + content: { type: "string", description: "Conteúdo do QR code" }, + format: { type: "string", enum: ["png", "webp", "svg"], default: "png" }, + size: { type: "number", default: 400, description: "Tamanho em pixels (100–2000)" }, + primaryColor: { type: "string", default: "#000000", description: "Cor hex do QR" }, + backgroundColor: { type: "string", default: "#FFFFFF", description: "Cor hex de fundo" } + }, + required: ["type", "content"] + } + }, + { + name: "generate_pix_qr", + description: + "Gera QR code PIX (BRCode/EMV) para recebimento de pagamento. " + + "Monte o payload automaticamente com chave, valor, nome e cidade.", + inputSchema: { + type: "object", + properties: { + pixKey: { + type: "string", + description: "Chave PIX: CPF (somente dígitos), email, telefone (+5511...) ou chave aleatória" + }, + amount: { + type: "number", + description: "Valor em reais (ex: 50.00). Omita para QR de valor aberto." + }, + merchantName: { + type: "string", + description: "Nome do recebedor (max 25 chars, ex: RICARDO CARNEIRO)" + }, + merchantCity: { + type: "string", + description: "Cidade do recebedor (max 15 chars, ex: SAO PAULO)" + }, + txId: { + type: "string", + default: "***", + description: "ID da transação (opcional, max 25 chars)" + }, + size: { type: "number", default: 400, description: "Tamanho em pixels" }, + primaryColor: { type: "string", default: "#000000" }, + backgroundColor: { type: "string", default: "#FFFFFF" } + }, + required: ["pixKey", "merchantName", "merchantCity"] + } + } + ] +})); + +// tools/call +server.setRequestHandler(CallToolRequestSchema, async (req) => { + const { name } = req.params; + const args = req.params.arguments ?? {}; + + // ── generate_pix_qr ────────────────────────────────────────────────────── + if (name === "generate_pix_qr") { + const { pixKey, amount, merchantName, merchantCity, txId = "***", + size = 400, primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args; + + if (!pixKey || !merchantName || !merchantCity) + return errorContent("pixKey, merchantName e merchantCity são obrigatórios."); + + let payload; + try { + payload = buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId }); + } catch (err) { + return errorContent(`Erro ao montar payload PIX: ${err.message}`); + } + + let data; + try { + data = await callApi({ type: "texto", content: payload, size, primaryColor, backgroundColor }); + } catch (err) { + return errorContent(`Falha na requisição: ${err.message}`); + } + + if (!data.success) return errorContent(data.message || data.error || "Erro desconhecido"); + + const pixFile = openImage(data.qrCodeBase64, "pix"); + return { + content: [ + { type: "image", data: data.qrCodeBase64, mimeType: data.mimeType || "image/png" }, + { + type: "text", + text: [ + `✅ QR code PIX gerado`, + pixFile ? `🖼 Aberto em: ${pixFile}` : "", + `🔑 Chave: ${pixKey}`, + `💰 Valor: ${amount ? `R$ ${amount.toFixed(2)}` : "aberto"}`, + `👤 Recebedor: ${merchantName} — ${merchantCity}`, + `⏱ ${data.generationTimeMs}ms` + ].filter(Boolean).join("\n") + } + ] + }; + } + + // ── generate_qr ────────────────────────────────────────────────────────── + if (name === "generate_qr") { + const { type, content, format = "png", size = 400, + primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args; + + if (!type || !content) return errorContent("'type' e 'content' são obrigatórios."); + + let data; + try { + data = await callApi({ type, content, outputFormat: format, size, primaryColor, backgroundColor }); + } catch (err) { + return errorContent(`Falha na requisição: ${err.message}`); + } + + if (!data.success) return errorContent(data.message || data.error || "Erro desconhecido"); + return successContent(data); + } + + return errorContent(`Tool desconhecida: ${name}`); +}); + +// ── Start ───────────────────────────────────────────────────────────────────── + +const transport = new StdioServerTransport(); +await server.connect(transport); +process.stderr.write(`[qrrapido-mcp] Servidor iniciado. Base URL: ${BASE_URL}\n`); diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json new file mode 100644 index 0000000..887eef6 --- /dev/null +++ b/mcp-server/package-lock.json @@ -0,0 +1,1148 @@ +{ + "name": "qrrapido-mcp-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "qrrapido-mcp-server", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "undici": "^6.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 0000000..2fb16c5 --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,17 @@ +{ + "name": "qrrapido-mcp-server", + "version": "0.2.0", + "private": true, + "type": "module", + "main": "index.mjs", + "scripts": { + "start:stdio": "node index.mjs", + "start:http": "node server-http.mjs", + "start": "node server-http.mjs" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "express": "^4.18.0", + "undici": "^6.0.0" + } +} diff --git a/mcp-server/server-http.mjs b/mcp-server/server-http.mjs new file mode 100644 index 0000000..5e639b3 --- /dev/null +++ b/mcp-server/server-http.mjs @@ -0,0 +1,219 @@ +/** + * QR Rápido MCP Server — HTTP/SSE transport (para n8n cloud e clientes remotos) + * + * Variáveis de ambiente: + * QR_BASE_URL — URL da API (default: https://qrrapido.site) + * PORT — porta HTTP (default: 3000) + * ALLOWED_KEYS — lista de API keys válidas separadas por vírgula (opcional; vazio = delega à API) + * + * Cada request deve enviar o header: X-API-Key: qr_xxx + * O server valida a key chamando a API antes de aceitar a sessão MCP. + * + * Endpoint n8n: https://mcp.qrrapido.site/mcp (ou porta 3000 internamente) + */ + +import express from "express"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.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"; + +const BASE_URL = process.env.QR_BASE_URL || "https://qrrapido.site"; +const PORT = parseInt(process.env.PORT || "3000", 10); + +// ── PIX EMV builder ─────────────────────────────────────────────────────────── + +function pixField(id, value) { + return `${id}${String(value.length).padStart(2, "0")}${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 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", pixField("05", txId.substring(0, 25))) + + "6304"; + return payload + crc16(payload); +} + +// ── API helpers ─────────────────────────────────────────────────────────────── + +async function callApi(apiKey, body) { + const res = await fetch(`${BASE_URL}/api/v1/QRManager/generate`, { + method: "POST", + headers: { "X-API-Key": apiKey, "Content-Type": "application/json" }, + body: JSON.stringify(body) + }); + return res.json(); +} + +async function validateKey(apiKey) { + // Basic format check — real auth happens on every tool call via the API + if (!apiKey || !apiKey.startsWith("qr_") || apiKey.length < 20) return false; + return true; +} + +function errorContent(msg) { + return { content: [{ type: "text", text: `❌ ${msg}` }], isError: true }; +} + +// ── MCP server factory (uma instância por sessão/request) ───────────────────── + +function createMcpServer(apiKey) { + const server = new Server( + { name: "qrrapido", version: "0.2.0" }, + { capabilities: { tools: {} } } + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "generate_qr", + description: "Gera QR code para URL, Wi-Fi, vCard, WhatsApp, Email, SMS ou texto. Para PIX use generate_pix_qr.", + 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" } + }, + required: ["type", "content"] + } + }, + { + name: "generate_pix_qr", + description: "Gera QR code PIX (BRCode/EMV) com payload completo para recebimento.", + 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" } + }, + required: ["pixKey", "merchantName", "merchantCity"] + } + } + ] + })); + + server.setRequestHandler(CallToolRequestSchema, async (req) => { + const { name } = req.params; + const args = req.params.arguments ?? {}; + + if (name === "generate_pix_qr") { + const { pixKey, amount, merchantName, merchantCity, txId = "***", + size = 400, primaryColor = "#000000", backgroundColor = "#FFFFFF" } = args; + if (!pixKey || !merchantName || !merchantCity) + return errorContent("pixKey, merchantName e merchantCity são obrigatórios."); + let payload; + try { payload = buildPixPayload({ pixKey, amount, merchantName, merchantCity, txId }); } + catch (e) { return errorContent(`Erro payload PIX: ${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"); + 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` } + ] + }; + } + + 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."); + 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"); + 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"}` } + ] + }; + } + + return errorContent(`Tool desconhecida: ${name}`); + }); + + return server; +} + +// ── Express app ─────────────────────────────────────────────────────────────── + +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 })); + +// MCP endpoint — cada POST cria sessão stateless +app.all("/mcp", async (req, res) => { + const apiKey = req.headers["x-api-key"] || req.query.key; + + if (!apiKey) { + res.status(401).json({ error: "X-API-Key header required" }); + return; + } + + const valid = await validateKey(apiKey); + if (!valid) { + res.status(401).json({ error: "Invalid or revoked API key" }); + return; + } + + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + const server = createMcpServer(apiKey); + + res.on("close", () => { transport.close(); server.close(); }); + + try { + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (err) { + if (!res.headersSent) + res.status(500).json({ error: "Internal server error", detail: err.message }); + } +}); + +app.listen(PORT, () => { + console.log(`[qrrapido-mcp-http] Servidor HTTP na porta ${PORT}`); + console.log(`[qrrapido-mcp-http] Base URL: ${BASE_URL}`); + console.log(`[qrrapido-mcp-http] Endpoint n8n: http://localhost:${PORT}/mcp`); +}); diff --git a/wwwroot/mcp/en/index.html b/wwwroot/mcp/en/index.html new file mode 100644 index 0000000..ebcf263 --- /dev/null +++ b/wwwroot/mcp/en/index.html @@ -0,0 +1,738 @@ + + + + + + QR Rápido MCP — QR code generator for AI agents and automations + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ MCP · REST API · n8n · Make.com +
+

+ QR codes for
+ AI agents
+ and automations +

+

+ Generate QR codes via MCP, REST API, or any HTTP client. + Inline base64 response — + no upload, no file hosting, ready to use instantly. +

+ +
+
+
<0.4s
+
avg generation
+
+
+
8 types
+
of QR code
+
+
+
base64
+
inline, no hosting
+
+
+
+
+
+
+ claude — MCP Tool Call +
+
+
+
+
+ + +
+
+ WORKS WITH +
+
🤖 Claude Desktop
+
🔄 n8n
+
⚙️ Make.com
+
Zapier
+
🔌 REST API
+
📡 Any HTTP client
+
+
+
+ + +
+
+
+ +

Three steps to start
generating

+

From zero to QR codes in your agent in under 2 minutes. No credit card.

+
+
+
+
01
+
+
👤
+
Create your account
+

Free login with Google or Microsoft. Free plan with 500 req/month included. No credit card required.

+
+
+
+
+
02
+
+
🔑
+
Copy your API key
+

Your key is auto-generated on first login. Displayed prominently — copy with one click, snippet ready.

+
+
+
+
+
03
+
+
+
Connect to your agent
+

Paste into Claude Desktop config, n8n HTTP node, or any client. Done — your agent generates QR codes.

+
+
+
+
+
+ + +
+
+
+ +

Connect however
you prefer

+

Native MCP, REST API, or HTTP Request — the same API serves all clients.

+
+
+
+ + + +
+ +
+
+
+
+
+ claude_desktop_config.json +
+ +
+
// ~/.config/claude/claude_desktop_config.json
+{
+  "mcpServers": {
+    "qrrapido": {
+      "command": "qrrapido-mcp",
+      "env": {
+        "QR_API_KEY": "qr_your_key_here"
+      }
+    }
+  }
+}
+
+// Claude Desktop now has access to tools:
+// → generate_qr(type, content, format, color, size)
+// → list_qr_history()
+// → get_qr_analytics(trackingId)
+
+
+ +
+
+
+
+
+ terminal +
+ +
+
# Generate a QR code via REST API
+curl -X POST https://qrrapido.site/api/v1/QRManager/generate \
+  -H "X-API-Key: qr_your_key_here" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "type":         "url",
+    "content":      "https://my-store.com/product/123",
+    "outputFormat": "png",
+    "size":         400
+  }'
+
+# Response: JSON with inline qrCodeBase64
+# No upload. No CDN. Ready to use.
+
+
+ +
+
+
+
+
+ n8n — HTTP Request Node +
+
+
+
+
+
🔄
+
HTTP Request
+
+
MethodPOST
+
URLhttps://qrrapido.site/api/v1/QRManager/generate
+
Auth TypeHeader Auth
+
Header NameX-API-Key
+
Header Valueqr_your_key_here
+
Body (JSON){"type":"url","content":"{{ $json.url }}"}
+
+
+
+
+
Output available
+
+

+ Use {{ $json.qrCodeBase64 }} in downstream nodes + to send via email, save to file, or return via webhook — no external URL needed. +

+
+
+
+
+
+
+
+ + +
+
+
+ +

8 QR code types,
one single endpoint

+

Pass "type" in the payload and QR Rápido handles the correct format.

+
+
+
🔗
URL
"url"

Links, products, landing pages

+
💸
PIX
"pix"

Brazilian instant payments

+
📶
Wi-Fi
"wifi"

Wireless network credentials

+
👤
vCard
"vcard"

Digital business cards

+
💬
WhatsApp
"whatsapp"

Pre-filled messages

+
📧
Email
"email"

Direct email composition

+
📱
SMS
"sms"

Pre-defined SMS text

+
📝
Text
"texto"

Any free-form text content

+
+
+
+ + +
+
+
+
+
+ +

Inline base64.
No hosting.

+

QR code returned directly in the JSON. Your agent receives it, uses it, moves on — no external URL, no upload.

+
+
+
+
📦
+
+
Image in JSON
+

Base64 embedded in the response. Agents use it directly — no dependency on external URLs or CDN.

+
+
+
+
+
+
Automatic cache
+

Identical QRs return from cache in milliseconds. fromCache: true signals a hit.

+
+
+
+
🎯
+
+
3 output formats
+

PNG, WebP (~40% smaller), or vector SVG. Choose per request via outputFormat.

+
+
+
+
+
+
+
+
+
+ response.json +
+ +
+
{
+  "success":          true,
+  "qrCodeBase64":     "iVBORw0KGgoAAAANSUhEUgAA...",
+  "qrId":             "6842a1f3b8c94d2e...",
+  "format":           "png",
+  "mimeType":         "image/png",
+  "generationTimeMs": 312,
+  "fromCache":        false,
+  "remainingCredits": 487,
+  "message":          "QR generated successfully"
+}
+
+
+
+
+
+ + +
+
+
+ +

Start free.
Scale when you need.

+

No credit card to start. Upgrade anytime.

+
+
+
+
FREE
+
Free
+

For testing and small projects

+
+
    +
  • 500 req/month
  • +
  • 10 req/minute
  • +
  • All 8 QR types
  • +
  • PNG, WebP, SVG
  • +
  • Automatic cache
  • +
+ Start free +
+
+
STARTER
+
R$39/mo
+

For automations and workflows

+
+
    +
  • 10,000 req/month
  • +
  • 50 req/minute
  • +
  • All 8 QR types
  • +
  • PNG, WebP, SVG
  • +
  • Email support
  • +
+ Get Starter +
+ +
+
BUSINESS
+
R$349/mo
+

For high volume and teams

+
+
    +
  • 500,000 req/month
  • +
  • 500 req/minute
  • +
  • Dynamic QR + tracking
  • +
  • 5 simultaneous API keys
  • +
  • SLA + dedicated support
  • +
+ Get Business +
+
+
+
+ + +
+
+
+

+ Up and running in
+ 2 minutes +

+

Free login. Key auto-generated. Snippet ready to copy.

+ Create free account → +
+
+
+ + + + + + + diff --git a/wwwroot/mcp/index.html b/wwwroot/mcp/index.html new file mode 100644 index 0000000..1b1934c --- /dev/null +++ b/wwwroot/mcp/index.html @@ -0,0 +1,1576 @@ + + + + + + QR Rápido MCP — QR codes para agentes de IA e automações + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ MCP · REST API · n8n · Make.com +
+ +

+ QR codes para
+ agentes de IA
+ e automações +

+ +

+ Gere QR codes via MCP, REST API ou qualquer HTTP client. + Resposta base64 inline — + sem upload, sem hosting de arquivo, pronto para usar. +

+ + + +
+
+
<0.4s
+
geração média
+
+
+
8 tipos
+
de QR code
+
+
+
base64
+
inline, sem hosting
+
+
+
+ + +
+
+
+
+
+ claude — MCP Tool Call +
+
+
+
+
+ + +
+
+ FUNCIONA COM +
+
🤖 Claude Desktop
+
🔄 n8n
+
⚙️ Make.com
+
Zapier
+
🔌 REST API
+
📡 Qualquer HTTP client
+
+
+
+ + +
+
+
+ +

Três passos para
começar a gerar

+

Do zero ao QR code no seu agente em menos de 2 minutos. Sem cartão de crédito.

+
+ +
+
+
01
+
+
👤
+
Crie sua conta
+

Login gratuito com Google ou Microsoft. Plano grátis com 500 req/mês incluso. Sem cartão de crédito.

+
+
+
+ +
+
02
+
+
🔑
+
Copie sua API key
+

Sua key é gerada automaticamente no primeiro acesso. Exibida em destaque — copie com um clique, snippet já pronto.

+
+
+
+ +
+
03
+
+
+
Conecte ao agente
+

Cole no config do Claude Desktop, nó HTTP do n8n ou qualquer client. Pronto — seu agente já gera QR codes.

+
+
+
+
+
+ + +
+
+
+ +

Conecte do jeito
que preferir

+

MCP nativo, REST API ou HTTP Request — a mesma API serve todos os clientes.

+
+ +
+
+ + + +
+ + +
+
+
+
+
+
+
+ claude_desktop_config.json +
+ +
+
// ~/.config/claude/claude_desktop_config.json
+{
+  "mcpServers": {
+    "qrrapido": {
+      "command": "qrrapido-mcp",
+      "env": {
+        "QR_API_KEY": "qr_sua_key_aqui"
+      }
+    }
+  }
+}
+
+// Claude Desktop passa a ter acesso às tools:
+// → generate_qr(type, content, format, color, size)
+// → list_qr_history()
+// → get_qr_analytics(trackingId)
+
+
+ + +
+
+
+
+
+
+
+ terminal +
+ +
+
# Gerar QR code via REST API
+curl -X POST https://qrrapido.site/api/v1/QRManager/generate \
+  -H "X-API-Key: qr_sua_key_aqui" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "type":         "url",
+    "content":      "https://minha-loja.com/produto/123",
+    "outputFormat": "png",
+    "size":         400,
+    "primaryColor": "#000000"
+  }'
+
+# Resposta: JSON com qrCodeBase64 inline
+# Sem upload. Sem CDN. Pronto para usar.
+
+
+ + +
+
+
+
+
+
+
+ n8n — HTTP Request Node +
+
+
+
+
+
🔄
+
HTTP Request
+
+
MethodPOST
+
URLhttps://qrrapido.site/api/v1/QRManager/generate
+
Auth TypeHeader Auth
+
Header NameX-API-Key
+
Header Valueqr_sua_key_aqui
+
Body (JSON){"type":"url","content":"{{ $json.url }}"}
+
+
+
+
+
Output disponível
+
+

+ Use {{ $json.qrCodeBase64 }} nos nós seguintes + para enviar por email, salvar em arquivo ou retornar via webhook — sem URL externa. +

+
+
+
+
+
+
+
+ + +
+
+
+ +

8 tipos de QR code,
um único endpoint

+

Passe o "type" no payload e o QR Rápido cuida do formato correto.

+
+ +
+
+ 🔗 +
URL
+
"url"
+

Links, produtos, landing pages

+
+
+ 💸 +
PIX
+
"pix"
+

Pagamentos instantâneos brasileiros

+
+
+ 📶 +
Wi-Fi
+
"wifi"
+

Credenciais de rede sem fio

+
+
+ 👤 +
vCard
+
"vcard"
+

Cartões de visita digitais

+
+
+ 💬 +
WhatsApp
+
"whatsapp"
+

Mensagens pré-preenchidas

+
+
+ 📧 +
Email
+
"email"
+

Composição de email direta

+
+
+ 📱 +
SMS
+
"sms"
+

Texto SMS pré-definido

+
+
+ 📝 +
Texto
+
"texto"
+

Qualquer conteúdo em texto livre

+
+
+
+
+ + +
+
+
+
+
+ +

Base64 inline.
Sem hosting.

+

O QR code retorna direto no JSON. Seu agente recebe, usa e segue em frente — sem URL externa, sem upload.

+
+ +
+
+
📦
+
+
Imagem no JSON
+

Base64 embutido na resposta. Agentes usam direto — sem depender de URL de terceiros ou CDN.

+
+
+
+
+
+
Cache automático
+

QRs idênticos retornam do cache em milissegundos. fromCache: true indica hit.

+
+
+
+
🎯
+
+
3 formatos de saída
+

PNG, WebP (~40% menor) ou SVG vetorial. Escolha por request via outputFormat.

+
+
+
+
+ +
+
+
+
+
+
+
+ response.json +
+ +
+
{
+  "success":          true,
+  "qrCodeBase64":     "iVBORw0KGgoAAAANSUhEUgAA...",
+  "qrId":             "6842a1f3b8c94d2e...",
+  "format":           "png",
+  "mimeType":         "image/png",
+  "generationTimeMs": 312,
+  "fromCache":        false,
+  "remainingCredits": 487,
+  "message":          "QR gerado com sucesso"
+}
+
+
+
+
+
+ + +
+
+
+ +

Comece grátis.
Escale quando precisar.

+

Sem cartão de crédito para começar. Upgrade a qualquer momento.

+
+ +
+ +
+
FREE
+
Grátis
+

Para testar e projetos pequenos

+
+
    +
  • 500 req/mês
  • +
  • 10 req/minuto
  • +
  • Todos os 8 tipos de QR
  • +
  • PNG, WebP, SVG
  • +
  • Cache automático
  • +
+ Começar grátis +
+ + +
+
STARTER
+
R$39/mês
+

Para automações e workflows

+
+
    +
  • 10.000 req/mês
  • +
  • 50 req/minuto
  • +
  • Todos os 8 tipos de QR
  • +
  • PNG, WebP, SVG
  • +
  • Suporte por email
  • +
+ Assinar Starter +
+ + + + + +
+
BUSINESS
+
R$349/mês
+

Para alto volume e equipes

+
+
    +
  • 500.000 req/mês
  • +
  • 500 req/minuto
  • +
  • QR dinâmico + tracking
  • +
  • 5 API keys simultâneas
  • +
  • SLA + suporte dedicado
  • +
+ Assinar Business +
+
+
+
+ + +
+
+
+

+ Comece em
+ 2 minutos +

+

Login gratuito. Key gerada automaticamente. Snippet pronto para copiar.

+ Criar conta grátis → +
+
+
+ + + + + + +