@toolprint/hypertool-mcp
Version:
HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers
1,044 lines ⢠52.3 kB
JavaScript
/**
* 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