Files
ealmeida 12f688ff7c feat: adicionar 5 novos painéis ao dashboard (MCPs, n8n, Paperclip, IA, Operações)
Expansão do dashboard de 3 para 8 páginas com dados reais do stack:
- MCPs: monitorização de 33 MCPs no gateway com ping e estado online/offline
- n8n: 14 workflows com último run, duração e falhas 24h
- Paperclip: 16 agentes operacionais, routines e issues (PostgreSQL)
- IA/Claude: visão das 3 camadas (189 skills, 72 agents, 39 MCPs, CARL)
- Operações: tickets Desk CRM por departamento + cobertura PROCs

16 ficheiros novos (3042 linhas), 3 existentes editados.
Nova dependência: pg (PostgreSQL client para Paperclip).
Audit: 0 vulnerabilidades (npm audit fix aplicado).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:58:48 +01:00

219 lines
7.7 KiB
TypeScript

/**
* MCPs Service
* Ping paralelo aos MCPs via gateway com cache de 60 segundos
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
export interface McpStatus {
name: string
port: number
category: string
enabled: boolean
status: 'online' | 'offline' | 'disabled' | 'unknown'
response_time_ms: number | null
last_check: string
tools_count?: number
}
export interface McpDashboard {
gateway_status: 'online' | 'offline'
total: number
online: number
offline: number
disabled: number
mcps: McpStatus[]
auth: {
method: string
token_expires: string | null
}
}
// Lista de MCPs com metadados (extraída de port-map.json e claude.json)
const MCP_LIST: Array<{ name: string; port: number; category: string; enabled: boolean }> = [
// CRM
{ name: 'desk-crm-v3', port: 3150, category: 'crm', enabled: true },
{ name: 'desk-project-minimal', port: 3153, category: 'crm', enabled: false },
// Infra
{ name: 'ssh-unified', port: 3192, category: 'infra', enabled: true },
{ name: 'filesystem', port: 0, category: 'infra', enabled: true },
{ name: 'chrome-devtools', port: 0, category: 'infra', enabled: true },
// AI
{ name: 'lightrag', port: 3160, category: 'ai', enabled: true },
{ name: 'notebooklm', port: 3190, category: 'ai', enabled: false },
{ name: 'context7', port: 3169, category: 'ai', enabled: false },
{ name: 'replicate', port: 3176, category: 'ai', enabled: false },
{ name: 'memory-supabase', port: 3151, category: 'ai', enabled: true },
// Tools
{ name: 'mcp-time', port: 3155, category: 'tools', enabled: true },
{ name: 'deepl', port: 3188, category: 'tools', enabled: true },
{ name: 'pexels', port: 3175, category: 'tools', enabled: false },
{ name: 'vimeo', port: 3177, category: 'tools', enabled: false },
{ name: 'drawio', port: 3184, category: 'tools', enabled: false },
// External
{ name: 'google-workspace', port: 3156, category: 'external', enabled: false },
{ name: 'google-analytics', port: 3164, category: 'external', enabled: false },
{ name: 'gsc', port: 3165, category: 'external', enabled: false },
{ name: 'youtube', port: 3166, category: 'external', enabled: false },
{ name: 'youtube-research', port: 3167, category: 'external', enabled: false },
{ name: 'moloni', port: 3158, category: 'external', enabled: false },
{ name: 'n8n', port: 3171, category: 'external', enabled: false },
{ name: 'gitea', port: 3162, category: 'external', enabled: true },
{ name: 'stitch', port: 0, category: 'external', enabled: false },
{ name: 'design-systems', port: 0, category: 'external', enabled: false },
// Gateway
{ name: 'authentik', port: 3191, category: 'gateway', enabled: false },
{ name: 'spaceship', port: 3189, category: 'gateway', enabled: false },
{ name: 'puppeteer', port: 3193, category: 'gateway', enabled: false },
{ name: 'lighthouse', port: 3194, category: 'gateway', enabled: false },
// Project
{ name: 'carl-mcp', port: 0, category: 'project', enabled: true },
{ name: 'reonic', port: 3187, category: 'project', enabled: false },
]
const GATEWAY_URL = process.env.MCP_GATEWAY_URL ?? 'https://gateway.descomplicar.pt'
const GATEWAY_TOKEN = process.env.MCP_GATEWAY_TOKEN ?? ''
const PING_TIMEOUT_MS = 5000
const CACHE_TTL_MS = 60_000
// Cache em memória
let cache: { data: McpDashboard; timestamp: number } | null = null
function buildHeaders(): Record<string, string> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (GATEWAY_TOKEN) {
headers['Authorization'] = `Bearer ${GATEWAY_TOKEN}`
}
return headers
}
async function pingGatewayHealth(): Promise<boolean> {
try {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), PING_TIMEOUT_MS)
const res = await fetch(`${GATEWAY_URL}/health`, { signal: controller.signal })
clearTimeout(timer)
return res.ok
} catch {
return false
}
}
async function pingMcp(name: string): Promise<{ ok: boolean; response_time_ms: number | null }> {
const start = Date.now()
try {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), PING_TIMEOUT_MS)
const res = await fetch(`${GATEWAY_URL}/v1/${name}/mcp`, {
method: 'GET',
headers: buildHeaders(),
signal: controller.signal,
})
clearTimeout(timer)
const elapsed = Date.now() - start
// Qualquer resposta HTTP não-5xx conta como online
return { ok: res.status < 500, response_time_ms: elapsed }
} catch {
return { ok: false, response_time_ms: null }
}
}
export async function getMcpDashboard(): Promise<McpDashboard> {
// Servir cache se ainda válido
if (cache !== null && Date.now() - cache.timestamp < CACHE_TTL_MS) {
return cache.data
}
const now = new Date().toISOString()
// Separar MCPs por tipo para determinar estratégia de ping
const enabledWithPort = MCP_LIST.filter(m => m.enabled && m.port > 0)
const enabledLocal = MCP_LIST.filter(m => m.enabled && m.port === 0)
const disabledMcps = MCP_LIST.filter(m => !m.enabled)
// Ping gateway health + todos os MCPs com porta em paralelo
const [gatewayResult, ...pingResults] = await Promise.allSettled([
pingGatewayHealth(),
...enabledWithPort.map(m => pingMcp(m.name)),
])
const gatewayStatus: 'online' | 'offline' =
gatewayResult.status === 'fulfilled' && gatewayResult.value ? 'online' : 'offline'
// MCPs enabled com porta gateway
const enabledWithPortStatuses: McpStatus[] = enabledWithPort.map((m, idx) => {
const result = pingResults[idx]
const pong = result.status === 'fulfilled'
? result.value
: { ok: false, response_time_ms: null }
return {
name: m.name,
port: m.port,
category: m.category,
enabled: true,
status: pong.ok ? 'online' : 'offline',
response_time_ms: pong.response_time_ms,
last_check: now,
} satisfies McpStatus
})
// MCPs enabled locais (sem porta no gateway) — assumir online se enabled
const enabledLocalStatuses: McpStatus[] = enabledLocal.map(m => ({
name: m.name,
port: m.port,
category: m.category,
enabled: true,
status: 'online' as const,
response_time_ms: null,
last_check: now,
}))
// MCPs disabled — sem ping, estado disabled
const disabledStatuses: McpStatus[] = disabledMcps.map(m => ({
name: m.name,
port: m.port,
category: m.category,
enabled: false,
status: 'disabled' as const,
response_time_ms: null,
last_check: now,
}))
const allMcps: McpStatus[] = [
...enabledWithPortStatuses,
...enabledLocalStatuses,
...disabledStatuses,
]
// Ordenar por categoria e depois por nome
allMcps.sort((a, b) => {
if (a.category !== b.category) return a.category.localeCompare(b.category)
return a.name.localeCompare(b.name)
})
const online = allMcps.filter(m => m.status === 'online').length
const offline = allMcps.filter(m => m.status === 'offline').length
const disabledCount = allMcps.filter(m => m.status === 'disabled').length
const data: McpDashboard = {
gateway_status: gatewayStatus,
total: allMcps.length,
online,
offline,
disabled: disabledCount,
mcps: allMcps,
auth: {
method: 'dual-layer: IP whitelist + Bearer token',
token_expires: null,
},
}
cache = { data, timestamp: Date.now() }
return data
}