/**
* Claude Desktop Configuration Generator
*
* Automatically generates Claude Desktop configuration for MCP server integration
*
* Created: 2025-07-31
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import { Logger } from 'winston';
import {
ClaudeDesktopConfig,
ClaudeDesktopMCPConfig,
ProjectInfo,
EnvironmentConfig,
PlatformInfo
} from './types.js';
export interface ClaudeConfigGenerationOptions {
/** Server name in Claude Desktop config */
serverName?: string;
/** Server executable path */
serverPath?: string;
/** Working directory for the server */
workingDirectory?: string;
/** Additional environment variables */
additionalEnv?: Record<string, string>;
/** Global installation (vs project-specific) */
global?: boolean;
/** Backup existing configuration */
backup?: boolean;
}
export class ClaudeDesktopConfigGenerator {
private logger: Logger;
private projectInfo: ProjectInfo;
private platformInfo: PlatformInfo;
constructor(projectInfo: ProjectInfo, platformInfo: PlatformInfo, logger: Logger) {
this.projectInfo = projectInfo;
this.platformInfo = platformInfo;
this.logger = logger;
}
/**
* Generate Claude Desktop configuration
*/
async generateConfiguration(
envConfig: EnvironmentConfig,
options: ClaudeConfigGenerationOptions = {}
): Promise<ClaudeDesktopConfig> {
const serverName = options.serverName || this.generateServerName();
const serverConfig = this.generateServerConfig(envConfig, options);
const config: ClaudeDesktopConfig = {
mcpServers: {
[serverName]: serverConfig
}
};
this.logger.debug(`Generated Claude Desktop config for server: ${serverName}`);
return config;
}
/**
* Generate server name for Claude Desktop configuration
*/
private generateServerName(): string {
const baseName = this.projectInfo.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
return `${baseName}-ultimate`;
}
/**
* Generate MCP server configuration
*/
private generateServerConfig(
envConfig: EnvironmentConfig,
options: ClaudeConfigGenerationOptions
): ClaudeDesktopMCPConfig {
const serverPath = this.resolveServerPath(options);
const env = this.generateEnvironmentVariables(envConfig, options);
const workingDirectory = options.workingDirectory || this.projectInfo.root;
return {
command: 'node',
args: [serverPath],
env,
cwd: workingDirectory
};
}
/**
* Resolve server executable path
*/
private resolveServerPath(options: ClaudeConfigGenerationOptions): string {
if (options.serverPath) {
return path.resolve(options.serverPath);
}
if (options.global) {
// Global installation - use npx or direct path
return 'npx @castplan/ultimate-automation start';
}
// Project-specific installation
const distPath = path.join(this.projectInfo.root, 'dist', 'index.js');
const srcPath = path.join(this.projectInfo.root, 'src', 'index.ts');
// Check if compiled version exists
try {
require.resolve(distPath);
return distPath;
} catch {
// Fallback to source (development mode)
return srcPath;
}
}
/**
* Generate environment variables for Claude Desktop
*/
private generateEnvironmentVariables(
envConfig: EnvironmentConfig,
options: ClaudeConfigGenerationOptions
): Record<string, string> {
const env: Record<string, string> = {};
// Copy default environment variables
for (const [key, value] of Object.entries(envConfig.defaults)) {
env[key] = value;
}
// Override with current environment variables
for (const [key, value] of Object.entries(envConfig.variables)) {
if (value !== undefined) {
env[key] = value;
}
}
// Add additional environment variables
if (options.additionalEnv) {
Object.assign(env, options.additionalEnv);
}
// Ensure critical paths are absolute
const projectRootKey = `${envConfig.prefix}_PROJECT_ROOT`;
if (env[projectRootKey]) {
env[projectRootKey] = path.resolve(env[projectRootKey]);
}
const databasePathKey = `${envConfig.prefix}_DATABASE_PATH`;
if (env[databasePathKey]) {
env[databasePathKey] = path.resolve(env[databasePathKey]);
}
return env;
}
/**
* Detect Claude Desktop configuration file path
*/
async detectClaudeConfigPath(): Promise<string | null> {
const possiblePaths = this.getClaudeConfigPaths();
for (const configPath of possiblePaths) {
try {
await fs.access(configPath);
this.logger.debug(`Found Claude Desktop config at: ${configPath}`);
return configPath;
} catch {
// Continue to next path
}
}
this.logger.warn('Claude Desktop configuration file not found');
return null;
}
/**
* Get possible Claude Desktop configuration paths
*/
private getClaudeConfigPaths(): string[] {
const homeDir = this.platformInfo.homeDir;
switch (this.platformInfo.os) {
case 'windows':
return [
path.join(homeDir, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'),
path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json')
];
case 'macos':
return [
path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
];
case 'linux':
return [
path.join(homeDir, '.config', 'claude', 'claude_desktop_config.json'),
path.join(process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config'), 'claude', 'claude_desktop_config.json')
];
default:
return [
path.join(homeDir, '.claude', 'claude_desktop_config.json')
];
}
}
/**
* Load existing Claude Desktop configuration
*/
async loadExistingConfiguration(configPath: string): Promise<ClaudeDesktopConfig> {
try {
const content = await fs.readFile(configPath, 'utf8');
const config = JSON.parse(content) as ClaudeDesktopConfig;
// Ensure mcpServers exists
if (!config.mcpServers) {
config.mcpServers = {};
}
return config;
} catch (error) {
this.logger.warn(`Failed to load existing Claude config: ${error}`);
return { mcpServers: {} };
}
}
/**
* Save configuration to Claude Desktop
*/
async saveConfiguration(
config: ClaudeDesktopConfig,
options: ClaudeConfigGenerationOptions = {}
): Promise<string> {
const configPath = await this.detectClaudeConfigPath() || this.getDefaultConfigPath();
// Create directory if it doesn't exist
await fs.mkdir(path.dirname(configPath), { recursive: true });
// Backup existing configuration if requested
if (options.backup) {
await this.backupConfiguration(configPath);
}
// Load existing configuration and merge
const existingConfig = await this.loadExistingConfiguration(configPath);
const mergedConfig = this.mergeConfigurations(existingConfig, config);
// Write configuration
await fs.writeFile(configPath, JSON.stringify(mergedConfig, null, 2), 'utf8');
this.logger.info(`Claude Desktop configuration saved to: ${configPath}`);
return configPath;
}
/**
* Get default configuration path for current platform
*/
private getDefaultConfigPath(): string {
const paths = this.getClaudeConfigPaths();
return paths[0]; // Use first (primary) path
}
/**
* Backup existing configuration
*/
private async backupConfiguration(configPath: string): Promise<void> {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${configPath}.backup.${timestamp}`;
await fs.copyFile(configPath, backupPath);
this.logger.info(`Configuration backed up to: ${backupPath}`);
} catch (error) {
this.logger.warn(`Failed to backup configuration: ${error}`);
}
}
/**
* Merge configurations, with new config taking precedence
*/
private mergeConfigurations(
existing: ClaudeDesktopConfig,
newConfig: ClaudeDesktopConfig
): ClaudeDesktopConfig {
return {
...existing,
mcpServers: {
...existing.mcpServers,
...newConfig.mcpServers
}
};
}
/**
* Remove server configuration from Claude Desktop
*/
async removeServerConfiguration(serverName?: string): Promise<void> {
const configPath = await this.detectClaudeConfigPath();
if (!configPath) {
this.logger.warn('Claude Desktop configuration not found');
return;
}
const config = await this.loadExistingConfiguration(configPath);
const targetServerName = serverName || this.generateServerName();
if (config.mcpServers[targetServerName]) {
delete config.mcpServers[targetServerName];
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
this.logger.info(`Removed server configuration: ${targetServerName}`);
} else {
this.logger.warn(`Server configuration not found: ${targetServerName}`);
}
}
/**
* Validate configuration
*/
validateConfiguration(config: ClaudeDesktopConfig): {
valid: boolean;
errors: string[];
warnings: string[];
} {
const errors: string[] = [];
const warnings: string[] = [];
// Check required fields
if (!config.mcpServers) {
errors.push('Missing mcpServers configuration');
return { valid: false, errors, warnings };
}
// Validate each server configuration
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
if (!serverConfig.command) {
errors.push(`Missing command for server: ${serverName}`);
}
if (!serverConfig.args || serverConfig.args.length === 0) {
warnings.push(`No arguments specified for server: ${serverName}`);
}
// Check if server path exists (if it's a file path)
if (serverConfig.args.length > 0) {
const serverPath = serverConfig.args[0];
if (serverPath && path.isAbsolute(serverPath)) {
// TODO: Add async file existence check if needed
}
}
// Validate environment variables
if (serverConfig.env) {
for (const [envKey, envValue] of Object.entries(serverConfig.env)) {
if (envKey.includes('_PROJECT_ROOT') || envKey.includes('_DATABASE_PATH')) {
if (!path.isAbsolute(envValue)) {
warnings.push(`Non-absolute path in ${envKey}: ${envValue}`);
}
}
}
}
}
return {
valid: errors.length === 0,
errors,
warnings
};
}
/**
* Generate configuration template for manual setup
*/
generateConfigurationTemplate(
envConfig: EnvironmentConfig,
options: ClaudeConfigGenerationOptions = {}
): string {
const config = this.generateServerConfig(envConfig, options);
const serverName = options.serverName || this.generateServerName();
const template = {
mcpServers: {
[serverName]: config
}
};
return JSON.stringify(template, null, 2);
}
}