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
JavaScript
#!/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;