Skip to main content
Glama

hypertool-mcp

add.ts13.1 kB
/** * Persona Add Command * * CLI command to install personas from folder paths or .htp archives * into the standard personas directory (~/.toolprint/hypertool-mcp/personas). * * @fileoverview Persona installation CLI command */ import { Command } from "commander"; import { resolve } from "path"; import { promises as fs } from "fs"; import inquirer from "inquirer"; import { theme, semantic } from "../../utils/theme.js"; import { createChildLogger } from "../../utils/logging.js"; import { installPersona, analyzeSource, checkPersonaExists, getStandardPersonasDir, SourceType, type InstallOptions, } from "../../persona/installer.js"; import { isPersonaError } from "../../persona/errors.js"; import { PersonaErrorCode } from "../../persona/types.js"; const logger = createChildLogger({ module: "persona-add-cli" }); /** * Command options interface */ interface AddCommandOptions { /** Force overwrite existing personas */ force?: boolean; /** Skip validation during installation */ skipValidation?: boolean; /** Create backup of existing persona before overwriting */ backup?: boolean; /** Custom installation directory */ installDir?: string; } /** * Check for environment variables in MCP config that need configuration */ async function checkEnvironmentVariables(installPath: string): Promise<{ hasEnvVars: boolean; envVars: Array<{ server: string; vars: string[] }>; configPath: string; }> { const mcpConfigPath = `${installPath}/mcp.json`; try { const configContent = await fs.readFile(mcpConfigPath, "utf-8"); const config = JSON.parse(configContent); const envVars: Array<{ server: string; vars: string[] }> = []; if (config.mcpServers) { for (const [serverName, serverConfig] of Object.entries( config.mcpServers )) { if ( typeof serverConfig === "object" && serverConfig !== null && "env" in serverConfig ) { const env = (serverConfig as any).env; if (env && typeof env === "object") { const vars = Object.keys(env); if (vars.length > 0) { envVars.push({ server: serverName, vars: vars, }); } } } } } return { hasEnvVars: envVars.length > 0, envVars, configPath: mcpConfigPath, }; } catch (error) { // If we can't read the MCP config, that's okay return { hasEnvVars: false, envVars: [], configPath: mcpConfigPath, }; } } /** * Interactive environment variable configuration using inquirer (simplified) */ async function interactiveEnvVarConfig( envCheck: { hasEnvVars: boolean; envVars: Array<{ server: string; vars: string[] }>; configPath: string; }, personaName: string ): Promise<void> { const { shouldConfigure } = await inquirer.prompt([ { type: "confirm", name: "shouldConfigure", message: "Configure now?", default: true, }, ]); if (!shouldConfigure) { console.log( theme.warning( `⏭️ Configure later with: ${theme.info("hypertool persona inspect " + personaName)}` ) ); return; } const envValues: Record<string, string> = {}; // Collect values for all environment variables for (const serverEnv of envCheck.envVars) { for (const envVar of serverEnv.vars) { const { value } = await inquirer.prompt([ { type: "input", name: "value", message: `${serverEnv.server}.${envVar}:`, validate: (input: string) => (input.trim() ? true : "Required"), }, ]); envValues[envVar] = value; } } // Update the MCP config file with the new values try { const configContent = await fs.readFile(envCheck.configPath, "utf-8"); const config = JSON.parse(configContent); // Update environment variables in the config for (const [serverName, serverConfig] of Object.entries( config.mcpServers )) { if ( typeof serverConfig === "object" && serverConfig !== null && "env" in serverConfig ) { const env = (serverConfig as any).env; if (env && typeof env === "object") { for (const [envVar, currentValue] of Object.entries(env)) { if (envValues[envVar]) { env[envVar] = envValues[envVar]; } } } } } // Write the updated config back to file await fs.writeFile(envCheck.configPath, JSON.stringify(config, null, 2)); console.log( theme.success( `✅ Configured! Run: ${theme.warning(`hypertool persona activate ${personaName}`)}` ) ); console.log(` ${theme.muted("Config:")} ${envCheck.configPath}`); } catch (error) { console.error( theme.error(`❌ Config failed. Edit manually: ${envCheck.configPath}`) ); } } /** * Display installation progress and results (simplified and compact) */ async function displayInstallationResult( sourcePath: string, result: any, sourceType: SourceType ): Promise<void> { if (result.success) { console.log(theme.success(`✅ Installed "${result.personaName}"`)); console.log(` ${theme.muted("Location:")} ${result.installPath}`); console.log(` ${theme.muted("Config:")} ${result.installPath}/mcp.json`); // Check for environment variables that need configuration const envCheck = await checkEnvironmentVariables(result.installPath); if (envCheck.hasEnvVars) { console.log(); console.log(theme.warning("⚙️ Configuration needed:")); for (const serverEnv of envCheck.envVars) { const vars = serverEnv.vars.join(", "); console.log( ` ${theme.info(serverEnv.server)}: ${theme.warning(vars)}` ); } // Offer interactive configuration await interactiveEnvVarConfig(envCheck, result.personaName); } else { console.log(); console.log( theme.success( `🚀 Ready to use! Run: ${theme.warning(`hypertool persona activate ${result.personaName}`)}` ) ); } } else { console.error(semantic.messageError("❌ Installation failed")); if (result.errors && result.errors.length > 0) { for (const error of result.errors) { console.error(` ${theme.error(error)}`); } } } } /** * Display helpful error messages based on error type */ function displayError(error: Error, sourcePath: string): void { console.error(semantic.messageError("❌ Failed to install persona:")); if (isPersonaError(error)) { console.error(` ${theme.error(error.message)}`); // Provide specific guidance based on error type switch (error.code) { case PersonaErrorCode.PERSONA_NOT_FOUND: console.log(); console.error( theme.info("💡 Check that the path exists and is accessible") ); console.error(theme.info("💡 Use absolute paths to avoid confusion")); break; case PersonaErrorCode.INVALID_SCHEMA: console.log(); console.error( theme.info( "💡 Use 'hypertool persona validate <path>' to see detailed validation errors" ) ); console.error( theme.info("💡 Ensure persona.yaml file has correct structure") ); break; case PersonaErrorCode.DUPLICATE_PERSONA_NAME: console.log(); console.error( theme.info("💡 Use --force to overwrite existing persona") ); console.error( theme.info("💡 Use --backup to create backup before overwriting") ); console.error( theme.info("💡 Use 'hypertool persona list' to see existing personas") ); break; case PersonaErrorCode.ARCHIVE_EXTRACTION_FAILED: console.log(); console.error(theme.info("💡 Ensure the .htp file is not corrupted")); console.error( theme.info( "💡 Check that you have write permissions to the installation directory" ) ); break; case PersonaErrorCode.FILE_SYSTEM_ERROR: console.log(); console.error(theme.info("💡 Check file permissions and disk space")); console.error( theme.info("💡 Ensure installation directory is writable") ); break; default: console.log(); console.error( theme.info("💡 Check the persona structure and try again") ); } if (error.suggestions && error.suggestions.length > 0) { console.log(); console.error(theme.info("💡 Suggestions:")); for (const suggestion of error.suggestions) { console.error(` • ${theme.info(suggestion)}`); } } } else { console.error(` ${theme.error(error.message)}`); } } /** * Create the add subcommand */ export function createAddCommand(): Command { return new Command("add") .description("Install a persona from a folder path or .htp archive") .argument("<path>", "Path to persona folder or .htp archive file") .option("--force", "Overwrite existing persona if it already exists", false) .option( "--backup", "Create backup of existing persona before overwriting", false ) .option( "--skip-validation", "Skip persona validation during installation", false ) .option( "--install-dir <dir>", "Custom installation directory (defaults to ~/.toolprint/hypertool-mcp/personas)" ) .action(async (sourcePath: string, options: AddCommandOptions) => { try { const resolvedSourcePath = resolve(sourcePath); // Analyze the source to understand what we're installing const sourceInfo = await analyzeSource(resolvedSourcePath); if (!sourceInfo.accessible) { throw new Error( `Source path is not accessible: ${resolvedSourcePath}` ); } // Check if persona already exists const installDir = options.installDir || getStandardPersonasDir(); if (sourceInfo.personaName) { const exists = await checkPersonaExists( sourceInfo.personaName, installDir ); if (exists) { if (!options.force) { console.log( theme.warning( `⚠️ Persona '${sourceInfo.personaName}' already exists` ) ); console.log(); console.log( theme.info( "Use --force to overwrite or --backup to create a backup first" ) ); process.exit(1); return; // Prevent further execution in test environment } else { console.log( theme.warning( `⚠️ Will overwrite existing persona '${sourceInfo.personaName}'` ) ); if (options.backup) { console.log( theme.info(" Creating backup before overwrite...") ); } console.log(); } } } // Prepare installation options const installOptions: InstallOptions = { force: options.force, backup: options.backup, skipValidation: options.skipValidation, installDir: options.installDir, }; // Install the persona const result = await installPersona(resolvedSourcePath, installOptions); // Display results await displayInstallationResult( resolvedSourcePath, result, sourceInfo.type ); // Exit with appropriate code process.exit(result.success ? 0 : 1); } catch (error) { displayError(error as Error, sourcePath); process.exit(1); } }) .addHelpText( "after", ` ${theme.label("Examples:")} ${theme.muted("# Install from folder")} ${theme.muted("hypertool persona add ./my-persona-folder")} ${theme.muted("# Install from archive")} ${theme.muted("hypertool persona add ./awesome-persona.htp")} ${theme.muted("# Force overwrite existing persona")} ${theme.muted("hypertool persona add ./updated-persona.htp --force")} ${theme.muted("# Create backup before overwriting")} ${theme.muted("hypertool persona add ./updated-persona.htp --force --backup")} ${theme.muted("# Install to custom directory")} ${theme.muted("hypertool persona add ./persona --install-dir /custom/path")} ${theme.label("Notes:")} ${theme.muted("• Persona folders must contain a valid persona.yaml file")} ${theme.muted("• Archive files must have .htp extension")} ${theme.muted("• Installation validates persona structure by default")} ${theme.muted("• Use --skip-validation to bypass validation checks")} ${theme.muted("• Backups are created with timestamp: persona.backup.YYYY-MM-DD...")}` ); }

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