index.js•22.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);
});