quality-mcp
Version:
An MCP server that analyzes to your codebase, with plugin support for DCD and Simian. 🏍️ "The only Zen you find on the tops of mountains is the Zen you bring up there."
569 lines (493 loc) • 15.5 kB
JavaScript
/**
* Plugin Manager
* Handles registration, lifecycle, and communication between MCP server and analysis plugins
*/
import { EventEmitter } from 'events';
import { createLogger } from '../utils/logger.js';
import {
CallToolRequestSchema,
ReadResourceRequestSchema,
GetPromptRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ListPromptsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const logger = createLogger('plugin-manager');
/**
* Dependency injection function for plugin manager
* @returns {Object} Dependencies object
*/
export function getDeps() {
return {
logger,
};
}
/**
* Abstract base class for analysis plugins
*/
export class AnalysisPlugin {
constructor(name, config = {}) {
this.name = name;
this.config = config;
this.enabled = true;
}
/**
* Initialize the plugin
* @returns {Promise<void>}
*/
async initialize() {
throw new Error('initialize() must be implemented by plugin');
}
/**
* Get the tools this plugin provides
* @returns {Array<Object>} Array of MCP tool definitions
*/
getTools() {
return [];
}
/**
* Get the resources this plugin provides
* @returns {Array<Object>} Array of MCP resource definitions
*/
getResources() {
return [];
}
/**
* Get the prompts this plugin provides
* @returns {Array<Object>} Array of MCP prompt definitions
*/
getPrompts() {
return [];
}
/**
* Execute a tool
* @param {string} toolName - Name of the tool to execute
* @param {Object} _params - Tool parameters
* @returns {Promise<Object>} Tool execution result
*/
async executeTool(toolName, _params) {
throw new Error(`Tool ${toolName} not implemented by plugin ${this.name}`);
}
/**
* Get a resource
* @param {string} resourceUri - URI of the resource to get
* @returns {Promise<Object>} Resource data
*/
async getResource(resourceUri) {
throw new Error(`Resource ${resourceUri} not implemented by plugin ${this.name}`);
}
/**
* Get a prompt
* @param {string} promptName - Name of the prompt to get
* @param {Object} _params - Prompt parameters
* @returns {Promise<Object>} Prompt data
*/
async getPrompt(promptName, _params) {
throw new Error(`Prompt ${promptName} not implemented by plugin ${this.name}`);
}
/**
* Shutdown the plugin
* @returns {Promise<void>}
*/
async shutdown() {
logger.info(`Shutting down plugin: ${this.name}`);
}
}
/**
* Plugin Manager - orchestrates multiple analysis plugins
*/
export class PluginManager extends EventEmitter {
constructor(server, _getDeps = getDeps) {
super();
this.server = server;
this.plugins = new Map();
this.toolRegistry = new Map();
this.resourceRegistry = new Map();
this.promptRegistry = new Map();
this.setupServerHandlers();
}
/**
* Set up MCP server request handlers
*/
setupServerHandlers(_getDeps = getDeps) {
const { logger } = _getDeps();
try {
// Register the get_plugin_status tool
this.toolRegistry.set('get_plugin_status', {
plugin: this,
definition: {
name: 'get_plugin_status',
description: 'Check the status and availability of all plugins and their tools',
inputSchema: {
type: 'object',
properties: {
plugin: {
type: 'string',
description:
'Specific plugin to check (optional - if not provided, checks all plugins)',
enum: ['dcd', 'simian', 'all'],
default: 'all',
},
},
required: [],
},
},
});
logger.debug('Plugin manager handlers setup completed');
} catch (error) {
logger.error('Failed to setup plugin manager handlers:', error);
throw error;
}
// Handle tool execution requests
this.server.setRequestHandler(CallToolRequestSchema, async request => {
try {
const { name, arguments: args = {} } = request.params;
if (!this.toolRegistry.has(name)) {
throw new Error(`Unknown tool: ${name}`);
}
const { plugin } = this.toolRegistry.get(name);
// Special handling for get_plugin_status tool
if (name === 'get_plugin_status') {
const result = await this.getPluginStatus();
// Return result in proper MCP format without double serialization
return this.formatToolResponse(result, name);
}
const result = await plugin.executeTool(name, args);
// Return result in proper MCP format without double serialization
return this.formatToolResponse(result, name);
} catch (error) {
logger.error('Tool execution error:', error);
throw error;
}
});
// Handle resource requests
this.server.setRequestHandler(ReadResourceRequestSchema, async request => {
try {
const { uri } = request.params;
if (!this.resourceRegistry.has(uri)) {
throw new Error(`Unknown resource: ${uri}`);
}
const { plugin } = this.resourceRegistry.get(uri);
const result = await plugin.getResource(uri);
// Return response in proper MCP format using the same pattern as tools
return {
contents: [
{
uri,
mimeType: result.mimeType || 'application/json',
text: this.formatResourceResponse(result.data, uri),
},
],
};
} catch (error) {
logger.error('Resource read error:', error);
throw error;
}
});
// Handle prompt requests
this.server.setRequestHandler(GetPromptRequestSchema, async request => {
try {
const { name, arguments: args = {} } = request.params;
if (!this.promptRegistry.has(name)) {
throw new Error(`Unknown prompt: ${name}`);
}
const { plugin } = this.promptRegistry.get(name);
const result = await plugin.getPrompt(name, args);
return result;
} catch (error) {
logger.error('Prompt get error:', error);
throw error;
}
});
// Handle tools list requests
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
try {
logger.debug('Handling ListToolsRequest');
const tools = [];
for (const { definition } of this.toolRegistry.values()) {
tools.push(definition);
}
logger.debug(`Returning ${tools.length} tools`);
return { tools };
} catch (error) {
logger.error('Error in ListToolsRequest handler:', error);
throw error;
}
});
// Handle resources list requests
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
try {
logger.debug('Handling ListResourcesRequest');
const resources = [];
for (const { definition } of this.resourceRegistry.values()) {
resources.push(definition);
}
logger.debug(`Returning ${resources.length} resources`);
return { resources };
} catch (error) {
logger.error('Error in ListResourcesRequest handler:', error);
throw error;
}
});
// Handle prompts list requests
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
try {
logger.debug('Handling ListPromptsRequest');
const prompts = [];
for (const { definition } of this.promptRegistry.values()) {
prompts.push(definition);
}
logger.debug(`Returning ${prompts.length} prompts`);
return { prompts };
} catch (error) {
logger.error('Error in ListPromptsRequest handler:', error);
throw error;
}
});
}
/**
* Format tool response in proper MCP format without double serialization
* @param {any} result - Tool execution result
* @param {string} toolName - Name of the tool for logging
* @returns {Object} Properly formatted MCP response
*/
formatToolResponse(result, toolName, _getDeps = getDeps) {
const { logger } = _getDeps();
// Handle different result types appropriately
let responseText;
if (typeof result === 'string') {
// If result is already a string, use it directly
responseText = result;
} else if (result === null || result === undefined) {
// Handle null/undefined gracefully
responseText = '{}';
} else {
// For objects, arrays, etc., serialize to JSON
try {
responseText = JSON.stringify(result, null, 2);
} catch (jsonError) {
logger.warn(`Failed to stringify tool result for ${toolName}, using fallback:`, jsonError);
responseText = JSON.stringify({
error: 'Failed to serialize result',
originalResult: String(result),
toolName,
});
}
}
// Return in proper MCP format
return {
content: [
{
type: 'text',
text: responseText,
},
],
};
}
/**
* Format resource response in proper MCP format without double serialization
* @param {any} data - Resource data
* @param {string} uri - Resource URI for logging
* @returns {string} Properly formatted response text
*/
formatResourceResponse(data, uri, _getDeps = getDeps) {
const { logger } = _getDeps();
// Handle different data types appropriately
if (typeof data === 'string') {
// If data is already a string, use it directly
return data;
} else if (data === null || data === undefined) {
// Handle null/undefined gracefully
return '{}';
} else {
// For objects, arrays, etc., serialize to JSON
try {
return JSON.stringify(data, null, 2);
} catch (jsonError) {
logger.warn(`Failed to stringify resource data for ${uri}, using fallback:`, jsonError);
return JSON.stringify({
error: 'Failed to serialize resource data',
originalData: String(data),
uri,
});
}
}
}
/**
* Register a plugin
* @param {string} name - Plugin name
* @param {AnalysisPlugin} plugin - Plugin instance
* @returns {Promise<void>}
*/
async register(name, plugin) {
logger.info(`Registering plugin: ${name}`);
try {
// Initialize the plugin
await plugin.initialize();
// Store plugin
this.plugins.set(name, plugin);
// Register plugin's capabilities
this.registerPluginTools(name, plugin);
this.registerPluginResources(name, plugin);
this.registerPluginPrompts(name, plugin);
logger.info(`Plugin registered successfully: ${name}`);
this.emit('pluginRegistered', name, plugin);
} catch (error) {
logger.error(`Failed to register plugin ${name}:`, error);
this.emit('error', name, error);
throw error;
}
}
/**
* Register tools from a plugin
*/
registerPluginTools(pluginName, plugin) {
const tools = plugin.getTools();
for (const tool of tools) {
const toolName = tool.name;
if (this.toolRegistry.has(toolName)) {
logger.warn(`Tool ${toolName} already registered, skipping`);
continue;
}
this.toolRegistry.set(toolName, {
plugin,
definition: tool,
});
logger.debug(`Registered tool: ${toolName} from plugin: ${pluginName}`);
}
}
/**
* Register resources from a plugin
*/
registerPluginResources(pluginName, plugin) {
const resources = plugin.getResources();
for (const resource of resources) {
const resourceUri = resource.uri;
if (this.resourceRegistry.has(resourceUri)) {
logger.warn(`Resource ${resourceUri} already registered, skipping`);
continue;
}
this.resourceRegistry.set(resourceUri, {
plugin,
definition: resource,
});
logger.debug(`Registered resource: ${resourceUri} from plugin: ${pluginName}`);
}
}
/**
* Register prompts from a plugin
*/
registerPluginPrompts(pluginName, plugin) {
const prompts = plugin.getPrompts();
for (const prompt of prompts) {
const promptName = prompt.name;
if (this.promptRegistry.has(promptName)) {
logger.warn(`Prompt ${promptName} already registered, skipping`);
continue;
}
this.promptRegistry.set(promptName, {
plugin,
definition: prompt,
});
logger.debug(`Registered prompt: ${promptName} from plugin: ${pluginName}`);
}
}
/**
* Get a plugin by name
* @param {string} name - Plugin name
* @returns {AnalysisPlugin|null}
*/
getPlugin(name) {
return this.plugins.get(name) || null;
}
/**
* Get all registered plugins
* @returns {Map<string, AnalysisPlugin>}
*/
getPlugins() {
return new Map(this.plugins);
}
/**
* Check if a plugin is registered
* @param {string} name - Plugin name
* @returns {boolean}
*/
hasPlugin(name) {
return this.plugins.has(name);
}
/**
* Shutdown all plugins
* @returns {Promise<void>}
*/
async shutdown(_getDeps = getDeps) {
const { logger } = _getDeps();
logger.info('Shutting down all plugins...');
const shutdownPromises = Array.from(this.plugins.values()).map(async plugin => {
try {
await plugin.shutdown();
} catch (error) {
logger.error(`Error shutting down plugin ${plugin.name}:`, error);
}
});
await Promise.all(shutdownPromises);
// Clear registries
this.plugins.clear();
this.toolRegistry.clear();
this.resourceRegistry.clear();
this.promptRegistry.clear();
logger.info('All plugins shutdown complete');
}
/**
* Get comprehensive status of all plugins
* @returns {Object} Status information for all plugins
*/
async getPluginStatus(_getDeps = getDeps) {
const { logger } = _getDeps();
logger.debug('Getting plugin status');
const available = [];
const unavailable = [];
let totalTools = 0;
for (const [pluginName, plugin] of this.plugins) {
try {
const status = await plugin.getPluginStatus();
if (status.tools && status.tools.length > 0) {
available.push(status);
totalTools += status.tools.length;
} else {
unavailable.push(status);
}
} catch (error) {
logger.warn(`Failed to get status for plugin ${pluginName}:`, error);
unavailable.push({
plugin: pluginName,
name: pluginName,
message: `Failed to get status: ${error.message}`,
});
}
}
const recommendations = [];
if (unavailable.length > 0) {
for (const plugin of unavailable) {
if (plugin.installation) {
if (plugin.plugin === 'dcd') {
recommendations.push(`Install DCD: ${plugin.installation.command}`);
} else if (plugin.plugin === 'simian') {
recommendations.push(
`Install Simian: ${plugin.installation.description} (requires commercial license)`
);
}
}
}
}
return {
available,
unavailable,
summary: {
totalPlugins: this.plugins.size,
availablePlugins: available.length,
totalTools,
recommendations,
},
};
}
}