Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
index.ts14.6 kB
// Copyright 2025 Chris Bunting // Brief: Plugin architecture for extensibility in MCP Code Analysis & Quality Server // Scope: Dynamic plugin loading and management system import { PluginInterface, MCPRequest, MCPResponse, LoggerInterface, ConfigInterface } from '@mcp-code-analysis/shared-types'; export interface PluginMetadata { name: string; version: string; description: string; author: string; license: string; homepage?: string; repository?: string; keywords: string[]; dependencies: Record<string, string>; devDependencies: Record<string, string>; engines: { node: string; [key: string]: string; }; } export interface PluginContext { logger: LoggerInterface; config: ConfigInterface; registerTool(tool: ToolDefinition): void; unregisterTool(toolName: string): void; getTool(toolName: string): ToolDefinition | undefined; emit(event: string, data: any): void; on(event: string, handler: (data: any) => void): void; off(event: string, handler: (data: any) => void): void; } export interface ToolDefinition { name: string; description: string; inputSchema: any; handler: (params: any) => Promise<any>; category?: string; tags?: string[]; } export interface PluginLoader { loadPlugin(pluginPath: string): Promise<LoadedPlugin>; unloadPlugin(pluginName: string): Promise<void>; reloadPlugin(pluginName: string): Promise<void>; getLoadedPlugins(): LoadedPlugin[]; getPlugin(pluginName: string): LoadedPlugin | undefined; } export interface LoadedPlugin { metadata: PluginMetadata; instance: PluginInterface; context: PluginContext; state: 'loaded' | 'active' | 'inactive' | 'error'; loadTime: Date; lastActive?: Date; error?: Error; } export class PluginManager implements PluginLoader { private plugins: Map<string, LoadedPlugin> = new Map(); private tools: Map<string, ToolDefinition> = new Map(); private eventHandlers: Map<string, ((data: any) => void)[]> = new Map(); private logger: LoggerInterface; private config: ConfigInterface; constructor(logger: LoggerInterface, config: ConfigInterface) { this.logger = logger; this.config = config; } async loadPlugin(pluginPath: string): Promise<LoadedPlugin> { try { // Dynamic import of the plugin const pluginModule = await import(pluginPath); const PluginClass = pluginModule.default || pluginModule.Plugin; if (!PluginClass) { throw new Error(`Plugin module at ${pluginPath} does not export a default class or Plugin class`); } // Create plugin instance const instance = new PluginClass(); // Validate plugin interface if (!this.validatePluginInterface(instance)) { throw new Error(`Plugin at ${pluginPath} does not implement the PluginInterface`); } // Extract metadata const metadata = this.extractMetadata(pluginModule, instance); // Create plugin context const context = this.createPluginContext(metadata.name); // Initialize plugin await instance.initialize(this.config.get(`plugins.${metadata.name}`, {})); // Store loaded plugin const loadedPlugin: LoadedPlugin = { metadata, instance, context, state: 'loaded', loadTime: new Date() }; this.plugins.set(metadata.name, loadedPlugin); // Activate plugin await this.activatePlugin(metadata.name); this.logger.info(`Loaded plugin: ${metadata.name} v${metadata.version}`); return loadedPlugin; } catch (error) { this.logger.error(`Failed to load plugin from ${pluginPath}:`, error); throw error; } } async unloadPlugin(pluginName: string): Promise<void> { const plugin = this.plugins.get(pluginName); if (!plugin) { throw new Error(`Plugin ${pluginName} is not loaded`); } try { // Deactivate plugin first await this.deactivatePlugin(pluginName); // Cleanup plugin await plugin.instance.cleanup(); // Remove from registry this.plugins.delete(pluginName); this.logger.info(`Unloaded plugin: ${pluginName}`); } catch (error) { this.logger.error(`Failed to unload plugin ${pluginName}:`, error); throw error; } } async reloadPlugin(pluginName: string): Promise<void> { const plugin = this.plugins.get(pluginName); if (!plugin) { throw new Error(`Plugin ${pluginName} is not loaded`); } const pluginPath = this.getPluginPath(pluginName); if (!pluginPath) { throw new Error(`Cannot determine path for plugin ${pluginName}`); } // Unload and reload await this.unloadPlugin(pluginName); await this.loadPlugin(pluginPath); } getLoadedPlugins(): LoadedPlugin[] { return Array.from(this.plugins.values()); } getPlugin(pluginName: string): LoadedPlugin | undefined { return this.plugins.get(pluginName); } async executeTool(toolName: string, params: any): Promise<any> { const tool = this.tools.get(toolName); if (!tool) { throw new Error(`Tool ${toolName} not found`); } try { this.logger.debug(`Executing tool: ${toolName}`); return await tool.handler(params); } catch (error) { this.logger.error(`Error executing tool ${toolName}:`, error); throw error; } } private async activatePlugin(pluginName: string): Promise<void> { const plugin = this.plugins.get(pluginName); if (!plugin || plugin.state === 'active') { return; } try { plugin.state = 'active'; plugin.lastActive = new Date(); this.logger.info(`Activated plugin: ${pluginName}`); // Emit activation event this.emit(`plugin:activated:${pluginName}`, { pluginName, timestamp: Date.now() }); } catch (error) { plugin.state = 'error'; plugin.error = error as Error; this.logger.error(`Failed to activate plugin ${pluginName}:`, error); throw error; } } private async deactivatePlugin(pluginName: string): Promise<void> { const plugin = this.plugins.get(pluginName); if (!plugin || plugin.state === 'inactive') { return; } try { plugin.state = 'inactive'; this.logger.info(`Deactivated plugin: ${pluginName}`); // Emit deactivation event this.emit(`plugin:deactivated:${pluginName}`, { pluginName, timestamp: Date.now() }); } catch (error) { plugin.state = 'error'; plugin.error = error as Error; this.logger.error(`Failed to deactivate plugin ${pluginName}:`, error); throw error; } } private validatePluginInterface(instance: any): instance is PluginInterface { return ( typeof instance === 'object' && typeof instance.name === 'string' && typeof instance.version === 'string' && typeof instance.initialize === 'function' && typeof instance.execute === 'function' && typeof instance.cleanup === 'function' ); } private extractMetadata(module: any, instance: PluginInterface): PluginMetadata { // Try to get metadata from package.json if available const packageJson = module.package || {}; return { name: instance.name, version: instance.version, description: instance.description || packageJson.description || '', author: packageJson.author || '', license: packageJson.license || 'MIT', homepage: packageJson.homepage, repository: packageJson.repository, keywords: packageJson.keywords || [], dependencies: packageJson.dependencies || {}, devDependencies: packageJson.devDependencies || {}, engines: packageJson.engines || { node: '>=18.0.0' } }; } private createPluginContext(pluginName: string): PluginContext { return { logger: this.logger, config: this.config, registerTool: (tool: ToolDefinition) => { this.tools.set(tool.name, tool); this.logger.debug(`Plugin ${pluginName} registered tool: ${tool.name}`); }, unregisterTool: (toolName: string) => { this.tools.delete(toolName); this.logger.debug(`Plugin ${pluginName} unregistered tool: ${toolName}`); }, getTool: (toolName: string) => { return this.tools.get(toolName); }, emit: (event: string, data: any) => { this.emit(event, data); }, on: (event: string, handler: (data: any) => void) => { this.on(event, handler); }, off: (event: string, handler: (data: any) => void) => { this.off(event, handler); } }; } private getPluginPath(_pluginName: string): string | undefined { // This is a simplified implementation // In a real scenario, you'd need to track where plugins were loaded from return undefined; } private emit(event: string, data: any): void { const handlers = this.eventHandlers.get(event); if (handlers) { handlers.forEach(handler => { try { handler(data); } catch (error) { this.logger.error(`Error in event handler for ${event}:`, error); } }); } } private on(event: string, handler: (data: any) => void): void { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, []); } this.eventHandlers.get(event)!.push(handler); } private off(event: string, handler: (data: any) => void): void { const handlers = this.eventHandlers.get(event); if (handlers) { const index = handlers.indexOf(handler); if (index > -1) { handlers.splice(index, 1); } } } async dispose(): Promise<void> { // Unload all plugins const pluginNames = Array.from(this.plugins.keys()); for (const pluginName of pluginNames) { try { await this.unloadPlugin(pluginName); } catch (error) { this.logger.error(`Error unloading plugin ${pluginName} during disposal:`, error); } } // Clear registries this.plugins.clear(); this.tools.clear(); this.eventHandlers.clear(); } } // Plugin discovery utilities export class PluginDiscovery { private logger: LoggerInterface; constructor(logger: LoggerInterface) { this.logger = logger; } async discoverPlugins(pluginsDir: string): Promise<string[]> { const fs = require('fs'); const path = require('path'); const pluginPaths: string[] = []; try { if (!fs.existsSync(pluginsDir)) { this.logger.warn(`Plugins directory does not exist: ${pluginsDir}`); return pluginPaths; } const entries = fs.readdirSync(pluginsDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { // Check for package.json or index.js/ts const pluginDir = path.join(pluginsDir, entry.name); const packageJsonPath = path.join(pluginDir, 'package.json'); const indexPath = path.join(pluginDir, 'index.js'); const tsIndexPath = path.join(pluginDir, 'index.ts'); if (fs.existsSync(packageJsonPath)) { pluginPaths.push(pluginDir); } else if (fs.existsSync(indexPath)) { pluginPaths.push(indexPath); } else if (fs.existsSync(tsIndexPath)) { pluginPaths.push(tsIndexPath); } } } this.logger.info(`Discovered ${pluginPaths.length} plugins in ${pluginsDir}`); return pluginPaths; } catch (error) { this.logger.error(`Error discovering plugins in ${pluginsDir}:`, error); return []; } } async validatePlugin(pluginPath: string): Promise<boolean> { try { // Try to load the plugin module const pluginModule = await import(pluginPath); const PluginClass = pluginModule.default || pluginModule.Plugin; if (!PluginClass) { return false; } // Try to create an instance const instance = new PluginClass(); // Check if it implements the required interface return ( typeof instance.name === 'string' && typeof instance.version === 'string' && typeof instance.initialize === 'function' && typeof instance.execute === 'function' && typeof instance.cleanup === 'function' ); } catch (error) { this.logger.debug(`Plugin validation failed for ${pluginPath}:`, error); return false; } } } // Factory functions export function createPluginManager( logger: LoggerInterface, config: ConfigInterface ): PluginManager { return new PluginManager(logger, config); } export function createPluginDiscovery( logger: LoggerInterface ): PluginDiscovery { return new PluginDiscovery(logger); } // Base plugin class for easier plugin development export abstract class BasePlugin implements PluginInterface { abstract name: string; abstract version: string; abstract description: string; protected context?: PluginContext; async initialize(_config: Record<string, any>): Promise<void> { // Default implementation - can be overridden } abstract execute(request: MCPRequest): Promise<MCPResponse>; async cleanup(): Promise<void> { // Default implementation - can be overridden } protected registerTool(tool: ToolDefinition): void { if (this.context) { this.context.registerTool(tool); } } protected unregisterTool(toolName: string): void { if (this.context) { this.context.unregisterTool(toolName); } } protected emit(event: string, data: any): void { if (this.context) { this.context.emit(event, data); } } protected on(event: string, handler: (data: any) => void): void { if (this.context) { this.context.on(event, handler); } } protected off(event: string, handler: (data: any) => void): void { if (this.context) { this.context.off(event, handler); } } // Helper method to create successful responses protected createSuccessResponse(result: any, requestId: string): MCPResponse { return { id: requestId, result, timestamp: new Date() }; } // Helper method to create error responses protected createErrorResponse( code: number, message: string, requestId: string, data?: any ): MCPResponse { return { id: requestId, error: { code, message, data }, timestamp: new Date() }; } }

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/cbunting99/mcp-code-analysis-server'

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