UNPKG

@toolprint/hypertool-mcp

Version:

HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers

1,044 lines • 52.3 kB
/**
 * Enhanced Hypertool MCP server with request routing capabilities
 */
import { MetaMCPServer } from "./base.js";
import { RequestRouter } from "../router/index.js";
import { ToolDiscoveryEngine, } from "../discovery/index.js";
import { ConnectionManager, } from "../connection/index.js";
import { ExtensionAwareConnectionFactory } from "../connection/extensionFactory.js";
import { ExtensionManager } from "../extensions/manager.js";
import { APP_NAME, APP_TECHNICAL_NAME } from "../config/index.js";
import { isDxtEnabledViaService, isConfigToolsMenuEnabledViaService, } from "../config/featureFlagService.js";
import ora from "ora";
// Helper to create conditional spinners
function createSpinner(text, isStdio) {
    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 } from "./tools/toolset/manager.js";
import { ConfigToolsManager } from "./tools/config-tools/manager.js";
import { createEnterConfigurationModeModule } from "./tools/common/enter-configuration-mode.js";
import chalk from "chalk";
import { output } from "../utils/output.js";
import { detectExternalMCPs, formatExternalMCPsMessage, } from "../scripts/shared/externalMcpDetector.js";
/**
 * Enhanced Hypertool MCP server with routing capabilities
 */
export class EnhancedMetaMCPServer extends MetaMCPServer {
    requestRouter;
    discoveryEngine;
    connectionManager;
    extensionManager;
    toolsetManager;
    personaManager;
    configToolsManager;
    configurationMode = false;
    enterConfigurationModeTool;
    runtimeOptions;
    configToolsMenuEnabled = true;
    serverInitOptions;
    currentServerConfigs = {};
    isPersonaMode = false;
    constructor(config) {
        super(config);
        this.toolsetManager = new ToolsetManager();
    }
    /**
     * Getter methods for safe access to shared objects
     */
    getPersonaManager() {
        return this.personaManager;
    }
    /**
     * Check if server is in persona mode
     */
    getIsPersonaMode() {
        return this.isPersonaMode;
    }
    getConnectionManager() {
        return this.connectionManager;
    }
    getDiscoveryEngine() {
        return this.discoveryEngine;
    }
    /**
     * Initialize PersonaManager early in boot sequence
     */
    async initializePersonaManager() {
        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
     */
    async updatePersonaMcpHandlers() {
        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) => {
                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 = {};
                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;
                        if ("type" in entryObj ||
                            "command" in entryObj ||
                            "url" in entryObj) {
                            serverConfigs[name] = entryObj;
                        }
                    }
                }
                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.updateMcpConfigHandlers?.(mcpConfigHandlers);
        // Only enable persona state persistence if a persona was requested
        // Otherwise, we should not restore any previous persona state
        if (this.personaManager.config) {
            // Check if persona was requested in the command line options
            const personaRequested = this.runtimeOptions?.persona;
            if (personaRequested) {
                this.personaManager.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.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.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)
     */
    async collectAllMcpConfigs(options) {
        logger.debug("Phase 2: Collecting all MCP configurations...");
        let allServerConfigs = {};
        // 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.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)
     */
    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, runtimeOptions) {
        this.runtimeOptions = runtimeOptions;
        this.serverInitOptions = options;
        await this.initializeRouting(options);
        await super.start(options);
    }
    async loadMcpConfigOrExit(options) {
        // Initialize spinner
        const isStdio = options.transport.type === "stdio";
        const mainSpinner = createSpinner("Loading MCP configuration...", isStdio);
        // Load configuration from database
        let serverConfigs = {};
        // 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
     */
    filterSelfReferencingServers(serverConfigs) {
        const filteredConfigs = {};
        const removedServers = [];
        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
     */
    isSelfReferencingServer(config) {
        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;
    }
    async connectToDownstreamServers(serverConfigs, options) {
        // 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.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
     */
    async initializeRouting(options) {
        // 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) => {
                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) => {
                    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.on?.("toolsChanged", async (event) => {
                // 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
     */
    async initializeToolModules() {
        const dependencies = {
            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
     */
    async initializePersona(personaName, _) {
        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
     */
    async loadPersonaMcpConfig(personaName) {
        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 = {};
            for (const [serverName, serverConfig] of Object.entries(mcpConfig.mcpServers)) {
                const config = serverConfig;
                // 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;
            }
            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
     */
    async initializeConfigurationMode(dependencies) {
        // 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
     */
    async checkToolsetStatus(debug) {
        try {
            const listResult = await this.toolsetManager.listSavedToolsets();
            const storedToolsets = listResult.success
                ? listResult.toolsets.reduce((acc, t) => ({ ...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) => 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
     */
    async getAvailableTools() {
        const tools = [];
        // 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
     */
    async isInternalTool(toolName) {
        // 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
     */
    async handleToolCall(name, args) {
        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() {
        try {
            // Stop routing services
            if (this.discoveryEngine) {
                // Remove event listeners
                this.discoveryEngine.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,
        };
    }
}
//# sourceMappingURL=enhanced.js.map