UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and

586 lines (585 loc) 24.1 kB
/** * MCP Tool Registry - Extended Registry with Tool Management * Updated to match industry standard camelCase interfaces */ import { MCPRegistry } from "./registry.js"; import { registryLogger } from "../utils/logger.js"; import { randomUUID } from "crypto"; import { shouldDisableBuiltinTools } from "../utils/toolUtils.js"; import { directAgentTools } from "../agent/directTools.js"; import { detectCategory, createMCPServerInfo } from "../utils/mcpDefaults.js"; export class MCPToolRegistry extends MCPRegistry { tools = new Map(); toolImpls = new Map(); // Store actual tool implementations toolExecutionStats = new Map(); builtInServerInfos = []; // DIRECT storage for MCPServerInfo constructor() { super(); // 🔧 CONDITIONAL: Only auto-register direct tools if not disabled via configuration if (!shouldDisableBuiltinTools()) { this.registerDirectTools(); } else { registryLogger.debug("Built-in direct tools disabled via configuration"); } } /** * Register all direct tools from directAgentTools */ registerDirectTools() { registryLogger.debug("Auto-registering direct tools..."); for (const [toolName, toolDef] of Object.entries(directAgentTools)) { const toolId = `direct.${toolName}`; const toolInfo = { name: toolName, description: toolDef.description || `Direct tool: ${toolName}`, inputSchema: {}, serverId: "direct", category: detectCategory({ isBuiltIn: true, serverId: "direct" }), }; this.tools.set(toolId, toolInfo); this.toolImpls.set(toolId, { execute: async (params, context) => { try { // Direct tools from AI SDK expect their specific parameter structure // Each tool validates its own parameters, so we safely pass them through const result = await toolDef.execute(params, { toolCallId: context?.sessionId || "unknown", messages: [], }); // Return the result wrapped in our standard format return { success: true, data: result, metadata: { toolName, serverId: "direct", executionTime: 0, }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), metadata: { toolName, serverId: "direct", executionTime: 0, }, }; } }, description: toolDef.description, inputSchema: {}, }); registryLogger.debug(`Registered direct tool: ${toolName} as ${toolId}`); } registryLogger.debug(`Auto-registered ${Object.keys(directAgentTools).length} direct tools`); } async registerServer(serverInfoOrId, serverConfigOrContext, context) { // Handle both signatures for backward compatibility let serverInfo; let finalContext; if (typeof serverInfoOrId === "string") { // Legacy signature: registerServer(serverId, serverConfig, context) const serverId = serverInfoOrId; finalContext = context; // Convert legacy call to MCPServerInfo format using smart defaults serverInfo = createMCPServerInfo({ id: serverId, name: serverId, tools: [], isExternal: true, }); } else { // New signature: registerServer(serverInfo, context) serverInfo = serverInfoOrId; finalContext = serverConfigOrContext; } const serverId = serverInfo.id; registryLogger.info(`Registering MCPServerInfo directly: ${serverId}`); // Use MCPServerInfo.tools array directly - ZERO conversions! const toolsObject = {}; for (const tool of serverInfo.tools) { toolsObject[tool.name] = { execute: tool.execute || (async () => { throw new Error(`Tool ${tool.name} has no execute function`); }), description: tool.description, inputSchema: tool.inputSchema, category: detectCategory({ existingCategory: serverInfo.metadata?.category, serverId: serverInfo.id, }), }; } const plugin = { metadata: { name: serverInfo.name, description: serverInfo.description, category: detectCategory({ existingCategory: serverInfo.metadata?.category, serverId: serverInfo.id, }), }, tools: toolsObject, configuration: {}, }; // Call the parent register method this.register(plugin); // Use MCPServerInfo.tools array directly - ZERO conversions! const tools = serverInfo.tools; registryLogger.debug(`Registering ${tools.length} tools for server ${serverId}:`, tools.map((t) => t.name)); for (const tool of tools) { // For custom tools, use just the tool name to avoid redundant serverId.toolName format // For other tools, use fully-qualified serverId.toolName to avoid collisions const isCustomTool = serverId.startsWith("custom-tool-"); const toolId = isCustomTool ? tool.name : `${serverId}.${tool.name}`; const toolInfo = { name: tool.name, description: tool.description, inputSchema: tool.inputSchema, outputSchema: undefined, // MCPServerInfo.tools doesn't have outputSchema serverId, category: detectCategory({ existingCategory: serverInfo.metadata?.category, serverId: serverInfo.id, }), permissions: [], // MCPServerInfo.tools doesn't have permissions }; // Register only with fully-qualified toolId to avoid collisions this.tools.set(toolId, toolInfo); // Store the actual tool implementation for execution using toolId as key this.toolImpls.set(toolId, { execute: tool.execute || (async () => { throw new Error(`Tool ${tool.name} has no execute function`); }), description: tool.description, inputSchema: tool.inputSchema, category: detectCategory({ existingCategory: serverInfo.metadata?.category, serverId: serverInfo.id, }), }); registryLogger.debug(`Registered tool '${tool.name}' with execute function:`, typeof tool.execute); } // Store MCPServerInfo directly - NO recreation needed! if (tools.length > 0) { const category = detectCategory({ existingCategory: serverInfo.metadata?.category, serverId: serverInfo.id, }); // Only store in builtInServerInfos if it's a real in-memory MCP server // Do NOT create fake servers for built-in direct tools if (category === "in-memory") { // Use the original MCPServerInfo directly - ZERO conversions! this.builtInServerInfos.push(serverInfo); registryLogger.debug(`Added ${category} server to builtInServerInfos: ${serverId} with ${tools.length} tools`); } } } /** * Execute a tool with enhanced context and automatic result wrapping * * This method handles both raw return values and ToolResult objects: * - Raw values (primitives, objects) are automatically wrapped in ToolResult format * - Existing ToolResult objects are enhanced with execution metadata * - All results include execution timing and context information * * @param toolName - Name of the tool to execute * @param args - Parameters to pass to the tool execution function * @param context - Execution context with session, user, and environment info * @returns Promise resolving to ToolResult object with data, metadata, and usage info * @throws Error if tool is not found or execution fails * * @example * ```typescript * // Tool that returns raw value * const result = await toolRegistry.executeTool("calculator", { a: 5, b: 3, op: "add" }); * // result.data === 8, result.metadata contains execution info * * // Tool that returns ToolResult * const result = await toolRegistry.executeTool("complexTool", { input: "test" }); * // result is enhanced ToolResult with additional metadata * ``` */ async executeTool(toolName, args, context) { const startTime = Date.now(); try { registryLogger.info(`Executing tool: ${toolName}`); // Try to find the tool by fully-qualified name first let tool = this.tools.get(toolName); // If not found, search for tool by name across all entries (for backward compatibility) let toolId = toolName; if (!tool) { for (const [candidateToolId, toolInfo] of this.tools.entries()) { if (toolInfo.name === toolName) { tool = toolInfo; toolId = candidateToolId; break; } } } if (!tool) { throw new Error(`Tool '${toolName}' not found in registry`); } // Create execution context if not provided const execContext = { sessionId: context?.sessionId || randomUUID(), userId: context?.userId, ...context, }; // Get the tool implementation using the resolved toolId const toolImpl = this.toolImpls.get(toolId); registryLogger.debug(`Looking for tool '${toolName}' (toolId: '${toolId}'), found: ${!!toolImpl}, type: ${typeof toolImpl?.execute}`); registryLogger.debug(`Available tools:`, Array.from(this.toolImpls.keys())); if (!toolImpl || typeof toolImpl?.execute !== "function") { throw new Error(`Tool '${toolName}' implementation not found or not executable`); } // Execute the actual tool registryLogger.debug(`Executing tool '${toolName}' with args:`, args); const toolResult = await toolImpl.execute(args, execContext); // Properly wrap raw results in ToolResult format let result; // Check if result is already a ToolResult object if (toolResult && typeof toolResult === "object" && "success" in toolResult && typeof toolResult.success === "boolean") { // Result is already a ToolResult, enhance with metadata const toolResultObj = toolResult; result = { ...toolResultObj, usage: { ...(toolResultObj.usage || {}), executionTime: Date.now() - startTime, }, metadata: { ...(toolResultObj.metadata || {}), toolName, serverId: tool.serverId, sessionId: execContext.sessionId, executionTime: Date.now() - startTime, }, }; } else { // Result is a raw value, wrap it in ToolResult format result = { success: true, data: toolResult, usage: { executionTime: Date.now() - startTime, }, metadata: { toolName, serverId: tool.serverId, sessionId: execContext.sessionId, executionTime: Date.now() - startTime, }, }; } // Update statistics const duration = Date.now() - startTime; this.updateStats(toolName, duration); registryLogger.debug(`Tool '${toolName}' executed successfully in ${duration}ms`); return result; } catch (error) { registryLogger.error(`Tool execution failed: ${toolName}`, error); // Return error in ToolResult format const errorResult = { success: false, data: null, error: error instanceof Error ? error.message : String(error), usage: { executionTime: Date.now() - startTime, }, metadata: { toolName, sessionId: context?.sessionId, }, }; return errorResult; } } async listTools(filterOrContext) { // FIXED: Return unique tools (avoid duplicates from dual registration) const uniqueTools = new Map(); for (const tool of this.tools.values()) { const key = `${tool.serverId || "unknown"}.${tool.name}`; if (!uniqueTools.has(key)) { uniqueTools.set(key, tool); } } let result = Array.from(uniqueTools.values()); // Determine if parameter is a filter object or just context let filter; if (filterOrContext) { // Check if it's a filter object (has filter-specific properties) or just context if ("sessionId" in filterOrContext || "userId" in filterOrContext) { // It's an ExecutionContext, treat as no filter filter = undefined; } else { // It's a filter object filter = filterOrContext; } } // Apply filters if provided if (filter) { if (filter.category) { result = result.filter((tool) => tool.category === filter.category); } if (filter.serverId) { result = result.filter((tool) => tool.serverId === filter.serverId); } if (filter.serverCategory) { result = result.filter((tool) => { const server = this.get(tool.serverId || ""); return server?.metadata?.category === filter.serverCategory; }); } if (filter.permissions && filter.permissions.length > 0) { result = result.filter((tool) => { const toolPermissions = tool.permissions || []; return filter.permissions.some((perm) => toolPermissions.includes(perm)); }); } } registryLogger.debug(`Listed ${result.length} unique tools (${filter ? "filtered" : "unfiltered"})`); return result; } /** * Get tool information with server details */ getToolInfo(toolName) { // Try to find the tool by fully-qualified name first let tool = this.tools.get(toolName); // If not found, search for tool by name across all entries (for backward compatibility) if (!tool) { for (const toolInfo of this.tools.values()) { if (toolInfo.name === toolName) { tool = toolInfo; break; } } } if (!tool) { return undefined; } return { tool, server: { id: tool.serverId || "unknown-server", }, }; } /** * Update execution statistics */ updateStats(toolName, executionTime) { const stats = this.toolExecutionStats.get(toolName) || { count: 0, totalTime: 0, }; stats.count += 1; stats.totalTime += executionTime; this.toolExecutionStats.set(toolName, stats); } /** * Get execution statistics */ getExecutionStats() { const result = {}; for (const [toolName, stats] of this.toolExecutionStats.entries()) { result[toolName] = { count: stats.count, totalTime: stats.totalTime, averageTime: stats.totalTime / stats.count, }; } return result; } /** * Clear execution statistics */ clearStats() { this.toolExecutionStats.clear(); } /** * Get built-in servers * @returns Array of MCPServerInfo for built-in tools */ getBuiltInServerInfos() { return this.builtInServerInfos; } /** * Get tools by category */ getToolsByCategory(category) { // Return unique tools by fully-qualified toolId const uniqueTools = new Map(); for (const [toolId, tool] of this.tools.entries()) { if (tool.category === category && !uniqueTools.has(toolId)) { uniqueTools.set(toolId, tool); } } return Array.from(uniqueTools.values()); } /** * Check if tool exists */ hasTool(toolName) { // Check by fully-qualified name first, then fallback to first matching tool name if (this.tools.has(toolName)) { return true; } for (const tool of this.tools.values()) { if (tool.name === toolName) { return true; } } return false; } /** * Register a tool with implementation directly * This is used for external MCP server tools */ registerTool(toolId, toolInfo, toolImpl) { registryLogger.debug(`Registering tool: ${toolId}`); // Import validation functions synchronously - they are pure functions let validateTool; let isToolNameAvailable; let suggestToolNames; try { // Try ES module import first const toolRegistrationModule = require("../sdk/toolRegistration.js"); ({ validateTool, isToolNameAvailable, suggestToolNames } = toolRegistrationModule); } catch (error) { // Fallback: skip validation if import fails (graceful degradation) registryLogger.warn("Tool validation module not available, skipping advanced validation", { error: error instanceof Error ? error.message : String(error), }); // Create minimal validation functions validateTool = () => { }; // No-op isToolNameAvailable = () => true; // Allow all names suggestToolNames = () => ["alternative_tool"]; } // Check if tool name is available (not reserved) if (!isToolNameAvailable(toolId)) { const suggestions = suggestToolNames(toolId); registryLogger.error(`Tool registration failed for ${toolId}: Name not available`); throw new Error(`Tool name '${toolId}' is not available (reserved or invalid format). ` + `Suggested alternatives: ${suggestions.slice(0, 3).join(", ")}`); } // Create a simplified tool object for validation const toolForValidation = { description: toolInfo.description || "", execute: async () => "", parameters: undefined, metadata: { category: toolInfo.category, serverId: toolInfo.serverId, }, }; // Use comprehensive validation logic try { validateTool(toolId, toolForValidation); } catch (error) { registryLogger.error(`Tool registration failed for ${toolId}:`, error instanceof Error ? error.message : String(error)); throw error; } this.tools.set(toolId, toolInfo); this.toolImpls.set(toolId, toolImpl); registryLogger.debug(`Successfully registered tool: ${toolId}`); } /** * Remove a tool */ removeTool(toolName) { // Remove by fully-qualified name first, then fallback to first matching tool name let removed = false; if (this.tools.has(toolName)) { this.tools.delete(toolName); this.toolExecutionStats.delete(toolName); registryLogger.info(`Removed tool: ${toolName}`); removed = true; } else { // Remove all tools with matching name for (const [toolId, tool] of Array.from(this.tools.entries())) { if (tool.name === toolName) { this.tools.delete(toolId); this.toolExecutionStats.delete(toolId); registryLogger.info(`Removed tool: ${toolId}`); removed = true; } } } return removed; } /** * Get tool count */ getToolCount() { return this.tools.size; } /** * Get comprehensive statistics */ getStats() { const servers = this.list(); // Get all registered servers const allTools = Array.from(this.tools.values()); // Count servers by category const serversByCategory = {}; for (const server of servers) { const category = server.metadata?.category || "uncategorized"; serversByCategory[category] = (serversByCategory[category] || 0) + 1; } // Count tools by category const toolsByCategory = {}; for (const tool of allTools) { const category = tool.category || "uncategorized"; toolsByCategory[category] = (toolsByCategory[category] || 0) + 1; } return { totalServers: servers.length, totalTools: allTools.length, serversByCategory, toolsByCategory, executionStats: this.getExecutionStats(), }; } /** * Unregister a server */ unregisterServer(serverId) { // Remove all tools for this server const removedTools = []; for (const [toolId, tool] of this.tools.entries()) { if (tool.serverId === serverId) { this.tools.delete(toolId); removedTools.push(toolId); } } // Remove from builtInServerInfos storage const originalLength = this.builtInServerInfos.length; this.builtInServerInfos = this.builtInServerInfos.filter((server) => server.id !== serverId); const removedFromBuiltIn = originalLength > this.builtInServerInfos.length; // Remove from parent registry const removed = this.unregister(serverId); registryLogger.info(`Unregistered server ${serverId}, removed ${removedTools.length} tools${removedFromBuiltIn ? " and server from builtInServerInfos" : ""}`); return removed; } } // Create default instance export const toolRegistry = new MCPToolRegistry(); export const defaultToolRegistry = toolRegistry;