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);
}