/** * 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 { const headers: Record = { 'Content-Type': 'application/json' } if (GATEWAY_TOKEN) { headers['Authorization'] = `Bearer ${GATEWAY_TOKEN}` } return headers } async function pingGatewayHealth(): Promise { 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 { // 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 }