UNPKG

aery-geminicli

Version:

Model Context Protocol (MCP) server for Gemini CLI integration with GitHub Copilot - includes advanced file reading, context management, and chat state tools

710 lines (706 loc) โ€ข 31.8 kB
#!/usr/bin/env node "use strict"; const { Server } = require('@modelcontextprotocol/sdk/server'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio'); const { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } = require('@modelcontextprotocol/sdk/types'); const { spawn } = require('child_process'); const { readFileSync, writeFileSync, existsSync, mkdirSync } = require('fs'); const { join } = require('path'); const { homedir } = require('os'); // MCP Server Implementation class GeminiCliMCPServer { constructor() { this.server = new Server({ name: 'aery-geminicli', version: '0.1.5', }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); this.setupErrorHandling(); } setupErrorHandling() { this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'gemini_explain_code', description: 'Explain code using Gemini AI with detailed analysis including structure, logic, and best practices', inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'The source code to explain (any programming language)', }, language: { type: 'string', description: 'Programming language hint (e.g., javascript, python, typescript) - optional but improves analysis', }, context: { type: 'string', description: 'Additional context about the code purpose or project - optional', }, }, required: ['code'], }, }, { name: 'gemini_query', description: 'Ask Gemini AI any development-related question with optional context for enhanced responses', inputSchema: { type: 'object', properties: { question: { type: 'string', description: 'The question to ask Gemini AI (development, architecture, debugging, etc.)', }, context: { type: 'string', description: 'Additional context, code snippets, or project information - optional', }, }, required: ['question'], }, }, { name: 'read_file_content', description: 'Read and return the contents of a file for analysis or processing', inputSchema: { type: 'object', properties: { filepath: { type: 'string', description: 'Absolute or relative path to the file to read', }, }, required: ['filepath'], }, }, { name: 'save_to_memory', description: 'Save important information to persistent memory (survives across sessions) with categorization', inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Unique identifier for the memory entry (use descriptive names)', }, content: { type: 'string', description: 'Information to save (analysis results, configurations, insights, etc.)', }, category: { type: 'string', description: 'Category for organization (e.g., "architecture", "security", "config") - optional, defaults to "general"', }, }, required: ['key', 'content'], }, }, { name: 'recall_from_memory', description: 'Recall previously saved information from persistent memory with optional filtering', inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Specific memory key to recall - optional, if empty returns all memories', }, category: { type: 'string', description: 'Filter by category (e.g., "architecture", "security") - optional', }, }, required: [], }, }, { name: 'compress_context', description: 'Compress large context or conversation into a concise, structured summary while preserving key information', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Large content to compress (conversations, analysis results, documentation)', }, focus: { type: 'string', description: 'What to focus on during compression (e.g., "technical details", "action items") - optional', }, }, required: ['content'], }, }, { name: 'save_chat_state', description: 'Save current chat conversation state for later restoration', inputSchema: { type: 'object', properties: { session_id: { type: 'string', description: 'Unique session identifier for this conversation', }, messages: { type: 'string', description: 'Chat messages to save (JSON string or formatted text)', }, }, required: ['session_id', 'messages'], }, }, { name: 'restore_chat_state', description: 'Restore a previously saved chat conversation state', inputSchema: { type: 'object', properties: { session_id: { type: 'string', description: 'Session identifier to restore (must match a previously saved session)', }, }, required: ['session_id'], }, }, // ADVANCED WORKFLOW TOOLS - These combine multiple AI operations for comprehensive analysis { name: 'workflow_analyze_architecture', description: '๐Ÿ—๏ธ WORKFLOW: Comprehensive codebase architecture analysis combining structure, patterns, and recommendations', inputSchema: { type: 'object', properties: { project_path: { type: 'string', description: 'Absolute path to the project root directory (must contain main project files)', }, save_analysis: { type: 'boolean', description: 'Whether to save analysis results to persistent memory for future reference (default: true)', }, }, required: ['project_path'], }, }, { name: 'workflow_smart_code_review', description: '๐Ÿ“‹ WORKFLOW: Intelligent multi-perspective code review analyzing security, performance, and maintainability', inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'Source code to review (can be function, class, file, or code snippet)', }, review_type: { type: 'string', description: 'Type of review to perform - "security" (vulnerabilities), "performance" (optimization), "maintainability" (code quality), or "all" (comprehensive)', enum: ['security', 'performance', 'maintainability', 'all'], }, language: { type: 'string', description: 'Programming language for better context (e.g., "javascript", "python", "typescript") - optional', }, }, required: ['code'], }, }, { name: 'workflow_project_understanding', description: '๐Ÿง  WORKFLOW: Deep project understanding combining technical analysis with business context and documentation', inputSchema: { type: 'object', properties: { project_path: { type: 'string', description: 'Absolute path to the project root directory', }, focus_areas: { type: 'string', description: 'Specific areas to focus analysis on, comma-separated (e.g., "API,database,authentication,frontend") - optional', }, }, required: ['project_path'], }, }, { name: 'workflow_context_manager', description: '๐Ÿ—‚๏ธ WORKFLOW: Smart context compression and memory management with automatic categorization and cleanup', inputSchema: { type: 'object', properties: { action: { type: 'string', description: 'Action to perform: "compress" (summarize content), "save" (store to memory), "recall" (retrieve from memory), or "clean" (remove old entries)', enum: ['compress', 'save', 'recall', 'clean'], }, content: { type: 'string', description: 'Content to process - required for "compress" and "save" actions', }, key: { type: 'string', description: 'Memory key identifier - required for "save" action, optional for "recall" (empty = all memories)', }, }, required: ['action'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'gemini_explain_code': return await this.explainCode(args.code, args.language, args.context); case 'gemini_query': return await this.queryGemini(args.question, args.context); case 'read_file_content': return await this.readFileContent(args.filepath); case 'save_to_memory': return await this.saveToMemory(args.key, args.content, args.category); case 'recall_from_memory': return await this.recallFromMemory(args.key, args.category); case 'compress_context': return await this.compressContext(args.content, args.focus); case 'save_chat_state': return await this.saveChatState(args.session_id, args.messages); case 'restore_chat_state': return await this.restoreChatState(args.session_id); // WORKFLOW HANDLERS case 'workflow_analyze_architecture': return await this.workflowAnalyzeArchitecture(args.project_path, args.save_analysis); case 'workflow_smart_code_review': return await this.workflowSmartCodeReview(args.code, args.review_type, args.language); case 'workflow_project_understanding': return await this.workflowProjectUnderstanding(args.project_path, args.focus_areas); case 'workflow_context_manager': return await this.workflowContextManager(args.action, args.content, args.key); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${errorMessage}`); } }); } // Core Tools Implementation async explainCode(code, language, context) { const prompt = `Explain this ${language || ''} code in detail:\n\n${code}\n\n${context ? `Context: ${context}` : ''}`; return await this.executeGeminiCommand(prompt); } async queryGemini(question, context) { const prompt = context ? `${question}\n\nContext: ${context}` : question; return await this.executeGeminiCommand(prompt); } async readFileContent(filepath) { try { const content = readFileSync(filepath, 'utf-8'); return { content: [{ type: 'text', text: `File content of ${filepath}:\n\n${content}`, }], }; } catch (error) { return { content: [{ type: 'text', text: `Error reading file ${filepath}: ${error instanceof Error ? error.message : String(error)}`, }], }; } } async saveToMemory(key, content, category) { const memoryPath = join(homedir(), '.gemini-cli-mcp-memory.json'); let memory = {}; if (existsSync(memoryPath)) { try { memory = JSON.parse(readFileSync(memoryPath, 'utf-8')); } catch (error) { // File exists but is corrupted, start fresh memory = {}; } } memory[key] = { content, category: category || 'general', timestamp: new Date().toISOString(), }; writeFileSync(memoryPath, JSON.stringify(memory, null, 2)); return { content: [{ type: 'text', text: `Saved to memory: ${key} (category: ${category || 'general'})`, }], }; } async recallFromMemory(key, category) { const memoryPath = join(homedir(), '.gemini-cli-mcp-memory.json'); if (!existsSync(memoryPath)) { return { content: [{ type: 'text', text: 'No memory file found. Nothing has been saved yet.', }], }; } try { const memory = JSON.parse(readFileSync(memoryPath, 'utf-8')); if (key) { const item = memory[key]; if (item) { return { content: [{ type: 'text', text: `Memory recall for "${key}":\n${item.content}\n\nSaved: ${item.timestamp}`, }], }; } else { return { content: [{ type: 'text', text: `No memory found for key: ${key}`, }], }; } } else { // List all memories, optionally filtered by category const entries = Object.entries(memory).filter(([_, item]) => !category || item.category === category); if (entries.length === 0) { return { content: [{ type: 'text', text: category ? `No memories found in category: ${category}` : 'No memories found.', }], }; } const list = entries.map(([k, item]) => `โ€ข ${k} (${item.category}) - ${item.timestamp}`).join('\n'); return { content: [{ type: 'text', text: `Saved memories${category ? ` in category "${category}"` : ''}:\n\n${list}`, }], }; } } catch (error) { return { content: [{ type: 'text', text: `Error reading memory: ${error instanceof Error ? error.message : String(error)}`, }], }; } } async compressContext(content, focus) { const prompt = `Compress the following content into a concise summary${focus ? `, focusing on: ${focus}` : ''}:\n\n${content}`; return await this.executeGeminiCommand(prompt); } async saveChatState(sessionId, messages) { const chatDir = join(homedir(), '.gemini-cli-mcp-chats'); if (!existsSync(chatDir)) { mkdirSync(chatDir, { recursive: true }); } const chatPath = join(chatDir, `${sessionId}.json`); const chatData = { sessionId, messages, timestamp: new Date().toISOString(), }; writeFileSync(chatPath, JSON.stringify(chatData, null, 2)); return { content: [{ type: 'text', text: `Chat state saved for session: ${sessionId}`, }], }; } async restoreChatState(sessionId) { const chatPath = join(homedir(), '.gemini-cli-mcp-chats', `${sessionId}.json`); if (!existsSync(chatPath)) { return { content: [{ type: 'text', text: `No chat state found for session: ${sessionId}`, }], }; } try { const chatData = JSON.parse(readFileSync(chatPath, 'utf-8')); return { content: [{ type: 'text', text: `Restored chat state from ${chatData.timestamp}:\n\n${chatData.messages}`, }], }; } catch (error) { return { content: [{ type: 'text', text: `Error restoring chat state: ${error instanceof Error ? error.message : String(error)}`, }], }; } } // Workflow Tools Implementation async workflowAnalyzeArchitecture(projectPath, saveAnalysis = true) { try { // Step 1: Read project structure const structurePrompt = `Analyze the architecture of the project at: ${projectPath}. Focus on: 1. Overall architecture patterns 2. Directory structure and organization 3. Key components and their relationships 4. Technology stack and frameworks used 5. Design patterns identified 6. Potential architectural improvements`; const analysisResult = await this.executeGeminiCommand(structurePrompt); // Step 2: Save to memory if requested if (saveAnalysis) { const timestamp = new Date().toISOString().split('T')[0]; await this.saveToMemory(`architecture_${timestamp}`, analysisResult.content[0].text, 'architecture'); } return { content: [{ type: 'text', text: `๐Ÿ—๏ธ Architecture Analysis Complete!\n\n${analysisResult.content[0].text}\n\n${saveAnalysis ? 'โœ… Analysis saved to memory for future reference.' : ''}`, }], }; } catch (error) { return { content: [{ type: 'text', text: `โŒ Architecture analysis failed: ${error instanceof Error ? error.message : String(error)}`, }], }; } } async workflowSmartCodeReview(code, reviewType = 'all', language) { const reviews = []; try { if (reviewType === 'security' || reviewType === 'all') { const securityPrompt = `Perform a security review of this ${language || ''} code. Look for: - Potential vulnerabilities - Security best practices violations - Input validation issues - Authentication/authorization problems - Data exposure risks Code: ${code}`; const securityResult = await this.executeGeminiCommand(securityPrompt); reviews.push(`๐Ÿ”’ SECURITY REVIEW:\n${securityResult.content[0].text}`); } if (reviewType === 'performance' || reviewType === 'all') { const performancePrompt = `Perform a performance review of this ${language || ''} code. Analyze: - Performance bottlenecks - Memory usage optimization - Algorithm efficiency - Database query optimization - Caching opportunities Code: ${code}`; const performanceResult = await this.executeGeminiCommand(performancePrompt); reviews.push(`โšก PERFORMANCE REVIEW:\n${performanceResult.content[0].text}`); } if (reviewType === 'maintainability' || reviewType === 'all') { const maintainabilityPrompt = `Perform a maintainability review of this ${language || ''} code. Focus on: - Code readability and clarity - SOLID principles adherence - Design patterns usage - Documentation quality - Testing considerations Code: ${code}`; const maintainabilityResult = await this.executeGeminiCommand(maintainabilityPrompt); reviews.push(`๐Ÿ”ง MAINTAINABILITY REVIEW:\n${maintainabilityResult.content[0].text}`); } return { content: [{ type: 'text', text: `๐Ÿ“‹ Smart Code Review Complete!\n\n${reviews.join('\n\n---\n\n')}`, }], }; } catch (error) { return { content: [{ type: 'text', text: `โŒ Code review failed: ${error instanceof Error ? error.message : String(error)}`, }], }; } } async workflowProjectUnderstanding(projectPath, focusAreas) { try { // Step 1: High-level overview const overviewPrompt = `Provide a comprehensive understanding of the project at: ${projectPath} ${focusAreas ? `Focus specifically on: ${focusAreas}` : ''} Analyze: 1. Project purpose and goals 2. Main functionality and features 3. Key technologies and frameworks 4. Entry points and main flows 5. Configuration and setup requirements 6. Documentation quality and coverage`; const overview = await this.executeGeminiCommand(overviewPrompt); // Step 2: Technical deep dive const technicalPrompt = `Provide a technical deep dive of the project structure: 1. Module organization and dependencies 2. Data flow and state management 3. API endpoints and interfaces 4. Database schema and models 5. Testing strategy and coverage 6. Deployment and infrastructure`; const technical = await this.executeGeminiCommand(technicalPrompt); // Step 3: Save comprehensive analysis const timestamp = new Date().toISOString().split('T')[0]; const fullAnalysis = `PROJECT UNDERSTANDING - ${timestamp}\n\nOVERVIEW:\n${overview.content[0].text}\n\nTECHNICAL ANALYSIS:\n${technical.content[0].text}`; await this.saveToMemory(`project_understanding_${timestamp}`, fullAnalysis, 'project_analysis'); return { content: [{ type: 'text', text: `๐Ÿง  Project Understanding Complete!\n\n${fullAnalysis}\n\nโœ… Complete analysis saved to memory.`, }], }; } catch (error) { return { content: [{ type: 'text', text: `โŒ Project understanding failed: ${error instanceof Error ? error.message : String(error)}`, }], }; } } async workflowContextManager(action, content, key) { try { switch (action) { case 'compress': if (!content) throw new Error('Content required for compress action'); return await this.compressContext(content); case 'save': if (!content || !key) throw new Error('Content and key required for save action'); return await this.saveToMemory(key, content, 'context_manager'); case 'recall': return await this.recallFromMemory(key, 'context_manager'); case 'clean': // Clean old memories (older than 30 days) const memoryPath = join(homedir(), '.gemini-cli-mcp-memory.json'); if (!existsSync(memoryPath)) { return { content: [{ type: 'text', text: '๐Ÿงน No memory file to clean.', }], }; } const memory = JSON.parse(readFileSync(memoryPath, 'utf-8')); const cutoffDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); let cleanedCount = 0; Object.keys(memory).forEach(k => { const itemDate = new Date(memory[k].timestamp); if (itemDate < cutoffDate) { delete memory[k]; cleanedCount++; } }); writeFileSync(memoryPath, JSON.stringify(memory, null, 2)); return { content: [{ type: 'text', text: `๐Ÿงน Context Manager: Cleaned ${cleanedCount} old memories (older than 30 days).`, }], }; default: throw new Error(`Unknown context manager action: ${action}`); } } catch (error) { return { content: [{ type: 'text', text: `โŒ Context manager failed: ${error instanceof Error ? error.message : String(error)}`, }], }; } } async executeGeminiCommand(prompt) { return new Promise((resolve, reject) => { const gemini = spawn('gemini', ['chat'], { stdio: ['pipe', 'pipe', 'pipe'], }); let output = ''; let errorOutput = ''; gemini.stdout.on('data', (data) => { output += data.toString(); }); gemini.stderr.on('data', (data) => { errorOutput += data.toString(); }); gemini.on('close', (code) => { if (code === 0) { resolve({ content: [{ type: 'text', text: output.trim() || 'No output received from Gemini', }], }); } else { reject(new Error(`Gemini command failed (exit code ${code}): ${errorOutput}`)); } }); gemini.on('error', (error) => { reject(new Error(`Failed to start gemini command: ${error.message}`)); }); // Send the prompt to gemini gemini.stdin.write(prompt); gemini.stdin.end(); }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Gemini CLI MCP Server running on stdio'); } } // Start the server async function main() { const server = new GeminiCliMCPServer(); await server.run(); } // Handle unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); }); if (typeof require !== 'undefined' && require.main === module) { main().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); } //# sourceMappingURL=index.js.map