Skip to main content
Glama
autoDiscovery.ts10.3 kB
import { randomUUID } from "node:crypto"; import { hostname } from "node:os"; import { readFileSync } from "node:fs"; import { logger } from "../utils/logger.js"; import type { Capability } from "../config/capabilities.js"; export interface ServerCapabilities { readonly serverId: string; readonly serverType: "mcp-server"; readonly hostname: string; readonly capabilities: readonly Capability[]; readonly tools: readonly string[]; readonly version: string; readonly metadata?: Record<string, unknown>; } export interface RegistrationResponse { readonly registered: boolean; readonly serverId: string; readonly assignedCapabilities: readonly string[]; readonly heartbeatIntervalMs: number; readonly registeredAt: string; } export interface HeartbeatResponse { readonly acknowledged: boolean; readonly nextHeartbeatMs: number; readonly commandsAvailable: number; } /** * AutoDiscoveryService handles MCP server registration and heartbeat * with central registry for distributed command routing. * * **Flow**: * 1. On startup: Register with central registry (POST /api/v1/servers/register) * 2. Receive server_id and heartbeat interval * 3. Start heartbeat worker (default every 30s) * 4. Central registry routes commands based on capability matching * * **Capabilities Advertised**: * - local-shell, local-sudo (always available) * - macos-wireless (if running on macOS) * - ssh-linux, ssh-mac (if SSH available) * - Tool-specific capabilities (network diagnostics, firewall, etc.) */ export class AutoDiscoveryService { private readonly serverId: string; private serverCapabilities: ServerCapabilities; private heartbeatTimer: NodeJS.Timeout | null = null; private registryEndpoint?: string; private heartbeatIntervalMs = 30000; // Default 30 seconds private isRegistered = false; private authService?: { getAccessToken: () => Promise<string> }; public constructor( capabilities: readonly Capability[], tools: readonly string[], registryEndpoint?: string, authService?: { getAccessToken: () => Promise<string> }, ) { this.serverId = process.env.IT_MCP_SERVER_ID ?? randomUUID(); this.registryEndpoint = registryEndpoint ?? process.env.IT_MCP_REGISTRY_URL; this.authService = authService; this.serverCapabilities = { serverId: this.serverId, serverType: "mcp-server", hostname: hostname(), capabilities, tools, version: this.getVersion(), metadata: { platform: process.platform, nodeVersion: process.version, startedAt: new Date().toISOString(), }, }; logger.info("AutoDiscoveryService initialized", { serverId: this.serverId, hostname: this.serverCapabilities.hostname, capabilities: capabilities.length, tools: tools.length, }); } /** * Register with central registry on startup */ public async register(): Promise<RegistrationResponse> { if (!this.registryEndpoint) { logger.warn("Registry endpoint not configured, skipping registration"); return { registered: false, serverId: this.serverId, assignedCapabilities: [], heartbeatIntervalMs: 0, registeredAt: new Date().toISOString(), }; } try { logger.info("Registering with central registry", { endpoint: this.registryEndpoint, serverId: this.serverId, }); // Prepare headers const headers: Record<string, string> = { "Content-Type": "application/json", }; // Add JWT token if auth service is available if (this.authService) { try { const token = await this.authService.getAccessToken(); headers["Authorization"] = `Bearer ${token}`; } catch (error) { logger.warn("Failed to get access token for registration", { error }); } } // Make HTTP POST request to registry const response = await fetch(`${this.registryEndpoint}/api/v1/servers/register`, { method: "POST", headers, body: JSON.stringify(this.serverCapabilities), }); if (!response.ok) { throw new Error(`Registration failed: HTTP ${response.status} ${response.statusText}`); } const data = await response.json() as RegistrationResponse; // Update internal state this.heartbeatIntervalMs = data.heartbeatIntervalMs || 30000; this.isRegistered = data.registered; logger.info("Registration successful", { registered: data.registered, heartbeatIntervalMs: this.heartbeatIntervalMs, assignedCapabilities: data.assignedCapabilities.length, }); return data; } catch (error) { logger.error("Registration failed", { error }); throw new Error(`Failed to register with registry: ${error}`); } } /** * Start heartbeat worker */ public startHeartbeat(): void { if (this.heartbeatTimer) { logger.warn("Heartbeat already running"); return; } if (!this.registryEndpoint) { logger.warn("Registry endpoint not configured, heartbeat disabled"); return; } logger.info("Starting heartbeat", { intervalMs: this.heartbeatIntervalMs }); this.heartbeatTimer = setInterval(() => { void this.sendHeartbeat(); }, this.heartbeatIntervalMs); } /** * Stop heartbeat worker */ public stopHeartbeat(): void { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; logger.info("Heartbeat stopped"); } } /** * Send heartbeat to registry */ private async sendHeartbeat(): Promise<HeartbeatResponse> { if (!this.registryEndpoint) { return { acknowledged: false, nextHeartbeatMs: this.heartbeatIntervalMs, commandsAvailable: 0, }; } try { // Prepare headers const headers: Record<string, string> = { "Content-Type": "application/json", }; // Add JWT token if auth service is available if (this.authService) { try { const token = await this.authService.getAccessToken(); headers["Authorization"] = `Bearer ${token}`; } catch (error) { logger.warn("Failed to get access token for heartbeat", { error }); } } // Prepare heartbeat payload const payload = { serverId: this.serverId, timestamp: new Date().toISOString(), status: "healthy", load: process.cpuUsage(), memory: process.memoryUsage(), }; // Make HTTP POST request to registry const response = await fetch( `${this.registryEndpoint}/api/v1/servers/${this.serverId}/heartbeat`, { method: "POST", headers, body: JSON.stringify(payload), }, ); if (!response.ok) { logger.warn(`Heartbeat failed: HTTP ${response.status}`, { serverId: this.serverId, }); return { acknowledged: false, nextHeartbeatMs: this.heartbeatIntervalMs, commandsAvailable: 0, }; } const data = await response.json() as HeartbeatResponse; logger.debug("Heartbeat acknowledged", { serverId: this.serverId, commandsAvailable: data.commandsAvailable, }); return data; } catch (error) { logger.error("Heartbeat failed", { error }); return { acknowledged: false, nextHeartbeatMs: this.heartbeatIntervalMs, commandsAvailable: 0, }; } } /** * Get server capabilities for advertisement */ public getCapabilities(): ServerCapabilities { return { ...this.serverCapabilities }; } /** * Update capabilities dynamically (e.g., SSH becomes available) */ public updateCapabilities(capabilities: readonly Capability[]): void { this.serverCapabilities = { ...this.serverCapabilities, capabilities, }; logger.info("Capabilities updated", { capabilities: capabilities.length, }); // TODO: Notify registry of capability changes // if (this.isRegistered && this.registryEndpoint) { // void this.notifyCapabilityChange(); // } } /** * Deregister from central registry on shutdown */ public async deregister(): Promise<void> { if (!this.registryEndpoint || !this.isRegistered) { logger.info("Not registered, skipping deregistration"); return; } try { logger.info("Deregistering from central registry"); // Prepare headers const headers: Record<string, string> = {}; // Add JWT token if auth service is available if (this.authService) { try { const token = await this.authService.getAccessToken(); headers["Authorization"] = `Bearer ${token}`; } catch (error) { logger.warn("Failed to get access token for deregistration", { error }); } } // Make HTTP DELETE request to registry const response = await fetch( `${this.registryEndpoint}/api/v1/servers/${this.serverId}`, { method: "DELETE", headers, }, ); if (!response.ok) { logger.warn(`Deregistration failed: HTTP ${response.status}`); } this.isRegistered = false; logger.info("Deregistered successfully"); } catch (error) { logger.error("Deregistration failed", { error }); } } /** * Get version from package.json */ private getVersion(): string { try { // Import package.json to get version // Note: In production builds, this path should be relative to dist/ const packageJsonPath = new URL("../../package.json", import.meta.url); const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); return packageJson.version || "0.1.0"; } catch (error) { logger.warn("Failed to read version from package.json", { error }); return "0.1.0"; } } /** * Cleanup: stop heartbeat and deregister */ public async destroy(): Promise<void> { this.stopHeartbeat(); await this.deregister(); logger.info("AutoDiscoveryService destroyed"); } }

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