UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with external MCP server integration, multi-provider support, and professional CLI. Connect to 65+ MCP servers for filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 9 major pr

374 lines (373 loc) 14.1 kB
/** * NeuroLink MCP Tool Registry System * Central registry for managing MCP servers and tools with execution capabilities * Supports tool discovery, registration, and orchestrated execution */ import { ContextManager, ContextValidator } from './context-manager.js'; /** * Central MCP Tool Registry * Manages all MCP servers and their tools with advanced execution capabilities */ export class MCPToolRegistry { servers = new Map(); tools = new Map(); contextManager; // Execution tracking executionCount = 0; totalExecutionTime = 0; errorCount = 0; constructor(contextManager) { this.contextManager = contextManager || new ContextManager(); } /** * Register an MCP server and all its tools * * @param server MCP server to register * @throws Error if server ID already exists */ async registerServer(server) { // Check for duplicate server ID if (this.servers.has(server.id)) { throw new Error(`Server with ID '${server.id}' is already registered`); } // Register the server this.servers.set(server.id, server); // Register all tools from the server for (const [toolName, tool] of Object.entries(server.tools)) { await this.registerToolFromServer(server, toolName, tool); } console.log(`[MCPRegistry] Registered server '${server.id}' with ${Object.keys(server.tools).length} tools`); } /** * Register a single tool from a server * * @param server Source server * @param toolName Tool name * @param tool Tool implementation */ async registerToolFromServer(server, toolName, tool) { const qualifiedName = `${server.id}.${toolName}`; const simpleName = toolName; const registration = { tool, serverId: server.id, serverTitle: server.title, serverCategory: server.category, qualifiedName, simpleName, registeredAt: Date.now() }; // Register with both qualified and simple names this.tools.set(qualifiedName, registration); // Only register simple name if it doesn't conflict if (!this.tools.has(simpleName)) { this.tools.set(simpleName, registration); } else { console.warn(`[MCPRegistry] Tool name conflict: '${simpleName}' already exists, use qualified name '${qualifiedName}'`); } } /** * Execute a tool with comprehensive error handling and context tracking * * @param toolName Tool name (simple or qualified) * @param params Tool parameters * @param context Execution context * @param options Execution options * @returns Tool execution result */ async executeTool(toolName, params, context, options = {}) { const startTime = Date.now(); const { validateInput = true, validatePermissions = true, trackMetrics = true, timeoutMs = 30000 } = options; try { // Find the tool const registration = this.tools.get(toolName); if (!registration) { throw new Error(`Tool not found: ${toolName}`); } const { tool, serverId, serverTitle } = registration; // Validate context if (validatePermissions) { const contextValidation = ContextValidator.validateContext(context); if (!contextValidation.isValid) { throw new Error(`Context validation failed: ${contextValidation.errors.join(', ')}`); } // Check tool permissions if (tool.permissions && !ContextValidator.hasPermissions(context, tool.permissions)) { throw new Error(`Insufficient permissions for tool '${toolName}'. Required: ${tool.permissions.join(', ')}`); } } // Validate input parameters if schema provided if (validateInput && tool.inputSchema) { try { tool.inputSchema.parse(params); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Input validation failed for tool '${toolName}': ${errorMessage}`); } } // Add tool to execution chain this.contextManager.addToToolChain(context, toolName); // Execute tool with timeout const executeWithTimeout = this.createTimeoutPromise(tool.execute(params, context), timeoutMs, `Tool '${toolName}' execution timeout`); const result = await executeWithTimeout; // Add execution metadata const executionTime = Date.now() - startTime; const enhancedResult = { ...result, metadata: { ...result.metadata, toolName, serverId, serverTitle, sessionId: context.sessionId, timestamp: Date.now(), executionTime } }; // Track metrics if (trackMetrics) { this.updateExecutionMetrics(executionTime, result.success); } // Validate output if schema provided if (tool.outputSchema && result.success) { try { tool.outputSchema.parse(result.data); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.warn(`[MCPRegistry] Output validation warning for tool '${toolName}': ${errorMessage}`); } } console.log(`[MCPRegistry] Executed tool '${toolName}' in ${executionTime}ms - ${result.success ? 'SUCCESS' : 'FAILED'}`); return enhancedResult; } catch (error) { const executionTime = Date.now() - startTime; // Track error metrics if (trackMetrics) { this.updateExecutionMetrics(executionTime, false); } const errorMessage = error instanceof Error ? error.message : String(error); const errorResult = { success: false, error: errorMessage, metadata: { toolName, sessionId: context.sessionId, timestamp: Date.now(), executionTime } }; console.error(`[MCPRegistry] Tool execution failed '${toolName}': ${errorMessage}`); return errorResult; } } /** * List all available tools with optional filtering * * @param criteria Search criteria for filtering tools * @returns Array of tool information */ listTools(criteria = {}) { const tools = []; // Get unique tools (prefer qualified names over simple names) const uniqueTools = new Map(); for (const [name, registration] of this.tools) { if (name.includes('.')) { // Qualified name uniqueTools.set(registration.qualifiedName, registration); } else if (!uniqueTools.has(registration.qualifiedName)) { uniqueTools.set(registration.qualifiedName, registration); } } for (const registration of uniqueTools.values()) { const { tool, serverId, serverTitle, serverCategory, qualifiedName, simpleName } = registration; // Apply filters if (criteria.name && !simpleName.toLowerCase().includes(criteria.name.toLowerCase())) { continue; } if (criteria.category && tool.category !== criteria.category) { continue; } if (criteria.serverId && serverId !== criteria.serverId) { continue; } if (criteria.serverCategory && serverCategory !== criteria.serverCategory) { continue; } if (criteria.implemented !== undefined && tool.isImplemented !== criteria.implemented) { continue; } if (criteria.permissions && criteria.permissions.length > 0) { const toolPermissions = tool.permissions || []; const hasAllPermissions = criteria.permissions.every(p => toolPermissions.includes(p)); if (!hasAllPermissions) { continue; } } tools.push({ name: simpleName, qualifiedName, description: tool.description, server: serverId, serverTitle, category: tool.category, serverCategory, permissions: tool.permissions, isImplemented: tool.isImplemented }); } return tools.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName)); } /** * Get detailed information about a specific tool * * @param toolName Tool name (simple or qualified) * @returns Detailed tool information or undefined if not found */ getToolInfo(toolName) { const registration = this.tools.get(toolName); if (!registration) { return undefined; } const server = this.servers.get(registration.serverId); if (!server) { return undefined; } return { tool: registration.tool, server, registration }; } /** * Get registry statistics * * @returns Comprehensive registry statistics */ getStats() { const toolsByCategory = {}; const serversByCategory = {}; // Count tools by category for (const registration of this.tools.values()) { const category = registration.tool.category || 'uncategorized'; toolsByCategory[category] = (toolsByCategory[category] || 0) + 1; } // Count servers by category for (const server of this.servers.values()) { const category = server.category || 'uncategorized'; serversByCategory[category] = (serversByCategory[category] || 0) + 1; } return { totalServers: this.servers.size, totalTools: new Set(Array.from(this.tools.values()).map(r => r.qualifiedName)).size, toolsByCategory, serversByCategory, executionCount: this.executionCount, averageExecutionTime: this.executionCount > 0 ? this.totalExecutionTime / this.executionCount : 0, errorRate: this.executionCount > 0 ? this.errorCount / this.executionCount : 0 }; } /** * Unregister a server and all its tools * * @param serverId Server ID to unregister * @returns Whether server was found and removed */ unregisterServer(serverId) { const server = this.servers.get(serverId); if (!server) { return false; } // Remove all tools from this server const toolsToRemove = []; for (const [name, registration] of this.tools) { if (registration.serverId === serverId) { toolsToRemove.push(name); } } for (const toolName of toolsToRemove) { this.tools.delete(toolName); } // Remove the server this.servers.delete(serverId); console.log(`[MCPRegistry] Unregistered server '${serverId}' and ${toolsToRemove.length} tools`); return true; } /** * Clear all servers and tools */ clear() { this.servers.clear(); this.tools.clear(); this.executionCount = 0; this.totalExecutionTime = 0; this.errorCount = 0; console.log('[MCPRegistry] Cleared all servers and tools'); } /** * Create timeout promise wrapper * * @param promise Promise to wrap * @param timeoutMs Timeout in milliseconds * @param timeoutMessage Error message for timeout * @returns Promise that rejects on timeout */ createTimeoutPromise(promise, timeoutMs, timeoutMessage) { return Promise.race([ promise, new Promise((_, reject) => { setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); }) ]); } /** * Update execution metrics * * @param executionTime Execution time in milliseconds * @param success Whether execution was successful */ updateExecutionMetrics(executionTime, success) { this.executionCount++; this.totalExecutionTime += executionTime; if (!success) { this.errorCount++; } } } /** * Default registry instance * Can be used across the application for consistent tool management */ export const defaultToolRegistry = new MCPToolRegistry(); /** * Utility function to register server with default registry * * @param server MCP server to register */ export async function registerServer(server) { return defaultToolRegistry.registerServer(server); } /** * Utility function to execute tool with default registry * * @param toolName Tool name to execute * @param params Tool parameters * @param context Execution context * @param options Execution options * @returns Tool execution result */ export async function executeTool(toolName, params, context, options) { return defaultToolRegistry.executeTool(toolName, params, context, options); } /** * Utility function to list tools with default registry * * @param criteria Search criteria * @returns Array of tool information */ export function listTools(criteria) { return defaultToolRegistry.listTools(criteria); }