Skip to main content
Glama

hypertool-mcp

index.tsโ€ข25.1 kB
#!/usr/bin/env node /** * HyperTool MCP server main entry point */ // Set max listeners early to prevent warnings from MCP SDK and other dependencies // This can be configured via HYPERTOOL_MAX_LISTENERS environment variable // Default is 10, but you may need to increase it if you have many MCP servers connected // Example: HYPERTOOL_MAX_LISTENERS=30 hypertool-mcp const maxListeners = process.env.HYPERTOOL_MAX_LISTENERS ? parseInt(process.env.HYPERTOOL_MAX_LISTENERS, 10) : 10; if (!isNaN(maxListeners) && maxListeners > 0) { process.setMaxListeners(maxListeners); } import { Command, Argument } from "commander"; import { RuntimeOptions, RuntimeTransportType } from "./types/runtime.js"; import { APP_DESCRIPTION, APP_VERSION, APP_TECHNICAL_NAME, APP_NAME, } from "./config/appConfig.js"; import type { TransportConfig } from "./server/types.js"; import { theme, semantic } from "./utils/theme.js"; import { getFeatureFlagService, isDxtEnabledViaService, } from "./config/featureFlagService.js"; // Logger will be initialized later with proper runtime options async function handleInstallOption(installArgs: string[], isDryRun: boolean) { // Default to 'all' if no option provided const option = installArgs && installArgs.length > 0 ? installArgs[0] : "all"; // Normalize option aliases const normalizedOption = (() => { switch (option.toLowerCase()) { case "all": return "all"; case "claude-desktop": case "cd": return "claude-desktop"; case "cursor": return "cursor"; case "claude-code": case "cc": return "claude-code"; default: return null; } })(); if (!normalizedOption) { console.error( semantic.messageError("โŒ Invalid install option: " + option) ); console.error( theme.warning( " Valid options: all (default), claude-desktop (cd), cursor, claude-code (cc)" ) ); throw new Error("Invalid install option"); } try { switch (normalizedOption) { case "all": const { GlobalSetup } = await import("./scripts/global/setup.js"); const globalSetup = new GlobalSetup(); await globalSetup.run(isDryRun); break; case "claude-desktop": const { ClaudeDesktopSetup } = await import( "./scripts/claude-desktop/setup.js" ); const setup = new ClaudeDesktopSetup(); await setup.run(isDryRun); break; case "cursor": const { CursorSetup } = await import("./scripts/cursor/setup.js"); const cursorSetup = new CursorSetup(); await cursorSetup.run(isDryRun); break; case "claude-code": const { installClaudeCodeCommands } = await import( "./scripts/claude-code/setup.js" ); await installClaudeCodeCommands({ dryRun: isDryRun }); break; default: throw new Error( semantic.messageError("โŒ Invalid install option: " + option) ); } } catch (error) { throw new Error( semantic.messageError(`โŒ Failed to run ${normalizedOption} setup:`), error as Error ); } } async function getAddToCommand(): Promise<Command> { const addToCommand = new Command("add-to") .description("Add HyperTool to an application") .addArgument( new Argument("[app]", "Application to add HyperTool to") .default("all") .choices(["all", "claude-desktop", "cd", "cursor", "claude-code", "cc"]) ) .option( "--dry-run", "Show what would be done without making changes", false ) .addHelpText( "after", ` ${theme.label("Application aliases:")} cd = claude-desktop cc = claude-code ${theme.label("Examples:")} ${theme.muted("hypertool-mcp add-to # Add to all detected apps")} ${theme.muted("hypertool-mcp add-to claude-desktop # Add to Claude Desktop only")} ${theme.muted("hypertool-mcp add-to cursor --dry-run # Preview Cursor setup")} ${theme.muted("hypertool-mcp add-to cc # Add to Claude Code")}` ) .action(async (app, options) => { await handleInstallOption([app], options.dryRun); }); return addToCommand; } // Define MCP server run options in a reusable way const mcpServerRunOptions = [ { flags: "--transport <type>", description: theme.info("Transport protocol to use") + " (http, stdio)", defaultValue: "stdio", }, { flags: "--port <number>", description: theme.info("Port number for HTTP transport") + " (only valid with --transport http)", }, { flags: "--debug", description: theme.info("Enable debug mode with verbose logging"), defaultValue: false, }, { flags: "--insecure", description: theme.warning("Allow tools with changed reference hashes") + semantic.messageError(" (insecure mode)"), defaultValue: false, }, { flags: "--equip-toolset <name>", description: theme.info("Toolset name to equip on startup") + "\n" + theme.muted("(with --persona: equips persona toolset)") + "\n" + theme.muted("(without --persona: equips regular toolset)"), }, { flags: "--mcp-config <path>", description: theme.info("Path to MCP configuration file") + " (.mcp.json)", defaultValue: undefined, }, { flags: "--log-level <level>", description: theme.info("Log level") + " (trace, debug, info, warn, error, fatal)", defaultValue: "info", }, { flags: "--linked-app <app-id>", description: theme.info("Link to specific application configuration") + "\n" + theme.label("Options: claude-desktop, cursor, claude-code"), }, { flags: "--profile <profile-id>", description: theme.info("Use specific profile for workspace/project") + theme.muted(" (basic support - full profile management TODO)"), }, { flags: "--group <name>", description: theme.info("Server group name to load servers from"), }, { flags: "-p, --persona <name>", description: theme.info("Persona name to activate on startup") + "\n" + theme.muted("(makes --mcp-config optional)"), }, ]; /** * Add MCP server run options to a command */ function addMcpServerOptions(command: Command): Command { mcpServerRunOptions.forEach((opt) => { if (opt.defaultValue !== undefined) { command.option(opt.flags, opt.description, opt.defaultValue); } else { command.option(opt.flags, opt.description); } }); return command; } /** * Run the MCP server with the given options */ async function runMcpServer(options: any): Promise<void> { // Validate transport type const transport = options.transport as RuntimeTransportType; if (!["http", "stdio"].includes(transport)) { console.error( semantic.messageError(`โŒ Invalid transport type: ${transport}`) ); console.error(theme.warning(" Valid options: http, stdio")); process.exit(1); } // Validate port is only used with http transport if (options.port && transport !== "http") { console.error( semantic.messageError( "โŒ --port flag can only be used with --transport http" ) ); process.exit(1); } const port = options.port ? parseInt(options.port) : 3000; if (isNaN(port) || port < 1 || port > 65535) { console.error( semantic.messageError(`โŒ Invalid port number: ${options.port}`) ); console.error(theme.warning(" Port must be between 1 and 65535")); process.exit(1); } // Validate log level const logLevel = options.logLevel; const validLogLevels = ["trace", "debug", "info", "warn", "error", "fatal"]; if (!validLogLevels.includes(logLevel)) { console.error(semantic.messageError(`โŒ Invalid log level: ${logLevel}`)); console.error( theme.warning(` Valid options: ${validLogLevels.join(", ")}`) ); process.exit(1); } const runtimeOptions: RuntimeOptions = { transport, port: transport === "http" ? port : undefined, debug: options.debug || false, insecure: options.insecure || false, equipToolset: options.equipToolset, configPath: options.mcpConfig, linkedApp: options.linkedApp, profile: options.profile, logLevel, group: options.group, persona: options.persona, }; // ! We use dynamic imports to ensure that the transport and logging configuration is initialized // ! in the correct order. // Initialize logger with transport-aware configuration const { getLogger } = await import("./utils/logging.js"); const logger = getLogger(undefined, runtimeOptions); // Set up process warning listener to capture Node.js warnings in logs process.on("warning", (warning) => { logger.warn("Node.js warning", { name: warning.name, message: warning.message, stack: warning.stack, }); }); // Dynamic imports for all modules that might create worker threads const { displayBanner, displayServerRuntimeInfo, output } = await import( "./utils/output.js" ); const { discoverMcpConfig } = await import("./config/mcpConfigLoader.js"); // Discover MCP configuration const configResult = await discoverMcpConfig( runtimeOptions.configPath, true, // Update preference when CLI path is provided runtimeOptions.linkedApp, runtimeOptions.profile ); // Handle configuration discovery results if (!configResult.configPath) { // If persona is provided, allow startup without explicit MCP config if (!runtimeOptions.persona) { console.error(semantic.messageError("โŒ No MCP configuration found")); console.error(""); console.error( theme.warning( configResult.errorMessage || "Unknown configuration error" ) ); console.error(""); console.error( theme.info( "๐Ÿ’ก Use --linked-app <app-id> to load app-specific config or" ) ); console.error( theme.info(" --mcp-config <path> to specify a configuration file or") ); console.error( theme.info(" --persona <name> to start with persona configuration") ); process.exit(1); } else { // When persona is provided but no MCP config found, create a minimal config if (runtimeOptions.debug) { logger.debug( `No MCP config found, but persona '${runtimeOptions.persona}' provided - will use minimal config` ); } } } // Display config source info in debug mode if (runtimeOptions.debug) { const sourceText = { cli: "command line argument", app: `application config (${runtimeOptions.linkedApp}${runtimeOptions.profile ? `/${runtimeOptions.profile}` : ""})`, preference: "user preference", discovered: "automatic discovery", none: "unknown source", }[configResult.source]; logger.debug(`Config source: ${sourceText}`); } // Create transport config from runtime options const { MetaMCPServerFactory } = await import("./server/index.js"); const transportConfig: TransportConfig = { type: runtimeOptions.transport, ...(runtimeOptions.transport === "http" && { port: runtimeOptions.port || 3000, host: "localhost", }), }; // Create server instance const server = MetaMCPServerFactory.createDefaultServer(transportConfig); // Setup graceful shutdown let isShuttingDown = false; const shutdown = async (signal?: string) => { // Prevent multiple shutdown attempts if (isShuttingDown) { return; } isShuttingDown = true; if (runtimeOptions.debug) { logger.debug(`Shutting down HyperTool server... (${signal || "manual"})`); } // Set a hard timeout to force exit if graceful shutdown fails const forceExitTimeout = setTimeout(() => { logger.error( "Forcefully exiting after timeout - graceful shutdown failed" ); process.exit(1); }, 5000); // 5 second timeout try { await server.stop(); clearTimeout(forceExitTimeout); process.exit(0); } catch (error) { logger.error("Error during shutdown:", error); clearTimeout(forceExitTimeout); process.exit(1); } }; // Handle signals - use setImmediate to ensure signal is processed immediately process.on("SIGINT", () => { setImmediate(() => shutdown("SIGINT")); }); process.on("SIGTERM", () => { setImmediate(() => shutdown("SIGTERM")); }); // For stdio mode, also handle SIGHUP which can occur when terminal closes if (runtimeOptions.transport === "stdio") { process.on("SIGHUP", () => { setImmediate(() => shutdown("SIGHUP")); }); // Handle uncaught exceptions process.on("uncaughtException", (error) => { logger.error("Uncaught exception:", error); shutdown("uncaughtException"); }); // In production, handle stdin end as a shutdown signal // Skip this in test environment to avoid premature shutdown if (process.env.NODE_ENV !== "test") { process.stdin.on("end", () => { logger.debug("stdin ended, shutting down..."); shutdown("stdin-end"); }); } } // Only show banner and clear terminal for HTTP transport // For stdio, any output to stdout will interfere with MCP protocol if (runtimeOptions.transport === "http") { output.clearTerminal(); displayBanner(APP_NAME); displayServerRuntimeInfo( runtimeOptions.transport, runtimeOptions.port, "localhost" ); } // Start server const initOptions = MetaMCPServerFactory.createInitOptions({ transport: transportConfig, debug: runtimeOptions.debug, configPath: configResult.configPath || undefined, // Use undefined when persona mode without config configSource: configResult.configSource, }); await server.start(initOptions, runtimeOptions); // Only show warnings and separator for HTTP transport if (runtimeOptions.transport === "http") { if (runtimeOptions.insecure) { logger.warn( semantic.messageError( "โš ๏ธ INSECURE MODE: Tools with changed reference hashes are allowed" ) ); } output.displaySeparator(); } } /** * Parse CLI arguments and set up the program structure */ async function parseCliArguments(): Promise<RuntimeOptions> { const program = new Command(); program .name(APP_TECHNICAL_NAME) .description( theme.success("๐Ÿš€ HyperTool MCP: Unified MCP Proxy Server") + "\n\n" + theme.info("Commands:") + "\n" + theme.success(" mcp run") + " - " + theme.muted("Run the MCP server (with --persona or --mcp-config)") + "\n" + theme.success(" persona") + " - " + theme.muted("Manage persona content packs") + "\n" + theme.success(" setup") + " - " + theme.muted("Interactive setup wizard") ) .version(APP_VERSION); // Add deprecated options as hidden options (they work but don't show in help) program .option("--dry-run", "", false) .option("--install [app]", "", undefined); // Hide the deprecated options from help const dryRunOption = program.options.find((opt) => opt.flags.includes("--dry-run") ); const installOption = program.options.find((opt) => opt.flags.includes("--install") ); if (dryRunOption) dryRunOption.hidden = true; if (installOption) installOption.hidden = true; // Add helpful examples section focusing on the two main runtime options program.addHelpText( "after", ` ${theme.success("๐ŸŽญ Persona Mode")} ${theme.muted("(Bundled MCP servers + toolsets)")} ${theme.warning("Setup:")} ${theme.info("hypertool-mcp persona add <path>")} ${theme.muted("# Add a persona")} ${theme.info("hypertool-mcp persona list")} ${theme.muted("# List available personas")} ${theme.warning("Run:")} ${theme.info("hypertool-mcp mcp run --persona dev")} ${theme.muted("# Start with persona")} ${theme.success("โš™๏ธ Standard Mode")} ${theme.muted("(Your .mcp.json files)")} ${theme.warning("Run:")} ${theme.info("hypertool-mcp mcp run --mcp-config ./config.json")} ${theme.muted("# Start server")} ${theme.label("Advanced Options:")} ${theme.muted("hypertool-mcp mcp run --help")} ${theme.muted("# See all MCP server options")} ${theme.muted("hypertool-mcp setup")} ${theme.muted("# Interactive setup wizard")} ${theme.label("Tool Organization:")} ${theme.muted("By default, configuration tools are in a separate mode.")} ${theme.muted("To show all tools together: export HYPERTOOL_ENABLE_CONFIG_TOOLS_MENU=false")}` ); // Add config subcommands const { createConfigCommands } = await import( "./config-manager/cli/index.js" ); program.addCommand(createConfigCommands()); // Add setup command directly const setupModule = await import("./commands/setup/index.js"); program.addCommand(setupModule.createSetupCommand()); // Add service management commands const { createServiceCommand } = await import("./commands/service/index.js"); program.addCommand(createServiceCommand()); // Add extension management commands (only if DXT is enabled) // Initialize feature flags early to determine if extensions should be available const featureFlagService = getFeatureFlagService(); await featureFlagService.initialize(); if (featureFlagService.isDxtEnabled()) { const { createExtensionsCommand } = await import( "./commands/extensions/index.js" ); program.addCommand(createExtensionsCommand()); } // Add persona management commands const { createPersonaCommand } = await import("./commands/persona/index.js"); program.addCommand(createPersonaCommand()); // Toolset management is available via MCP tools when server is running // No top-level CLI commands needed per docs/NAVIGATION.md // Add mcp command with subcommands const mcpCommand = new Command("mcp").description( "MCP server operations and management" ); // Add 'run' subcommand for running the MCP server const runCommand = new Command("run").description( "Run the MCP server (default if no subcommand specified)" ); // Add all MCP server options to the run command addMcpServerOptions(runCommand); runCommand.action(async (options) => { // Run the MCP server with the given options await runMcpServer(options); }); mcpCommand.addCommand(runCommand); // Add MCP server management commands directly const { createListCommand, createGetCommand, createAddCommand, createRemoveCommand, createGroupCommands, } = await import("./mcp-manager/cli/index.js"); mcpCommand.addCommand(createListCommand()); mcpCommand.addCommand(createGetCommand()); mcpCommand.addCommand(createAddCommand()); mcpCommand.addCommand(createRemoveCommand()); mcpCommand.addCommand(createGroupCommands()); program.addCommand(mcpCommand); // Check if this is first run (no config exists) const { SetupWizard } = await import("./commands/setup/setup.js"); const isFirstRun = await SetupWizard.isFirstRun(); // If no command is specified, default to 'mcp run' or 'setup' // But only if the first argument isn't already a known command const cliArgs = process.argv.slice(2); const knownCommands = [ "config", "mcp", "setup", "service", "persona", "help", ]; // Add extensions to known commands only if DXT is enabled if (featureFlagService.isDxtEnabled()) { knownCommands.push("extensions"); } const knownMcpSubcommands = ["run", "list", "get", "add", "remove"]; // Check if any argument is a known command let hasCommand = false; let hasMcpCommand = false; for (const arg of cliArgs) { if (knownCommands.includes(arg)) { hasCommand = true; if (arg === "mcp") { hasMcpCommand = true; } break; } } // Also check for special global options that should not trigger mcp command const hasSpecialGlobalOption = cliArgs.includes("--help") || cliArgs.includes("-h") || cliArgs.includes("--version") || cliArgs.includes("-V") || cliArgs.some((arg) => arg.startsWith("--install")); // If we have 'mcp' command but no subcommand, insert 'run' if (hasMcpCommand) { const mcpIndex = cliArgs.indexOf("mcp"); const nextArg = cliArgs[mcpIndex + 1]; // If there's no next arg or it's an option (starts with --), add 'run' if (!nextArg || nextArg.startsWith("--")) { // Insert 'run' after 'mcp' command process.argv.splice(2 + mcpIndex + 1, 0, "run"); } } // If no arguments at all, default to 'setup' for first run or show help else if (cliArgs.length === 0) { if (isFirstRun) { // Check feature flag to determine whether to show setup wizard const featureFlagService = getFeatureFlagService(); await featureFlagService.initialize(); if (featureFlagService.isSetupWizardEnabled()) { // Show setup wizard (existing behavior) console.log(theme.success("๐ŸŽฏ Welcome to Hypertool MCP!")); console.log( theme.info(" No configuration detected. Let's get you set up!\n") ); console.log(theme.muted(" Running: hypertool-mcp setup\n")); process.argv.push("setup"); } else { // Primary onboarding path: show main help console.log(theme.success("๐Ÿš€ Hypertool MCP is ready!")); console.log(theme.info(" To get started, use one of:")); console.log( theme.info(" โ€ข hypertool-mcp mcp run --mcp-config <path>") ); console.log(theme.info(" โ€ข hypertool-mcp mcp run --persona <name>")); console.log(theme.info(" โ€ข hypertool-mcp setup (interactive)")); console.log(""); process.argv.push("--help"); } } else { // Not first run, just show help process.argv.push("--help"); } } await program.parseAsync(); // If we get here and no command was executed, we're done const args = process.argv.slice(2); if (args.length > 0 && (args[0] === "config" || args[0] === "help")) { // Subcommand was handled, exit process.exit(0); } const options = program.opts(); // Validate that --dry-run is only used with --install if (options.dryRun && !options.install) { console.error( semantic.messageError("โŒ --dry-run flag can only be used with --install") ); console.error( theme.warning( " Usage: hypertool-mcp --install claude-desktop --dry-run" ) ); throw new Error("Invalid install option"); } if (options.install !== undefined) { // Show deprecation warning console.error(""); console.error(theme.warning("โš ๏ธ DEPRECATION WARNING")); console.error( theme.warning( " The --install flag is deprecated and will be removed in a future version." ) ); console.error( theme.info(" Please use the modern setup command instead:") ); console.error(theme.info("")); console.error(theme.success(" hypertool-mcp setup")); console.error(theme.info("")); console.error( theme.muted( " The setup command provides a better interactive experience with" ) ); console.error( theme.muted(" more configuration options and non-interactive support.") ); console.error(""); // Wait a moment for the user to see the warning await new Promise((resolve) => setTimeout(resolve, 2000)); // options.install will be true if no app specified, or a string if app specified const installApp = typeof options.install === "string" ? options.install : "all"; await handleInstallOption([installApp], options.dryRun); process.exit(0); } // If we get here, the MCP server options will be handled by the mcp subcommand return {} as RuntimeOptions; } /** * Main entry point for HyperTool MCP server */ async function main(): Promise<void> { try { // Parse CLI arguments - this will handle commands and options await parseCliArguments(); } catch (error) { // Use console.error since logger may not be initialized yet console.error( semantic.messageError(`โŒ Failed to start HyperTool server:`), error ); process.exit(1); } } // Export main server components for programmatic use export { MetaMCPServer } from "./server/base.js"; export { EnhancedMetaMCPServer } from "./server/enhanced.js"; export { MetaMCPServerFactory } from "./server/factory.js"; export type { MetaMCPServerConfig, TransportConfig, ServerInitOptions, } from "./server/types.js"; export type { RuntimeOptions, RuntimeTransportType } from "./types/runtime.js"; export { discoverMcpConfig } from "./config/mcpConfigLoader.js"; export { startServer, SmitheryConfig } from "./server.js"; // This file is now used as a library - binary entry point is in bin.ts await main();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/toolprint/hypertool-mcp'

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