claudegem
Version:
Seamless integration of Claude Code CLI + Gemini CLI for context-aware code reasoning
439 lines (382 loc) • 15.4 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import chalk from 'chalk';
import axios from 'axios';
import * as cheerio from 'cheerio';
const execAsync = promisify(exec);
class ClaudeGemMCPServer {
constructor() {
this.server = new Server(
{
name: 'claudegem-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
this.setupLogging();
}
setupLogging() {
this.server.onerror = (error) => {
console.error(chalk.red('[MCP Server Error]'), error);
};
}
async runLlamaCommand(query) {
try {
console.error(chalk.yellow(`[MCP] Using Llama fallback: ${query.substring(0, 100)}...`));
const tmpFile = `/tmp/claudegem_llama_${Date.now()}.txt`;
await execAsync(`echo ${JSON.stringify(query)} > ${JSON.stringify(tmpFile)}`);
try {
// Try to use ollama with available llama models
const { stdout } = await execAsync(`ollama run llama3.2 "$(cat ${JSON.stringify(tmpFile)})" 2>/dev/null || ollama run llama3 "$(cat ${JSON.stringify(tmpFile)})" 2>/dev/null || ollama run phi3 "$(cat ${JSON.stringify(tmpFile)})"`, {
timeout: 60000, // Increased to 60 seconds
maxBuffer: 1024 * 1024
});
return stdout.trim();
} catch (ollamaError) {
// If ollama not available, try llama.cpp
try {
const { stdout } = await execAsync(`llama "$(cat ${JSON.stringify(tmpFile)})"`, {
timeout: 30000,
maxBuffer: 1024 * 1024
});
return stdout.trim();
} catch (llamaError) {
throw new Error(`Llama not available: ${ollamaError.message}`);
}
} finally {
await execAsync(`rm ${JSON.stringify(tmpFile)}`).catch(() => {});
}
} catch (error) {
console.error(chalk.red('[MCP] Llama error:'), error.message);
throw error;
}
}
async searchInternet(query, maxResults = 5) {
try {
console.error(chalk.blue(`[MCP] Searching internet: ${query.substring(0, 100)}...`));
// Use DuckDuckGo search (privacy-friendly, no API key required)
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
const response = await axios.get(searchUrl, {
timeout: 15000,
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
const $ = cheerio.load(response.data);
const results = [];
// Extract search results from DuckDuckGo
$('.result').each((i, element) => {
if (i >= maxResults) return false;
const title = $(element).find('.result__title a').text().trim();
const url = $(element).find('.result__title a').attr('href');
const snippet = $(element).find('.result__snippet').text().trim();
if (title && url && snippet) {
results.push({
title,
url,
snippet
});
}
});
if (results.length === 0) {
// Fallback: Try a simpler search approach
const fallbackResults = await this.searchWithFallback(query, maxResults);
return fallbackResults;
}
const searchSummary = `Internet search results for "${query}":\n\n` +
results.map((result, index) =>
`${index + 1}. **${result.title}**\n ${result.url}\n ${result.snippet}\n`
).join('\n');
console.error(chalk.green(`[MCP] Found ${results.length} search results`));
return searchSummary;
} catch (error) {
console.error(chalk.red('[MCP] Internet search error:'), error.message);
// Try fallback search methods
try {
const fallbackResults = await this.searchWithFallback(query, maxResults);
return fallbackResults;
} catch (fallbackError) {
return `❌ Internet search failed: ${error.message}\n\nTried searching for: "${query}"\n\nPlease try rephrasing your search query or check internet connection.`;
}
}
}
async searchWithFallback(query, maxResults = 3) {
try {
console.error(chalk.yellow(`[MCP] Using fallback search method...`));
// Use a simple web search via curl (as fallback)
const searchCommand = `curl -s "https://www.google.com/search?q=${encodeURIComponent(query)}&num=${maxResults}" -H "User-Agent: Mozilla/5.0"`;
const { stdout } = await execAsync(searchCommand, { timeout: 10000 });
// Basic extraction of results from Google search
const lines = stdout.split('\n');
const results = [];
for (let i = 0; i < lines.length && results.length < maxResults; i++) {
const line = lines[i];
if (line.includes('<h3>') && line.includes('href=')) {
const match = line.match(/href="([^"]+)"/);
if (match) {
results.push(`Search result: ${match[1]}`);
}
}
}
if (results.length > 0) {
return `Internet search results for "${query}" (fallback method):\n\n` +
results.map((result, index) => `${index + 1}. ${result}`).join('\n');
} else {
return `❌ No search results found for "${query}". Please try a different search query.`;
}
} catch (error) {
throw new Error(`Fallback search failed: ${error.message}`);
}
}
async runMistralCommand(query) {
try {
console.error(chalk.magenta(`[MCP] Using Mistral Devstral fallback: ${query.substring(0, 100)}...`));
const tmpFile = `/tmp/claudegem_mistral_${Date.now()}.txt`;
await execAsync(`echo ${JSON.stringify(query)} > ${JSON.stringify(tmpFile)}`);
try {
// Try ollama with Mistral models (future: mistral-devstral-medium)
const { stdout } = await execAsync(`ollama run mistral-devstral-medium "$(cat ${JSON.stringify(tmpFile)})" 2>/dev/null || ollama run mistral "$(cat ${JSON.stringify(tmpFile)})" 2>/dev/null || ollama run phi3 "$(cat ${JSON.stringify(tmpFile)})"`, {
timeout: 45000, // Optimized timeout for Mistral
maxBuffer: 1024 * 1024
});
return stdout.trim();
} catch (ollamaError) {
// Try other mistral implementations
try {
const { stdout } = await execAsync(`mistral "$(cat ${JSON.stringify(tmpFile)})"`, {
timeout: 30000,
maxBuffer: 1024 * 1024
});
return stdout.trim();
} catch (mistralError) {
throw new Error(`Mistral not available: ${ollamaError.message}`);
}
} finally {
await execAsync(`rm ${JSON.stringify(tmpFile)}`).catch(() => {});
}
} catch (error) {
console.error(chalk.red('[MCP] Mistral error:'), error.message);
throw error;
}
}
async runGeminiCommand(query, model = 'gemini-2.5-pro') {
try {
console.error(chalk.blue(`[MCP] Delegating to Gemini: ${query.substring(0, 100)}...`));
const tmpFile = `/tmp/claudegem_mcp_${Date.now()}.txt`;
await execAsync(`echo ${JSON.stringify(query)} > ${JSON.stringify(tmpFile)}`);
try {
// Use shorter timeout and no debug output to prevent hanging
const { stdout } = await execAsync(`gemini --model ${model} -y -a -p "$(cat ${JSON.stringify(tmpFile)})"`, {
timeout: 15000, // 15 second timeout
maxBuffer: 1024 * 1024 // 1MB buffer
});
// Clean up Gemini output by removing debug messages
const cleanOutput = stdout
.split('\n')
.filter(line => !line.includes('[DEBUG]') && !line.includes('Flushing log events'))
.filter(line => line.trim() !== '')
.join('\n')
.trim();
return cleanOutput || stdout.trim();
} finally {
await execAsync(`rm ${JSON.stringify(tmpFile)}`).catch(() => {});
}
} catch (error) {
console.error(chalk.red('[MCP] Gemini delegation error:'), error.message);
// Check if it's a quota error (429 or quota exceeded message)
const isQuotaError = error.message.includes('429') ||
error.message.includes('quota exceeded') ||
error.message.includes('RESOURCE_EXHAUSTED') ||
error.message.includes('rateLimitExceeded');
if (isQuotaError) {
console.error(chalk.red('🚨 GEMINI API QUOTA EXHAUSTED - Switching to free alternatives...'));
// Try Llama first (fast fallback)
try {
const llamaResult = await this.runLlamaCommand(query);
console.error(chalk.yellow('✅ Using Llama as fallback due to Gemini quota exhaustion'));
return `⚠️ Gemini API quota exhausted - Using Llama fallback:\n\n${llamaResult}`;
} catch (llamaError) {
console.error(chalk.yellow('[MCP] Llama fallback failed, trying Mistral...'));
// Try Mistral as final fallback (when available)
try {
const mistralResult = await this.runMistralCommand(query);
console.error(chalk.magenta('✅ Using Mistral as fallback due to Gemini quota exhaustion'));
return `⚠️ Gemini API quota exhausted, Llama unavailable - Using Mistral fallback:\n\n${mistralResult}`;
} catch (mistralError) {
console.error(chalk.red('[MCP] All AI models failed'));
return `🚨 GEMINI API QUOTA EXHAUSTED and no fallback models available.\n\nGemini Error: ${error.message}\nLlama Error: ${llamaError.message}\nMistral Error: ${mistralError.message}\n\nPlease wait for Gemini quota reset or install Ollama with llama3.2 or mistral-devstral-medium models.`;
}
}
}
// For non-quota errors, return the original error
return `Error executing command: ${error.message}`;
}
}
setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'bash_with_gemini',
description: 'Execute bash commands through Gemini AI for enhanced processing',
inputSchema: {
type: 'object',
properties: {
command: {
type: 'string',
description: 'The bash command to execute via Gemini'
},
gemini_model: {
type: 'string',
description: 'Gemini model to use (default: gemini-2.5-pro)',
default: 'gemini-2.5-pro'
}
},
required: ['command']
}
},
{
name: 'analyze_with_gemini',
description: 'Analyze code or files using Gemini AI for context',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Analysis query or file path to analyze'
},
gemini_model: {
type: 'string',
description: 'Gemini model to use (default: gemini-2.5-pro)',
default: 'gemini-2.5-pro'
}
},
required: ['query']
}
},
{
name: 'search_internet',
description: 'Search the internet for information and return relevant results',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query to find information on the internet'
},
max_results: {
type: 'number',
description: 'Maximum number of search results to return (default: 5)',
default: 5
}
},
required: ['query']
}
}
]
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'bash_with_gemini':
return await this.handleBashWithGemini(args);
case 'analyze_with_gemini':
return await this.handleAnalyzeWithGemini(args);
case 'search_internet':
return await this.handleSearchInternet(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
console.error(chalk.red(`[MCP] Tool ${name} error:`), error.message);
return {
content: [
{
type: 'text',
text: `Error executing ${name}: ${error.message}`
}
]
};
}
});
}
async handleBashWithGemini(args) {
const { command, gemini_model = 'gemini-2.5-pro' } = args;
const prompt = [
'You are a command execution assistant. Execute the following bash command and return the result:',
'',
`Command: ${command}`,
'',
'Execute this command and provide the output. If the command cannot be executed, explain why.',
'Return the command output or error message with minimal explanation.'
].join('\n');
const result = await this.runGeminiCommand(prompt, gemini_model);
return {
content: [
{
type: 'text',
text: result
}
]
};
}
async handleAnalyzeWithGemini(args) {
const { query, gemini_model = 'gemini-2.5-pro' } = args;
const prompt = [
'You are a code analysis assistant. Analyze the following query or codebase:',
'',
`Query: ${query}`,
'',
'Provide detailed analysis, insights, or answer the query with relevant code context.',
'Focus on code structure, patterns, potential issues, and improvements.'
].join('\n');
const result = await this.runGeminiCommand(prompt, gemini_model);
return {
content: [
{
type: 'text',
text: result
}
]
};
}
async handleSearchInternet(args) {
const { query, max_results = 5 } = args;
const result = await this.searchInternet(query, max_results);
return {
content: [
{
type: 'text',
text: result
}
]
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error(chalk.green('[MCP] ClaudeGem MCP Server started'));
}
}
// Start the server
const server = new ClaudeGemMCPServer();
server.run().catch((error) => {
console.error(chalk.red('[MCP] Fatal error:'), error);
process.exit(1);
});