UNPKG

@toolprint/hypertool-mcp

Version:

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

527 lines • 22.1 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) { 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) { 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 function createShowCommand() { 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, command) { 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, linkedApp) { const servers = []; let configPath; // 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; 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; 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) { const applications = []; const registry = configManager.registry; try { const apps = await registry.getEnabledApplications(); for (const [, app] of Object.entries(apps)) { const appDef = app; const installed = await registry.isApplicationInstalled(appDef); const platformConfig = registry.getPlatformConfig(appDef); let configPath; 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; 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) => 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) { const toolsets = []; 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; // Find which apps use this toolset const apps = []; 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 = []; let serverGroups = {}; if (ts.tools && Array.isArray(ts.tools)) { toolDetails = ts.tools.map((tool) => { 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; }, {}); } 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; } async function checkHealth(basePath, servers, applications) { const issues = []; const warnings = []; const suggestions = []; // 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, configPath) { 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; }, {}); 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) { 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) { 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); } //# sourceMappingURL=show.js.map