Skip to main content
Glama
registry.ts•7.27 kB
/** * Plugin Registry * * Manages plugin lifecycle and registration */ import type { MCPPlugin, PluginRegistryEntry, PluginContext, HealthStatus, } from '../types/plugin.js'; import type { Logger } from '../../utils/logger.js'; import { EventType, EventSeverity } from '../types/events.js'; /** * Plugin registry for managing loaded plugins */ export class PluginRegistry { private plugins = new Map<string, PluginRegistryEntry>(); private logger: Logger; constructor(logger: Logger) { this.logger = logger; } /** * Register a plugin */ register(plugin: MCPPlugin, config: any): void { if (this.plugins.has(plugin.metadata.id)) { throw new Error(`Plugin already registered: ${plugin.metadata.id}`); } const entry: PluginRegistryEntry = { plugin, config, loadedAt: new Date(), state: plugin.state, }; this.plugins.set(plugin.metadata.id, entry); this.logger.info(`Registered plugin: ${plugin.metadata.id} (${plugin.metadata.name})`); } /** * Unregister a plugin */ async unregister(pluginId: string): Promise<boolean> { const entry = this.plugins.get(pluginId); if (!entry) { return false; } // Stop plugin if running if (entry.state === 'running') { await entry.plugin.stop(); } // Cleanup if (entry.plugin.cleanup) { await entry.plugin.cleanup(); } this.plugins.delete(pluginId); this.logger.info(`Unregistered plugin: ${pluginId}`); return true; } /** * Get plugin by ID */ get(pluginId: string): MCPPlugin | undefined { return this.plugins.get(pluginId)?.plugin; } /** * Get plugin entry (with metadata) */ getEntry(pluginId: string): PluginRegistryEntry | undefined { return this.plugins.get(pluginId); } /** * Check if plugin exists */ has(pluginId: string): boolean { return this.plugins.has(pluginId); } /** * Get all registered plugins */ getAll(): MCPPlugin[] { return Array.from(this.plugins.values()).map(entry => entry.plugin); } /** * Get all plugin entries */ getAllEntries(): PluginRegistryEntry[] { return Array.from(this.plugins.values()); } /** * Get plugins by category */ getByCategory(category: string): MCPPlugin[] { return Array.from(this.plugins.values()) .filter(entry => entry.plugin.metadata.category === category) .map(entry => entry.plugin); } /** * Get all plugin IDs */ getPluginIds(): string[] { return Array.from(this.plugins.keys()); } /** * Initialize all registered plugins */ async initializeAll(context: PluginContext): Promise<void> { // Resolve dependency order const sorted = this.resolveDependencies(); for (const pluginId of sorted) { const entry = this.plugins.get(pluginId); if (!entry) continue; try { this.logger.info(`Initializing plugin: ${pluginId}`); await entry.plugin.initialize(context); entry.state = entry.plugin.state; } catch (error) { this.logger.error(`Failed to initialize plugin ${pluginId}:`, error); entry.state = 'error'; } } } /** * Start all initialized plugins */ async startAll(): Promise<void> { for (const entry of this.plugins.values()) { if (entry.state === 'initialized' || entry.state === 'stopped') { try { this.logger.info(`Starting plugin: ${entry.plugin.metadata.id}`); await entry.plugin.start(); entry.state = entry.plugin.state; } catch (error) { this.logger.error(`Failed to start plugin ${entry.plugin.metadata.id}:`, error); entry.state = 'error'; } } } } /** * Stop all running plugins */ async stopAll(): Promise<void> { // Stop in reverse dependency order const sorted = this.resolveDependencies().reverse(); for (const pluginId of sorted) { const entry = this.plugins.get(pluginId); if (!entry) continue; if (entry.state === 'running') { try { this.logger.info(`Stopping plugin: ${pluginId}`); await entry.plugin.stop(); entry.state = entry.plugin.state; } catch (error) { this.logger.error(`Failed to stop plugin ${pluginId}:`, error); } } } } /** * Health check all plugins */ async healthCheckAll(): Promise<Map<string, HealthStatus>> { const results = new Map<string, HealthStatus>(); for (const [pluginId, entry] of this.plugins) { try { const health = await entry.plugin.healthCheck(); entry.health = health; results.set(pluginId, health); } catch (error) { const health: HealthStatus = { healthy: false, status: 'unhealthy', message: error instanceof Error ? error.message : 'Health check failed', timestamp: new Date(), }; entry.health = health; results.set(pluginId, health); } } return results; } /** * Resolve plugin dependencies and return sorted list */ private resolveDependencies(): string[] { const visited = new Set<string>(); const sorted: string[] = []; const visiting = new Set<string>(); const visit = (pluginId: string) => { if (visited.has(pluginId)) return; if (visiting.has(pluginId)) { throw new Error(`Circular dependency detected: ${pluginId}`); } const entry = this.plugins.get(pluginId); if (!entry) return; visiting.add(pluginId); const dependencies = entry.plugin.getDependencies(); for (const depId of dependencies) { if (!this.plugins.has(depId)) { throw new Error(`Missing dependency: ${depId} for plugin ${pluginId}`); } visit(depId); } visiting.delete(pluginId); visited.add(pluginId); sorted.push(pluginId); }; // Visit all plugins for (const pluginId of this.plugins.keys()) { visit(pluginId); } return sorted; } /** * Get plugin statistics */ getStats(): { total: number; byState: Record<string, number>; byCategory: Record<string, number>; healthy: number; unhealthy: number; } { const stats = { total: this.plugins.size, byState: {} as Record<string, number>, byCategory: {} as Record<string, number>, healthy: 0, unhealthy: 0, }; for (const entry of this.plugins.values()) { // Count by state stats.byState[entry.state] = (stats.byState[entry.state] || 0) + 1; // Count by category const category = entry.plugin.metadata.category; stats.byCategory[category] = (stats.byCategory[category] || 0) + 1; // Count health if (entry.health) { if (entry.health.healthy) { stats.healthy++; } else { stats.unhealthy++; } } } return stats; } /** * Clear all plugins */ async clear(): Promise<void> { await this.stopAll(); for (const entry of this.plugins.values()) { if (entry.plugin.cleanup) { await entry.plugin.cleanup(); } } this.plugins.clear(); } }

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/vespo92/OPNSenseMCP'

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