Skip to main content
Glama
loader.ts•6.57 kB
/** * Plugin Loader * * Discovers and loads plugins from filesystem */ import { readdir, readFile, access } from 'fs/promises'; import { join, resolve } from 'path'; import type { MCPPlugin, PluginConfig, PluginLoaderOptions, PluginContext, } from '../types/plugin.js'; import type { Logger } from '../../utils/logger.js'; import { PluginRegistry } from './registry.js'; /** * Plugin loader for discovering and loading plugins */ export class PluginLoader { private registry: PluginRegistry; private logger: Logger; private options: PluginLoaderOptions; constructor( registry: PluginRegistry, logger: Logger, options: PluginLoaderOptions ) { this.registry = registry; this.logger = logger; this.options = options; } /** * Discover plugins in directory */ async discover(): Promise<string[]> { const pluginDir = resolve(this.options.pluginsDirectory); try { await access(pluginDir); } catch { this.logger.warn(`Plugin directory not found: ${pluginDir}`); return []; } const discovered: string[] = []; try { // Read category directories const categories = await readdir(pluginDir, { withFileTypes: true }); for (const category of categories) { if (!category.isDirectory()) continue; const categoryPath = join(pluginDir, category.name); const plugins = await readdir(categoryPath, { withFileTypes: true }); for (const plugin of plugins) { if (!plugin.isDirectory()) continue; const pluginPath = join(categoryPath, plugin.name); const configPath = join(pluginPath, 'config.json'); try { await access(configPath); discovered.push(pluginPath); this.logger.debug(`Discovered plugin: ${plugin.name} in ${category.name}`); } catch { this.logger.debug(`Skipping ${plugin.name}: no config.json`); } } } this.logger.info(`Discovered ${discovered.length} plugins`); return discovered; } catch (error) { this.logger.error('Error discovering plugins:', error); return []; } } /** * Load plugin from path */ async loadPlugin(pluginPath: string): Promise<MCPPlugin | null> { try { // Read config const configPath = join(pluginPath, 'config.json'); const configContent = await readFile(configPath, 'utf-8'); const config: PluginConfig = JSON.parse(configContent); // Check if plugin is enabled if (!this.isPluginEnabled(config.metadata.id)) { this.logger.debug(`Plugin ${config.metadata.id} is disabled`); return null; } // Load plugin module const indexPath = join(pluginPath, 'index.js'); try { await access(indexPath); } catch { // Try TypeScript file if JavaScript doesn't exist const tsPath = join(pluginPath, 'index.ts'); try { await access(tsPath); this.logger.warn(`Plugin ${config.metadata.id} has TypeScript source but no compiled JS. Please compile first.`); return null; } catch { this.logger.error(`Plugin ${config.metadata.id} missing index.js/index.ts`); return null; } } // Import plugin const module = await import(indexPath); const PluginClass = module.default || module[Object.keys(module)[0]]; if (!PluginClass) { this.logger.error(`Plugin ${config.metadata.id} has no default export`); return null; } // Instantiate plugin const pluginConfig = { ...config.config, ...(this.options.configs?.[config.metadata.id] || {}), }; const plugin: MCPPlugin = new PluginClass(pluginConfig); // Validate plugin if (!this.validatePlugin(plugin)) { this.logger.error(`Plugin ${config.metadata.id} failed validation`); return null; } // Register plugin this.registry.register(plugin, config); this.logger.info(`Loaded plugin: ${config.metadata.id} v${config.metadata.version}`); return plugin; } catch (error) { this.logger.error(`Error loading plugin from ${pluginPath}:`, error); return null; } } /** * Load all discovered plugins */ async loadAll(): Promise<MCPPlugin[]> { const pluginPaths = await this.discover(); const loaded: MCPPlugin[] = []; for (const path of pluginPaths) { const plugin = await this.loadPlugin(path); if (plugin) { loaded.push(plugin); } } this.logger.info(`Loaded ${loaded.length} plugins`); return loaded; } /** * Check if plugin is enabled */ private isPluginEnabled(pluginId: string): boolean { // Check disabled list first if (this.options.disabled?.includes(pluginId)) { return false; } // If enabled list exists, plugin must be in it if (this.options.enabled && this.options.enabled.length > 0) { return this.options.enabled.includes(pluginId); } // Default: enabled return true; } /** * Validate plugin implementation */ private validatePlugin(plugin: any): plugin is MCPPlugin { // Check required properties if (!plugin.metadata) { this.logger.error('Plugin missing metadata'); return false; } // Check required metadata fields const required = ['id', 'name', 'version', 'description', 'category']; for (const field of required) { if (!plugin.metadata[field]) { this.logger.error(`Plugin metadata missing required field: ${field}`); return false; } } // Check required methods const methods = [ 'initialize', 'start', 'stop', 'healthCheck', 'getTools', 'getResources', 'getPrompts', 'getDependencies', ]; for (const method of methods) { if (typeof plugin[method] !== 'function') { this.logger.error(`Plugin missing required method: ${method}`); return false; } } return true; } /** * Reload a specific plugin */ async reloadPlugin(pluginId: string, pluginPath: string): Promise<boolean> { try { // Unregister existing plugin await this.registry.unregister(pluginId); // Load fresh instance const plugin = await this.loadPlugin(pluginPath); return plugin !== null; } catch (error) { this.logger.error(`Error reloading plugin ${pluginId}:`, error); return false; } } }

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