Skip to main content
Glama

hypertool-mcp

show.tsโ€ข20.7 kB
/** * CLI command for showing HyperTool configuration overview */ import { Command } from "commander"; import { theme } from "../../utils/theme.js"; import { promises as fs } from "fs"; import { join } from "path"; import { homedir } from "os"; import { ConfigurationManager } from "../index.js"; import { output } from "../../utils/output.js"; import { createShowServersCommand } from "./show-servers.js"; import { createShowGroupsCommand } from "./show-groups.js"; import { createShowSourcesCommand } from "./show-sources.js"; import { createShowConflictsCommand } from "./show-conflicts.js"; import { showInteractive } from "./show-interactive.js"; /** * Check if a server configuration references HyperTool itself */ function checkSelfReferencingServer(config: any): boolean { 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) { const argLower = arg.toLowerCase(); if ( argLower.includes("hypertool-mcp") || argLower.includes("@toolprint/hypertool-mcp") ) { return true; } } } } return false; } /** * Check if a server configuration is using a development build */ function checkDevelopmentBuild(config: any): boolean { if (config.type === "stdio" && config.command && config.args) { const command = config.command.toLowerCase(); const args = config.args || []; // Check if using node with a local dist/bin.js path if ((command === "node" || command.endsWith("/node")) && args.length > 0) { // Check if it has the mcp run pattern with a local path let hasLocalPath = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; // Check for development build patterns if ( arg.includes("/dist/bin.js") || arg.includes("/hypertool-mcp/dist/") || (arg.includes("dist") && arg.includes("bin.js")) ) { hasLocalPath = true; } // Check for mcp run pattern if (i < args.length - 1 && arg === "mcp" && args[i + 1] === "run") { // hasMcpRun = true; // Not used currently } } // Return true if we have a local path (with or without mcp run) return hasLocalPath; } } return false; } export interface ServerInfo { name: string; type: string; details: string; source?: string; healthy: boolean; warning?: string; isDevelopment?: boolean; developmentPath?: string; } export interface ApplicationStatus { name: string; installed: boolean; configPath?: string; hasConfig?: boolean; isDevelopmentBuild?: boolean; developmentPath?: string; mcpConfigPath?: string; } export interface ToolDetail { namespacedName: string; serverName: string; toolName: string; description?: string; parameters?: string[]; } export interface ToolsetInfo { name: string; description?: string; toolCount: number; autoGenerated: boolean; apps: string[]; toolDetails?: ToolDetail[]; serverGroups?: Record<string, ToolDetail[]>; } export function createShowCommand(): Command { const show = new Command("show"); show .description("Show HyperTool configuration with interactive navigation") .option("--json", "Output in JSON format instead of interactive mode") .action(async (options, command) => { // If no subcommand, show overview await showOverview(options, command); }) .addCommand(createShowServersCommand()) .addCommand(createShowGroupsCommand()) .addCommand(createShowSourcesCommand()) .addCommand(createShowConflictsCommand()); return show; } async function showOverview(options: any, command: any): Promise<void> { try { // Get global options from parent command // We need to go up two levels: show -> config -> program const globalOptions = command.parent?.parent?.opts() || {}; const linkedApp = globalOptions.linkedApp; // Always use interactive mode unless JSON output is requested if (!options.json) { await showInteractive({}, linkedApp); return; } const configManager = new ConfigurationManager(); await configManager.initialize(); const basePath = join(homedir(), ".toolprint/hypertool-mcp"); // Gather all information const { servers: mcpServers, configPath } = await getMcpServers( basePath, linkedApp ); const applications = await getApplicationStatus(configManager); const toolsets = await getToolsets(basePath); const healthStatus = await checkHealth(basePath, mcpServers, applications); if (options.json) { // Output as JSON const result = { mcpServers, mcpConfigPath: configPath, applications: linkedApp ? undefined : applications, toolsets, health: healthStatus, }; output.log(JSON.stringify(result, null, 2)); } else { // Display formatted output output.displayHeader("๐Ÿ› ๏ธ HyperTool Configuration Overview"); output.displaySpaceBuffer(1); // MCP Servers section displayMcpServers(mcpServers, configPath); // Applications section (only if not using --linked-app) if (!linkedApp) { displayApplications(applications); } // Toolsets section displayToolsets(toolsets); // Help link output.displayHelpContext( " For detailed help, visit: https://github.com/toolprint/hypertool-mcp" ); } } catch (error) { output.error("โŒ Failed to show configuration:"); output.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } export async function getMcpServers( basePath: string, linkedApp?: string ): Promise<{ servers: ServerInfo[]; configPath: string }> { const servers: ServerInfo[] = []; let configPath: string; // Determine which config to load if (linkedApp) { // Try app-specific config first configPath = join(basePath, "mcp", `${linkedApp}.json`); try { await fs.access(configPath); } catch { // Fall back to global config configPath = join(basePath, "mcp.json"); } } else { // Use global config configPath = join(basePath, "mcp.json"); } try { const content = await fs.readFile(configPath, "utf-8"); const config = JSON.parse(content); if (config.mcpServers) { for (const [name, serverConfig] of Object.entries(config.mcpServers)) { const server = serverConfig as any; let details = ""; switch (server.type) { case "stdio": details = server.command || "Unknown command"; if (server.args && Array.isArray(server.args)) { details += " " + server.args.join(" "); } break; case "http": case "sse": case "websocket": details = server.url || "Unknown URL"; break; default: details = "Unknown transport details"; } // Get source from metadata if available let source = "Unknown"; if (config._metadata?.sources?.[name]) { source = config._metadata.sources[name].app; } // Check for self-referencing HyperTool configuration const isSelfReferencing = checkSelfReferencingServer(server); // Check for development build const isDevelopment = checkDevelopmentBuild(server); let developmentPath: string | undefined; if (isDevelopment && server.args) { // Extract the development path from args for (const arg of server.args) { if (arg.includes("/dist/bin.js")) { developmentPath = arg; break; } } } servers.push({ name, type: server.type || "unknown", details, source, healthy: !isSelfReferencing, warning: isSelfReferencing ? "Self-referencing HyperTool configuration" : undefined, isDevelopment, developmentPath, }); } } } catch { // Config file doesn't exist or is invalid } return { servers, configPath }; } export async function getApplicationStatus( configManager: ConfigurationManager ): Promise<ApplicationStatus[]> { const applications: ApplicationStatus[] = []; const registry = (configManager as any).registry; try { const apps = await registry.getEnabledApplications(); for (const [, app] of Object.entries(apps)) { const appDef = app as any; const installed = await registry.isApplicationInstalled(appDef); const platformConfig = registry.getPlatformConfig(appDef); let configPath: string | undefined; let hasConfig = false; if (installed && platformConfig) { configPath = registry.resolvePath(platformConfig.configPath); if (appDef.detection.type === "project-local") { configPath = join( process.cwd(), platformConfig.configPath.replace("./", "") ); } if (configPath) { try { await fs.access(configPath); // Check if the config actually contains HyperTool const content = await fs.readFile(configPath, "utf-8"); const config = JSON.parse(content); // Check if config has HyperTool entry if (config.mcpServers?.["hypertool"]) { hasConfig = true; // Check if it's using a development build const hypertoolConfig = config.mcpServers["hypertool"]; // Extract MCP config path from args let mcpConfigPath: string | undefined; if (hypertoolConfig.args && Array.isArray(hypertoolConfig.args)) { const mcpConfigIndex = hypertoolConfig.args.indexOf("--mcp-config"); if ( mcpConfigIndex !== -1 && mcpConfigIndex < hypertoolConfig.args.length - 1 ) { mcpConfigPath = hypertoolConfig.args[mcpConfigIndex + 1]; } } if (checkDevelopmentBuild(hypertoolConfig)) { applications.push({ name: appDef.name, installed, configPath, hasConfig, isDevelopmentBuild: true, developmentPath: hypertoolConfig.args?.find((arg: string) => arg.includes("/dist/bin.js") ), mcpConfigPath, }); continue; } else { applications.push({ name: appDef.name, installed, configPath, hasConfig, isDevelopmentBuild: false, mcpConfigPath, }); continue; } } else { hasConfig = false; } } catch { hasConfig = false; } } } applications.push({ name: appDef.name, installed, configPath, hasConfig, isDevelopmentBuild: false, }); } } catch { // Error getting applications } return applications; } export async function getToolsets(basePath: string): Promise<ToolsetInfo[]> { const toolsets: ToolsetInfo[] = []; try { const prefsPath = join(basePath, "config.json"); const content = await fs.readFile(prefsPath, "utf-8"); const prefs = JSON.parse(content); if (prefs.toolsets) { for (const [id, toolset] of Object.entries(prefs.toolsets)) { const ts = toolset as any; // Find which apps use this toolset const apps: string[] = []; if (prefs.appDefaults) { for (const [appId, defaultToolset] of Object.entries( prefs.appDefaults )) { if (defaultToolset === id) { apps.push(appId); } } } // Parse tool details if tools exist let toolDetails: ToolDetail[] = []; let serverGroups: Record<string, ToolDetail[]> = {}; if (ts.tools && Array.isArray(ts.tools)) { toolDetails = ts.tools.map((tool: any) => { const namespacedName = tool.namespacedName || ""; const parts = namespacedName.split("."); const serverName = parts[0] || "unknown"; const toolName = parts.slice(1).join(".") || namespacedName; return { namespacedName, serverName, toolName, description: tool.description, parameters: tool.parameters, }; }); // Group tools by server serverGroups = toolDetails.reduce( (groups, tool) => { if (!groups[tool.serverName]) { groups[tool.serverName] = []; } groups[tool.serverName].push(tool); return groups; }, {} as Record<string, ToolDetail[]> ); } toolsets.push({ name: ts.name || id, description: ts.description, toolCount: ts.tools?.length || 0, autoGenerated: ts.metadata?.autoGenerated || false, apps, toolDetails, serverGroups, }); } } } catch { // Preferences file doesn't exist or is invalid } return toolsets; } interface HealthStatus { healthy: boolean; issues: string[]; warnings: string[]; suggestions: string[]; } async function checkHealth( basePath: string, servers: ServerInfo[], applications: ApplicationStatus[] ): Promise<HealthStatus> { const issues: string[] = []; const warnings: string[] = []; const suggestions: string[] = []; // Check if config directory exists try { await fs.access(basePath); } catch { issues.push("HyperTool configuration directory not found"); suggestions.push( `Run 'hypertool-mcp config backup' to initialize configuration` ); } // Check if any servers are configured if (servers.length === 0) { warnings.push("No MCP servers configured"); suggestions.push( `Run 'hypertool-mcp config backup' to import MCP servers from applications` ); } // Check if any applications are configured const installedApps = applications.filter((app) => app.installed); if (installedApps.length === 0) { warnings.push("No supported applications detected"); suggestions.push( "Install Claude Desktop, Cursor, or Claude Code to use HyperTool" ); } // Check if applications have HyperTool linked const linkableApps = applications.filter((app) => app.installed); const linkedApps = linkableApps.filter((app) => app.hasConfig); if (linkableApps.length > 0 && linkedApps.length === 0) { warnings.push("HyperTool not linked to any applications"); suggestions.push( `Run 'hypertool-mcp config link' to link HyperTool to applications` ); } // Check for invalid configurations const invalidServers = servers.filter((s) => !s.healthy); if (invalidServers.length > 0) { for (const server of invalidServers) { if (server.warning) { issues.push(`Server '${server.name}': ${server.warning}`); } } } // Check for toolset issues try { const prefsPath = join(basePath, "config.json"); const content = await fs.readFile(prefsPath, "utf-8"); const prefs = JSON.parse(content); if (!prefs.toolsets || Object.keys(prefs.toolsets).length === 0) { warnings.push("No toolsets configured"); suggestions.push( "Toolsets help organize MCP tools by application context" ); } } catch { // Preferences file doesn't exist } const healthy = issues.length === 0; return { healthy, issues, warnings, suggestions, }; } function displayMcpServers(servers: ServerInfo[], configPath: string): void { output.displaySubHeader(`๐Ÿ“ก MCP Servers (from ${configPath})`); if (servers.length === 0) { output.warn(" No MCP servers configured"); } else { output.info(` Total: ${servers.length} server(s)`); output.displaySpaceBuffer(1); // Group by transport type const byType = servers.reduce( (acc, server) => { if (!acc[server.type]) acc[server.type] = []; acc[server.type].push(server); return acc; }, {} as Record<string, ServerInfo[]> ); for (const [type, typeServers] of Object.entries(byType)) { output.displayInstruction( ` ${theme.warning(type.toUpperCase())} Transport (${typeServers.length}):` ); for (const server of typeServers) { const healthIcon = server.healthy ? "๐ŸŸข" : "๐Ÿ”ด"; output.info(` ${healthIcon} ${theme.primary(server.name)}`); output.info(` ${theme.muted(server.details)}`); if (server.source) { output.info(` ${theme.muted(`Source: ${server.source}`)}`); } if (server.isDevelopment && server.developmentPath) { output.warn( ` โš ๏ธ ${theme.warning("Using development build from:")} ${theme.muted(server.developmentPath)}` ); } if (server.warning) { output.warn(` โš ๏ธ ${server.warning}`); } } output.displaySpaceBuffer(1); } } } function displayApplications(applications: ApplicationStatus[]): void { output.displaySubHeader("๐Ÿ–ฅ๏ธ Applications"); if (applications.length === 0) { output.warn(" No applications configured"); } else { const installed = applications.filter((app) => app.installed); const configured = applications.filter((app) => app.hasConfig); output.info(` Detected: ${installed.length}/${applications.length}`); output.info(` Linked: ${configured.length}/${installed.length}`); output.displaySpaceBuffer(1); for (const app of applications) { const status = app.installed ? (app.hasConfig ? "โœ…" : "โš ๏ธ ") : "โŒ"; output.displayInstruction(` ${status} ${theme.primary(app.name)}`); if (app.installed) { if (app.isDevelopmentBuild && app.developmentPath) { output.warn(` โš ๏ธ ${theme.warning("Using development build")}`); output.info(` ${theme.muted(app.developmentPath)}`); } else if (app.configPath) { output.info(` ${theme.muted(app.configPath)}`); } // Show MCP config path for linked apps if (app.hasConfig && app.mcpConfigPath) { output.info(` ๐Ÿ“„ MCP Config: ${theme.muted(app.mcpConfigPath)}`); } if (!app.hasConfig) { output.info(` ${theme.warning("Not linked to HyperTool")}`); } } else { output.info(` ${theme.muted("Not installed")}`); } } } output.displaySpaceBuffer(1); } function displayToolsets(toolsets: ToolsetInfo[]): void { output.displaySubHeader("๐Ÿงฐ Toolsets"); if (toolsets.length === 0) { output.warn(" No toolsets configured"); } else { output.info(` Total: ${toolsets.length} toolset(s)`); output.displaySpaceBuffer(1); for (const toolset of toolsets) { const autoGen = toolset.autoGenerated ? theme.warning(" (auto)") : ""; output.displayInstruction(` โ€ข ${theme.primary(toolset.name)}${autoGen}`); if (toolset.description) { output.info(` ${theme.muted(toolset.description)}`); } output.info(` ${theme.muted(`Tools: ${toolset.toolCount}`)}`); if (toolset.apps.length > 0) { output.info( ` ${theme.muted(`Used by: ${toolset.apps.join(", ")}`)}` ); } } } output.displaySpaceBuffer(1); }

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