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
1,206 lines (1,048 loc) • 38.9 kB
JavaScript
/**
* A2A Bridge MCP Server (Node.js)
* Bridges Model Context Protocol (MCP) with Agent-to-Agent (A2A) protocol
* Enables Claude Code to communicate with Gemini CLI
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
InitializeRequestSchema,
InitializedNotificationSchema,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import { v4 as uuidv4 } from 'uuid';
import fetch from 'node-fetch';
import { pathToFileURL } from 'url';
import CredentialManager from './gemini/credential-manager.js';
const execAsync = promisify(exec);
// Configuration - Local-first MCP server (no network URLs needed)
const DATA_DIR = process.env.A2A_MCP_DATA_DIR || path.join(os.homedir(), '.a2a-bridge');
const REGISTERED_AGENTS_FILE = path.join(DATA_DIR, 'registered_agents.json');
const TASK_AGENT_MAPPING_FILE = path.join(DATA_DIR, 'task_agent_mapping.json');
// Enhanced logging and platform identification
const LOG_LEVEL = process.env.A2A_MCP_LOG_LEVEL || 'error';
const PLATFORM_ID = process.env.A2A_MCP_PLATFORM || `standalone-${os.platform()}`;
// Enhanced logging function
function logWithLevel(level, ...args) {
const levels = { error: 0, warn: 1, info: 2, debug: 3 };
const currentLevel = levels[LOG_LEVEL] || 0;
const messageLevel = levels[level] || 0;
if (messageLevel <= currentLevel) {
const timestamp = new Date().toISOString();
console.error(`[${timestamp}] [${PLATFORM_ID}] [${level.toUpperCase()}]`, ...args);
}
}
// Initialize credential manager
const credentialManager = new CredentialManager();
// Initialize data directory creation (moved to main function)
let dataDirInitialized = false;
// Global storage
let registeredAgents = {};
let taskAgentMapping = {};
let activeTasks = {};
// Session tracking for intelligent fallback
let quotaFailureTracker = {
failures: new Map(), // model -> [{timestamp, error}, ...]
lastSuccessfulModel: null,
sessionStartTime: Date.now()
};
// Enhanced error patterns for comprehensive detection
const ERROR_PATTERNS = {
QUOTA_EXCEEDED: [
'429',
'Quota exceeded',
'rateLimitExceeded',
'RESOURCE_EXHAUSTED',
'Fallback to Flash model failed',
'Fallback to',
'Resource has been exhausted',
'Too Many Requests',
'Rate limit exceeded',
'503',
'Quota exhausted',
'Usage limit exceeded',
'API quota exceeded',
'quota metric',
'limit.*exceeded'
],
AUTHENTICATION: [
'authentication',
'auth',
'invalid_key',
'unauthorized',
'401',
'API key not valid',
'Invalid credentials'
],
INVALID_REQUEST: [
'400',
'INVALID_ARGUMENT',
'Bad Request',
'invalid request format',
'Malformed request',
'Invalid model name'
],
INTERNAL_FALLBACK_DETECTED: [
'Attempting to fallback',
'Falling back to',
'Auto-retry enabled',
'Internal fallback',
'Model fallback'
]
};
// Model fallback chain priority
const MODEL_FALLBACK_CHAIN = [
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-1.5-flash',
'gemini-1.5-pro'
];
/**
* Save data to JSON file
*/
async function saveToJson(data, filepath) {
try {
await fs.writeJson(filepath, data, { spaces: 2 });
console.error(`Data saved to ${filepath}`);
} catch (error) {
console.error(`Failed to save to ${filepath}: ${error.message}`);
}
}
/**
* Load data from JSON file
*/
async function loadFromJson(filepath) {
try {
if (await fs.pathExists(filepath)) {
const data = await fs.readJson(filepath);
console.error(`Data loaded from ${filepath}`);
return data;
}
} catch (error) {
console.error(`Failed to load from ${filepath}: ${error.message}`);
}
return {};
}
/**
* Save data on exit
*/
async function saveDataOnExit() {
console.error('Saving data before exit...');
await saveToJson(registeredAgents, REGISTERED_AGENTS_FILE);
await saveToJson(taskAgentMapping, TASK_AGENT_MAPPING_FILE);
console.error('Data saved successfully');
}
/**
* Initialize data storage with error recovery
*/
async function initializeStorage() {
try {
registeredAgents = await loadFromJson(REGISTERED_AGENTS_FILE);
console.error(`Loaded ${Object.keys(registeredAgents).length} registered agents`);
} catch (error) {
console.error(`Failed to load registered agents, starting fresh: ${error.message}`);
registeredAgents = {};
}
try {
taskAgentMapping = await loadFromJson(TASK_AGENT_MAPPING_FILE);
console.error(`Loaded ${Object.keys(taskAgentMapping).length} task mappings`);
} catch (error) {
console.error(`Failed to load task mappings, starting fresh: ${error.message}`);
taskAgentMapping = {};
}
// MCP server is now local-first - no auto-registration needed
console.error('MCP server configured for local operation');
console.error('Storage initialization complete');
}
/**
* Check if Gemini CLI is available with detailed diagnostics
*/
async function checkGeminiCLI() {
try {
// Check if Gemini CLI is in PATH
const pathCheckCommand = process.platform === 'win32' ? 'where gemini' : 'which gemini';
const { stdout: pathResult } = await execAsync(pathCheckCommand, { timeout: 5000 });
if (!pathResult.trim()) {
throw new Error('Gemini CLI not found in PATH. Please install with: npm install -g @google/gemini-cli');
}
console.error(`Gemini CLI found at: ${pathResult.trim()}`);
// Test authentication by running version command
try {
const { stdout: versionResult } = await execAsync('gemini --version', { timeout: 10000 });
console.error(`Gemini CLI version: ${versionResult.trim()}`);
} catch (versionError) {
console.error(`Warning: Could not get Gemini CLI version: ${versionError.message}`);
}
// Test basic authentication
try {
const { stdout: authTest } = await execAsync('gemini config list', { timeout: 10000 });
console.error('Gemini CLI authentication appears to be configured');
} catch (authError) {
console.error(`Warning: Authentication test failed: ${authError.message}`);
console.error('You may need to run: gemini auth');
}
return true;
} catch (error) {
const errorMessage = `Gemini CLI not available: ${error.message}`;
console.error(errorMessage);
// Provide helpful installation instructions
console.error('\nTo fix this issue:');
console.error('1. Install Gemini CLI: npm install -g @google/gemini-cli');
console.error('2. Authenticate: gemini auth');
console.error('3. Or set GOOGLE_API_KEY environment variable');
throw new Error(errorMessage);
}
}
/**
* Check if text contains any error patterns
*/
function detectError(text, patternType) {
if (!text || !ERROR_PATTERNS[patternType]) return false;
return ERROR_PATTERNS[patternType].some(pattern =>
text.toLowerCase().includes(pattern.toLowerCase())
);
}
/**
* Record a model failure for intelligent fallback
*/
function recordModelFailure(model, error) {
if (!quotaFailureTracker.failures.has(model)) {
quotaFailureTracker.failures.set(model, []);
}
const failures = quotaFailureTracker.failures.get(model);
failures.push({
timestamp: Date.now(),
error: error
});
// Keep only recent failures (last 10 minutes)
const tenMinutesAgo = Date.now() - (10 * 60 * 1000);
quotaFailureTracker.failures.set(model,
failures.filter(f => f.timestamp > tenMinutesAgo)
);
console.error(`[FALLBACK] Recorded failure for model ${model}: ${error}`);
}
/**
* Record a model success
*/
function recordModelSuccess(model) {
quotaFailureTracker.lastSuccessfulModel = model;
console.error(`[FALLBACK] Model ${model} succeeded, updating last successful model`);
}
/**
* Get the best model to try based on recent failures
*/
function getBestModel(requestedModel = 'gemini-2.5-pro') {
// If we have recent failures for the requested model, try the next in chain
const recentFailures = quotaFailureTracker.failures.get(requestedModel) || [];
const recentFailureTime = Date.now() - (5 * 60 * 1000); // 5 minutes ago
const hasRecentFailures = recentFailures.some(f => f.timestamp > recentFailureTime);
if (hasRecentFailures) {
// Find next model in fallback chain that hasn't failed recently
const startIndex = MODEL_FALLBACK_CHAIN.indexOf(requestedModel);
for (let i = startIndex + 1; i < MODEL_FALLBACK_CHAIN.length; i++) {
const candidateModel = MODEL_FALLBACK_CHAIN[i];
const candidateFailures = quotaFailureTracker.failures.get(candidateModel) || [];
const hasRecentCandidateFailures = candidateFailures.some(f => f.timestamp > recentFailureTime);
if (!hasRecentCandidateFailures) {
console.error(`[FALLBACK] Preemptively selecting ${candidateModel} due to recent failures with ${requestedModel}`);
return candidateModel;
}
}
}
return requestedModel;
}
/**
* Get next model in fallback chain
*/
function getNextModelInChain(currentModel) {
const currentIndex = MODEL_FALLBACK_CHAIN.indexOf(currentModel);
if (currentIndex >= 0 && currentIndex < MODEL_FALLBACK_CHAIN.length - 1) {
return MODEL_FALLBACK_CHAIN[currentIndex + 1];
}
return null;
}
/**
* Execute Gemini CLI command with intelligent fallback and session tracking
*/
async function executeGemini(prompt, requestedModel = 'gemini-2.5-pro', options = {}) {
const { timeout = 60000, maxLength = 10000 } = options;
// Sanitize inputs
const sanitizedPrompt = prompt.replace(/[`$\\]/g, '').substring(0, maxLength);
// Check for valid credentials (API key or OAuth)
let apiKey;
try {
apiKey = await credentialManager.retrieveAPIKey();
} catch (error) {
// API key not found, check for OAuth credentials
const oauthCredsPath = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
if (!await fs.pathExists(oauthCredsPath)) {
throw new Error('No authentication found. Please run "gemini auth" for OAuth or configure API key.');
}
logWithLevel('info', 'Using OAuth authentication for Gemini CLI');
apiKey = null; // OAuth doesn't need API key in environment
}
// Use platform-appropriate temporary directory with fallbacks
let tempDir;
if (process.platform === 'win32') {
tempDir = process.env.TEMP || process.env.TMP || path.join(os.homedir(), 'AppData', 'Local', 'Temp');
} else if (process.platform === 'darwin') {
tempDir = process.env.TMPDIR || '/tmp';
} else {
// Linux and other Unix-like systems
tempDir = process.env.TMPDIR || process.env.TMP || '/tmp';
}
// Ensure temp directory exists and is accessible
try {
await fs.ensureDir(tempDir);
} catch (error) {
console.error(`[PLATFORM] Temp directory ${tempDir} not accessible, using fallback`);
tempDir = os.tmpdir(); // Node.js built-in fallback
}
// Smart model selection: First try without specifying model to let Gemini CLI choose
// Only specify model if we're in fallback mode due to quota failures
let currentModel = getBestModel(requestedModel);
let attemptCount = 0;
const maxAttempts = MODEL_FALLBACK_CHAIN.length;
// Strategy: First attempt without model specification to let Gemini choose available model
// If that fails with quota error, explicitly try each model in fallback chain
const useExplicitModel = quotaFailureTracker.failures.size > 0;
while (attemptCount < maxAttempts) {
attemptCount++;
logWithLevel('info', `Attempt ${attemptCount}/${maxAttempts}${useExplicitModel ? ` with explicit model: ${currentModel}` : ' with auto-selected model'}`);
// Platform-specific command construction with proper escaping
let command;
let execOptions = {
timeout,
maxBuffer: 2 * 1024 * 1024, // 2MB
env: {
...process.env
}
};
// Add API key to environment only if available (not needed for OAuth)
if (apiKey) {
execOptions.env.GOOGLE_API_KEY = apiKey;
execOptions.env.GEMINI_API_KEY = apiKey;
}
// NEW STRATEGY: Use -p flag without -m flag on first attempt to let Gemini choose available model
// Only use explicit model selection if we've detected quota failures
const modelFlag = (useExplicitModel && attemptCount > 1) ? ` -m "${currentModel.replace(/[^a-zA-Z0-9.-]/g, '')}"` : '';
if (process.platform === 'win32') {
// Windows: Use cmd /c with proper path escaping
const escapedTempDir = tempDir.replace(/"/g, '""');
const escapedPrompt = sanitizedPrompt.replace(/"/g, '""');
command = `cmd /c "cd /D "${escapedTempDir}" && gemini -p "${escapedPrompt}"${modelFlag} --all-files=false"`;
execOptions.shell = true;
} else {
// Unix-like systems: Use bash with proper escaping
const escapedTempDir = tempDir.replace(/'/g, '\'\\\'\'');
const escapedPrompt = sanitizedPrompt.replace(/'/g, '\'\\\'\'');
command = `cd '${escapedTempDir}' && gemini -p '${escapedPrompt}'${modelFlag} --all-files=false`;
execOptions.shell = '/bin/bash';
execOptions.env.PWD = tempDir; // Unix-specific working directory
}
// Keep environment clean for better Gemini CLI compatibility
try {
const { stdout, stderr } = await execAsync(command, execOptions);
// Check for different types of errors in both stdout and stderr
const fullOutput = `${stdout} ${stderr}`;
// Check if Gemini CLI's internal fallback failed (the main problem we're fixing)
if (fullOutput.includes('Fallback to Flash model failed') ||
fullOutput.includes('Fallback to') && fullOutput.includes('failed')) {
logWithLevel('warn', `Gemini CLI internal fallback failed for model ${currentModel} - implementing smart fallback`);
recordModelFailure(currentModel, 'CLI internal fallback failed');
// Treat this as a quota error and proceed with our own fallback
const nextModel = getNextModelInChain(currentModel);
if (nextModel && attemptCount < maxAttempts) {
currentModel = nextModel;
logWithLevel('info', `CLI fallback failed, switching to model: ${currentModel}`);
continue;
}
}
// Check for quota/rate limit errors
if (detectError(fullOutput, 'QUOTA_EXCEEDED')) {
const errorMsg = `Quota/rate limit exceeded with model ${currentModel}`;
logWithLevel('warn', errorMsg);
recordModelFailure(currentModel, errorMsg);
// Try next model in chain
const nextModel = getNextModelInChain(currentModel);
if (nextModel && attemptCount < maxAttempts) {
currentModel = nextModel;
logWithLevel('info', `Switching to model: ${currentModel}`);
continue;
} else {
throw new Error('All models in fallback chain have exceeded quota. Please try again later.');
}
}
// Check for authentication errors
if (detectError(fullOutput, 'AUTHENTICATION')) {
recordModelFailure(currentModel, 'Authentication error');
throw new Error('Gemini authentication error. Please run "gemini auth" or set GOOGLE_API_KEY.');
}
// Check for invalid request errors
if (detectError(fullOutput, 'INVALID_REQUEST')) {
recordModelFailure(currentModel, 'Invalid request format');
throw new Error('Gemini API error: Invalid request format. Please check your API key and prompt.');
}
// Check for other errors in stderr
if (stderr && stderr.includes('Error') && !detectError(stderr, 'QUOTA_EXCEEDED')) {
console.error('Gemini CLI stderr:', stderr);
recordModelFailure(currentModel, `CLI error: ${stderr.trim()}`);
throw new Error(`Gemini CLI error: ${stderr.trim()}`);
}
// Clean up Gemini CLI output
let response = stdout.trim();
response = response.replace(/^Data collection is disabled\.\s*\n?/i, '');
response = response.replace(/^Loaded cached credentials\.\s*\n?/i, '');
// Remove any context setup messages
response = response.replace(/^This is the Gemini CLI\..*$/gm, '');
response = response.replace(/^Today's date is.*$/gm, '');
response = response.replace(/^My operating system.*$/gm, '');
response = response.replace(/^I'm currently working.*$/gm, '');
response = response.replace(/^Here is the folder structure.*$/gm, '');
response = response.replace(/^Got it\. Thanks for the context!.*$/gm, '');
response = response.trim();
// Handle empty response
if (!response) {
if (stderr.trim() && !detectError(stderr, 'QUOTA_EXCEEDED')) {
console.error('No stdout, checking stderr:', stderr);
recordModelFailure(currentModel, 'Empty response');
return stderr.trim();
}
recordModelFailure(currentModel, 'Empty response from model');
return 'No response from Gemini';
}
// Success! Record the successful model and return response
recordModelSuccess(currentModel);
logWithLevel('info', `Success with model: ${currentModel}`);
return response;
} catch (error) {
const errorMsg = error.message;
console.error(`[FALLBACK] Model ${currentModel} failed: ${errorMsg}`);
// Check if this is a quota error in the exception
if (detectError(errorMsg, 'QUOTA_EXCEEDED')) {
recordModelFailure(currentModel, errorMsg);
// Try next model in chain
const nextModel = getNextModelInChain(currentModel);
if (nextModel && attemptCount < maxAttempts) {
currentModel = nextModel;
console.error(`[FALLBACK] Exception-based fallback to: ${currentModel}`);
continue;
} else {
throw new Error('All models in fallback chain have exceeded quota. Please try again later.');
}
}
// For non-quota errors, record failure and rethrow
recordModelFailure(currentModel, errorMsg);
// If we have more models to try, continue
const nextModel = getNextModelInChain(currentModel);
if (nextModel && attemptCount < maxAttempts) {
currentModel = nextModel;
console.error(`[FALLBACK] General error fallback to: ${currentModel}`);
continue;
}
throw new Error(`Gemini execution failed after trying ${attemptCount} model(s): ${errorMsg}`);
}
}
throw new Error('All models in fallback chain failed');
}
/**
* Fetch agent card from URL with retries and fallbacks
*/
async function fetchAgentCard(url, maxRetries = 3) {
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// First try the well-known location
const wellKnownUrl = `${url.replace(/\/$/, '')}/.well-known/agent.json`;
try {
const response = await fetch(wellKnownUrl, {
timeout: 5000,
headers: { 'User-Agent': 'A2A-Bridge-MCP/1.0.0' }
});
if (response.ok) {
const data = await response.json();
console.error(`Successfully fetched agent card from well-known location on attempt ${attempt}`);
return data;
}
} catch (error) {
console.error(`Attempt ${attempt}: Failed to fetch from well-known URL ${wellKnownUrl}: ${error.message}`);
}
// Try the main endpoint
try {
const response = await fetch(url, {
timeout: 5000,
headers: { 'User-Agent': 'A2A-Bridge-MCP/1.0.0' }
});
if (response.ok) {
const data = await response.json();
if (typeof data === 'object' && data.name) {
console.error(`Successfully fetched agent card from main URL on attempt ${attempt}`);
return data;
}
}
} catch (error) {
console.error(`Attempt ${attempt}: Failed to fetch from main URL ${url}: ${error.message}`);
}
// Wait before retry (except on last attempt)
if (attempt < maxRetries) {
const delayMs = attempt * 1000; // Progressive delay: 1s, 2s, 3s
console.error(`Waiting ${delayMs}ms before retry...`);
await delay(delayMs);
}
} catch (error) {
console.error(`Attempt ${attempt}: Unexpected error during agent card fetch: ${error.message}`);
}
}
console.error(`All ${maxRetries} attempts failed, returning default agent card for ${url}`);
// Return default agent card if all attempts fail
return {
name: 'Unknown Agent',
url: url,
version: '1.0.0',
description: 'Agent discovered without card (connection failed)',
capabilities: { streaming: false },
error: 'Failed to discover agent capabilities'
};
}
/**
* Register an A2A agent (supports both local and remote agents)
*/
async function registerAgent(url) {
try {
let agentInfo;
// Handle local agents (no network calls)
if (url.startsWith('local:')) {
const localName = url.replace('local:', '');
agentInfo = {
url: url,
name: localName || 'Local Agent',
description: 'Local agent (no network required)',
capabilities: { streaming: false, local: true }
};
console.error(`Registering local agent: ${agentInfo.name}`);
} else {
// Handle remote agents (existing behavior)
const agentCard = await fetchAgentCard(url);
agentInfo = {
url: url,
name: agentCard.name || 'Unknown Agent',
description: agentCard.description || 'No description provided',
capabilities: agentCard.capabilities || {}
};
console.error(`Registering remote agent: ${agentInfo.name}`);
}
registeredAgents[url] = agentInfo;
// Save to disk immediately
await saveToJson(registeredAgents, REGISTERED_AGENTS_FILE);
console.error(`Successfully registered agent: ${agentInfo.name}`);
return {
status: 'success',
agent: agentInfo
};
} catch (error) {
console.error(`Failed to register agent: ${error.message}`);
return {
status: 'error',
message: `Failed to register agent: ${error.message}`
};
}
}
/**
* List all registered agents
*/
async function listAgents() {
return Object.values(registeredAgents);
}
/**
* Send message to A2A agent
*/
async function sendMessage(agentUrl, message, sessionId = null) {
if (!registeredAgents[agentUrl]) {
return {
status: 'error',
message: `Agent not registered: ${agentUrl}`
};
}
try {
const taskId = uuidv4();
taskAgentMapping[taskId] = agentUrl;
console.error(`Sending message to agent: ${message}`);
// Forward the message using JSON-RPC 2.0
const result = await forwardTaskToAgent(agentUrl, message, taskId, sessionId);
// Save task mapping to disk
await saveToJson(taskAgentMapping, TASK_AGENT_MAPPING_FILE);
return {
status: 'success',
task_id: taskId,
result: result
};
} catch (error) {
console.error(`Error sending message: ${error.message}`);
return {
status: 'error',
message: `Error sending message: ${error.message}`
};
}
}
/**
* Forward task to A2A agent using JSON-RPC 2.0 with retries
*/
async function forwardTaskToAgent(agentUrl, message, taskId, sessionId = null, maxRetries = 2) {
const rpcRequest = {
jsonrpc: '2.0',
id: taskId,
method: 'sendTask',
params: {
id: taskId,
message: {
role: 'user',
parts: [{ type: 'text', text: message }]
}
}
};
if (sessionId) {
rpcRequest.params.sessionId = sessionId;
}
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.error(`Attempt ${attempt}: Forwarding task ${taskId} to ${agentUrl}`);
const response = await fetch(agentUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'A2A-Bridge-MCP/1.0.0'
},
body: JSON.stringify(rpcRequest),
timeout: 30000
});
if (response.ok) {
const rpcResponse = await response.json();
console.error(`Successfully forwarded task ${taskId} on attempt ${attempt}`);
if (rpcResponse.result) {
return rpcResponse.result;
} else if (rpcResponse.error) {
throw new Error(`Agent RPC error: ${JSON.stringify(rpcResponse.error)}`);
} else {
return { status: 'completed', response: rpcResponse };
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
lastError = error;
console.error(`Attempt ${attempt}: Error forwarding task to agent: ${error.message}`);
if (attempt < maxRetries) {
const delay = attempt * 1000;
console.error(`Waiting ${delay}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
console.error(`All ${maxRetries} attempts failed for task ${taskId}`);
throw lastError || new Error('Failed to forward task after multiple attempts');
}
/**
* Get task result from A2A agent
*/
async function getTaskResult(taskId) {
if (!taskAgentMapping[taskId]) {
return {
status: 'error',
message: `Task ID not found: ${taskId}`
};
}
const agentUrl = taskAgentMapping[taskId];
try {
console.error(`Retrieving task result for task_id: ${taskId}`);
const result = await getTaskFromAgent(agentUrl, taskId);
return {
status: 'success',
task_id: taskId,
result: result
};
} catch (error) {
console.error(`Error retrieving task result: ${error.message}`);
return {
status: 'error',
message: `Error retrieving task result: ${error.message}`
};
}
}
/**
* Get task from A2A agent
*/
async function getTaskFromAgent(agentUrl, taskId) {
const rpcRequest = {
jsonrpc: '2.0',
id: uuidv4(),
method: 'getTask',
params: { id: taskId }
};
try {
const response = await fetch(agentUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(rpcRequest),
timeout: 30000
});
if (response.ok) {
const rpcResponse = await response.json();
if (rpcResponse.result) {
return rpcResponse.result;
} else if (rpcResponse.error) {
throw new Error(`Agent error: ${JSON.stringify(rpcResponse.error)}`);
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
console.error(`Error getting task from agent: ${error.message}`);
throw error;
}
}
// Create MCP server
const server = new Server(
{
name: 'a2a-bridge-mcp-server',
version: '1.4.0',
},
{
capabilities: {
tools: {},
},
}
);
// Handle MCP initialization handshake (required by MCP specification)
server.setRequestHandler(InitializeRequestSchema, async (request) => {
console.error('Received initialize request from MCP client');
return {
protocolVersion: '2024-11-05',
capabilities: {
tools: true,
resources: false,
prompts: false
},
serverInfo: {
name: 'a2a-bridge-mcp-server',
version: '1.4.0'
}
};
});
// Handle initialized notification (completes MCP handshake)
server.setNotificationHandler(InitializedNotificationSchema, async () => {
console.error('MCP client initialized successfully - handshake complete');
});
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'register_agent',
description: 'Register an A2A agent with the bridge server',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the A2A agent',
},
},
required: ['url'],
},
},
{
name: 'list_agents',
description: 'List all registered A2A agents',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'send_message',
description: 'Send a message to an A2A agent',
inputSchema: {
type: 'object',
properties: {
agent_url: {
type: 'string',
description: 'URL of the A2A agent',
},
message: {
type: 'string',
description: 'Message to send',
},
session_id: {
type: 'string',
description: 'Optional session ID for multi-turn conversations',
},
},
required: ['agent_url', 'message'],
},
},
{
name: 'get_task_result',
description: 'Retrieve the result of a task from an A2A agent',
inputSchema: {
type: 'object',
properties: {
task_id: {
type: 'string',
description: 'ID of the task to retrieve',
},
},
required: ['task_id'],
},
},
{
name: 'gemini_chat',
description: 'Chat directly with Google Gemini AI (requires Gemini CLI)',
inputSchema: {
type: 'object',
properties: {
prompt: {
type: 'string',
description: 'The message or question for Gemini',
},
model: {
type: 'string',
description: 'Gemini model to use (optional)',
default: 'gemini-2.0-flash-exp',
enum: [
'gemini-2.0-flash-exp',
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-pro'
]
},
},
required: ['prompt'],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'register_agent': {
const { url } = args;
if (!url) {
throw new McpError(ErrorCode.InvalidParams, 'URL is required');
}
const result = await registerAgent(url);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'list_agents': {
const agents = await listAgents();
return {
content: [
{
type: 'text',
text: JSON.stringify(agents, null, 2),
},
],
};
}
case 'send_message': {
const { agent_url, message, session_id } = args;
if (!agent_url || !message) {
throw new McpError(ErrorCode.InvalidParams, 'agent_url and message are required');
}
const result = await sendMessage(agent_url, message, session_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_task_result': {
const { task_id } = args;
if (!task_id) {
throw new McpError(ErrorCode.InvalidParams, 'task_id is required');
}
const result = await getTaskResult(task_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'gemini_chat': {
const { prompt, model = 'gemini-2.5-pro' } = args;
if (!prompt) {
return {
isError: true,
content: [
{
type: 'text',
text: 'Error: Prompt is required for Gemini chat',
},
],
};
}
try {
// Check if Gemini CLI is available
await checkGeminiCLI();
const response = await executeGemini(prompt, model);
// Track successful model for future reference
quotaFailureTracker.lastSuccessfulModel = model;
return {
content: [
{
type: 'text',
text: response,
},
],
};
} catch (error) {
logWithLevel('error', `Gemini chat failed with model ${model}:`, error.message);
// Check if this is a quota/rate limit error
const isQuotaError = detectError(error.message, 'QUOTA_EXCEEDED');
const isAuthError = detectError(error.message, 'AUTHENTICATION');
let errorType = 'Unknown';
let retryAdvice = '';
let fallbackInfo = '';
if (isQuotaError) {
errorType = 'Quota/Rate Limit';
retryAdvice = 'The system attempted automatic fallback through the model chain: gemini-2.5-pro → gemini-2.5-flash → gemini-1.5-flash → gemini-1.5-pro';
fallbackInfo = `\n\nFallback Status:\n- Requested model: ${model}\n- Last successful model: ${quotaFailureTracker.lastSuccessfulModel || 'None'}\n- Session start: ${new Date(quotaFailureTracker.sessionStartTime).toISOString()}\n- Total failures tracked: ${Array.from(quotaFailureTracker.failures.entries()).reduce((sum, [, failures]) => sum + failures.length, 0)}`;
} else if (isAuthError) {
errorType = 'Authentication';
retryAdvice = 'Please check your Google API key or run "gemini auth" to authenticate';
} else {
errorType = 'API/Network';
retryAdvice = 'This may be a temporary issue with the Gemini service';
}
// Return comprehensive error information for Claude to understand
return {
isError: true,
content: [
{
type: 'text',
text: `🚨 Gemini AI ${errorType} Error\n\nModel: ${model}\nError: ${error.message}\n\n💡 What happened:\n${retryAdvice}\n\n🔄 Intelligent Fallback System:\nThe MCP server includes an intelligent model fallback system that automatically tries alternative models when quota limits are hit. If you're seeing this error, it means the fallback system attempted to use all available models in the chain.${fallbackInfo}\n\n⚡ Suggestions:\n${isQuotaError ? '• Try again in a few minutes when quota resets\n• Consider using a different Google account\n• Check Google AI Studio quota limits' : isAuthError ? '• Verify your Google API key is valid\n• Run: gemini auth\n• Check your Google Cloud project settings' : '• Retry the request\n• Check your internet connection\n• Verify Gemini service status'}`,
},
],
};
}
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
// Only throw McpError for actual protocol issues, not tool execution problems
if (error instanceof McpError) {
throw error;
}
logWithLevel('error', 'Unexpected MCP server error:', error.message);
throw new McpError(
ErrorCode.InternalError,
`MCP server error: ${error.message}`
);
}
});
// Main function
async function main() {
try {
// Ensure data directory exists
await fs.ensureDir(DATA_DIR);
dataDirInitialized = true;
// Initialize storage
await initializeStorage();
// Set up exit handlers (Windows and Unix compatible)
process.on('SIGINT', async () => {
console.error('Shutting down A2A Bridge MCP Server (SIGINT)...');
await saveDataOnExit();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.error('Shutting down A2A Bridge MCP Server (SIGTERM)...');
await saveDataOnExit();
process.exit(0);
});
// Windows-specific signal handling
if (process.platform === 'win32') {
process.on('SIGBREAK', async () => {
console.error('Shutting down A2A Bridge MCP Server (SIGBREAK)...');
await saveDataOnExit();
process.exit(0);
});
}
// Handle uncaught exceptions and unhandled rejections
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error.message);
console.error('Stack:', error.stack);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
// Create transport and connect with error handling
const transport = new StdioServerTransport();
try {
await server.connect(transport);
// Keep the process alive for MCP communication (essential for Windows)
process.stdin.resume();
console.error('A2A Bridge MCP Server started successfully');
// Additional Windows-specific setup
if (process.platform === 'win32') {
console.error('Windows mode: stdio transport active, process.stdin resumed');
// Ensure stdin doesn't close unexpectedly
process.stdin.on('end', () => {
console.error('stdin ended - MCP client disconnected');
});
process.stdin.on('error', (error) => {
console.error('stdin error:', error.message);
});
}
} catch (transportError) {
console.error('Failed to connect MCP transport:', transportError.message);
console.error('Transport error details:', transportError.stack);
throw transportError;
}
} catch (error) {
console.error('Failed to start A2A Bridge MCP Server:', error.message);
process.exit(1);
}
}
// Start server if this file is run directly
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
}
export {
registerAgent,
listAgents,
sendMessage,
getTaskResult,
executeGemini,
checkGeminiCLI,
main,
server
};