Skip to main content
Glama

Solid Multi-Tenant DevOps MCP Server

comprehensive-http-server.js36.4 kB
#!/usr/bin/env node /** * COMPREHENSIVE SOLID MCP SERVER - HTTP MODE * * Runs as Docker container on port 8092 * Provides HTTP endpoints for health checks and tool management * Monitored by admin dashboard at http://localhost:8080/admin */ import express from 'express'; import cors from 'cors'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import fetch from 'node-fetch'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const PORT = process.env.PORT || 8092; // ============================================================================ // SECURITY CONFIGURATION - PRODUCTION HARDENED // ============================================================================ // Load security environment variables const MCP_API_KEY = process.env.MCP_API_KEY || 'CHANGE_ME_IN_PRODUCTION'; const NODE_ENV = process.env.NODE_ENV || 'development'; const ALLOWED_IPS = process.env.ALLOWED_IPS ? process.env.ALLOWED_IPS.split(',') : []; const ENABLE_IP_WHITELIST = process.env.ENABLE_IP_WHITELIST === 'true'; const RATE_LIMIT_WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000'); // 1 minute const RATE_LIMIT_MAX_REQUESTS = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '1000'); // Warn if using default API key in production if (NODE_ENV === 'production' && MCP_API_KEY === 'CHANGE_ME_IN_PRODUCTION') { console.error('⚠️ WARNING: Using default MCP_API_KEY in production! Set MCP_API_KEY environment variable.'); process.exit(1); } // Rate limiting store (in-memory, use Redis in production for multi-instance) const rateLimitStore = new Map(); // ============================================================================ // AUTHENTICATION MIDDLEWARE // ============================================================================ function authenticateMCPRequest(req, res, next) { // Public health endpoint - no auth required if (req.path === '/health') { return next(); } const apiKey = req.headers['x-api-key']; const authHeader = req.headers['authorization']; // Check for API key if (apiKey && apiKey === MCP_API_KEY) { req.authenticated = true; req.authMethod = 'api-key'; return next(); } // Check for Bearer token (JWT from frontend) if (authHeader && authHeader.startsWith('Bearer ')) { // Forward JWT to backend for validation - backend will validate req.authenticated = true; req.authMethod = 'jwt'; return next(); } // No valid authentication return res.status(401).json({ error: 'Unauthorized', message: 'Valid X-API-Key header or Authorization Bearer token required', hint: 'Add header: X-API-Key: <your-mcp-api-key>', documentation: 'See MCP_API_KEY_AUTHENTICATION.md for setup instructions' }); } // ============================================================================ // IP WHITELIST MIDDLEWARE (Optional - for production) // ============================================================================ function checkIPWhitelist(req, res, next) { if (!ENABLE_IP_WHITELIST || ALLOWED_IPS.length === 0) { return next(); } const clientIP = req.ip || req.connection.remoteAddress; const forwardedFor = req.headers['x-forwarded-for']; const realIP = forwardedFor ? forwardedFor.split(',')[0].trim() : clientIP; // Allow localhost/internal IPs if (realIP === '::1' || realIP === '127.0.0.1' || realIP.startsWith('192.168.') || realIP.startsWith('10.')) { return next(); } if (!ALLOWED_IPS.includes(realIP)) { console.warn(`⚠️ Blocked request from unauthorized IP: ${realIP}`); return res.status(403).json({ error: 'Forbidden', message: 'IP address not whitelisted', your_ip: realIP }); } next(); } // ============================================================================ // RATE LIMITING MIDDLEWARE // ============================================================================ function rateLimitMiddleware(req, res, next) { const clientIP = req.ip || req.connection.remoteAddress; const now = Date.now(); const key = `rate_limit:${clientIP}`; // Clean up old entries (older than window) const cutoff = now - RATE_LIMIT_WINDOW_MS; for (const [k, v] of rateLimitStore.entries()) { if (v.resetAt < cutoff) { rateLimitStore.delete(k); } } // Get or create rate limit entry let rateLimit = rateLimitStore.get(key); if (!rateLimit || rateLimit.resetAt < now) { rateLimit = { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS }; } rateLimit.count++; rateLimitStore.set(key, rateLimit); // Check if limit exceeded if (rateLimit.count > RATE_LIMIT_MAX_REQUESTS) { const retryAfter = Math.ceil((rateLimit.resetAt - now) / 1000); res.set('Retry-After', retryAfter); res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS); res.set('X-RateLimit-Remaining', 0); res.set('X-RateLimit-Reset', new Date(rateLimit.resetAt).toISOString()); return res.status(429).json({ error: 'Too Many Requests', message: `Rate limit exceeded. Maximum ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 1000} seconds.`, retry_after_seconds: retryAfter, limit: RATE_LIMIT_MAX_REQUESTS, window_seconds: RATE_LIMIT_WINDOW_MS / 1000 }); } // Set rate limit headers res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS); res.set('X-RateLimit-Remaining', RATE_LIMIT_MAX_REQUESTS - rateLimit.count); res.set('X-RateLimit-Reset', new Date(rateLimit.resetAt).toISOString()); next(); } // ============================================================================ // APPLY MIDDLEWARE // ============================================================================ // CORS - restrict in production const corsOptions = NODE_ENV === 'production' ? { origin: process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : false, credentials: true } : {}; app.use(cors(corsOptions)); app.use(express.json()); app.use(rateLimitMiddleware); app.use(checkIPWhitelist); app.use(authenticateMCPRequest); // Load environment variables const envPath = join(__dirname, '..', '.env'); let BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8090'; try { const envContent = readFileSync(envPath, 'utf-8'); const env = {}; envContent.split('\n').forEach(line => { const match = line.match(/^([^=# ]+)=(.*)$/); if (match) { env[match[1]] = match[2].trim(); } }); // Prioritize env var over .env file if (!process.env.BACKEND_URL) { BACKEND_URL = env.BACKEND_URL || BACKEND_URL; } } catch (error) { console.log('No .env file found, using environment or defaults'); } // Load tool registry const toolsPath = join(__dirname, '..', 'generated-tools.json'); const { tools, categories } = JSON.parse(readFileSync(toolsPath, 'utf-8')); console.log(`📦 Loaded ${tools.length} tools across ${Object.keys(categories).length} categories`); // Helper function to make API calls async function makeRequest(url, options = {}) { try { const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', 'Host': 'localhost', ...options.headers, }, redirect: 'follow', // Follow redirects automatically }); const text = await response.text(); let data; try { data = JSON.parse(text); } catch { data = text; } return { status: response.status, ok: response.ok, data, }; } catch (error) { return { status: 0, ok: false, error: error.message, }; } } // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', server: 'Solid MCP Comprehensive Server', version: '2.0.0', tools: tools.length, categories: Object.keys(categories).length, backend: BACKEND_URL, uptime: process.uptime(), timestamp: new Date().toISOString() }); }); // Get server stats app.get('/api/stats', (req, res) => { const stats = { total_tools: tools.length, total_categories: Object.keys(categories).length, by_method: {}, by_category: Object.entries(categories).map(([name, tools]) => ({ name, count: tools.length, })).sort((a, b) => b.count - a.count), }; tools.forEach(tool => { stats.by_method[tool.method] = (stats.by_method[tool.method] || 0) + 1; }); res.json(stats); }); // List all tools app.get('/api/tools', (req, res) => { const { category, method, search } = req.query; let filteredTools = tools; if (category) { filteredTools = filteredTools.filter(t => t.category === category); } if (method) { filteredTools = filteredTools.filter(t => t.method === method); } if (search) { const searchLower = search.toLowerCase(); filteredTools = filteredTools.filter(t => t.name.toLowerCase().includes(searchLower) || t.description.toLowerCase().includes(searchLower) ); } res.json({ total: filteredTools.length, tools: filteredTools, }); }); // Get tool by name app.get('/api/tools/:name', (req, res) => { const tool = tools.find(t => t.name === req.params.name); if (!tool) { return res.status(404).json({ error: `Tool not found: ${req.params.name}`, }); } res.json(tool); }); // Call a tool app.post('/api/tools/:name/call', async (req, res) => { const toolName = req.params.name; const args = req.body || {}; const tool = tools.find(t => t.name === toolName); if (!tool) { return res.status(404).json({ error: `Tool not found: ${toolName}`, }); } try { // Build URL with path parameters let url = `${BACKEND_URL}${tool.endpoint}`; // Replace path parameters Object.entries(args).forEach(([key, value]) => { url = url.replace(`{${key}}`, value); }); // Build query string for GET requests if (tool.method === 'GET') { const queryParams = new URLSearchParams(); Object.entries(args).forEach(([key, value]) => { if (!tool.endpoint.includes(`{${key}}`) && value !== undefined) { queryParams.append(key, value); } }); const queryString = queryParams.toString(); if (queryString) { url += `?${queryString}`; } } // Make request const options = { method: tool.method, }; // Add body for POST/PUT/PATCH if (['POST', 'PUT', 'PATCH'].includes(tool.method) && args.body) { options.body = typeof args.body === 'string' ? args.body : JSON.stringify(args.body); } const result = await makeRequest(url, options); res.json({ tool: tool.name, category: tool.category, endpoint: tool.endpoint, method: tool.method, url: url, status: result.status, ok: result.ok, response: result.data, timestamp: new Date().toISOString(), }); } catch (error) { res.status(500).json({ error: error.message, tool: tool.name, stack: error.stack, }); } }); // Get categories app.get('/api/categories', (req, res) => { res.json({ total: Object.keys(categories).length, categories: Object.entries(categories).map(([name, categoryTools]) => ({ name, tool_count: categoryTools.length, sample_tools: categoryTools.slice(0, 3).map(t => t.name), })), }); }); // ============================================================================ // MULTI-TENANT MONITORING - AI-FIRST DEVOPS FOR 10K MERCHANTS // ============================================================================ // Get all companies/tenants app.get('/api/tenants', async (req, res) => { try { const result = await makeRequest(`${BACKEND_URL}/api/v1/superadmin/companies`); if (!result.ok) { return res.status(result.status).json({ error: 'Failed to fetch tenants', details: result.data, }); } const companies = result.data; res.json({ total: companies.length, tenants: companies.map(c => ({ id: c.id, name: c.name, slug: c.slug, subdomain: c.subdomain, created_at: c.created_at, })), }); } catch (error) { res.status(500).json({ error: 'Internal server error', message: error.message, }); } }); // Health check ALL tenants (AI-first DevOps) app.get('/api/tenants/health', async (req, res) => { try { // Get all companies const companiesResult = await makeRequest(`${BACKEND_URL}/api/v1/superadmin/companies`); if (!companiesResult.ok) { return res.status(companiesResult.status).json({ error: 'Failed to fetch companies', details: companiesResult.data, }); } const companies = companiesResult.data; // Check health for each tenant in parallel (limit to 10 concurrent) const batchSize = 10; const healthChecks = []; for (let i = 0; i < companies.length; i += batchSize) { const batch = companies.slice(i, i + batchSize); const batchChecks = batch.map(async (company) => { try { // Check if agents are healthy for this tenant const agentsResult = await makeRequest( `${BACKEND_URL}/api/v1/agents?company_id=${company.id}&limit=1` ); return { company_id: company.id, company_name: company.name, slug: company.slug, subdomain: company.subdomain, status: agentsResult.ok ? 'healthy' : 'degraded', agents_accessible: agentsResult.ok, checked_at: new Date().toISOString(), }; } catch (error) { return { company_id: company.id, company_name: company.name, slug: company.slug, subdomain: company.subdomain, status: 'error', agents_accessible: false, error: error.message, checked_at: new Date().toISOString(), }; } }); const batchResults = await Promise.all(batchChecks); healthChecks.push(...batchResults); } // Calculate summary stats const summary = { total_tenants: healthChecks.length, healthy: healthChecks.filter(c => c.status === 'healthy').length, degraded: healthChecks.filter(c => c.status === 'degraded').length, error: healthChecks.filter(c => c.status === 'error').length, checked_at: new Date().toISOString(), }; res.json({ summary, tenants: healthChecks, }); } catch (error) { res.status(500).json({ error: 'Health check failed', message: error.message, }); } }); // Test MCP tools for a specific tenant app.post('/api/tenants/:company_id/test', async (req, res) => { try { const { company_id } = req.params; const { tool_names } = req.body; // array of tool names to test const toolsToTest = tool_names || ['agents', 'crm.contacts', 'products.']; const results = []; for (const toolName of toolsToTest) { const tool = tools.find(t => t.name === toolName); if (!tool) { results.push({ tool: toolName, status: 'not_found', error: 'Tool not found in registry', }); continue; } try { // Build URL with company_id let url = `${BACKEND_URL}${tool.endpoint}`; if (tool.method === 'GET') { url += `?company_id=${company_id}&limit=1`; } const testResult = await makeRequest(url, { method: tool.method }); results.push({ tool: toolName, status: testResult.ok ? 'pass' : 'fail', http_status: testResult.status, response_ok: testResult.ok, }); } catch (error) { results.push({ tool: toolName, status: 'error', error: error.message, }); } } const summary = { company_id: parseInt(company_id), total_tools_tested: results.length, passed: results.filter(r => r.status === 'pass').length, failed: results.filter(r => r.status === 'fail').length, errors: results.filter(r => r.status === 'error').length, }; res.json({ summary, results, }); } catch (error) { res.status(500).json({ error: 'Tenant test failed', message: error.message, }); } }); // ============================================================================ // INFRASTRUCTURE HEALTH CHECKS - FOR DATABASES AND CACHES // ============================================================================ // Check PostgreSQL health app.get('/api/infrastructure/health/postgres', async (req, res) => { try { const result = await makeRequest(`${BACKEND_URL}/api/v1/_health`); if (result.ok) { res.json({ status: 'healthy', service: 'postgres' }); } else { res.status(503).json({ status: 'unhealthy', service: 'postgres' }); } } catch (error) { res.status(503).json({ status: 'unhealthy', service: 'postgres', error: error.message }); } }); // Check Redis health app.get('/api/infrastructure/health/redis', async (req, res) => { try { const result = await makeRequest(`${BACKEND_URL}/api/v1/_health`); if (result.ok) { res.json({ status: 'healthy', service: 'redis' }); } else { res.status(503).json({ status: 'unhealthy', service: 'redis' }); } } catch (error) { res.status(503).json({ status: 'unhealthy', service: 'redis', error: error.message }); } }); // ============================================================================ // ERROR LOGGING AND MANAGEMENT - AI-FIRST DEVOPS // ============================================================================ // In-memory error storage (in production, this would be a database) const errors = []; let errorIdCounter = 1; // Log a new error app.post('/api/errors/log', (req, res) => { const { service, severity = 'error', error_type, message, details = {}, stack_trace = null, company_id = null } = req.body; const error = { id: `error-${errorIdCounter++}`, timestamp: new Date().toISOString(), service, severity, error_type, message, details, stack_trace, company_id, resolved: false, resolution_notes: null, auto_fix_attempted: false, auto_fix_successful: false }; errors.unshift(error); console.log(`[ERROR LOG] ${severity.toUpperCase()} - ${service}: ${message}`); res.json({ success: true, error_id: error.id, message: 'Error logged successfully' }); }); // List errors with filtering app.get('/api/errors', (req, res) => { const { severity, resolved, service, limit = 50 } = req.query; let filteredErrors = [...errors]; if (severity) { filteredErrors = filteredErrors.filter(e => e.severity === severity); } if (resolved !== undefined) { const resolvedBool = resolved === 'true'; filteredErrors = filteredErrors.filter(e => e.resolved === resolvedBool); } if (service) { filteredErrors = filteredErrors.filter(e => e.service === service); } const limitNum = parseInt(limit); const result = filteredErrors.slice(0, limitNum); res.json({ total: filteredErrors.length, returned: result.length, errors: result }); }); // Get error by ID app.get('/api/errors/:id', (req, res) => { const error = errors.find(e => e.id === req.params.id); if (!error) { return res.status(404).json({ error: 'Error not found', id: req.params.id }); } res.json(error); }); // Attempt to auto-fix an error app.post('/api/errors/:id/fix', async (req, res) => { const error = errors.find(e => e.id === req.params.id); if (!error) { return res.status(404).json({ error: 'Error not found', id: req.params.id }); } error.auto_fix_attempted = true; // Auto-fix logic based on error type let fixSuccess = false; let fixNotes = ''; try { switch (error.error_type) { case 'health_check_failed': // Re-test the service if (error.details.service_name) { const testResult = await makeRequest(`${BACKEND_URL}/api/v1/_health`); if (testResult.ok) { fixSuccess = true; fixNotes = 'Service is now responding correctly'; } else { fixNotes = 'Service still not responding'; } } break; case 'connection_failed': fixNotes = 'Connection errors require manual investigation'; break; default: fixNotes = 'No automatic fix available for this error type'; } } catch (err) { fixNotes = `Fix attempt failed: ${err.message}`; } error.auto_fix_successful = fixSuccess; if (fixSuccess) { error.resolved = true; error.resolution_notes = `Auto-fixed: ${fixNotes}`; } res.json({ success: true, error_id: error.id, fix_attempted: true, fix_successful: fixSuccess, notes: fixNotes, error: error }); }); // Mark error as resolved app.post('/api/errors/:id/resolve', (req, res) => { const error = errors.find(e => e.id === req.params.id); if (!error) { return res.status(404).json({ error: 'Error not found', id: req.params.id }); } error.resolved = true; error.resolution_notes = req.body.resolution_notes || 'Manually resolved'; res.json({ success: true, error_id: error.id, message: 'Error marked as resolved' }); }); // Get error statistics app.get('/api/errors/stats/summary', (req, res) => { const stats = { total: errors.length, by_severity: { critical: errors.filter(e => e.severity === 'critical').length, error: errors.filter(e => e.severity === 'error').length, warning: errors.filter(e => e.severity === 'warning').length, info: errors.filter(e => e.severity === 'info').length }, resolved: errors.filter(e => e.resolved).length, unresolved: errors.filter(e => !e.resolved).length, auto_fixed: errors.filter(e => e.auto_fix_successful).length, by_service: {} }; // Count errors by service errors.forEach(error => { if (!stats.by_service[error.service]) { stats.by_service[error.service] = 0; } stats.by_service[error.service]++; }); res.json(stats); }); // ============================================================================ // MCP ADMIN ENDPOINTS - AI-FIRST DEVOPS DASHBOARD // ============================================================================ // Get system mapping - shows health of all companies and their MCP tools app.get('/api/mcp/admin/mapping/system', async (req, res) => { try { // Get all companies const companiesResult = await makeRequest(`${BACKEND_URL}/api/v1/superadmin/companies`); if (!companiesResult.ok) { return res.status(companiesResult.status).json({ error: 'Failed to fetch companies', details: companiesResult.data, }); } const companies = companiesResult.data; // Check health for each company (batch processing) const batchSize = 10; const companyHealthData = []; for (let i = 0; i < companies.length; i += batchSize) { const batch = companies.slice(i, i + batchSize); const batchChecks = batch.map(async (company) => { try { // Test critical endpoints for this company const criticalEndpoints = [ { name: 'agents', method: 'GET', url: `${BACKEND_URL}/api/v1/agents?company_id=${company.id}&limit=1` }, { name: 'products', method: 'GET', url: `${BACKEND_URL}/api/v1/products?company_id=${company.id}&limit=1` }, { name: 'orders', method: 'GET', url: `${BACKEND_URL}/api/v1/crm/ecommerce/orders?company_id=${company.id}&limit=1` }, { name: 'customers', method: 'GET', url: `${BACKEND_URL}/api/v1/crm/contacts?company_id=${company.id}&limit=1` }, { name: 'analytics', method: 'GET', url: `${BACKEND_URL}/api/v1/analytics/health?company_id=${company.id}` }, ]; // Check all critical endpoints const endpointChecks = await Promise.all( criticalEndpoints.map(async (endpoint) => { const options = { method: endpoint.method }; if (endpoint.body) { options.body = JSON.stringify(endpoint.body); } const result = await makeRequest(endpoint.url, options); return { name: endpoint.name, healthy: result.ok }; }) ); // Calculate health based on critical endpoints const healthyCount = endpointChecks.filter(e => e.healthy).length; const totalChecked = endpointChecks.length; const healthPercentage = (healthyCount / totalChecked) * 100; return { company_id: company.id, company_name: company.name, tools_count: tools.length, healthy_count: healthyCount, degraded_count: totalChecked - healthyCount, unhealthy_count: totalChecked - healthyCount, health_percentage: healthPercentage, }; } catch (error) { return { company_id: company.id, company_name: company.name, tools_count: tools.length, healthy_count: 0, degraded_count: 0, unhealthy_count: 5, health_percentage: 0, }; } }); const batchResults = await Promise.all(batchChecks); companyHealthData.push(...batchResults); } // Calculate platform-wide health const totalHealthy = companyHealthData.filter(c => c.health_percentage >= 80).length; const totalDegraded = companyHealthData.filter(c => c.health_percentage >= 50 && c.health_percentage < 80).length; const totalUnhealthy = companyHealthData.filter(c => c.health_percentage < 50).length; let platformStatus = 'healthy'; if (totalUnhealthy > companies.length * 0.1) platformStatus = 'unhealthy'; else if (totalDegraded > companies.length * 0.2) platformStatus = 'degraded'; // Get tool categories health const toolCategories = {}; Object.entries(categories).forEach(([name, categoryTools]) => { toolCategories[name] = { total: categoryTools.length, healthy: categoryTools.length, degraded: 0, unhealthy: 0, }; }); // Get AI agents status from first company (for now, will aggregate across all companies later) let aiAgents = []; if (companies.length > 0) { const agentsResult = await makeRequest(`${BACKEND_URL}/api/v1/agents?company_id=${companies[0].id}`); if (agentsResult.ok && agentsResult.data.agents) { aiAgents = agentsResult.data.agents.map(agent => ({ name: agent.name, status: agent.status, active_sessions: agent.stats?.total_actions || 0, last_activity: agent.last_active_at || agent.updated_at, })); } } // Fallback to mock data if no agents found if (aiAgents.length === 0) { aiAgents = [ { name: 'ADA (Orchestrator)', status: 'active', active_sessions: 0, last_activity: new Date().toISOString(), }, ]; } res.json({ system_map: { platform_health: { total_companies: companies.length, healthy_companies: totalHealthy, degraded_companies: totalDegraded, unhealthy_companies: totalUnhealthy, total_tools: tools.length, healthy_tools: tools.length, platform_status: platformStatus, }, companies: companyHealthData, tool_categories: toolCategories, ai_agents: aiAgents, total_mcp_tools: tools.length, last_updated: new Date().toISOString(), }, }); } catch (error) { res.status(500).json({ error: 'Failed to generate system map', message: error.message, }); } }); // Get critical alerts app.get('/api/mcp/admin/alerts/critical', async (req, res) => { try { const limit = parseInt(req.query.limit) || 20; // Get unhealthy companies const companiesResult = await makeRequest(`${BACKEND_URL}/api/v1/superadmin/companies`); if (!companiesResult.ok) { return res.json({ critical_alerts: [], }); } const companies = companiesResult.data; const alerts = []; // Check a sample of companies for issues const samplesToCheck = Math.min(5, companies.length); for (let i = 0; i < samplesToCheck; i++) { const company = companies[i]; try { const agentsResult = await makeRequest( `${BACKEND_URL}/api/v1/agents?company_id=${company.id}&limit=1` ); if (!agentsResult.ok) { alerts.push({ alert_type: 'service_degraded', severity: 'warning', company_id: company.id, company_name: company.name, tool_name: 'agents', message: `Agent service not responding for ${company.name}`, detected_at: new Date().toISOString(), }); } } catch (error) { alerts.push({ alert_type: 'service_error', severity: 'critical', company_id: company.id, company_name: company.name, message: `Critical error checking ${company.name}: ${error.message}`, detected_at: new Date().toISOString(), }); } } res.json({ critical_alerts: alerts.slice(0, limit), }); } catch (error) { res.status(500).json({ error: 'Failed to fetch critical alerts', message: error.message, }); } }); // Get real-time health stream app.get('/api/mcp/admin/stream/health', async (req, res) => { try { const limit = parseInt(req.query.limit) || 50; // Get recent companies and their health const companiesResult = await makeRequest(`${BACKEND_URL}/api/v1/superadmin/companies`); if (!companiesResult.ok) { return res.json({ recent_events: [], }); } const companies = companiesResult.data; const events = []; // Generate recent health events for a sample of companies const samplesToCheck = Math.min(10, companies.length); for (let i = 0; i < samplesToCheck; i++) { const company = companies[i]; try { const agentsResult = await makeRequest( `${BACKEND_URL}/api/v1/agents?company_id=${company.id}&limit=1` ); events.push({ company_id: company.id, company_name: company.name, service: 'agents', status: agentsResult.ok ? 'healthy' : 'degraded', timestamp: new Date().toISOString(), indicator: agentsResult.ok ? '✅' : '⚠️', }); } catch (error) { events.push({ company_id: company.id, company_name: company.name, service: 'agents', status: 'unhealthy', timestamp: new Date().toISOString(), indicator: '❌', }); } } res.json({ recent_events: events.slice(0, limit), }); } catch (error) { res.status(500).json({ error: 'Failed to fetch health stream', message: error.message, }); } }); // Error handler app.use((err, req, res, next) => { console.error('Error:', err); res.status(500).json({ error: 'Internal server error', message: err.message, }); }); // Start server app.listen(PORT, '0.0.0.0', () => { console.log(` ╔════════════════════════════════════════════════════════════╗ ║ 🔧 Solid MCP Comprehensive Server - HTTP Mode ║ ╠════════════════════════════════════════════════════════════╣ ║ Status: Running ║ ║ Port: ${PORT} ║ ║ Tools: ${tools.length} tools ║ ║ Categories: ${Object.keys(categories).length} categories ║ ║ Backend: ${BACKEND_URL} ║ ╠════════════════════════════════════════════════════════════╣ ║ Endpoints: ║ ║ GET /health - Health check ║ ║ GET /api/services/health/all - All services health ║ ║ GET /api/stats - Server statistics ║ ║ GET /api/tools - List all tools ║ ║ GET /api/tools/:name - Get tool details ║ ║ POST /api/tools/:name/call - Call a tool ║ ║ GET /api/categories - List categories ║ ╠════════════════════════════════════════════════════════════╣ ║ Admin Dashboard: ║ ║ http://localhost:8080/admin/mcp-server ║ ╚════════════════════════════════════════════════════════════╝ `); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('Received SIGTERM, shutting down gracefully...'); process.exit(0); }); process.on('SIGINT', () => { console.log('Received SIGINT, shutting down gracefully...'); process.exit(0); }); // ============================================================================ // ALL SERVICES HEALTH CHECK - FOR SUPER ADMIN DASHBOARD // ============================================================================ // Check ALL platform services (Agent Engine, Frontend, Backend, etc.) app.get('/api/services/health/all', async (req, res) => { try { const services = [ { name: 'Backend API', url: `${BACKEND_URL}/api/v1/_health`, type: 'api' }, { name: 'Agent Engine', url: 'http://host.docker.internal:8091/health', type: 'service' }, { name: 'Frontend', url: 'http://host.docker.internal:3000', type: 'service' }, { name: 'Public CMS', url: 'http://host.docker.internal:3001', type: 'service' }, { name: 'Super Admin', url: 'http://host.docker.internal:8080', type: 'service' }, { name: 'MCP Comprehensive Server', url: `http://localhost:${PORT}/health`, type: 'service' }, { name: 'PostgreSQL', url: `http://localhost:${PORT}/api/infrastructure/health/postgres`, type: 'database' }, { name: 'Redis', url: `http://localhost:${PORT}/api/infrastructure/health/redis`, type: 'cache' } ]; const healthChecks = await Promise.all( services.map(async (service) => { try { const result = await makeRequest(service.url); return { name: service.name, type: service.type, status: result.ok ? 'connected' : 'disconnected', http_status: result.status, checked_at: new Date().toISOString() }; } catch (error) { return { name: service.name, type: service.type, status: 'disconnected', error: error.message, checked_at: new Date().toISOString() }; } }) ); const connectedCount = healthChecks.filter(s => s.status === 'connected').length; const totalCount = healthChecks.length; res.json({ summary: { total: totalCount, connected: connectedCount, disconnected: totalCount - connectedCount, health_percentage: Math.round((connectedCount / totalCount) * 100) }, services: healthChecks }); } catch (error) { res.status(500).json({ error: 'Failed to check services health', message: error.message }); } });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Adam-Camp-King/solid-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server