comprehensive-http-server.js•36.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
});
}
});