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
JavaScript
;
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