Skip to main content
Glama
index.ts14.2 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import express from "express"; import { CommandRunner } from "./utils/commandRunner.js"; import { logger } from "./utils/logger.js"; // Service imports import { SQLitePlannerService } from "./services/sqlitePlanner.js"; import { StructuredThinkingService } from "./services/structuredThinking.js"; import { PostgresManagerService } from "./services/postgresManager.js"; import { RedisManagerService } from "./services/redisManager.js"; import { ServerAdminService } from "./services/serverAdmin.js"; import { DatabaseDiagnosticsService } from "./services/databaseDiagnostics.js"; import { KeycloakManagerService } from "./services/keycloakManager.js"; import { NginxMonitoringService } from "./services/nginxMonitoring.js"; import { SystemMetricsService } from "./services/systemMetrics.js"; import { AutoDiscoveryService } from "./services/autoDiscovery.js"; import { KeycloakAuthService } from "./services/keycloakAuth.js"; import { CommandQueueService } from "./services/commandQueue.js"; import { DatabaseSyncService } from "./services/databaseSync.js"; import { HealthCheckService } from "./services/healthCheck.js"; import { AutomatedMaintenanceService } from "./services/automatedMaintenance.js"; import { ReportingHubService } from "./services/reportingHub.js"; import { CAPABILITIES } from "./config/capabilities.js"; // Tool registration import { registerTools } from "./tools/registerTools.js"; const instructions = ` This Model Context Protocol server exposes on-server management utilities for Ubuntu infrastructure hosting PostgreSQL, Redis, Keycloak, and NGINX. SERVER-MCP runs directly on the target Ubuntu server and provides: - Direct PostgreSQL management (via pg client) - connections, replication, bloat, slow queries, vacuum, reindex, backup - Direct Redis management (via ioredis client) - info, memory, keyspace, slow log, clients, BGSAVE - Keycloak Admin API - realm stats, sessions, clients, events, users, secret rotation - NGINX monitoring - access log stats, error log analysis, config testing, reload - System metrics - uptime, load, memory, disk, processes, network, journal errors - Database diagnostics - PostgreSQL, Redis, NGINX, Keycloak, firewall, system health checks - Ubuntu server administration - packages, services, Docker, PM2, networking, security Recommended usage: - Run with elevated privileges when invoking tools that require system access. Many commands automatically add \`sudo\`. - All operations execute locally (no SSH) - this server runs on the target infrastructure. - PostgreSQL and Redis connections use direct database clients (not shell commands) for better performance. - Keycloak operations require valid KEYCLOAK_CLIENT_ID and KEYCLOAK_CLIENT_SECRET environment variables. - NGINX operations default to standard log locations but can be overridden with parameters. Environment variables: - SERVER_MCP_ALLOW_SUDO=true|false (default: true) — disable automatic sudo prefixing - POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD — PostgreSQL connection - REDIS_HOST, REDIS_PORT, REDIS_PASSWORD — Redis connection - KEYCLOAK_BASE_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET — Keycloak Admin API - NGINX_ACCESS_LOG, NGINX_ERROR_LOG, NGINX_CONFIG_PATH — NGINX file paths - SERVER_MCP_LOG_LEVEL — Winston log level (debug, info, warn, error) Integration: - Designed to work with IT-MCP's distributed agent architecture - Registers with central agent registry and processes remote commands - Syncs structured thoughts to central PostgreSQL database - Sends heartbeat to Redis for distributed coordination `.trim(); const createServer = () => { // Validate required environment variables const requiredEnvVars = [ "POSTGRES_HOST", "POSTGRES_USER", "POSTGRES_PASSWORD", ]; const missingVars = requiredEnvVars.filter(varName => !process.env[varName]); if (missingVars.length > 0) { logger.error("Missing required environment variables", { missing: missingVars, hint: "Please set these variables in /etc/server-mcp/server-mcp.env or your environment", }); throw new Error(`Missing required environment variables: ${missingVars.join(", ")}`); } // Validate optional but recommended variables const recommendedVars = ["REDIS_HOST", "KEYCLOAK_BASE_URL"]; const missingRecommended = recommendedVars.filter(varName => !process.env[varName]); if (missingRecommended.length > 0) { logger.warn("Missing recommended environment variables - some features may be unavailable", { missing: missingRecommended, }); } const server = new McpServer( { name: "server-mcp", version: "1.0.0", }, { capabilities: { tools: {}, logging: {}, }, instructions, }, ); // Initialize CommandRunner const allowSudo = process.env.SERVER_MCP_ALLOW_SUDO !== "false"; const runner = new CommandRunner(allowSudo); // Initialize SQLite and structured thinking const sqlitePath = process.env.SERVER_MCP_SQLITE_PATH ?? "./mcp_cache.db"; const planner = new SQLitePlannerService(sqlitePath); const structuredThinking = new StructuredThinkingService(planner); // Bootstrap markdown resources asynchronously void structuredThinking.bootstrapFromWorkspace().catch(() => undefined); // Initialize database management services with proper error handling let postgresManager: PostgresManagerService | null = null; try { postgresManager = new PostgresManagerService(runner); logger.info("PostgresManagerService initialized"); } catch (error) { logger.warn("PostgresManagerService initialization failed - PostgreSQL operations will be unavailable", { error }); } let redisManager: RedisManagerService | null = null; try { redisManager = new RedisManagerService(); logger.info("RedisManagerService initialized"); } catch (error) { logger.warn("RedisManagerService initialization failed - Redis operations will be unavailable", { error }); } let keycloakManager: KeycloakManagerService | null = null; try { keycloakManager = new KeycloakManagerService(); logger.info("KeycloakManagerService initialized"); } catch (error) { logger.warn("KeycloakManagerService initialization failed - Keycloak operations will be unavailable", { error }); } // Initialize server administration and monitoring services const services = { postgresManager, redisManager, serverAdmin: new ServerAdminService(runner), databaseDiagnostics: new DatabaseDiagnosticsService(runner), keycloakManager, nginxMonitoring: new NginxMonitoringService(runner), systemMetrics: new SystemMetricsService(runner), structuredThinking, reportingHub: new ReportingHubService(structuredThinking), }; // Initialize agent coordination services (optional) let autoDiscovery: AutoDiscoveryService | undefined; let keycloakAuth: KeycloakAuthService | undefined; let commandQueue: CommandQueueService | undefined; let databaseSync: DatabaseSyncService | undefined; // Get all tool names for capability advertisement const toolNames = [ "database-diagnostics", "postgres-manage", "redis-manage", "keycloak-manage", "nginx-monitor", "system-metrics", "structured-thinking", ]; // Keycloak authentication (optional - for API access and agent registry) // Initialize FIRST so AutoDiscoveryService can use it const keycloakServerUrl = process.env.KEYCLOAK_BASE_URL; const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID; const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET; if (keycloakServerUrl && keycloakClientId && keycloakClientSecret) { try { const keycloakConfig = { serverUrl: keycloakServerUrl, realm: process.env.KEYCLOAK_REALM ?? "master", clientId: keycloakClientId, clientSecret: keycloakClientSecret, }; keycloakAuth = new KeycloakAuthService(keycloakConfig); void keycloakAuth.authenticate().then(() => { logger.info("Keycloak authentication successful"); }).catch((error) => { logger.warn("Keycloak authentication failed", { error }); }); } catch (error) { logger.warn("KeycloakAuthService initialization failed", { error }); } } // Auto-discovery and registration (optional - requires IT_MCP_REGISTRY_URL) // Initialized AFTER Keycloak so JWT tokens can be used const registryUrl = process.env.IT_MCP_REGISTRY_URL; if (registryUrl) { try { const capabilities = CAPABILITIES.map(c => c.name); // Pass keycloakAuth for JWT authentication (if available) autoDiscovery = new AutoDiscoveryService(capabilities, toolNames, registryUrl, keycloakAuth); // Start heartbeat in background void autoDiscovery.register().then(() => { autoDiscovery?.startHeartbeat(); logger.info("Agent auto-discovery registered and heartbeat started"); }).catch((error) => { logger.warn("Agent registration failed - continuing without registry", { error }); }); } catch (error) { logger.warn("AutoDiscoveryService initialization failed", { error }); } } else { logger.info("IT_MCP_REGISTRY_URL not set - running in standalone mode"); } // Command queue (local SQLite queue for distributed work) const queuePath = process.env.SERVER_MCP_QUEUE_PATH ?? "./mcp_command_queue.db"; try { commandQueue = new CommandQueueService(queuePath); logger.info("CommandQueueService initialized", { queuePath }); } catch (error) { logger.warn("CommandQueueService initialization failed", { error }); } // Database sync (SQLite ↔ PostgreSQL for structured thoughts) const postgresConnectionString = process.env.POSTGRES_CONNECTION_STRING; if (postgresConnectionString) { try { databaseSync = new DatabaseSyncService(planner, { postgresConnectionString, syncIntervalMs: 60000, enableAutoSync: true, }); logger.info("DatabaseSyncService initialized and auto-sync started"); } catch (error) { logger.warn("DatabaseSyncService initialization failed", { error }); } } // Automated maintenance service (optional) let automatedMaintenance: AutomatedMaintenanceService | undefined; const enableMaintenance = process.env.AUTO_MAINTENANCE_ENABLED !== "false"; if (enableMaintenance) { try { automatedMaintenance = new AutomatedMaintenanceService(postgresManager, redisManager, runner); automatedMaintenance.start(); logger.info("AutomatedMaintenanceService started"); } catch (error) { logger.warn("AutomatedMaintenanceService initialization failed", { error }); } } // Health check service const healthCheck = new HealthCheckService(postgresManager, redisManager, keycloakManager); // Start health check HTTP server const healthPort = parseInt(process.env.HEALTH_CHECK_PORT ?? "9090", 10); const app = express(); app.get("/health", async (_req, res) => { try { const health = await healthCheck.getHealth(); const statusCode = health.status === "healthy" ? 200 : health.status === "degraded" ? 200 : 503; res.status(statusCode).json(health); } catch (error) { logger.error("Health check failed", { error }); res.status(503).json({ status: "unhealthy", error: error instanceof Error ? error.message : String(error), }); } }); app.get("/", (_req, res) => { res.json({ name: "SERVER-MCP", version: "1.0.0", endpoints: { health: "/health", }, }); }); const httpServer = app.listen(healthPort, () => { logger.info(`Health check endpoint available at http://localhost:${healthPort}/health`); }); // Register all tools const toolDependencies = { ...services, databaseSync, }; registerTools(server, toolDependencies); logger.info("SERVER-MCP initialized successfully", { allowSudo, sqlitePath, servicesCount: Object.keys(services).length, agentCoordination: { autoDiscovery: !!autoDiscovery, keycloakAuth: !!keycloakAuth, commandQueue: !!commandQueue, databaseSync: !!databaseSync, automatedMaintenance: !!automatedMaintenance, }, }); return { server, httpServer, cleanup: async () => { // Close HTTP server await new Promise<void>((resolve) => { httpServer.close(() => { logger.info("Health check HTTP server closed"); resolve(); }); }); // Cleanup coordination services if (automatedMaintenance) { automatedMaintenance.destroy(); } if (autoDiscovery) { await autoDiscovery.destroy(); } if (keycloakAuth) { await keycloakAuth.destroy(); } if (commandQueue) { commandQueue.close(); } if (databaseSync) { await databaseSync.destroy(); } // Close database connections if (postgresManager) { await postgresManager.close(); } if (redisManager) { await redisManager.close(); } // Note: SQLitePlannerService doesn't have a close() method // The database will be closed automatically when the process exits logger.info("All services cleaned up"); }, }; }; const main = async () => { logger.info("Starting SERVER-MCP server"); const { server, cleanup } = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); logger.info("SERVER-MCP server running on stdio"); // Handle graceful shutdown const shutdown = async (signal: string) => { logger.info(`Received ${signal}, shutting down gracefully`); await server.close(); await cleanup(); logger.info("Shutdown complete"); process.exit(0); }; process.on("SIGINT", () => void shutdown("SIGINT")); process.on("SIGTERM", () => void shutdown("SIGTERM")); }; main().catch((error) => { logger.error("Fatal error in main", { error }); process.exit(1); });

Latest Blog Posts

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/acampkin95/MCPCentralManager'

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