UNPKG

@toolprint/hypertool-mcp

Version:

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

357 lines • 13.5 kB
/** * Shared utility functions for MCP setup scripts * Used by Claude Desktop, Claude Code, and other MCP integrations */ import { promises as fs } from "fs"; import { join } from "path"; import { homedir } from "os"; import inquirer from "inquirer"; import { output } from "../../utils/output.js"; import { theme } from "../../utils/theme.js"; /** * Check if a file exists */ export async function fileExists(path) { try { await fs.access(path); return true; } catch { return false; } } /** * Read and parse a JSON configuration file */ export async function readJsonFile(path) { try { const content = await fs.readFile(path, "utf8"); return JSON.parse(content); } catch (error) { throw new Error(`Failed to read JSON from ${path}: ${error}`); } } /** * Write a JSON configuration file */ export async function writeJsonFile(path, data) { try { await fs.writeFile(path, JSON.stringify(data, null, 2), "utf8"); } catch (error) { throw new Error(`Failed to write JSON to ${path}: ${error}`); } } /** * Ensure a directory exists */ export async function ensureDirectoryExists(path) { try { await fs.mkdir(path, { recursive: true }); } catch (error) { throw new Error(`Failed to create directory ${path}: ${error}`); } } /** * Validate that an MCP configuration exists and is valid */ export async function validateMcpConfiguration(configPath) { // Check if config exists if (!(await fileExists(configPath))) { output.error("❌ No MCP configuration found."); output.warn(" Please create a configuration file first."); throw new Error("No MCP configuration found"); } // Try to parse the JSON try { await readJsonFile(configPath); } catch (error) { output.error("❌ Invalid MCP configuration:"); output.warn(` ${error}`); output.warn(" Please fix the JSON syntax in your configuration file."); throw new Error("Invalid MCP configuration"); } } /** * Create a backup of the original configuration file */ export async function createConfigBackup(context) { if (context.dryRun) { output.info(`[DRY RUN] Would create backup: ${context.backupPath}`); return; } // Check if backup already exists if (await fileExists(context.backupPath)) { // If backup exists and current config only has hypertool, skip backup const currentConfig = await readJsonFile(context.originalConfigPath); const serverNames = Object.keys(currentConfig.mcpServers || {}); if (serverNames.length === 1 && serverNames[0].toLowerCase().includes("hypertool")) { // Hypertool is already configured and backup exists - no need to overwrite return; } const { overwrite } = await inquirer.prompt([ { type: "confirm", name: "overwrite", message: theme.warning("⚠️ Backup file already exists. Overwrite?"), default: false, }, ]); if (!overwrite) { output.warn("🛑 Backup skipped. Exiting without changes."); process.exit(0); } } try { // Read current .mcp.json to get any new servers const currentConfig = await readJsonFile(context.originalConfigPath); const newServers = { ...currentConfig.mcpServers }; // Remove hypertool from new servers Object.keys(newServers).forEach((key) => { if (key.toLowerCase().includes("hypertool")) { delete newServers[key]; } }); let backupConfig; // If backup already exists, merge new servers into it (additive) if (await fileExists(context.backupPath)) { const existingBackup = await readJsonFile(context.backupPath); backupConfig = { mcpServers: { ...existingBackup.mcpServers, ...newServers, // New servers take precedence }, }; } else { // First time - create backup with current servers backupConfig = { mcpServers: newServers, }; } const backupContent = JSON.stringify(backupConfig, null, 2); await fs.writeFile(context.backupPath, backupContent, "utf8"); } catch (error) { output.error("❌ Failed to create backup:"); output.error(error instanceof Error ? error.message : String(error)); throw error; } } /** * Migrate existing MCP servers to HyperTool configuration */ export async function migrateToHyperToolConfig(context) { const originalConfig = await readJsonFile(context.originalConfigPath); if (!originalConfig.mcpServers) { originalConfig.mcpServers = {}; } // Use backup as the source of truth for servers if (!(await fileExists(context.backupPath))) { throw new Error("Backup file not found. Cannot migrate servers safely."); } const backupConfig = await readJsonFile(context.backupPath); const serversFromBackup = backupConfig.mcpServers || {}; if (context.dryRun) { const serverCount = Object.keys(serversFromBackup).length; output.info(`[DRY RUN] Would copy ${serverCount} servers from backup to: ${context.hyperToolConfigPath}`); if (false) { output.info(`[DRY RUN] Would also sync servers to database (NeDB mode enabled)`); } return originalConfig; } const hyperToolConfig = { mcpServers: serversFromBackup, }; await writeJsonFile(context.hyperToolConfigPath, hyperToolConfig); // If NeDB is enabled, we should notify that database sync will happen // when HyperTool starts up and reads the config file if (false) { output.info("📊 Database mode detected - servers will be synced to database on first run"); } return originalConfig; } /** * Prompt user for cleanup options (automatic vs manual) * Note: We're simplifying to always do automated cleanup */ export async function promptForCleanupOptions() { // Always return true for automated cleanup - we've removed the manual option return true; } /** * Create HyperTool proxy configuration */ export function createHyperToolProxyConfig(hyperToolConfigPath) { return { hypertool: { type: "stdio", command: "npx", args: [ "-y", "@toolprint/hypertool-mcp@latest", "--mcp-config", hyperToolConfigPath, ], }, }; } /** * Update MCP configuration with HyperTool proxy */ export async function updateMcpConfigWithHyperTool(context, originalConfig, _shouldCleanup, hyperToolConfigPath) { const hyperToolProxy = createHyperToolProxyConfig(hyperToolConfigPath); if (context.dryRun) { output.info(`[DRY RUN] Would update config with Hypertool proxy: ${context.originalConfigPath}`); return; } // Always do automated cleanup (only HyperTool in config) const newConfig = { ...originalConfig, mcpServers: hyperToolProxy, }; await writeJsonFile(context.originalConfigPath, newConfig); } /** * Display setup summary */ export async function displaySetupSummary(context, shouldCleanup, integrationName) { if (context.dryRun) { return; // Summary already shown in displaySetupPlan for dry run } output.displaySpaceBuffer(1); output.success(`🎉 ${integrationName} integration setup complete!`); output.displaySpaceBuffer(1); const hyperToolConfig = await readJsonFile(context.hyperToolConfigPath); const serverCount = Object.keys(hyperToolConfig.mcpServers).length; output.displaySubHeader("📋 Changes completed:"); output.success(`✅ ${serverCount} MCP server(s) migrated to HyperTool configuration`); output.success("✅ HyperTool proxy added to MCP configuration"); output.success("✅ Original configuration backed up"); if (shouldCleanup) { output.success("✅ Automated cleanup completed"); } else { output.warn("⚠️ Manual cleanup selected - you may have duplicate servers"); } // Show database mode information if enabled if (false) { output.displaySpaceBuffer(1); output.info("📊 Database Mode: ENABLED"); output.displayInstruction(" HyperTool is using database-backed configuration storage"); output.displayInstruction(" To manage servers, use: hypertool-mcp config show servers"); } output.displaySpaceBuffer(1); output.displaySubHeader("🔄 Next steps:"); output.displayInstruction(`1. Restart ${integrationName}`); output.displayInstruction("2. Your MCP tools are now proxied through HyperTool"); output.displayInstruction("3. Use HyperTool commands to manage toolsets"); output.displaySpaceBuffer(1); output.displaySubHeader("🔄 To restore original configuration:"); output.displayTerminalInstruction(`cp "${context.backupPath}" "${context.originalConfigPath}"`); } /** * Display what the setup process will do before getting user consent */ export async function displaySetupPlan(context, originalConfig, integrationName) { const serverCount = Object.keys(originalConfig.mcpServers || {}).length; const serverNames = Object.keys(originalConfig.mcpServers || {}); if (context.dryRun) { output.displayHeader("📋 Dry Run - Changes Preview"); output.displaySpaceBuffer(1); output.info("📁 Files that would be created/modified:"); output.displayInstruction(`• Backup: ${context.backupPath}`); output.displayInstruction(`• HyperTool config: ${context.hyperToolConfigPath}`); output.displayInstruction(`• Updated MCP config: ${context.originalConfigPath}`); output.displaySpaceBuffer(1); output.info(`🔄 ${serverCount} MCP server(s) would be migrated:`); if (serverCount > 0) { serverNames.forEach((name) => { output.displayInstruction(`• ${name}`); }); } else { output.displayInstruction("• No existing servers to migrate"); } output.displaySpaceBuffer(1); output.info("✨ Result: HyperTool proxy replaces all servers in .mcp.json"); output.info("💡 Original servers remain accessible through HyperTool"); if (false) { output.displaySpaceBuffer(1); output.info("📊 Database Mode: ENABLED"); output.info(" • Servers will be stored in NeDB database"); output.info(" • Use 'hypertool-mcp config show' commands to manage"); } output.displaySpaceBuffer(1); return true; } // Check if HyperTool already exists (check for any server with "hypertool" in the name) const hasHypertool = Object.keys(originalConfig.mcpServers || {}).some((key) => key.toLowerCase().includes("hypertool")); if (hasHypertool) { output.warn("⚠️ HyperTool is already configured in MCP configuration"); const { shouldContinue } = await inquirer.prompt([ { type: "confirm", name: "shouldContinue", message: "Do you want to reinstall and update the configuration?", default: true, }, ]); if (!shouldContinue) { output.info("Installation cancelled."); return false; } } else { // Show what will happen output.displaySubHeader("This installer will:"); output.displayInstruction(`1. Backup your current MCP configuration`); output.displayInstruction(`2. Move your ${serverCount} existing MCP servers to HyperTool config`); output.displayInstruction(`3. Configure ${integrationName} to use HyperTool as a proxy`); output.displayInstruction(`4. Provide options for configuration cleanup`); output.displaySpaceBuffer(1); const { shouldProceed } = await inquirer.prompt([ { type: "confirm", name: "shouldProceed", message: "Proceed with installation?", default: true, }, ]); if (!shouldProceed) { output.info("Installation cancelled."); return false; } } return true; } /** * Check if Hypertool slash commands are already installed globally */ export async function hasClaudeCodeGlobalHypertoolSlashCommands() { const globalCommandsDir = join(homedir(), ".claude/commands/ht"); try { const exists = await fileExists(globalCommandsDir); if (!exists) return false; // Check if the directory has command files const files = await fs.readdir(globalCommandsDir); const mdFiles = files.filter((f) => f.endsWith(".md")); // We expect at least the core commands const expectedCommands = [ "list-all-tools.md", "new-toolset.md", "list-toolsets.md", ]; const hasAllCommands = expectedCommands.every((cmd) => mdFiles.includes(cmd)); return hasAllCommands; } catch { return false; } } //# sourceMappingURL=mcpSetupUtils.js.map