12f688ff7c
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>
219 lines
7.7 KiB
TypeScript
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
|
|
}
|