Skip to main content
Glama

hypertool-mcp

enhanced.ts•50.6 kB
/** * Enhanced Hypertool MCP server with request routing capabilities */ import { MetaMCPServer } from "./base.js"; import { MetaMCPServerConfig, ServerInitOptions } from "./types.js"; import { RuntimeOptions } from "../types/runtime.js"; import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { IRequestRouter, RequestRouter } from "../router/index.js"; import { IToolDiscoveryEngine, ToolDiscoveryEngine, } from "../discovery/index.js"; import { IConnectionManager, ConnectionManager, ConnectionState, } from "../connection/index.js"; import { ExtensionAwareConnectionFactory } from "../connection/extensionFactory.js"; import { ExtensionManager } from "../extensions/manager.js"; import { APP_NAME, APP_TECHNICAL_NAME, ServerConfig } from "../config/index.js"; import { isDxtEnabledViaService, isConfigToolsMenuEnabledViaService, } from "../config/featureFlagService.js"; import ora from "ora"; // Helper to create conditional spinners function createSpinner(text: string, isStdio: boolean) { if (isStdio) { // Return a mock spinner that does nothing in stdio mode return { start: () => ({ succeed: () => {}, fail: () => {}, warn: () => {} }), succeed: () => {}, fail: () => {}, warn: () => {}, }; } return ora(text).start(); } import { createChildLogger } from "../utils/logging.js"; import { PersonaManager } from "../persona/manager.js"; const logger = createChildLogger({ module: "server/enhanced" }); // Note: All mcp-tools functionality now handled by ToolsetManager import { ToolsetManager, ToolsetChangeEvent } from "./tools/toolset/manager.js"; import { ConfigToolsManager } from "./tools/config-tools/manager.js"; import { createEnterConfigurationModeModule } from "./tools/common/enter-configuration-mode.js"; import { DiscoveredToolsChangedEvent } from "../discovery/types.js"; import { ToolDependencies, ToolModule } from "./tools/index.js"; import chalk from "chalk"; import { output } from "../utils/output.js"; import { theme, semantic } from "../utils/theme.js"; import { detectExternalMCPs, formatExternalMCPsMessage, } from "../scripts/shared/externalMcpDetector.js"; /** * Enhanced Hypertool MCP server with routing capabilities */ export class EnhancedMetaMCPServer extends MetaMCPServer { private requestRouter?: IRequestRouter; private discoveryEngine?: IToolDiscoveryEngine; private connectionManager?: IConnectionManager; private extensionManager?: ExtensionManager; private toolsetManager: ToolsetManager; private personaManager?: PersonaManager; private configToolsManager?: ConfigToolsManager; private configurationMode: boolean = false; private enterConfigurationModeTool?: ToolModule; private runtimeOptions?: RuntimeOptions; private configToolsMenuEnabled: boolean = true; private serverInitOptions?: ServerInitOptions; private currentServerConfigs: Record<string, ServerConfig> = {}; private isPersonaMode: boolean = false; constructor(config: MetaMCPServerConfig) { super(config); this.toolsetManager = new ToolsetManager(); } /** * Getter methods for safe access to shared objects */ getPersonaManager(): PersonaManager | undefined { return this.personaManager; } /** * Check if server is in persona mode */ getIsPersonaMode(): boolean { return this.isPersonaMode; } getConnectionManager(): IConnectionManager | undefined { return this.connectionManager; } getDiscoveryEngine(): IToolDiscoveryEngine | undefined { return this.discoveryEngine; } /** * Initialize PersonaManager early in boot sequence */ private async initializePersonaManager(): Promise<void> { logger.debug("Initializing PersonaManager..."); // Create persona manager with placeholder handlers that will be updated later this.personaManager = new PersonaManager({ getToolDiscoveryEngine: () => this.discoveryEngine, toolsetManager: this.toolsetManager, autoDiscover: true, validateOnActivation: true, persistState: false, // Disabled during early boot, will be enabled later stateKey: "hypertool-persona-runtime-state", bridgeOptions: { allowPartialToolsets: true, }, defaultLoadOptions: { validateOnLoad: true, validationOptions: { // Skip tool availability checking during initial load since persona // may define its own MCP servers that aren't running yet checkToolAvailability: false, validateMcpConfig: true, includeWarnings: true, }, }, // Placeholder MCP configuration handlers - will be updated later mcpConfigHandlers: { getCurrentConfig: async () => null, setCurrentConfig: async () => {}, restartConnections: async () => {}, }, }); // Initialize the persona manager await this.personaManager.initialize(); logger.debug("PersonaManager initialized successfully"); } /** * Update PersonaManager MCP configuration handlers with actual implementations */ private async updatePersonaMcpHandlers(): Promise<void> { if (!this.personaManager) { return; } logger.debug("Updating PersonaManager MCP configuration handlers..."); // Update the MCP configuration handlers with the actual implementations const mcpConfigHandlers = { getCurrentConfig: async () => { // Return current MCP configuration in the expected format if (Object.keys(this.currentServerConfigs).length > 0) { return { mcpServers: this.currentServerConfigs }; } return null; }, setCurrentConfig: async (config: any) => { if (!config.mcpServers) { return; } const serverNames = Object.keys(config.mcpServers); logger.info( `Applying persona MCP configuration with ${serverNames.length} server${serverNames.length !== 1 ? "s" : ""}: ${serverNames.join(", ")}` ); // Stop existing connections if (this.connectionManager) { logger.info("Stopping existing MCP server connections..."); await this.connectionManager.stop(); } // Filter self-referencing servers and convert ServerEntry to ServerConfig const serverConfigs: Record<string, ServerConfig> = {}; for (const [name, entry] of Object.entries(config.mcpServers)) { // Only include actual ServerConfig entries, skip extensions if (typeof entry === "object" && entry !== null) { const entryObj = entry as Record<string, any>; if ( "type" in entryObj || "command" in entryObj || "url" in entryObj ) { serverConfigs[name] = entryObj as ServerConfig; } } } const filteredConfigs = this.filterSelfReferencingServers(serverConfigs); const filteredNames = Object.keys(filteredConfigs); if (filteredNames.length < serverNames.length) { const excludedNames = serverNames.filter( (name) => !filteredNames.includes(name) ); logger.info( `Excluded ${excludedNames.length} server${excludedNames.length !== 1 ? "s" : ""} (self-referencing or invalid): ${excludedNames.join(", ")}` ); } // Update stored configurations this.currentServerConfigs = filteredConfigs; // Initialize new connection manager with new configs if (Object.keys(filteredConfigs).length > 0) { logger.info( `Starting connections to ${filteredNames.length} MCP server${filteredNames.length !== 1 ? "s" : ""}...` ); await this.connectToDownstreamServers( filteredConfigs, this.serverInitOptions || { transport: { type: "stdio" } } ); // Note: Discovery engine recreation removed - MCP configs are now handled // during boot sequence phases. The discovery engine maintains its connection // to the ConnectionManager which handles the actual server connections. logger.debug( "MCP configuration applied via boot sequence - discovery engine remains intact" ); } }, restartConnections: async () => { if (this.connectionManager) { // Stop and restart connections with current configs await this.connectionManager.stop(); if (Object.keys(this.currentServerConfigs).length > 0) { await this.connectToDownstreamServers( this.currentServerConfigs, this.serverInitOptions || { transport: { type: "stdio" } } ); // Note: Discovery engine restart removed - the discovery engine maintains // its connection to the ConnectionManager which handles server state logger.debug( "Connections restarted - discovery engine automatically detects changes" ); } } }, }; // Update the handlers in the PersonaManager // Note: We'll need to add a method to PersonaManager to update these handlers (this.personaManager as any).updateMcpConfigHandlers?.(mcpConfigHandlers); // Only enable persona state persistence if a persona was requested // Otherwise, we should not restore any previous persona state if ((this.personaManager as any).config) { // Check if persona was requested in the command line options const personaRequested = this.runtimeOptions?.persona; if (personaRequested) { (this.personaManager as any).config.persistState = true; logger.debug( "Enabled persona state persistence after boot sequence (persona requested)" ); // Restore any persisted persona state now that all dependencies are available try { await (this.personaManager as any).restorePersistedState?.(); logger.debug("Restored persisted persona state after boot sequence"); } catch (error) { logger.warn("Failed to restore persisted persona state:", error); } } else { logger.debug( "Skipping persona state restoration (no persona requested)" ); // Clear any persisted state from previous runs since we're not using a persona try { await (this.personaManager as any).clearPersistedState?.(); logger.debug( "Cleared persisted persona state (no persona requested)" ); } catch (error) { // Ignore errors when clearing state logger.debug("Could not clear persisted persona state:", error); } } } logger.debug("PersonaManager MCP configuration handlers updated"); } /** * Phase 2: Collect ALL MCP configurations (persona + file + extensions) */ private async collectAllMcpConfigs( options: ServerInitOptions ): Promise<Record<string, ServerConfig>> { logger.debug("Phase 2: Collecting all MCP configurations..."); let allServerConfigs: Record<string, ServerConfig> = {}; // First, load persona MCP config if persona is specified if (this.runtimeOptions?.persona && this.personaManager) { const result = await this.personaManager.getPersonaMcpServers( this.runtimeOptions.persona ); if (result.success && result.serverConfigs) { allServerConfigs = { ...allServerConfigs, ...result.serverConfigs }; const serverCount = Object.keys(result.serverConfigs).length; if (serverCount > 0) { logger.info( `Collected ${serverCount} MCP server${serverCount !== 1 ? "s" : ""} from persona configuration` ); } } else if (!result.success) { logger.warn(`Failed to load persona MCP config: ${result.error}`); } } // Second, load regular MCP config from file/database (if not in persona-only mode) if (!this.runtimeOptions?.persona || options.configPath) { const fileServerConfigs = await this.loadMcpConfigOrExit(options); allServerConfigs = { ...allServerConfigs, ...fileServerConfigs }; const serverCount = Object.keys(fileServerConfigs).length; if (serverCount > 0) { logger.info( `Collected ${serverCount} MCP server${serverCount !== 1 ? "s" : ""} from file/database configuration` ); } } // Third, handle group loading (if group is specified, replace with group servers) if (this.runtimeOptions?.group) { logger.info(`Loading servers from group: ${this.runtimeOptions.group}`); try { const { ServerSyncManager } = await import( "../config-manager/serverSync.js" ); const { getCompositeDatabaseService } = await import( "../db/compositeDatabaseService.js" ); const dbService = getCompositeDatabaseService(); const syncManager = new ServerSyncManager(dbService); const groupServers = await syncManager.getServersForGroup( this.runtimeOptions.group ); allServerConfigs = {}; for (const server of groupServers) { allServerConfigs[server.name] = server.config; } const isStdioTransport = options.transport.type === "stdio"; if (!isStdioTransport) { console.log( `Loaded ${groupServers.length} servers from group "${this.runtimeOptions.group}"` ); } } catch (error) { const errorMsg = `Failed to load group "${this.runtimeOptions.group}": ${(error as Error).message}`; logger.error(errorMsg); process.exit(1); } } // Fourth, load extension configs and merge with existing configs (only if DXT is enabled) if ((await isDxtEnabledViaService()) && this.extensionManager) { const extensionConfigs = this.extensionManager.getEnabledExtensionsAsServerConfigs(); allServerConfigs = { ...allServerConfigs, ...extensionConfigs }; const extensionCount = Object.keys(extensionConfigs).length; if (extensionCount > 0) { logger.info( `Collected ${extensionCount} extension server${extensionCount !== 1 ? "s" : ""}: ${Object.keys(extensionConfigs).join(", ")}` ); } } const totalCount = Object.keys(allServerConfigs).length; logger.debug( `Phase 2 complete: Collected ${totalCount} total MCP server configuration${totalCount !== 1 ? "s" : ""}` ); return allServerConfigs; } /** * Handle mode change request from ConfigToolsManager (toggle mode) */ private handleConfigToolsModeChange = async () => { this.configurationMode = !this.configurationMode; logger.debug( `Mode changed via ConfigToolsManager: ${this.configurationMode ? "configuration" : "normal"}` ); await this.notifyToolsChanged(); }; /** * Enhanced start method with routing initialization */ async start( options: ServerInitOptions, runtimeOptions?: RuntimeOptions ): Promise<void> { this.runtimeOptions = runtimeOptions; this.serverInitOptions = options; await this.initializeRouting(options); await super.start(options); } private async loadMcpConfigOrExit( options: ServerInitOptions ): Promise<Record<string, ServerConfig>> { // Initialize spinner const isStdio = options.transport.type === "stdio"; const mainSpinner = createSpinner("Loading MCP configuration...", isStdio); // Load configuration from database let serverConfigs: Record<string, ServerConfig> = {}; // Check if we're in persona-only mode (persona specified but no config path) if ( this.runtimeOptions?.persona && (!options.configPath || options.configPath === "") ) { logger.debug( `Persona-only mode detected for "${this.runtimeOptions.persona}" - checking for persona MCP config` ); // Try to find and load the persona's mcp.json file try { const personaMcpConfig = await this.loadPersonaMcpConfig( this.runtimeOptions.persona ); if (personaMcpConfig) { serverConfigs = personaMcpConfig; const serverCount = Object.keys(serverConfigs).length; mainSpinner.succeed( `Loaded ${serverCount} MCP server${serverCount !== 1 ? "s" : ""} from persona configuration` ); logger.debug( `Loaded persona MCP config with servers: ${Object.keys(serverConfigs).join(", ")}` ); } else { mainSpinner.succeed( "Persona mode: Starting with minimal configuration" ); logger.debug( "No persona MCP config found - continuing with empty configuration" ); } } catch (error) { mainSpinner.warn( "Failed to load persona MCP config - continuing with minimal configuration" ); logger.warn( `Error loading persona MCP config: ${error instanceof Error ? error.message : String(error)}` ); } return serverConfigs; } try { const { loadMcpConfig } = await import("../config/mcpConfigLoader.js"); const config = await loadMcpConfig( options.configPath || "", options.configSource ); serverConfigs = config.mcpServers || {}; const serverCount = Object.keys(serverConfigs).length; if (options.configSource) { mainSpinner.succeed( `Loaded ${serverCount} MCP server${serverCount !== 1 ? "s" : ""} from database. | Source: ${chalk.yellow(options.configSource.type)}` ); } else { mainSpinner.succeed( `Loaded ${serverCount} MCP server${serverCount !== 1 ? "s" : ""} from database.` ); } } catch (error) { mainSpinner.fail("Failed to load MCP configuration from database"); logger.error(`\nāŒ FATAL ERROR: Failed to load MCP configuration`); logger.error( ` Error: ${error instanceof Error ? error.message : String(error)}` ); logger.error( `\nšŸ’” Resolution: Run 'hypertool-mcp config backup' to import configurations.` ); logger.error( `\n🚫 ${APP_NAME} server cannot start without configuration.` ); process.exit(1); } return serverConfigs; } /** * Filter out server configurations that would cause HyperTool to connect to itself */ private filterSelfReferencingServers( serverConfigs: Record<string, ServerConfig> ): Record<string, ServerConfig> { const filteredConfigs: Record<string, ServerConfig> = {}; const removedServers: string[] = []; for (const [serverName, config] of Object.entries(serverConfigs)) { if (this.isSelfReferencingServer(config)) { removedServers.push(serverName); logger.warn( `āš ļø Skipped server "${serverName}" - detected HyperTool self-reference (would cause recursion)` ); } else { filteredConfigs[serverName] = config; } } if (removedServers.length > 0) { logger.warn(` āš ļø WARNING: Self-referencing servers removed from configuration Removed servers: ${removedServers.join(", ")} These servers appear to be HyperTool MCP itself, which would cause infinite recursion. HyperTool cannot connect to itself as a downstream server. If you intended to connect to a different MCP server, please check your configuration and ensure the command/URL points to the correct server. `); } return filteredConfigs; } /** * Check if a server configuration references HyperTool itself */ private isSelfReferencingServer(config: ServerConfig): boolean { if (config.type === "stdio" && config.command) { // Check for common patterns that indicate HyperTool MCP const command = config.command.toLowerCase(); const args = config.args || []; // Direct command references if (command === "hypertool-mcp" || command.endsWith("/hypertool-mcp")) { return true; } // NPX references to our package if ((command === "npx" || command.endsWith("/npx")) && args.length > 0) { for (const arg of args) { const argLower = arg.toLowerCase(); if ( argLower === "@toolprint/hypertool-mcp" || argLower === "hypertool-mcp" || argLower.includes("@toolprint/hypertool-mcp") ) { return true; } } } // Node references to our package if ( (command === "node" || command.endsWith("/node")) && args.length > 0 ) { for (const arg of args) { // Check for actual HyperTool MCP executable files, not just paths containing the name if ( arg.includes("@toolprint/hypertool-mcp") || (arg.includes("hypertool-mcp") && (arg.endsWith("/bin.js") || arg.endsWith("/index.js") || (arg.endsWith("/server.js") && !arg.includes("/extensions/")))) // Exclude extension servers ) { return true; } } } } // For HTTP/SSE servers, we could check if they're pointing to our own HTTP server // but that would require knowing our own running port/URL which is complex // For now, we focus on stdio self-references which are the most common case return false; } private async connectToDownstreamServers( serverConfigs: Record<string, ServerConfig>, options: ServerInitOptions ): Promise<void> { // Load server settings with proper priority (env > config > default) const { loadServerSettings } = await import("../config/serverSettings.js"); const serverSettings = await loadServerSettings(); // Create extension-aware connection factory const connectionFactory = new ExtensionAwareConnectionFactory(); if (this.extensionManager) { connectionFactory.setExtensionManager(this.extensionManager); } // Create connection manager with configured pool settings and extension-aware factory this.connectionManager = new ConnectionManager( { maxConcurrentConnections: serverSettings.maxConcurrentConnections, }, connectionFactory ); const isStdio = options.transport.type === "stdio"; // Log the connection pool configuration if in debug mode or if env var is set if (options.debug || process.env.HYPERTOOL_MAX_CONNECTIONS) { const { logServerSettingsSource } = await import( "../config/serverSettings.js" ); await logServerSettingsSource(); } let mainSpinner = createSpinner( `šŸ”— Setting up Connection Manager (max ${serverSettings.maxConcurrentConnections} connections)...`, isStdio ); // Filter out any configurations that would cause HyperTool to connect to itself const filteredConfigs = this.filterSelfReferencingServers(serverConfigs); if (Object.keys(filteredConfigs).length === 0) { mainSpinner.succeed("No MCP servers configured"); // Still initialize the connection manager with empty configs so the server can start await this.connectionManager.initialize({}); return; } await this.connectionManager.initialize(filteredConfigs); mainSpinner.succeed("šŸ”— Connection manager initialized"); // Log individual server connection attempts for (const [sName, config] of Object.entries(filteredConfigs)) { logger.debug( `Attempting to connect to MCP server '${sName}' (${config.type})...` ); } await this.connectionManager.start(); for (const [sName, _] of Object.entries(serverConfigs)) { const serverSpinner = createSpinner( `Checking connection to [${sName}] MCP <-> [${serverConfigs[sName].type}]...`, isStdio ); try { const cmStatus = this.connectionManager.status[sName]; if (cmStatus.state === "connected") { serverSpinner.succeed( `Connected to [${sName}] MCP <-> [${serverConfigs[sName].type}]` ); logger.info( `āœ… Successfully connected to MCP server '${sName}' (${serverConfigs[sName].type})` ); } else { serverSpinner.fail( `Failed to connect to [${sName}] MCP <-> [${serverConfigs[sName].type}]` ); logger.warn( `āŒ Failed to connect to MCP server '${sName}' (${serverConfigs[sName].type}) - state: ${cmStatus.state}` ); } } catch (error) { const errorMsg = (error as Error).message; serverSpinner.fail( `Failed to check connection to [${sName}] MCP <-> [${serverConfigs[sName].type}]: ${errorMsg}` ); logger.error( `āŒ Connection error for MCP server '${sName}' (${serverConfigs[sName].type}): ${errorMsg}` ); } } } /** * Initialize routing components */ private async initializeRouting(options: ServerInitOptions): Promise<void> { // Only show output for non-stdio transports const isStdio = options.transport.type === "stdio"; if (!isStdio) { output.displaySubHeader("Initializing Routing and Discovery"); output.displaySpaceBuffer(); } try { // Initialize database before loading configs const { getCompositeDatabaseService } = await import( "../db/compositeDatabaseService.js" ); const dbService = getCompositeDatabaseService(); await dbService.init(); logger.debug("Database initialized successfully"); // Initialize PersonaManager early in boot sequence (after database, before MCP config load) await this.initializePersonaManager(); // Initialize extension manager (only if DXT is enabled) if (await isDxtEnabledViaService()) { this.extensionManager = new ExtensionManager(); await this.extensionManager.initialize(); logger.debug("Extension manager initialized successfully"); } else { logger.debug("Extension manager disabled via feature flag"); } // Phase 2: Collect ALL MCP configurations (persona + file + extensions) const mergedConfigs = await this.collectAllMcpConfigs(options); // Sync all collected servers with database if (Object.keys(mergedConfigs).length > 0) { const { ServerSyncManager } = await import( "../config-manager/serverSync.js" ); const syncManager = new ServerSyncManager(dbService); await syncManager.syncServers(mergedConfigs); logger.debug("All server configurations synced with database"); } // Detect external MCPs const externalMCPs = await detectExternalMCPs(); if (externalMCPs.length > 0) { output.displaySpaceBuffer(); const message = formatExternalMCPsMessage(externalMCPs); console.log(chalk.yellow(message)); output.displaySpaceBuffer(); } // Store current configurations for persona MCP integration this.currentServerConfigs = mergedConfigs; // Phase 3: Single connection attempt to all servers logger.debug("Phase 3: Connecting to all MCP servers..."); await this.connectToDownstreamServers(mergedConfigs, options); // Phase 4: Discovery engine initialization logger.debug("Phase 4: Initializing tool discovery engine..."); const isStdio = options.transport.type === "stdio"; let mainSpinner = createSpinner( "Initializing tool discovery engine...", isStdio ); this.discoveryEngine = new ToolDiscoveryEngine(this.connectionManager!); await this.discoveryEngine.initialize({ autoDiscovery: true, enableMetrics: true, }); // Set discovery engine reference in toolset manager this.toolsetManager.setDiscoveryEngine(this.discoveryEngine); // Phase 5: Update PersonaManager handlers with actual implementations logger.debug( "Phase 5: Updating PersonaManager MCP configuration handlers..." ); await this.updatePersonaMcpHandlers(); // Listen for toolset changes and notify clients this.toolsetManager.on( "toolsetChanged", async (event: ToolsetChangeEvent) => { if (options.debug) { logger.info( `Toolset ${event.changeType}: ${event.newToolset?.name || "none"}` ); } await this.notifyToolsChanged(); } ); // Also listen for persona toolset changes and notify clients if (this.personaManager) { this.personaManager.on("toolset-changed", async (event: any) => { if (options.debug) { logger.info( `Persona toolset changed: ${event.toolsetName} for persona ${event.personaName}` ); } await this.notifyToolsChanged(); }); } mainSpinner.succeed("Tool discovery engine initialized"); // Start discovery and show tool count mainSpinner = createSpinner( "Discovering tools from connected servers...", isStdio ); await this.discoveryEngine.start(); const discoveredTools = this.discoveryEngine.getAvailableTools(true); const toolCount = discoveredTools.length; const connectedServers = this.connectionManager!.getConnectedServers(); if (toolCount > 0) { mainSpinner.succeed( `Discovered ${toolCount} tool${toolCount !== 1 ? "s" : ""} from ${connectedServers.length} connected server${connectedServers.length !== 1 ? "s" : ""}` ); // Only output tool server status in non-stdio mode to avoid interfering with MCP protocol if (!isStdio) { await this.discoveryEngine.outputToolServerStatus(); } } else { mainSpinner.warn("No tools discovered from connected servers"); } // Initialize request router mainSpinner = createSpinner("Initializing request router...", isStdio); this.requestRouter = new RequestRouter( this.discoveryEngine, this.connectionManager! ); await this.requestRouter.initialize({ enableLogging: options.debug || false, enableMetrics: true, }); mainSpinner.succeed("Request router initialized"); // Listen for tool discovery changes and notify clients (this.discoveryEngine as any).on?.( "toolsChanged", async (event: DiscoveredToolsChangedEvent) => { // If we have an active toolset, it might need re-validation const activeToolsetInfo = this.toolsetManager.getActiveToolsetInfo(); if (activeToolsetInfo) { if (options.debug) { logger.info( `Tools changed while toolset "${activeToolsetInfo.name}" is equipped. ` + `Server: ${event.serverName}, Changes: +${event.summary.added} ~${event.summary.updated} -${event.summary.removed}` ); } } // Note: ToolsetManager will automatically handle toolset validation // and emit toolsetChanged events if active tools are affected // Always notify clients about tool changes await this.notifyToolsChanged(); } ); // Initialize tool modules after all dependencies are set up // This also restores the last equipped toolset and sets initial mode await this.initializeToolModules(); await this.checkToolsetStatus(options.debug); } catch (error) { logger.error("Failed to initialize routing:", error); throw error; } } /** * Initialize tool modules with dependency injection */ private async initializeToolModules(): Promise<void> { const dependencies: ToolDependencies = { toolsetManager: this.toolsetManager, discoveryEngine: this.discoveryEngine, runtimeOptions: this.runtimeOptions, personaManager: this.personaManager, }; // Initialize persona FIRST if specified // This ensures persona is active before any toolset operations if (this.runtimeOptions?.persona) { await this.initializePersona(this.runtimeOptions.persona, dependencies); // Persona mode is set within initializePersona when successful } // THEN handle toolset equipping (after persona is potentially active) // This ensures toolset operations route to the correct delegate based on persona state if (this.runtimeOptions?.equipToolset) { // User explicitly specified a toolset to equip logger.info(`Equipping toolset: ${this.runtimeOptions!.equipToolset!}`); // Route through the appropriate delegate based on persona state // If a persona is active, it should handle the toolset operation const activePersona = this.personaManager?.getActivePersona(); if (activePersona && this.personaManager) { // Persona is active - route through PersonaManager delegate logger.info( `āœ… Routing toolset equip through PersonaManager for persona: ${activePersona.persona.config.name}` ); logger.debug( `Attempting to equip toolset: ${this.runtimeOptions!.equipToolset!}` ); const result = await this.personaManager.equipToolset( this.runtimeOptions!.equipToolset! ); if (!result.success) { logger.error( `Failed to equip toolset via PersonaManager: ${result.error}` ); } else { logger.info( `āœ… Successfully equipped toolset "${this.runtimeOptions!.equipToolset!}" via PersonaManager` ); } } else { // No persona active - use ToolsetManager directly logger.info( `No persona active - routing toolset equip through ToolsetManager directly` ); const result = await this.toolsetManager.equipToolset( this.runtimeOptions!.equipToolset! ); if (!result.success) { logger.error(`Failed to equip toolset: ${result.error}`); } else { logger.info( `āœ… Successfully equipped toolset "${this.runtimeOptions!.equipToolset!}" via ToolsetManager` ); } } } else { // No explicit toolset specified, try to restore the last equipped one // Only restore if no persona is active (personas handle their own defaults) const activePersona = this.personaManager?.getActivePersona(); if (!activePersona) { const restored = await this.toolsetManager.restoreLastEquippedToolset(); if (!restored) { logger.debug("No previously equipped toolset to restore"); } } else { logger.debug( "Persona active - skipping toolset restoration (persona handles defaults)" ); } } // Initialize configuration mode components await this.initializeConfigurationMode(dependencies); } /** * Initialize and activate a persona */ private async initializePersona( personaName: string, _: ToolDependencies ): Promise<void> { try { logger.info(`Activating persona: ${personaName}`); if (!this.personaManager) { throw new Error( "PersonaManager not initialized. Call initializePersonaManager() first." ); } // Update MCP configuration handlers now that we have the actual dependencies await this.updatePersonaMcpHandlers(); // Activate the specified persona const result = await this.personaManager.activatePersona(personaName); if (result.success) { // Set persona mode flag this.isPersonaMode = true; logger.info(`Successfully activated persona: ${personaName}`); // If persona has a default toolset and no explicit toolset was provided, // activate the persona's default toolset if (result.activatedToolset && !this.runtimeOptions?.equipToolset) { logger.info( `Auto-activating persona's default toolset: ${result.activatedToolset}` ); } if (result.warnings && result.warnings.length > 0) { for (const warning of result.warnings) { logger.warn(`Persona activation warning: ${warning}`); } } } else { logger.error(`Failed to activate persona: ${personaName}`); if (result.errors && result.errors.length > 0) { for (const error of result.errors) { logger.error(`Persona activation error: ${error}`); } } // Check if persona was not found and provide helpful guidance if ( result.errors && result.errors.some((e) => e.includes("not found")) ) { logger.error(""); logger.error("šŸ’” To add and manage personas:"); logger.error( " hypertool-mcp persona --help # See persona management guide" ); logger.error( " hypertool-mcp persona add <path> # Add a persona from folder or .htp file" ); logger.error( " hypertool-mcp persona list # List available personas" ); logger.error(""); logger.error(" Or use regular MCP config instead:"); logger.error( " hypertool-mcp --mcp-config <path> # Use standard MCP configuration" ); logger.error(""); } } } catch (error) { logger.error(`Error initializing persona ${personaName}:`, error); } } /** * Load MCP configuration from persona directory */ private async loadPersonaMcpConfig( personaName: string ): Promise<Record<string, ServerConfig> | null> { try { const path = await import("path"); const fs = await import("fs/promises"); // Look for persona in the personas directory const personaDir = path.join(process.cwd(), "personas", personaName); const mcpConfigPath = path.join(personaDir, "mcp.json"); // Check if mcp.json exists try { await fs.access(mcpConfigPath); } catch { logger.debug( `No mcp.json found for persona "${personaName}" at ${mcpConfigPath}` ); return null; } // Load and parse the mcp.json file const configContent = await fs.readFile(mcpConfigPath, "utf-8"); const mcpConfig = JSON.parse(configContent); // Validate basic structure if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object") { throw new Error("Invalid MCP config: missing 'mcpServers' field"); } // Transform the config format from persona format to internal format const serverConfigs: Record<string, ServerConfig> = {}; for (const [serverName, serverConfig] of Object.entries( mcpConfig.mcpServers )) { const config = serverConfig as any; // Transform 'transport' field to 'type' if present if (config.transport) { config.type = config.transport; delete config.transport; } // Set default type to 'stdio' if command is present but no type specified if (!config.type && config.command) { config.type = "stdio"; } serverConfigs[serverName] = config as ServerConfig; } const serverNames = Object.keys(serverConfigs); logger.info( `Successfully loaded persona MCP config from ${mcpConfigPath} with ${serverNames.length} server${serverNames.length !== 1 ? "s" : ""}: ${serverNames.join(", ")}` ); return serverConfigs; } catch (error) { logger.error( `Failed to load persona MCP config for "${personaName}": ${error instanceof Error ? error.message : String(error)}` ); throw error; } } /** * Initialize configuration mode components */ private async initializeConfigurationMode( dependencies: ToolDependencies ): Promise<void> { // Check if configuration tools menu is enabled via feature flag this.configToolsMenuEnabled = await isConfigToolsMenuEnabledViaService(); if (!this.configToolsMenuEnabled) { logger.info( "Configuration tools menu disabled - all tools exposed together" ); // When dynamic config menu is disabled, we still create ConfigToolsManager to have access to config tools this.configToolsManager = new ConfigToolsManager(dependencies); // Don't set configuration mode or create mode switching tools return; } // Normal configuration mode setup // Create ConfigToolsManager with mode change callback this.configToolsManager = new ConfigToolsManager( dependencies, this.handleConfigToolsModeChange ); // Create enter-configuration-mode tool that server will manage this.enterConfigurationModeTool = createEnterConfigurationModeModule( dependencies, this.handleConfigToolsModeChange ); // Determine initial mode based on toolset status // At this point, toolset has already been restored if one was saved const hasEquippedToolset = this.toolsetManager.hasActiveToolset(); this.configurationMode = !hasEquippedToolset; logger.debug( `Initial configuration mode: ${this.configurationMode ? "configuration" : "normal"} (toolset equipped: ${hasEquippedToolset})` ); } /** * Check toolset status and warn if no toolset is equipped */ private async checkToolsetStatus(debug?: boolean): Promise<void> { try { const listResult = await this.toolsetManager.listSavedToolsets(); const storedToolsets = listResult.success ? listResult.toolsets.reduce( (acc: any, t: any) => ({ ...acc, [t.name]: t }), {} ) : {}; const hasToolsets = Object.keys(storedToolsets).length > 0; const activeToolsetInfo = this.toolsetManager.getActiveToolsetInfo(); if (!activeToolsetInfo && !hasToolsets) { logger.warn(` āš ļø WARNING: No toolsets configured ${APP_TECHNICAL_NAME} is running but no toolsets have been created yet. This means no underlying MCP server tools will be exposed. šŸ’” Next steps: 1. Use 'list-available-tools' to see what tools are available 2. Use 'build-toolset' to create a toolset with specific tools 3. Use the '--equip-toolset' flag or 'equip-toolset' tool to activate a toolset Example: Create a dev toolset with git and docker tools `); } else if (!activeToolsetInfo && hasToolsets) { const toolsetNames = listResult.success ? listResult.toolsets.map((t: any) => t.name) : []; logger.warn(` āš ļø WARNING: No toolset equipped You have ${toolsetNames.length} saved toolset(s) but none are currently equipped. Available toolsets: ${toolsetNames.join(", ")} šŸ’” Use the '--equip-toolset' flag or 'equip-toolset' tool to activate a toolset and expose its tools. `); } else if (debug && activeToolsetInfo) { logger.info( `āœ… Toolset "${activeToolsetInfo.name}" is equipped and active` ); } } catch (error) { if (debug) { logger.warn("Could not check toolset status:", error); } } } /** * Get available tools based on current mode */ protected async getAvailableTools(): Promise<Tool[]> { const tools: Tool[] = []; // When dynamic config menu is disabled: return all tools together if (!this.configToolsMenuEnabled) { // Add all configuration tools if (this.configToolsManager) { try { const configTools = this.configToolsManager.getMcpTools(); tools.push(...configTools); logger.debug( `All tools mode: added ${configTools.length} configuration tools` ); } catch (error) { logger.error("Failed to get configuration tools:", error); } } // Add all toolset tools try { // Use PersonaManager for tools when in persona mode const mcpTools = this.isPersonaMode && this.personaManager ? this.personaManager.getMcpTools() : this.toolsetManager.getMcpTools(); tools.push(...mcpTools); logger.debug(`All tools mode: added ${mcpTools.length} toolset tools`); } catch (error) { logger.error("Failed to get toolset tools:", error); } logger.debug(`All tools mode: returning ${tools.length} total tools`); return tools; } // Configuration mode logic if (this.configurationMode) { // Configuration mode: show only configuration tools if (this.configToolsManager) { try { const configTools = this.configToolsManager.getMcpTools(); tools.push(...configTools); logger.debug( `Configuration mode: returning ${configTools.length} configuration tools` ); } catch (error) { logger.error("Failed to get configuration tools:", error); } } } else { // Normal mode: show toolset tools + enter-configuration-mode // Add tools from appropriate manager based on persona mode try { // Use PersonaManager for tools when in persona mode const mcpTools = this.isPersonaMode && this.personaManager ? this.personaManager.getMcpTools() : this.toolsetManager.getMcpTools(); const source = this.isPersonaMode ? "persona manager" : "toolset manager"; tools.push(...mcpTools); logger.debug( `Normal mode: got ${mcpTools.length} tools from ${source}` ); } catch (error) { logger.error("Failed to get toolset tools:", error); } // Add enter-configuration-mode tool if (this.enterConfigurationModeTool) { tools.push(this.enterConfigurationModeTool.definition); } } logger.debug( `Total tools available: ${tools.length} (mode: ${this.configurationMode ? "configuration" : "normal"})` ); return tools; } /** * Check if a tool is an internal tool that should be handled directly */ private async isInternalTool(toolName: string): Promise<boolean> { // Import the CONFIG_TOOL_NAMES to get the list of internal tools const { CONFIG_TOOL_NAMES } = await import( "./tools/config-tools/registry.js" ); const internalTools = new Set([ ...CONFIG_TOOL_NAMES, "enter-configuration-mode", ]); return internalTools.has(toolName); } /** * Handle tool call requests with mode-based routing */ protected async handleToolCall(name: string, args?: any): Promise<any> { try { // FIRST: Check if this is an internal tool - handle directly, regardless of mode if (await this.isInternalTool(name)) { // Handle enter-configuration-mode tool if ( name === "enter-configuration-mode" && this.enterConfigurationModeTool ) { return await this.enterConfigurationModeTool.handler(args); } // Handle configuration tools if (this.configToolsManager) { const configTools = this.configToolsManager.getMcpTools(); const isConfigTool = configTools.some((tool) => tool.name === name); if (isConfigTool) { return await this.configToolsManager.handleToolCall(name, args); } } // If we get here, it's an internal tool but no handler is available throw new Error( `Internal tool "${name}" is not available or not properly initialized` ); } // SECOND: Handle external tools based on mode // When dynamic config menu is disabled: route external tools through router if (!this.configToolsMenuEnabled) { const originalToolName = this.toolsetManager.getOriginalToolName(name); const toolNameForRouter = originalToolName || name; if (!this.requestRouter) { throw new Error( "Request router is not available. Server may not be fully initialized." ); } const response = await this.requestRouter.routeToolCall({ name: toolNameForRouter, arguments: args, }); return response; } // Configuration mode: only internal tools should be available if (this.configurationMode) { throw new Error( `Tool "${name}" is not available in configuration mode. Only configuration tools are allowed.` ); } else { // Normal mode: route external tools const originalToolName = this.toolsetManager.getOriginalToolName(name); const toolNameForRouter = originalToolName || name; if (!this.requestRouter) { throw new Error( "Request router is not available. Server may not be fully initialized." ); } const response = await this.requestRouter.routeToolCall({ name: toolNameForRouter, arguments: args, }); return response; } } catch (error) { logger.error(`Tool call failed (${name}):`, error); throw error; } } /** * Enhanced stop method */ async stop(): Promise<void> { try { // Stop routing services if (this.discoveryEngine) { // Remove event listeners (this.discoveryEngine as any).removeAllListeners?.("toolsChanged"); await this.discoveryEngine.stop(); } if (this.connectionManager) { await this.connectionManager.stop(); } // Close database try { const { getCompositeDatabaseService } = await import( "../db/compositeDatabaseService.js" ); const dbService = getCompositeDatabaseService(); await dbService.close(); logger.debug("Database closed successfully"); } catch (error) { logger.error("Error closing database:", error); } // Stop the base server await super.stop(); } catch (error) { logger.error("Error stopping enhanced server:", error); throw error; } } /** * Get routing statistics (if enabled) */ getRoutingStats() { if (!this.requestRouter) { return null; } return { router: this.requestRouter.getStats(), discovery: this.discoveryEngine?.getStats(), connections: this.connectionManager?.status, }; } }

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/toolprint/hypertool-mcp'

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