Skip to main content
Glama

Solid Multi-Tenant DevOps MCP Server

index.js22.6 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import fetch from 'node-fetch'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { readFileSync } from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Load environment variables from .env file const envPath = join(__dirname, '..', '.env'); 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(); } }); const BACKEND_URL = env.BACKEND_URL || 'http://localhost:8090'; const SUPER_ADMIN_URL = env.SUPER_ADMIN_URL || 'http://localhost:8080'; const AGENTS_URL = env.AGENTS_URL || 'http://localhost:8091'; // Helper function to make API calls async function makeRequest(url, options = {}) { try { const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, }); 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, }; } } // Create server instance const server = new Server( { name: 'solid-devops-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // ========== TENANT MANAGEMENT ========== { name: 'list_all_tenants', description: 'List all tenant companies in the system with stats (thousands of customers)', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max number of tenants to return (default: 100)', default: 100, }, offset: { type: 'number', description: 'Offset for pagination', default: 0, }, }, }, }, { name: 'get_tenant_details', description: 'Get detailed information about a specific tenant/company', inputSchema: { type: 'object', properties: { company_id: { type: 'number', description: 'The company/tenant ID', }, }, required: ['company_id'], }, }, { name: 'get_platform_analytics', description: 'Get platform-wide analytics across all tenants', inputSchema: { type: 'object', properties: {}, }, }, // ========== SYSTEM HEALTH ========== { name: 'check_all_services_health', description: 'Check health status of all Solid services (Backend, Agents, Docker)', inputSchema: { type: 'object', properties: {}, }, }, { name: 'check_docker_containers', description: 'Check status of all Docker containers (PostgreSQL, Redis, Backend)', inputSchema: { type: 'object', properties: {}, }, }, // ========== TENANT-SPECIFIC MONITORING ========== { name: 'get_tenant_agents', description: 'Get all AI agents for a specific tenant with their status', inputSchema: { type: 'object', properties: { company_id: { type: 'number', description: 'The company/tenant ID', }, }, required: ['company_id'], }, }, { name: 'get_tenant_agent_messages', description: 'Get inter-agent communication messages for a tenant', inputSchema: { type: 'object', properties: { company_id: { type: 'number', description: 'The company/tenant ID', }, limit: { type: 'number', description: 'Max messages to return', default: 50, }, }, required: ['company_id'], }, }, { name: 'get_tenant_products', description: 'Get products/inventory for a specific tenant', inputSchema: { type: 'object', properties: { company_id: { type: 'number', description: 'The company/tenant ID', }, limit: { type: 'number', description: 'Max products to return', default: 50, }, }, required: ['company_id'], }, }, { name: 'get_tenant_token_usage', description: 'Get AI token usage and billing for a specific tenant', inputSchema: { type: 'object', properties: { company_id: { type: 'number', description: 'The company/tenant ID', }, }, required: ['company_id'], }, }, // ========== ERROR MONITORING ========== { name: 'list_recent_errors', description: 'List recent errors across all tenants or specific tenant', inputSchema: { type: 'object', properties: { company_id: { type: 'number', description: 'Optional: Filter by specific tenant', }, severity: { type: 'string', enum: ['critical', 'error', 'warning', 'info'], description: 'Filter by severity level', }, limit: { type: 'number', description: 'Max errors to return', default: 50, }, }, }, }, { name: 'get_error_details', description: 'Get detailed information about a specific error', inputSchema: { type: 'object', properties: { error_id: { type: 'number', description: 'The error ID', }, }, required: ['error_id'], }, }, { name: 'resolve_error', description: 'Mark an error as resolved', inputSchema: { type: 'object', properties: { error_id: { type: 'number', description: 'The error ID', }, resolution_notes: { type: 'string', description: 'Notes about how the error was resolved', }, }, required: ['error_id'], }, }, // ========== PERFORMANCE MONITORING ========== { name: 'get_performance_summary', description: 'Get overall system performance metrics', inputSchema: { type: 'object', properties: { company_id: { type: 'number', description: 'Optional: Filter by specific tenant', }, }, }, }, { name: 'get_slow_endpoints', description: 'Find slow API endpoints that need optimization', inputSchema: { type: 'object', properties: { threshold_ms: { type: 'number', description: 'Response time threshold in milliseconds (default: 1000)', default: 1000, }, limit: { type: 'number', description: 'Max endpoints to return', default: 20, }, }, }, }, // ========== TENANT PROVISIONING ========== { name: 'provision_new_tenant', description: 'Provision a new tenant/company in the system', inputSchema: { type: 'object', properties: { company_name: { type: 'string', description: 'Company name', }, owner_email: { type: 'string', description: 'Owner email address', }, plan: { type: 'string', enum: ['starter', 'professional', 'enterprise'], description: 'Subscription plan', default: 'starter', }, demo_mode: { type: 'boolean', description: 'Enable demo mode with sample data', default: false, }, }, required: ['company_name', 'owner_email'], }, }, // ========== BULK OPERATIONS ========== { name: 'bulk_check_tenant_health', description: 'Check health of multiple tenants at once', inputSchema: { type: 'object', properties: { company_ids: { type: 'array', items: { type: 'number', }, description: 'Array of company IDs to check', }, }, required: ['company_ids'], }, }, { name: 'search_tenants', description: 'Search for tenants by name, email, or other criteria', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (company name, email, etc.)', }, limit: { type: 'number', description: 'Max results to return', default: 50, }, }, required: ['query'], }, }, // ========== DATABASE OPERATIONS ========== { name: 'execute_raw_query', description: 'Execute a raw SQL query (READ-ONLY for safety)', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'SQL query to execute (must be SELECT)', }, }, required: ['query'], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { // ========== TENANT MANAGEMENT ========== case 'list_all_tenants': { const { limit = 100, offset = 0 } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/superadmin/companies?limit=${limit}&offset=${offset}` ); return { content: [ { type: 'text', text: JSON.stringify({ total_tenants: Array.isArray(result.data) ? result.data.length : 0, tenants: result.data, }, null, 2), }, ], }; } case 'get_tenant_details': { const { company_id } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/superadmin/companies/${company_id}` ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } case 'get_platform_analytics': { const result = await makeRequest( `${BACKEND_URL}/api/v1/superadmin/analytics` ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } // ========== SYSTEM HEALTH ========== case 'check_all_services_health': { const services = [ { name: 'Backend', url: BACKEND_URL, healthPath: '/' }, { name: 'Agents', url: AGENTS_URL, healthPath: '/health' }, ]; const results = await Promise.all( services.map(async (service) => { const result = await makeRequest(`${service.url}${service.healthPath}`); return { service: service.name, url: service.url, status: result.ok ? 'healthy' : 'unhealthy', statusCode: result.status, response: result.data, }; }) ); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } case 'check_docker_containers': { const containers = ['solid-postgres', 'solid-redis', 'solid-backend']; const results = []; for (const container of containers) { try { const { execSync } = await import('child_process'); const output = execSync( `docker inspect ${container} --format='{{.State.Status}} {{.State.Health.Status}}' 2>&1 || echo "not_found"`, { encoding: 'utf-8' } ); results.push({ container, status: output.trim(), }); } catch (error) { results.push({ container, status: 'error', error: error.message, }); } } return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } // ========== TENANT-SPECIFIC MONITORING ========== case 'get_tenant_agents': { const { company_id } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/agents?company_id=${company_id}` ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } case 'get_tenant_agent_messages': { const { company_id, limit = 50 } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/agents/messages?company_id=${company_id}&limit=${limit}` ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } case 'get_tenant_products': { const { company_id, limit = 50 } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/products?company_id=${company_id}&limit=${limit}` ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } case 'get_tenant_token_usage': { const { company_id } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/tokens/analytics?company_id=${company_id}&period=30d` ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } // ========== ERROR MONITORING ========== case 'list_recent_errors': { const { company_id, severity, limit = 50 } = args; let url = `${BACKEND_URL}/api/v1/superadmin/errors?limit=${limit}`; if (company_id) url += `&company_id=${company_id}`; if (severity) url += `&severity=${severity}`; const result = await makeRequest(url); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } case 'get_error_details': { const { error_id } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/superadmin/errors/${error_id}` ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } case 'resolve_error': { const { error_id, resolution_notes } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/superadmin/errors/${error_id}/resolve`, { method: 'POST', body: JSON.stringify({ resolution_notes }), } ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } // ========== PERFORMANCE MONITORING ========== case 'get_performance_summary': { const { company_id } = args; let url = `${BACKEND_URL}/api/v1/performance/summary`; if (company_id) url += `?company_id=${company_id}`; const result = await makeRequest(url); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } case 'get_slow_endpoints': { const { threshold_ms = 1000, limit = 20 } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/performance/analyze/endpoints?threshold=${threshold_ms}&limit=${limit}` ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } // ========== TENANT PROVISIONING ========== case 'provision_new_tenant': { const { company_name, owner_email, plan = 'starter', demo_mode = false } = args; const result = await makeRequest( `${BACKEND_URL}/api/v1/superadmin/provision`, { method: 'POST', body: JSON.stringify({ company_name, owner_email, plan, demo_mode, }), } ); return { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }; } // ========== BULK OPERATIONS ========== case 'bulk_check_tenant_health': { const { company_ids } = args; const results = await Promise.all( company_ids.map(async (company_id) => { const [agentsRes, productsRes, errorsRes] = await Promise.all([ makeRequest(`${BACKEND_URL}/api/v1/agents?company_id=${company_id}`), makeRequest(`${BACKEND_URL}/api/v1/products?company_id=${company_id}&limit=1`), makeRequest(`${BACKEND_URL}/api/v1/superadmin/errors?company_id=${company_id}&limit=5`), ]); const agents = Array.isArray(agentsRes.data) ? agentsRes.data : []; const activeAgents = agents.filter(a => a.status === 'active').length; const errors = Array.isArray(errorsRes.data) ? errorsRes.data.length : 0; return { company_id, health_status: errors > 10 ? 'unhealthy' : errors > 3 ? 'warning' : 'healthy', active_agents: activeAgents, total_agents: agents.length, recent_errors: errors, has_products: productsRes.ok, }; }) ); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } case 'search_tenants': { const { query, limit = 50 } = args; // Get all tenants and filter client-side for now const result = await makeRequest( `${BACKEND_URL}/api/v1/superadmin/companies?limit=${limit * 2}` ); if (Array.isArray(result.data)) { const filtered = result.data.filter(company => company.name?.toLowerCase().includes(query.toLowerCase()) || company.slug?.toLowerCase().includes(query.toLowerCase()) || company.id?.toString() === query ).slice(0, limit); return { content: [ { type: 'text', text: JSON.stringify({ query, results_count: filtered.length, results: filtered, }, null, 2), }, ], }; } return { content: [ { type: 'text', text: JSON.stringify({ error: 'Failed to search tenants', result: result.data }, null, 2), }, ], }; } // ========== DATABASE OPERATIONS ========== case 'execute_raw_query': { const { query } = args; // Safety check: only allow SELECT queries if (!query.trim().toLowerCase().startsWith('select')) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Only SELECT queries are allowed for safety', }, null, 2), }, ], isError: true, }; } // This would need a dedicated endpoint in your backend return { content: [ { type: 'text', text: JSON.stringify({ error: 'Raw query execution not yet implemented in backend API', suggestion: 'Use specific API endpoints instead', }, null, 2), }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error.message}\nStack: ${error.stack}`, }, ], isError: true, }; } }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Solid Multi-Tenant DevOps MCP Server running on stdio'); console.error(`Connected to: ${BACKEND_URL}`); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

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