jezweb-mcp-core
Version:
Jezweb Model Context Protocol (MCP) Core - A universal server for providing AI tools and resources, designed for seamless integration with various AI models and clients. Features adaptable multi-provider support, comprehensive tool and resource management
227 lines • 8.66 kB
JavaScript
/**
* Tool Registry - Central registry for managing and executing tool handlers
*
* This class implements the Registry pattern and acts as a facade for tool execution:
* - Maintains a map of tool names to handler instances
* - Provides registration and execution methods
* - Handles unknown tools with proper error messages
* - Supports dynamic handler registration for extensibility
*
* Usage:
* 1. Register handlers: registry.register('assistant-create', new AssistantCreateHandler(context))
* 2. Execute tools: await registry.execute('assistant-create', args)
*/
import { MCPError, ErrorCodes } from '../types/index.js';
/**
* Central registry for tool handlers
*
* Responsibilities:
* - Store and manage tool handler instances
* - Route tool execution requests to appropriate handlers
* - Provide introspection capabilities
* - Handle registration and deregistration of handlers
*/
export class ToolRegistry {
handlers = new Map();
context;
constructor(context) {
this.context = context;
}
/**
* Register a tool handler
*
* @param toolName - The name of the tool (e.g., 'assistant-create')
* @param handler - The handler instance
* @throws MCPError if tool is already registered
*/
register(toolName, handler) {
try {
console.log(`[ToolRegistry] DEBUG: Attempting to register tool: ${toolName}`);
if (this.handlers.has(toolName)) {
const error = new MCPError(ErrorCodes.INTERNAL_ERROR, `Tool '${toolName}' is already registered. Cannot register duplicate handlers.`);
console.error(`[ToolRegistry] ERROR: ${error.message}`);
throw error;
}
// Validate that the handler's tool name matches the registration name
const handlerToolName = handler.getToolName();
console.log(`[ToolRegistry] DEBUG: Handler reports tool name: ${handlerToolName}`);
if (handlerToolName !== toolName) {
const error = new MCPError(ErrorCodes.INTERNAL_ERROR, `Handler tool name '${handlerToolName}' does not match registration name '${toolName}'.`);
console.error(`[ToolRegistry] ERROR: ${error.message}`);
throw error;
}
this.handlers.set(toolName, handler);
this.logRegistration(toolName, handler);
console.log(`[ToolRegistry] SUCCESS: Registered tool: ${toolName} (total: ${this.handlers.size})`);
}
catch (error) {
console.error(`[ToolRegistry] FATAL ERROR registering ${toolName}:`, error);
throw error;
}
}
/**
* Unregister a tool handler
*
* @param toolName - The name of the tool to unregister
* @returns true if handler was found and removed, false otherwise
*/
unregister(toolName) {
const removed = this.handlers.delete(toolName);
if (removed) {
console.log(`[ToolRegistry] Unregistered handler for tool: ${toolName}`);
}
return removed;
}
/**
* Execute a tool by name
*
* @param toolName - The name of the tool to execute
* @param args - The arguments to pass to the tool
* @returns Promise resolving to the tool execution result
* @throws MCPError if tool is not found or execution fails
*/
async execute(toolName, args) {
const handler = this.handlers.get(toolName);
if (!handler) {
throw this.createUnknownToolError(toolName);
}
try {
// Update context with current tool name for logging
const executionContext = {
...this.context,
toolName
};
// Create a new handler instance with updated context for this execution
const contextualHandler = Object.create(Object.getPrototypeOf(handler));
Object.assign(contextualHandler, handler);
contextualHandler.context = executionContext;
return await contextualHandler.handle(args);
}
catch (error) {
// Re-throw with additional context if it's not already an MCPError
if (!(error instanceof MCPError)) {
throw new MCPError(ErrorCodes.INTERNAL_ERROR, `Tool execution failed for '${toolName}': ${error instanceof Error ? error.message : 'Unknown error'}`, { originalError: error, toolName, args: this.sanitizeArgsForLogging(args) });
}
throw error;
}
}
/**
* Check if a tool is registered
*
* @param toolName - The name of the tool to check
* @returns true if the tool is registered, false otherwise
*/
isRegistered(toolName) {
return this.handlers.has(toolName);
}
/**
* Get all registered tool names
*
* @returns Array of registered tool names
*/
getRegisteredTools() {
return Array.from(this.handlers.keys()).sort();
}
/**
* Get registry statistics
*
* @returns Statistics about the registry
*/
getStats() {
const handlersByCategory = {};
for (const handler of this.handlers.values()) {
const category = handler.getCategory();
handlersByCategory[category] = (handlersByCategory[category] || 0) + 1;
}
return {
totalHandlers: this.handlers.size,
handlersByCategory,
registeredTools: this.getRegisteredTools()
};
}
/**
* Get handler for a specific tool (for testing/debugging)
*
* @param toolName - The name of the tool
* @returns The handler instance or undefined if not found
*/
getHandler(toolName) {
return this.handlers.get(toolName);
}
/**
* Clear all registered handlers (useful for testing)
*/
clear() {
const count = this.handlers.size;
this.handlers.clear();
console.log(`[ToolRegistry] Cleared ${count} registered handlers`);
}
/**
* Register multiple handlers at once
*
* @param handlerMap - Map of tool names to handler instances
*/
registerBatch(handlerMap) {
for (const [toolName, handler] of Object.entries(handlerMap)) {
this.register(toolName, handler);
}
}
/**
* Create an error for unknown tools with helpful suggestions
*/
createUnknownToolError(toolName) {
const registeredTools = this.getRegisteredTools();
if (registeredTools.length === 0) {
return new MCPError(ErrorCodes.METHOD_NOT_FOUND, `Tool not found: '${toolName}'. No tools are currently registered.`);
}
// Find similar tool names for suggestions
const suggestions = this.findSimilarTools(toolName, registeredTools);
const suggestionText = suggestions.length > 0
? ` Did you mean: ${suggestions.join(', ')}?`
: '';
return new MCPError(ErrorCodes.METHOD_NOT_FOUND, `Tool not found: '${toolName}'.${suggestionText} Available tools: ${registeredTools.join(', ')}.`);
}
/**
* Find similar tool names for suggestions
*/
findSimilarTools(toolName, availableTools) {
const suggestions = [];
const lowerToolName = toolName.toLowerCase();
for (const tool of availableTools) {
const lowerTool = tool.toLowerCase();
// Exact prefix match
if (lowerTool.startsWith(lowerToolName) || lowerToolName.startsWith(lowerTool)) {
suggestions.push(tool);
}
// Contains match
else if (lowerTool.includes(lowerToolName) || lowerToolName.includes(lowerTool)) {
suggestions.push(tool);
}
}
return suggestions.slice(0, 3); // Limit to 3 suggestions
}
/**
* Log handler registration
*/
logRegistration(toolName, handler) {
console.log(`[ToolRegistry] Registered handler for tool: ${toolName} (category: ${handler.getCategory()})`);
}
/**
* Sanitize arguments for logging (remove sensitive data)
*/
sanitizeArgsForLogging(args) {
if (!args || typeof args !== 'object') {
return args;
}
const sanitized = { ...args };
// Remove or mask sensitive fields
const sensitiveFields = ['api_key', 'token', 'password', 'secret'];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
}
}
//# sourceMappingURL=tool-registry.js.map