@toolprint/hypertool-mcp
Version:
HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers
191 lines • 7.49 kB
JavaScript
/**
* App detection step - Scans for installed MCP-compatible applications
*/
import inquirer from "inquirer";
import { promises as fs } from "fs";
import { join } from "path";
import { homedir } from "os";
import { output } from "../../utils/output.js";
import { theme } from "../../utils/theme.js";
export class AppDetectionStep {
name = "appDetection";
async run(state) {
output.displaySpaceBuffer(1);
output.info(theme.info("🔍 Scanning for MCP-compatible applications..."));
output.displaySpaceBuffer(1);
// Detect installed applications
const detectedApps = await this.detectApplications();
if (detectedApps.length === 0) {
output.warn("⚠️ No supported applications detected");
output.info("");
output.info("Supported applications:");
output.info(" • Claude Desktop (macOS)");
output.info(" • Cursor");
output.info(" • Claude Code");
output.displaySpaceBuffer(1);
// In non-interactive mode, check if specific apps were requested
if (state.nonInteractive) {
const requestedApps = state.apps;
if (requestedApps && requestedApps.length > 0) {
output.error("None of the requested applications were detected");
return { ...state, cancelled: true };
}
// If no specific apps requested, continue with empty list
return { ...state, detectedApps: [] };
}
// Ask if they want to continue anyway (interactive mode)
const { shouldContinue } = await inquirer.prompt([
{
type: "confirm",
name: "shouldContinue",
message: "Continue with manual setup?",
default: false,
},
]);
if (!shouldContinue) {
return { ...state, cancelled: true };
}
return { ...state, detectedApps: [] };
}
// Display detected apps
output.success("Found the following applications:");
for (const app of detectedApps) {
const icon = app.hasExistingConfig ? "✓" : "○";
const configInfo = app.hasExistingConfig
? theme.success(` (${app.serverCount} servers)`)
: theme.muted(" (no config)");
output.info(` ${icon} ${app.displayName}${configInfo}`);
}
output.displaySpaceBuffer(1);
// In non-interactive mode, select apps based on options
if (state.nonInteractive) {
let selectedApps;
// Check if specific apps were requested via CLI
const requestedApps = state.apps;
if (requestedApps && requestedApps.length > 0) {
// Filter to only requested apps that were detected
selectedApps = detectedApps
.filter((app) => requestedApps.includes(app.id))
.map((app) => app.id);
if (selectedApps.length === 0) {
output.error("None of the requested applications were detected");
return { ...state, cancelled: true };
}
}
else {
// Default to all detected apps
selectedApps = detectedApps.map((app) => app.id);
}
return {
...state,
detectedApps,
selectedApps,
};
}
// Ask which apps to configure
const { selectedApps } = await inquirer.prompt([
{
type: "checkbox",
name: "selectedApps",
message: "Which applications would you like to configure?",
choices: detectedApps.map((app) => ({
name: app.displayName,
value: app.id,
checked: false, // Start with all unchecked (opt-in)
})),
// No validation - allow empty selection
},
]);
return {
...state,
detectedApps,
selectedApps,
};
}
async detectApplications() {
const apps = [];
// Check Claude Desktop (macOS)
const claudeDesktopPath = join(homedir(), "Library/Application Support/Claude");
const claudeConfigPath = join(claudeDesktopPath, "claude_desktop_config.json");
if (await this.pathExists(claudeDesktopPath)) {
const hasConfig = await this.pathExists(claudeConfigPath);
let serverCount = 0;
if (hasConfig) {
try {
const content = await fs.readFile(claudeConfigPath, "utf-8");
const config = JSON.parse(content);
serverCount = Object.keys(config.mcpServers || {}).length;
}
catch {
// Ignore parse errors
}
}
apps.push({
id: "claude-desktop",
displayName: "🖥️ Claude Desktop",
configPath: claudeConfigPath,
detected: true,
hasExistingConfig: hasConfig,
serverCount,
});
}
// Check Cursor
const cursorPath = join(homedir(), ".cursor");
const cursorConfigPath = join(cursorPath, "mcp.json");
if (await this.pathExists(cursorPath)) {
const hasConfig = await this.pathExists(cursorConfigPath);
let serverCount = 0;
if (hasConfig) {
try {
const content = await fs.readFile(cursorConfigPath, "utf-8");
const config = JSON.parse(content);
serverCount = Object.keys(config.mcpServers || {}).length;
}
catch {
// Ignore parse errors
}
}
apps.push({
id: "cursor",
displayName: "✏️ Cursor",
configPath: cursorConfigPath,
detected: true,
hasExistingConfig: hasConfig,
serverCount,
});
}
// Check Claude Code (global)
const claudeCodePath = join(homedir(), ".claude.json");
if (await this.pathExists(claudeCodePath)) {
let serverCount = 0;
try {
const content = await fs.readFile(claudeCodePath, "utf-8");
const config = JSON.parse(content);
serverCount = Object.keys(config.mcpServers || {}).length;
}
catch {
// Ignore parse errors
// .claude.json exists but may not have mcpServers yet
}
apps.push({
id: "claude-code",
displayName: "🤖 Claude Code",
configPath: claudeCodePath,
detected: true,
hasExistingConfig: true, // File exists, so we consider it configured
serverCount,
});
}
return apps;
}
async pathExists(path) {
try {
await fs.access(path);
return true;
}
catch {
return false;
}
}
}
//# sourceMappingURL=appDetection.js.map