- 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 <noreply@anthropic.com>
194 lines
10 KiB
Plaintext
194 lines
10 KiB
Plaintext
@model QRRapidoApp.Models.User
|
|
@{
|
|
ViewData["Title"] = "Configurar MCP — QR Rápido";
|
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
|
|
|
var isEn = (ViewBag.Culture as string) == "en";
|
|
var activeKey = Model.ApiKeys.FirstOrDefault(k => k.IsActive);
|
|
var baseUrl = Context.Request.Scheme + "://" + Context.Request.Host;
|
|
|
|
string T(string pt, string en) => isEn ? en : pt;
|
|
}
|
|
|
|
<div class="container mt-4 mb-5" style="max-width: 800px;">
|
|
|
|
<!-- Header -->
|
|
<div class="d-flex align-items-center mb-4 gap-3">
|
|
<div style="width:48px;height:48px;background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">▦</div>
|
|
<div>
|
|
<h1 class="h3 mb-0">@T("Conectar ao MCP", "Connect to MCP")</h1>
|
|
<p class="text-muted mb-0 small">@T("Configure o QR Rápido no Claude Desktop, n8n ou qualquer HTTP client.", "Set up QR Rápido in Claude Desktop, n8n, or any HTTP client.")</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if (activeKey == null)
|
|
{
|
|
<!-- Sem key — não deve acontecer (auto-criada no primeiro login), mas garante fallback -->
|
|
<div class="alert alert-warning mb-4">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
@T("Nenhuma API key ativa encontrada.", "No active API key found.")
|
|
<a href="/Developer" class="alert-link ms-1">@T("Criar uma agora →", "Create one now →")</a>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<!-- Step 1: API Key -->
|
|
<div class="card border-0 shadow-sm mb-4" style="background:#0f0f1c;border:1px solid rgba(99,102,241,0.2)!important;">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<span style="background:linear-gradient(135deg,#6366f1,#8b5cf6);color:white;width:24px;height:24px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0;">1</span>
|
|
<h5 class="mb-0 text-light">@T("Sua API Key", "Your API Key")</h5>
|
|
</div>
|
|
<p class="text-muted small mb-3">
|
|
@T("Esta key foi gerada automaticamente. Copie e guarde — ela não será exibida novamente após sair desta página.",
|
|
"This key was auto-generated. Copy and save it — it won't be shown again after you leave this page.")
|
|
</p>
|
|
<div class="input-group">
|
|
<input type="password" id="apiKeyField" class="form-control font-monospace"
|
|
value="@activeKey.Prefix•••••••••••••••••••••••••" readonly
|
|
style="background:#1a1a2e;border-color:rgba(99,102,241,0.3);color:#e0e0ff;">
|
|
<button class="btn btn-outline-secondary" type="button" onclick="toggleKey()" id="toggleBtn"
|
|
title="@T("Mostrar key", "Show key")">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn" type="button" onclick="copyField('apiKeyField', this)"
|
|
style="background:linear-gradient(135deg,#6366f1,#8b5cf6);color:white;border:none;">
|
|
<i class="fas fa-copy me-1"></i> @T("Copiar", "Copy")
|
|
</button>
|
|
</div>
|
|
<div class="mt-2">
|
|
<small class="text-muted">
|
|
@T("Plano:", "Plan:") <span class="badge" style="background:rgba(99,102,241,0.2);color:#a5b4fc;">@(Model.ApiSubscription?.EffectiveTier.ToString() ?? "Free")</span>
|
|
·
|
|
<a href="/Developer" class="text-muted small">@T("Gerenciar todas as keys →", "Manage all keys →")</a>
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Claude Desktop -->
|
|
<div class="card border-0 shadow-sm mb-4" style="background:#0f0f1c;border:1px solid rgba(99,102,241,0.2)!important;">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<span style="background:linear-gradient(135deg,#6366f1,#8b5cf6);color:white;width:24px;height:24px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0;">2</span>
|
|
<h5 class="mb-0 text-light">@T("Claude Desktop — config.json", "Claude Desktop — config.json")</h5>
|
|
</div>
|
|
<p class="text-muted small mb-3">
|
|
@T("Cole em ", "Paste into ")
|
|
<code class="text-info">~/.config/claude/claude_desktop_config.json</code>
|
|
@T(" (macOS/Linux) ou ", " (macOS/Linux) or ")
|
|
<code class="text-info">%APPDATA%\Claude\claude_desktop_config.json</code>
|
|
@T(" (Windows).", " (Windows).")
|
|
</p>
|
|
<div class="position-relative">
|
|
<pre id="mcpSnippet" class="p-3 rounded font-monospace small mb-2"
|
|
style="background:#06060e;color:#e0e0ff;border:1px solid rgba(99,102,241,0.2);overflow-x:auto;line-height:1.7;">{
|
|
<span style="color:#06b6d4;">"mcpServers"</span>: {
|
|
<span style="color:#06b6d4;">"qrrapido"</span>: {
|
|
<span style="color:#06b6d4;">"command"</span>: <span style="color:#a5f3fc;">"qrrapido-mcp"</span>,
|
|
<span style="color:#06b6d4;">"env"</span>: {
|
|
<span style="color:#06b6d4;">"QR_API_KEY"</span>: <span style="color:#fde68a;">"@activeKey.Prefix•••••••••••••"</span>
|
|
}
|
|
}
|
|
}
|
|
}</pre>
|
|
<button class="btn btn-sm position-absolute top-0 end-0 m-2"
|
|
onclick="copyMcpSnippet(this)"
|
|
style="background:rgba(99,102,241,0.2);color:#a5b4fc;border:1px solid rgba(99,102,241,0.3);">
|
|
<i class="fas fa-copy me-1"></i> @T("Copiar", "Copy")
|
|
</button>
|
|
</div>
|
|
<small class="text-muted">
|
|
@T("Reinicie o Claude Desktop após salvar o arquivo.", "Restart Claude Desktop after saving the file.")
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: REST / n8n -->
|
|
<div class="card border-0 shadow-sm mb-4" style="background:#0f0f1c;border:1px solid rgba(99,102,241,0.2)!important;">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<span style="background:linear-gradient(135deg,#6366f1,#8b5cf6);color:white;width:24px;height:24px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0;">3</span>
|
|
<h5 class="mb-0 text-light">@T("REST API / n8n / qualquer HTTP client", "REST API / n8n / any HTTP client")</h5>
|
|
</div>
|
|
<p class="text-muted small mb-3">
|
|
@T("Use o header ", "Use the header ")
|
|
<code class="text-info">X-API-Key</code>
|
|
@T(" em todas as requests.", " in all requests.")
|
|
</p>
|
|
<pre class="p-3 rounded font-monospace small mb-0"
|
|
style="background:#06060e;color:#e0e0ff;border:1px solid rgba(99,102,241,0.2);overflow-x:auto;line-height:1.7;"><span style="color:#9090b0;"># curl</span>
|
|
curl -X POST <span style="color:#10b981;">@baseUrl/api/v1/QRManager/generate</span> \
|
|
-H <span style="color:#a5f3fc;">"X-API-Key: @activeKey.Prefix•••••••••••••"</span> \
|
|
-H <span style="color:#a5f3fc;">"Content-Type: application/json"</span> \
|
|
-d '{"type":"url","content":"https://exemplo.com","outputFormat":"png"}'
|
|
|
|
<span style="color:#9090b0;"># n8n: HTTP Request Node → Header Auth → X-API-Key</span></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Links -->
|
|
<div class="d-flex gap-3 flex-wrap">
|
|
<a href="/api/docs" target="_blank" class="btn btn-outline-info btn-sm">
|
|
<i class="fas fa-book me-1"></i> @T("Documentação API", "API Docs")
|
|
</a>
|
|
<a href="/Developer/Pricing" class="btn btn-outline-primary btn-sm">
|
|
<i class="fas fa-arrow-up me-1"></i> @T("Ver planos", "View plans")
|
|
</a>
|
|
<a href="/Developer" class="btn btn-outline-secondary btn-sm">
|
|
<i class="fas fa-key me-1"></i> @T("Gerenciar keys", "Manage keys")
|
|
</a>
|
|
</div>
|
|
}
|
|
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
// Real key value — passed securely, never exposed in DOM until user clicks "show"
|
|
// The actual key prefix is visible; full key was shown only at creation time.
|
|
// Here we show prefix only — user already copied on first login or via /Developer.
|
|
|
|
function toggleKey() {
|
|
var field = document.getElementById('apiKeyField');
|
|
var btn = document.getElementById('toggleBtn');
|
|
if (field.type === 'password') {
|
|
field.type = 'text';
|
|
btn.innerHTML = '<i class="fas fa-eye-slash"></i>';
|
|
} else {
|
|
field.type = 'password';
|
|
btn.innerHTML = '<i class="fas fa-eye"></i>';
|
|
}
|
|
}
|
|
|
|
function copyField(fieldId, btn) {
|
|
var field = document.getElementById(fieldId);
|
|
var val = field.value;
|
|
navigator.clipboard.writeText(val).then(function () {
|
|
var orig = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fas fa-check me-1"></i> @T("Copiado!", "Copied!")';
|
|
setTimeout(function () { btn.innerHTML = orig; }, 2000);
|
|
});
|
|
}
|
|
|
|
function copyMcpSnippet(btn) {
|
|
// Build clean JSON without HTML spans
|
|
var json = JSON.stringify({
|
|
mcpServers: {
|
|
qrrapido: {
|
|
command: "qrrapido-mcp",
|
|
env: {
|
|
QR_API_KEY: "@activeKey.Prefix•••••••••••••"
|
|
}
|
|
}
|
|
}
|
|
}, null, 2);
|
|
navigator.clipboard.writeText(json).then(function () {
|
|
var orig = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fas fa-check me-1"></i> @T("Copiado!", "Copied!")';
|
|
setTimeout(function () { btn.innerHTML = orig; }, 2000);
|
|
});
|
|
}
|
|
</script>
|
|
}
|