UNPKG

a2a-bridge-mcp-server

Version:

Agent-to-Agent Bridge MCP Server with intelligent model fallback, cross-platform support, and automatic installation for Claude Desktop and Claude Code

760 lines (651 loc) • 28.6 kB
#!/usr/bin/env node /** * Auto-Recovery System * Diagnoses and automatically fixes common A2A Bridge issues */ import { exec } from 'child_process'; import { promisify } from 'util'; import chalk from 'chalk'; import fs from 'fs-extra'; import path from 'path'; import os from 'os'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); import GeminiCLIInstaller from '../gemini/installer.js'; import GeminiCLIAuthenticator from '../gemini/authenticator.js'; import CredentialManager from '../gemini/credential-manager.js'; import UnifiedConfigManager from '../config/unified-manager.js'; const execAsync = promisify(exec); class AutoRecovery { constructor() { this.geminiInstaller = new GeminiCLIInstaller(); this.geminiAuth = new GeminiCLIAuthenticator(); this.credentialManager = new CredentialManager(); this.configManager = new UnifiedConfigManager(); this.issues = []; this.fixes = []; } /** * Diagnose all potential issues */ async diagnose() { console.log(chalk.bold.blue('šŸ” Diagnosing A2A Bridge issues...\n')); this.issues = []; const diagnostics = [ { name: 'System Requirements', check: this.checkSystemRequirements.bind(this) }, { name: 'Gemini CLI Installation', check: this.checkGeminiInstallation.bind(this) }, { name: 'Gemini CLI Authentication', check: this.checkGeminiAuthentication.bind(this) }, { name: 'Configuration Files', check: this.checkConfigurations.bind(this) }, { name: 'MCP Server', check: this.checkMCPServer.bind(this) }, { name: 'Quota/Fallback System', check: this.checkQuotaFallback.bind(this) }, { name: 'Permissions', check: this.checkPermissions.bind(this) } ]; for (const diagnostic of diagnostics) { console.log(chalk.cyan(`šŸ” Checking ${diagnostic.name}...`)); try { const result = await diagnostic.check(); if (result.issues && result.issues.length > 0) { this.issues.push(...result.issues); console.log(chalk.red(` āŒ Found ${result.issues.length} issue(s)`)); result.issues.forEach(issue => { console.log(chalk.gray(` • ${issue.description}`)); }); } else { console.log(chalk.green(' āœ… No issues found')); } } catch (error) { console.log(chalk.red(` āŒ Diagnostic failed: ${error.message}`)); this.issues.push({ type: 'diagnostic-error', description: `Failed to check ${diagnostic.name}: ${error.message}`, severity: 'high', category: diagnostic.name.toLowerCase().replace(' ', '-') }); } } console.log(chalk.bold.blue(`\nšŸ“‹ Diagnosis complete: ${this.issues.length} issue(s) found`)); return this.issues; } /** * Automatically repair found issues */ async repair() { if (this.issues.length === 0) { console.log(chalk.green('āœ… No issues to repair')); return true; } console.log(chalk.bold.blue(`\nšŸ”§ Auto-repairing ${this.issues.length} issue(s)...\n`)); this.fixes = []; let successCount = 0; // Group issues by category for efficient repair const issueGroups = this.groupIssuesByCategory(); for (const [category, categoryIssues] of Object.entries(issueGroups)) { console.log(chalk.cyan(`šŸ”§ Repairing ${category} issues...`)); try { const repairResult = await this.repairCategory(category, categoryIssues); if (repairResult.success) { console.log(chalk.green(` āœ… ${category} issues repaired`)); successCount += categoryIssues.length; this.fixes.push({ category, action: repairResult.action, issues: categoryIssues.length }); } else { console.log(chalk.red(` āŒ Failed to repair ${category}: ${repairResult.error}`)); } } catch (error) { console.log(chalk.red(` āŒ Repair failed for ${category}: ${error.message}`)); } } const success = successCount === this.issues.length; if (success) { console.log(chalk.bold.green(`\nāœ… All ${this.issues.length} issue(s) repaired successfully`)); } else { console.log(chalk.bold.yellow(`\nāš ļø Repaired ${successCount}/${this.issues.length} issue(s)`)); } return success; } /** * Check system requirements */ async checkSystemRequirements() { const issues = []; // Check Node.js version const nodeVersion = process.version.replace('v', ''); const [major] = nodeVersion.split('.'); if (parseInt(major) < 18) { issues.push({ type: 'node-version', description: `Node.js 18+ required (found ${nodeVersion})`, severity: 'high', category: 'system-requirements' }); } // Check npm availability try { await execAsync('npm --version', { timeout: 5000 }); } catch (error) { issues.push({ type: 'npm-missing', description: 'npm not found in PATH', severity: 'high', category: 'system-requirements' }); } return { issues }; } /** * Check Gemini CLI installation */ async checkGeminiInstallation() { const issues = []; try { const isInstalled = await this.geminiInstaller.checkInstallation(); if (!isInstalled) { issues.push({ type: 'gemini-not-installed', description: 'Gemini CLI not found', severity: 'high', category: 'gemini-installation' }); } else { // Test basic functionality const functionalityOk = await this.geminiInstaller.testBasicFunctionality(); if (!functionalityOk) { issues.push({ type: 'gemini-not-functional', description: 'Gemini CLI installed but not functional', severity: 'high', category: 'gemini-installation' }); } } } catch (error) { issues.push({ type: 'gemini-check-failed', description: `Cannot verify Gemini CLI installation: ${error.message}`, severity: 'medium', category: 'gemini-installation' }); } return { issues }; } /** * Check Gemini CLI authentication */ async checkGeminiAuthentication() { const issues = []; try { const isAuthenticated = await this.geminiAuth.testAuthentication(); if (!isAuthenticated) { issues.push({ type: 'gemini-not-authenticated', description: 'Gemini CLI not authenticated', severity: 'high', category: 'gemini-authentication' }); } // Check for stored credentials const hasCredentials = await this.credentialManager.hasStoredCredentials(); if (!hasCredentials && !isAuthenticated) { issues.push({ type: 'no-stored-credentials', description: 'No stored API credentials found', severity: 'medium', category: 'gemini-authentication' }); } // Test stored credentials if they exist if (hasCredentials) { const credentialsValid = await this.credentialManager.testStoredCredentials(); if (!credentialsValid) { issues.push({ type: 'invalid-credentials', description: 'Stored credentials are invalid', severity: 'high', category: 'gemini-authentication' }); } } } catch (error) { issues.push({ type: 'auth-check-failed', description: `Cannot verify authentication: ${error.message}`, severity: 'medium', category: 'gemini-authentication' }); } return { issues }; } /** * Check configuration files */ async checkConfigurations() { const issues = []; // Check Claude Desktop configuration try { const claudeDesktopPath = this.getClaudeDesktopConfigPath(); if (await fs.pathExists(claudeDesktopPath)) { const config = await fs.readJson(claudeDesktopPath); if (!config.mcpServers || !config.mcpServers['a2a-bridge']) { issues.push({ type: 'claude-desktop-not-configured', description: 'A2A Bridge not configured in Claude Desktop', severity: 'medium', category: 'configuration' }); } else { // Validate configuration structure const a2aConfig = config.mcpServers['a2a-bridge']; if (!a2aConfig.command || !a2aConfig.args) { issues.push({ type: 'claude-desktop-invalid-config', description: 'Invalid A2A Bridge configuration in Claude Desktop', severity: 'high', category: 'configuration' }); } // Check if server file exists const serverPath = a2aConfig.args?.[0]; if (serverPath && !await fs.pathExists(serverPath)) { issues.push({ type: 'server-file-missing', description: `MCP server file not found: ${serverPath}`, severity: 'high', category: 'configuration' }); } } } } catch (error) { issues.push({ type: 'claude-desktop-config-error', description: `Cannot read Claude Desktop configuration: ${error.message}`, severity: 'medium', category: 'configuration' }); } // Check Claude Code configuration try { const claudeCodePath = path.join(os.homedir(), '.claude.json'); if (await fs.pathExists(claudeCodePath)) { const config = await fs.readJson(claudeCodePath); if (!config.mcpServers || !config.mcpServers['a2a-bridge']) { // Check CLI configuration as fallback try { const { stdout } = await execAsync('claude config list --scope user', { timeout: 10000 }); if (!stdout.includes('a2a-bridge')) { issues.push({ type: 'claude-code-not-configured', description: 'A2A Bridge not configured in Claude Code', severity: 'low', category: 'configuration' }); } } catch (cliError) { issues.push({ type: 'claude-code-not-configured', description: 'A2A Bridge not configured in Claude Code', severity: 'low', category: 'configuration' }); } } } } catch (error) { // Claude Code config is optional, so this is a low-priority issue issues.push({ type: 'claude-code-config-error', description: `Cannot read Claude Code configuration: ${error.message}`, severity: 'low', category: 'configuration' }); } return { issues }; } /** * Check MCP Server */ async checkMCPServer() { const issues = []; // Check if server script exists const serverPath = path.resolve(__dirname, '..', '..', 'dist', 'index.js'); if (!await fs.pathExists(serverPath)) { issues.push({ type: 'server-script-missing', description: 'MCP server script not found', severity: 'high', category: 'mcp-server' }); } else { // Check syntax try { await execAsync(`node -c "${serverPath}"`, { timeout: 10000 }); } catch (error) { issues.push({ type: 'server-syntax-error', description: 'MCP server script has syntax errors', severity: 'high', category: 'mcp-server' }); } } // Check data directory const dataDir = path.join(os.homedir(), '.a2a-bridge'); try { await fs.ensureDir(dataDir); } catch (error) { issues.push({ type: 'data-directory-error', description: `Cannot access data directory: ${error.message}`, severity: 'medium', category: 'mcp-server' }); } return { issues }; } /** * Check quota/fallback system */ async checkQuotaFallback() { const issues = []; try { // Test a simple Gemini CLI call to check for quota errors console.log(chalk.gray(' Testing Gemini CLI for quota status...')); const { stdout, stderr } = await execAsync('gemini --version', { timeout: 10000 }); // Check for quota error patterns in recent Gemini CLI usage const geminiErrorLogPath = '/tmp/gemini-client-error*'; try { const { stdout: errorLogs } = await execAsync(`ls ${geminiErrorLogPath} 2>/dev/null | head -1`, { timeout: 5000 }); if (errorLogs.trim()) { // Found recent error logs, check for quota issues const { stdout: logContent } = await execAsync(`cat ${errorLogs.trim()}`, { timeout: 5000 }); if (logContent.includes('Quota exceeded') || logContent.includes('rateLimitExceeded') || logContent.includes('429') || logContent.includes('RESOURCE_EXHAUSTED')) { issues.push({ type: 'quota-exceeded', description: 'Gemini API quota limits detected in recent usage', severity: 'medium', category: 'quota-fallback', solution: 'MCP server v1.6.0+ includes intelligent fallback system to handle quota limits automatically' }); } if (logContent.includes('Fallback to Flash model failed') || logContent.includes('Fallback to') && logContent.includes('failed')) { issues.push({ type: 'fallback-system-failure', description: 'Gemini CLI internal fallback system is failing', severity: 'high', category: 'quota-fallback', solution: 'Update to MCP server v1.6.0+ which bypasses CLI fallback and implements smart model switching' }); } } } catch (error) { // No error logs found or can't read them - that's actually good console.log(chalk.gray(' No recent quota error logs found (good)')); } // Check if MCP server version supports intelligent fallback const packagePath = path.resolve(__dirname, '..', '..', 'package.json'); if (await fs.pathExists(packagePath)) { const packageData = await fs.readJson(packagePath); const version = packageData.version; const majorMinor = version.split('.').slice(0, 2).join('.'); const versionNum = parseFloat(majorMinor); if (versionNum < 1.6) { issues.push({ type: 'outdated-fallback-system', description: `MCP server v${version} does not include intelligent quota fallback`, severity: 'medium', category: 'quota-fallback', solution: 'Update to v1.6.0+ for intelligent model fallback when hitting quota limits' }); } } // Test Claude configuration for proper MCP server setup const configPaths = [ path.join(os.homedir(), '.claude.json'), path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json') ]; let foundProperConfig = false; for (const configPath of configPaths) { if (await fs.pathExists(configPath)) { try { const configContent = await fs.readFile(configPath, 'utf8'); // Check for Docker-based configuration (problematic) if (configContent.includes('"command": "docker"') && configContent.includes('a2a-bridge')) { issues.push({ type: 'docker-based-mcp-config', description: 'Claude is configured to use Docker-based MCP server instead of npm package', severity: 'high', category: 'quota-fallback', solution: 'Update Claude configuration to use npm-based MCP server for proper fallback functionality' }); } // Check for proper npm-based configuration if (configContent.includes('a2a-bridge-mcp') || (configContent.includes('"command": "node"') && configContent.includes('a2a-bridge-mcp-server'))) { foundProperConfig = true; } } catch (error) { // Config file exists but can't read it console.log(chalk.gray(` Could not read config: ${configPath}`)); } } } if (!foundProperConfig) { issues.push({ type: 'missing-npm-mcp-config', description: 'Claude is not configured to use npm-based a2a-bridge-mcp-server', severity: 'medium', category: 'quota-fallback', solution: 'Run: a2a-bridge-mcp setup to configure Claude products properly' }); } } catch (error) { issues.push({ type: 'quota-check-failed', description: `Failed to check quota/fallback system: ${error.message}`, severity: 'low', category: 'quota-fallback' }); } return { issues }; } /** * Check permissions */ async checkPermissions() { const issues = []; // Check home directory write access try { const testPath = path.join(os.homedir(), '.a2a-bridge-permission-test'); await fs.writeFile(testPath, 'test'); await fs.remove(testPath); } catch (error) { issues.push({ type: 'home-directory-permission', description: 'Cannot write to home directory', severity: 'high', category: 'permissions' }); } return { issues }; } /** * Group issues by category for efficient repair */ groupIssuesByCategory() { const groups = {}; for (const issue of this.issues) { if (!groups[issue.category]) { groups[issue.category] = []; } groups[issue.category].push(issue); } return groups; } /** * Repair issues by category */ async repairCategory(category, issues) { switch (category) { case 'gemini-installation': return await this.repairGeminiInstallation(issues); case 'gemini-authentication': return await this.repairGeminiAuthentication(issues); case 'configuration': return await this.repairConfigurations(issues); case 'mcp-server': return await this.repairMCPServer(issues); case 'permissions': return await this.repairPermissions(issues); default: return { success: false, error: 'Unknown category' }; } } /** * Repair Gemini CLI installation issues */ async repairGeminiInstallation(issues) { try { console.log(chalk.gray(' Reinstalling Gemini CLI...')); await this.geminiInstaller.forceReinstall(); return { success: true, action: 'Reinstalled Gemini CLI' }; } catch (error) { return { success: false, error: error.message }; } } /** * Repair Gemini CLI authentication issues */ async repairGeminiAuthentication(issues) { try { console.log(chalk.gray(' Reconfiguring authentication...')); // Clear invalid credentials first await this.credentialManager.clearCredentials(); // Force new authentication await this.geminiAuth.enforceAuthentication(); return { success: true, action: 'Reconfigured authentication' }; } catch (error) { return { success: false, error: error.message }; } } /** * Repair configuration issues */ async repairConfigurations(issues) { try { console.log(chalk.gray(' Rebuilding configurations...')); // Detect environment and reconfigure const environment = await this.configManager.detectEnvironment(); this.configManager.init(path.resolve(__dirname, '..', '..', 'dist', 'index.js')); await this.configManager.configureAllProducts(); return { success: true, action: 'Rebuilt configurations' }; } catch (error) { return { success: false, error: error.message }; } } /** * Repair MCP server issues */ async repairMCPServer(issues) { try { console.log(chalk.gray(' Rebuilding MCP server...')); // Rebuild the package const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); const packageDir = path.resolve(__dirname, '..', '..'); await execAsync('npm run build', { cwd: packageDir, timeout: 60000 }); return { success: true, action: 'Rebuilt MCP server' }; } catch (error) { return { success: false, error: error.message }; } } /** * Repair permission issues */ async repairPermissions(issues) { try { console.log(chalk.gray(' Fixing permissions...')); // Create data directory with proper permissions const dataDir = path.join(os.homedir(), '.a2a-bridge'); await fs.ensureDir(dataDir); // On Unix systems, ensure proper permissions if (os.platform() !== 'win32') { await execAsync(`chmod 755 "${dataDir}"`); } return { success: true, action: 'Fixed permissions' }; } catch (error) { return { success: false, error: error.message }; } } /** * Get Claude Desktop config path */ getClaudeDesktopConfigPath() { const homeDir = os.homedir(); switch (os.platform()) { case 'darwin': return path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'); case 'win32': return path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json'); case 'linux': return path.join(homeDir, '.config', 'Claude', 'claude_desktop_config.json'); default: throw new Error(`Unsupported platform: ${os.platform()}`); } } /** * Show diagnostic results */ showDiagnosticResults() { if (this.issues.length === 0) { console.log(chalk.green('\nāœ… No issues found - A2A Bridge is healthy!')); return; } console.log(chalk.bold.yellow('\nāš ļø Issues Found:')); console.log(chalk.gray('─'.repeat(50))); const severityOrder = { high: 1, medium: 2, low: 3 }; const sortedIssues = this.issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]); for (const issue of sortedIssues) { const severityIcon = { high: 'šŸ”“', medium: '🟔', low: '🟢' }[issue.severity]; console.log(`${severityIcon} ${issue.description}`); console.log(chalk.gray(` Category: ${issue.category}`)); } console.log(chalk.gray('─'.repeat(50))); console.log(chalk.blue('šŸ’” Run "a2a-bridge-mcp repair" to fix these issues automatically')); } /** * Show repair results */ showRepairResults() { if (this.fixes.length === 0) { console.log(chalk.yellow('\nāš ļø No repairs were performed')); return; } console.log(chalk.bold.green('\nāœ… Repair Summary:')); console.log(chalk.gray('─'.repeat(50))); for (const fix of this.fixes) { console.log(`āœ… ${fix.action} (${fix.issues} issue${fix.issues > 1 ? 's' : ''})`); } console.log(chalk.gray('─'.repeat(50))); console.log(chalk.green('šŸŽ‰ Run "a2a-bridge-mcp test" to verify the repairs')); } } export default AutoRecovery;