QrRapido/Views/Developer/Mcp.cshtml
Ricardo Carneiro a3238ca6c5 feat: MCP server + landing page + OAuth returnUrl fix
- 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>
2026-05-07 21:23:50 -03:00

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>
&nbsp;·&nbsp;
<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>
}