UNPKG

claudegem

Version:

Seamless integration of Claude Code CLI + Gemini CLI for context-aware code reasoning

439 lines (382 loc) 15.4 kB
#!/usr/bin/env node 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); });