Skip to main content
Glama
unified-tool-manager.tsโ€ข23.9 kB
/** * Unified Tool Manager * * Combines MCP tools and internal tools into a single interface for LLM services. * Handles tool routing, execution, and conflict resolution between different tool sources. */ import { logger } from '../../logger/index.js'; import { MCPManager } from '../../mcp/manager.js'; import { InternalToolManager } from './manager.js'; import { ToolExecutionResult } from '../../mcp/types.js'; import { isInternalToolName } from './types.js'; import { EventManager } from '../../events/event-manager.js'; import { SessionEvents } from '../../events/event-types.js'; import { v4 as uuidv4 } from 'uuid'; /** * Configuration for the unified tool manager */ export interface UnifiedToolManagerConfig { /** * Whether to enable internal tools * @default true */ enableInternalTools?: boolean; /** * Whether to enable MCP tools * @default true */ enableMcpTools?: boolean; /** * How to handle tool name conflicts * @default 'prefix-internal' */ conflictResolution?: 'prefix-internal' | 'prefer-internal' | 'prefer-mcp' | 'error'; /** * Timeout for tool execution in milliseconds * @default 30000 */ executionTimeout?: number; /** * Operating mode - affects which tools are exposed * - 'cli': Only search tools exposed to Cipher's LLM (background tools still executable) * - 'default': Only ask_cipher tool exposed to external MCP clients * - 'aggregator': All tools exposed to external MCP clients * - 'api': Similar to CLI mode * @default 'default' */ mode?: 'cli' | 'default' | 'aggregator' | 'api'; } /** * Combined tool information for LLM services */ export interface CombinedToolSet { [toolName: string]: { description: string; parameters: any; source: 'internal' | 'mcp'; }; } /** * Unified Tool Manager that combines MCP and internal tools */ export class UnifiedToolManager { private mcpManager: MCPManager; private internalToolManager: InternalToolManager; private config: Required<UnifiedToolManagerConfig>; private eventManager?: EventManager; private toolsAlreadyLogged = false; private embeddingManager?: any; // Reference to embedding manager for status checking constructor( mcpManager: MCPManager, internalToolManager: InternalToolManager, config: UnifiedToolManagerConfig = {} ) { this.mcpManager = mcpManager; this.internalToolManager = internalToolManager; this.config = { enableInternalTools: true, enableMcpTools: true, conflictResolution: 'prefix-internal', executionTimeout: 30000, mode: 'default', ...config, }; } /** * Set the event manager for emitting tool execution events */ setEventManager(eventManager: EventManager): void { this.eventManager = eventManager; } /** * Set the embedding manager for checking embedding status */ setEmbeddingManager(embeddingManager: any): void { this.embeddingManager = embeddingManager; } /** * Check if embeddings are disabled globally */ private areEmbeddingsDisabled(): boolean { // Simple check: if embedding manager doesn't have available embeddings, disable tools if (this.embeddingManager) { return !this.embeddingManager.hasAvailableEmbeddings(); } return true; // No embedding manager means embeddings disabled } /** * Check if a tool is embedding-related and should be excluded when embeddings are disabled */ private isEmbeddingRelatedTool(toolName: string): boolean { const embeddingToolPatterns = [ 'extract_and_operate_memory', 'search_memory', 'search_reasoning', 'store_reasoning_memory', 'extract_reasoning_steps', 'evaluate_reasoning', 'memory_operation', 'knowledge_search', 'vector_search', 'embedding', 'similarity', 'cipher_extract_and_operate_memory', 'cipher_search_memory', 'cipher_store_reasoning_memory', 'cipher_extract_reasoning_steps', 'cipher_evaluate_reasoning', 'cipher_search_reasoning_patterns', // Workspace memory tools still need embeddings 'cipher_workspace_search', 'cipher_workspace_store', 'workspace_search', 'workspace_store', ]; return embeddingToolPatterns.some(pattern => toolName.toLowerCase().includes(pattern.toLowerCase()) ); } /** * Get all available tools from both sources * Filters tools based on mode: * - CLI mode: Only search tools + MCP tools (background tools excluded from agent access) * - Default MCP mode: Only ask_cipher tool * - Aggregator MCP mode: All tools */ async getAllTools(): Promise<CombinedToolSet> { if (!this.toolsAlreadyLogged && this.config.mode !== 'aggregator') { logger.debug('UnifiedToolManager: Getting all tools'); } const combinedTools: CombinedToolSet = {}; logger.debug('UnifiedToolManager: Tools config args', this.config); try { // MCP Default mode: Only expose ask_cipher tool if (this.config.mode === 'default') { // TODO: Add ask_cipher tool implementation combinedTools['ask_cipher'] = { description: 'Ask Cipher to perform tasks using its internal tools and capabilities', parameters: { type: 'object', properties: { query: { type: 'string', description: 'The task or question to ask Cipher', }, }, required: ['query'], }, source: 'internal', }; logger.debug('UnifiedToolManager: Default MCP mode - only ask_cipher tool exposed'); return combinedTools; } // Get MCP tools if enabled (for CLI and aggregator modes) if ( this.config.enableMcpTools && (this.config.mode === 'cli' || this.config.mode === 'aggregator' || this.config.mode === 'api') ) { if (!this.toolsAlreadyLogged && this.config.mode !== 'aggregator') { logger.debug('UnifiedToolManager: Loading MCP tools'); } try { const mcpTools = await this.mcpManager.getAllTools(); if (!this.toolsAlreadyLogged && this.config.mode !== 'aggregator') { logger.debug(`UnifiedToolManager: Retrieved ${Object.keys(mcpTools).length} MCP tools`); } for (const [toolName, tool] of Object.entries(mcpTools)) { combinedTools[toolName] = { description: tool.description, parameters: tool.parameters, source: 'mcp', }; } if (!this.toolsAlreadyLogged && this.config.mode !== 'aggregator') { logger.debug(`UnifiedToolManager: Loaded ${Object.keys(mcpTools).length} MCP tools`); } } catch (error) { logger.warn('UnifiedToolManager: Failed to load MCP tools', { error }); } } // Get internal tools if enabled if (this.config.enableInternalTools) { if (!this.toolsAlreadyLogged && this.config.mode !== 'aggregator') { logger.debug('UnifiedToolManager: Loading internal tools'); } try { const internalTools = this.internalToolManager.getAllTools(); if (!this.toolsAlreadyLogged && this.config.mode !== 'aggregator') { logger.debug( `UnifiedToolManager: Retrieved ${Object.keys(internalTools).length} internal tools` ); } for (const [toolName, tool] of Object.entries(internalTools)) { // Check if embeddings are disabled and this is an embedding-related tool if (this.areEmbeddingsDisabled() && this.isEmbeddingRelatedTool(toolName)) { logger.debug( `UnifiedToolManager: Skipping embedding-related tool '${toolName}' - embeddings are disabled` ); continue; } // Mode-specific tool filtering if (this.config.mode === 'cli') { // In CLI mode, only search-related tools are exposed to the agent. // Background tools are executed after the AI response and are not directly callable. const isAgentAccessible = (tool.agentAccessible ?? true) && // Default to true if not specified !toolName.includes('extract_and_operate_memory') && !toolName.includes('store_reasoning'); if (!isAgentAccessible) { continue; } } else if (this.config.mode === 'aggregator') { // Aggregator mode: Expose ALL tools (no filtering) } else { // Default/API modes: Skip background tools that are not agent-accessible if (tool.agentAccessible === false) { logger.debug( `UnifiedToolManager: Skipping internal-only tool '${toolName}' in ${ this.config.mode } mode` ); continue; } } const normalizedName = toolName.startsWith('cipher_') ? toolName : `cipher_${toolName}`; // Handle conflicts if (combinedTools[normalizedName]) { const conflictHandled = this.handleToolConflict(normalizedName, tool, combinedTools); if (!conflictHandled) continue; } combinedTools[normalizedName] = { description: tool.description, parameters: tool.parameters, source: 'internal', }; } // Logging for different modes if (this.config.mode === 'cli') { const searchToolCount = Object.keys(combinedTools).filter( name => combinedTools[name]?.source === 'internal' && (name.includes('search') || name.includes('memory_') || name.includes('knowledge_')) ).length; logger.debug( `UnifiedToolManager: CLI mode - ${searchToolCount} search tools accessible to LLM` ); } else if (this.config.mode !== 'aggregator') { logger.debug( `UnifiedToolManager: Loaded ${Object.keys(internalTools).length} internal tools (${Object.keys(combinedTools).filter(name => combinedTools[name]?.source === 'internal').length} agent-accessible)` ); } } catch (error) { logger.warn('UnifiedToolManager: Failed to load internal tools', { error }); } } if (!this.toolsAlreadyLogged && this.config.mode !== 'aggregator') { logger.debug( `UnifiedToolManager: Combined tools loaded successfully (mode: ${this.config.mode})` ); this.toolsAlreadyLogged = true; } return combinedTools; } catch (error) { logger.error('UnifiedToolManager: Failed to get all tools', { error }); throw error; } } /** * Execute a tool by routing to the appropriate manager */ async executeTool(toolName: string, args: any, sessionId?: string): Promise<ToolExecutionResult> { const executionId = uuidv4(); const startTime = Date.now(); const toolType = this.config.enableInternalTools && isInternalToolName(toolName) ? 'internal' : 'mcp'; // Emit tool execution started event if (this.eventManager && sessionId) { logger.debug('UnifiedToolManager: Emitting tool execution started event', { toolName, toolType, sessionId, executionId, argsKeys: args ? Object.keys(args) : [], args: args, }); this.eventManager.emitSessionEvent(sessionId, SessionEvents.TOOL_EXECUTION_STARTED, { toolName, toolType, sessionId, executionId, timestamp: startTime, args: args, // Include tool arguments }); } try { logger.debug(`UnifiedToolManager: Executing tool '${toolName}'`, { toolName, hasArgs: !!args, sessionId, executionId, }); let result: ToolExecutionResult; // Check if embeddings are disabled and this is an embedding-related tool if (this.areEmbeddingsDisabled() && this.isEmbeddingRelatedTool(toolName)) { logger.warn( `UnifiedToolManager: Blocking execution of embedding-related tool '${toolName}' - embeddings are disabled` ); throw new Error( `Tool '${toolName}' is not available - embeddings are disabled for this session` ); } // Determine which manager should handle this tool if (this.config.enableInternalTools && isInternalToolName(toolName)) { // Internal tool execution if (!this.internalToolManager.isInternalTool(toolName)) { throw new Error(`Internal tool '${toolName}' not found`); } logger.debug(`UnifiedToolManager: Routing '${toolName}' to internal tool manager`); result = await this.internalToolManager.executeTool(toolName, args); } else if (this.config.enableMcpTools) { // MCP tool execution logger.debug(`UnifiedToolManager: Routing '${toolName}' to MCP manager`); result = await this.mcpManager.executeTool(toolName, args); } else { throw new Error(`Tool '${toolName}' not available - no suitable manager enabled`); } // Emit tool execution completed event if (this.eventManager && sessionId) { this.eventManager.emitSessionEvent(sessionId, SessionEvents.TOOL_EXECUTION_COMPLETED, { toolName, toolType, sessionId, executionId, duration: Date.now() - startTime, success: true, result: result, timestamp: Date.now(), }); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const duration = Date.now() - startTime; // Emit tool execution failed event if (this.eventManager && sessionId) { this.eventManager.emitSessionEvent(sessionId, SessionEvents.TOOL_EXECUTION_FAILED, { toolName, toolType, sessionId, executionId, error: errorMessage, duration, timestamp: Date.now(), }); } logger.error(`UnifiedToolManager: Tool execution failed for '${toolName}'`, { toolName, error: errorMessage, sessionId, executionId, duration, }); throw error; } } /** * Execute a tool without triggering redundant tool loading (for background operations) * This method bypasses the normal tool loading process when tools are already loaded */ async executeToolWithoutLoading( toolName: string, args: any, sessionId?: string ): Promise<ToolExecutionResult> { const executionId = uuidv4(); const startTime = Date.now(); const toolType = this.config.enableInternalTools && isInternalToolName(toolName) ? 'internal' : 'mcp'; // Emit tool execution started event if (this.eventManager && sessionId) { logger.debug('UnifiedToolManager: Emitting tool execution started event', { toolName, toolType, sessionId, executionId, argsKeys: args ? Object.keys(args) : [], args: args, }); this.eventManager.emitSessionEvent(sessionId, SessionEvents.TOOL_EXECUTION_STARTED, { toolName, toolType, sessionId, executionId, timestamp: startTime, args: args, // Include tool arguments }); } try { logger.debug(`UnifiedToolManager: Executing tool '${toolName}' (without loading)`, { toolName, hasArgs: !!args, sessionId, executionId, }); let result: ToolExecutionResult; // Determine which manager should handle this tool if (this.config.enableInternalTools && isInternalToolName(toolName)) { // Internal tool execution if (!this.internalToolManager.isInternalTool(toolName)) { throw new Error(`Internal tool '${toolName}' not found`); } logger.debug( `UnifiedToolManager: Routing '${toolName}' to internal tool manager (without loading)` ); result = await this.internalToolManager.executeTool(toolName, args); } else if (this.config.enableMcpTools) { // MCP tool execution logger.debug(`UnifiedToolManager: Routing '${toolName}' to MCP manager (without loading)`); result = await this.mcpManager.executeTool(toolName, args); } else { throw new Error(`Tool '${toolName}' not available - no suitable manager enabled`); } // Emit tool execution completed event if (this.eventManager && sessionId) { this.eventManager.emitSessionEvent(sessionId, SessionEvents.TOOL_EXECUTION_COMPLETED, { toolName, toolType, sessionId, executionId, duration: Date.now() - startTime, success: true, result: result, timestamp: Date.now(), }); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const duration = Date.now() - startTime; // Emit tool execution failed event if (this.eventManager && sessionId) { this.eventManager.emitSessionEvent(sessionId, SessionEvents.TOOL_EXECUTION_FAILED, { toolName, toolType, sessionId, executionId, error: errorMessage, duration, timestamp: Date.now(), }); } logger.error( `UnifiedToolManager: Tool execution failed for '${toolName}' (without loading)`, { toolName, error: errorMessage, sessionId, executionId, duration, } ); throw error; } } /** * Check if a background tool exists (for internal execution, not agent access) */ isBackgroundToolAvailable(toolName: string): boolean { try { if (this.config.enableInternalTools && isInternalToolName(toolName)) { const tool = this.internalToolManager.getTool(toolName); return !!tool; } return false; } catch { return false; } } /** * Check if a tool is available (to agents) based on current mode */ async isToolAvailable(toolName: string): Promise<boolean> { try { // Default MCP mode: Only ask_cipher tool available if (this.config.mode === 'default') { return toolName === 'ask_cipher'; } if (this.config.enableInternalTools && isInternalToolName(toolName)) { // Check if tool exists const tool = this.internalToolManager.getTool(toolName); if (!tool) return false; // Mode-specific availability if (this.config.mode === 'cli') { // CLI mode: Only search tools accessible to LLM const isSearchTool = toolName.includes('search') || toolName.includes('memory_') || toolName.includes('knowledge_') || toolName.includes('vector_') || toolName === 'extract_and_operate_memory' || toolName === 'cipher_extract_and_operate_memory'; return isSearchTool && tool.agentAccessible !== false; } else if (this.config.mode === 'aggregator') { // Aggregator mode: All tools available return true; } else { // API mode: Only agent-accessible tools return tool.agentAccessible !== false; } } else if ( this.config.enableMcpTools && (this.config.mode === 'cli' || this.config.mode === 'aggregator' || this.config.mode === 'api') ) { const mcpTools = await this.mcpManager.getAllTools(); return toolName in mcpTools; } return false; } catch (error) { logger.error(`UnifiedToolManager: Error checking tool availability for '${toolName}'`, { error, }); return false; } } /** * Get tool source (internal or mcp) for agent-accessible tools */ async getToolSource(toolName: string): Promise<'internal' | 'mcp' | null> { try { // Default MCP mode: Only ask_cipher tool if (this.config.mode === 'default') { return toolName === 'ask_cipher' ? 'internal' : null; } if (this.config.enableInternalTools && isInternalToolName(toolName)) { // Check if tool exists const tool = this.internalToolManager.getTool(toolName); if (!tool) return null; // Mode-specific source determination if (this.config.mode === 'cli') { // CLI mode: Only search tools accessible const isSearchTool = toolName.includes('search') || toolName.includes('memory_') || toolName.includes('knowledge_') || toolName.includes('vector_') || toolName === 'extract_and_operate_memory' || toolName === 'cipher_extract_and_operate_memory'; return isSearchTool && tool.agentAccessible !== false ? 'internal' : null; } else if (this.config.mode === 'aggregator') { // Aggregator mode: All tools available return 'internal'; } else { // API mode: Only agent-accessible tools return tool.agentAccessible !== false ? 'internal' : null; } } else if ( this.config.enableMcpTools && (this.config.mode === 'cli' || this.config.mode === 'aggregator' || this.config.mode === 'api') ) { const mcpTools = await this.mcpManager.getAllTools(); return toolName in mcpTools ? 'mcp' : null; } return null; } catch (error) { logger.error(`UnifiedToolManager: Error determining tool source for '${toolName}'`, { error, }); return null; } } /** * Get tools formatted for specific LLM providers */ async getToolsForProvider( provider: | 'openai' | 'anthropic' | 'openrouter' | 'aws' | 'azure' | 'qwen' | 'gemini' | 'deepseek' ): Promise<any[]> { logger.info(`UnifiedToolManager: Getting tools for provider: ${provider}`); const allTools = await this.getAllTools(); logger.info(`UnifiedToolManager: Got ${Object.keys(allTools).length} total tools`); switch (provider) { case 'openai': case 'openrouter': logger.info('UnifiedToolManager: Formatting tools for OpenAI'); return this.formatToolsForOpenAI(allTools); case 'qwen': return this.formatToolsForOpenAI(allTools); case 'gemini': logger.info('UnifiedToolManager: Formatting tools for Gemini'); return this.formatToolsForGemini(allTools); case 'anthropic': logger.info('UnifiedToolManager: Formatting tools for Anthropic'); return this.formatToolsForAnthropic(allTools); case 'aws': logger.info('UnifiedToolManager: Formatting tools for AWS (Anthropic-compatible)'); return this.formatToolsForAnthropic(allTools); // AWS Bedrock uses Anthropic-compatible format case 'azure': logger.info('UnifiedToolManager: Formatting tools for Azure (OpenAI-compatible)'); return this.formatToolsForOpenAI(allTools); // Azure OpenAI uses OpenAI-compatible format case 'deepseek': logger.info('UnifiedToolManager: Formatting tools for Deepseek'); return this.formatToolsForOpenAI(allTools); // Deepseek uses OpenAI-compatible format default: throw new Error(`Unsupported provider: ${provider}`); } } /** * Get manager statistics */ getStats(): { internalTools: any; mcpTools: any; config: Required<UnifiedToolManagerConfig>; } { return { internalTools: this.config.enableInternalTools ? this.internalToolManager.getManagerStats() : null, mcpTools: this.config.enableMcpTools ? { clientCount: this.mcpManager.getClients().size, failedConnections: Object.keys(this.mcpManager.getFailedConnections()).length, } : null, config: this.config, }; } /** * Handle tool name conflicts */ private handleToolConflict( toolName: string, _internalTool: any, _existingTools: CombinedToolSet ): boolean { switch (this.config.conflictResolution) { case 'prefix-internal': // Tool already has cipher_ prefix, so conflict shouldn't occur return true; case 'prefer-internal': logger.warn( `UnifiedToolManager: Tool conflict for '${toolName}', preferring internal tool` ); return true; case 'prefer-mcp': logger.warn(`UnifiedToolManager: Tool conflict for '${toolName}', preferring MCP tool`); return false; case 'error': throw new Error(`Tool name conflict: '${toolName}' exists in both MCP and internal tools`); default: return true; } } /** * Format tools for OpenAI/OpenRouter (function calling format) */ private formatToolsForOpenAI(tools: CombinedToolSet): any[] { return Object.entries(tools).map(([name, tool]) => ({ type: 'function', function: { name, description: tool.description, parameters: tool.parameters, }, })); } /** * Format tools for Anthropic (tool use format) */ private formatToolsForAnthropic(tools: CombinedToolSet): any[] { return Object.entries(tools).map(([name, tool]) => ({ name, description: tool.description, input_schema: tool.parameters, })); } /** * Format tools for Gemini (function calling format - same as OpenAI) */ private formatToolsForGemini(tools: CombinedToolSet): any[] { return Object.entries(tools).map(([name, tool]) => ({ type: 'function', function: { name, description: tool.description, parameters: tool.parameters, }, })); } }

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/campfirein/cipher'

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