UNPKG

@yusukedev/gemini-cli-mcp

Version:

A secure Model Context Protocol server for Gemini CLI with automatic fallback, extended timeout (180s), and model transparency features

297 lines (296 loc) 11.9 kB
#!/usr/bin/env node // Polyfill for TransformStream (Node.js v18+) import { TransformStream } from "stream/web"; if (typeof globalThis.TransformStream === "undefined") { // @ts-ignore globalThis.TransformStream = TransformStream; } /** * Gemini CLI MCP Server * Provides access to Gemini CLI functionality through MCP tools * Supports chat and web search capabilities */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { exec, spawn } from "child_process"; import { promisify } from "util"; import { writeFile, unlink } from "fs/promises"; import { join, dirname } from "path"; import { tmpdir, platform } from "os"; import { fileURLToPath } from "url"; // ES modules equivalent of __dirname const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const execAsync = promisify(exec); /** * Execute a Gemini CLI command with specified model */ async function executeGeminiCommand(input, useFlashModel = false) { const tempFile = join(tmpdir(), `gemini-input-${Date.now()}-${Math.random().toString(36).substring(7)}.txt`); const model = useFlashModel ? 'gemini-2.5-flash' : 'gemini-2.5-pro'; const isWindows = platform() === 'win32'; try { await writeFile(tempFile, input, 'utf8'); console.error(`[Gemini CLI] Using ${model}`); return new Promise((resolve, reject) => { let stdoutData = ''; let stderrData = ''; const timeoutMs = 120000; // 2 minutes const spawnOptions = { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, NODE_OPTIONS: '--experimental-global-webcrypto --experimental-fetch --no-warnings' } }; // Use wrapper script for better compatibility const wrapperPath = join(__dirname, '..', 'wrapper.cjs'); let childProcess; if (isWindows) { spawnOptions.shell = true; spawnOptions.windowsHide = true; childProcess = spawn(`node "${wrapperPath}" -m ${model}`, [], spawnOptions); } else { childProcess = spawn('node', [wrapperPath, '-m', model], spawnOptions); } // Set timeout const timeoutId = setTimeout(() => { childProcess.kill(); reject(new Error(`Timeout after ${timeoutMs / 1000}s`)); }, timeoutMs); // Write input and close stdin childProcess.stdin?.write(input); childProcess.stdin?.end(); // Collect stdout childProcess.stdout?.on('data', (data) => { stdoutData += data.toString(); }); // Collect stderr childProcess.stderr?.on('data', (data) => { const chunk = data.toString(); stderrData += chunk; // Check for quota errors in Pro model only if (!useFlashModel && chunk.toLowerCase().includes('quota exceeded')) { childProcess.kill(); clearTimeout(timeoutId); reject(new McpError(ErrorCode.InternalError, 'Gemini 2.5 Pro quota exceeded. Please use gemini_chat_flash instead.')); return; } }); // Handle completion childProcess.on('close', (code) => { clearTimeout(timeoutId); unlink(tempFile).catch(() => { }); if (code !== 0 && stderrData && !stdoutData) { reject(new Error(`Gemini CLI error: ${stderrData}`)); return; } const response = stdoutData || stderrData || ''; if (!response.trim()) { reject(new Error('Empty response from Gemini')); return; } resolve(response); }); // Handle process errors childProcess.on('error', async (error) => { clearTimeout(timeoutId); await unlink(tempFile).catch(() => { }); const errorMsg = error.message.toLowerCase(); if (errorMsg.includes('enoent') || errorMsg.includes('command not found')) { reject(new McpError(ErrorCode.InternalError, 'Gemini CLI not found. Please ensure it is installed and in your PATH.')); } else { reject(new McpError(ErrorCode.InternalError, `Failed to execute Gemini CLI: ${error.message}`)); } }); }); } catch (error) { await unlink(tempFile).catch(() => { }); if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Failed to execute Gemini CLI: ${error instanceof Error ? error.message : String(error)}`); } } /** * Validate Gemini CLI installation */ async function validateGeminiCLI() { try { const { stdout } = await execAsync('gemini --version', { timeout: 10000 }); const version = stdout.toString().trim(); if (version) { console.error(`[Gemini CLI] Found: ${version}`); return true; } return false; } catch (error) { console.error('[Gemini CLI] Validation failed:', error instanceof Error ? error.message : String(error)); return false; } } /** * Create an MCP server with Gemini CLI capabilities */ const server = new Server({ name: "gemini-cli-mcp", version: "1.5.3", }, { capabilities: { tools: {}, }, }); /** * Handler that lists available tools */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "gemini_chat_pro", description: "Chat with Gemini 2.5 Pro for development consultation and code assistance. If quota is exceeded, you will receive a message to use gemini_chat_flash instead.", inputSchema: { type: "object", properties: { message: { type: "string", description: "The message or question to send to Gemini" }, context: { type: "string", description: "Optional context or code to include in the conversation", default: "" } }, required: ["message"] } }, { name: "gemini_chat_flash", description: "Chat with Gemini 2.5 Flash for development consultation and code assistance. Use this when gemini_chat_pro quota is exceeded.", inputSchema: { type: "object", properties: { message: { type: "string", description: "The message or question to send to Gemini" }, context: { type: "string", description: "Optional context or code to include in the conversation", default: "" } }, required: ["message"] } }, ] }; }); /** * Handler for tool execution */ server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "gemini_chat_pro": { const message = String(request.params.arguments?.message || ""); const context = String(request.params.arguments?.context || ""); if (!message.trim()) { throw new McpError(ErrorCode.InvalidParams, "Message cannot be empty"); } try { let fullPrompt = message; if (context.trim()) { fullPrompt = `Context:\n${context}\n\nQuestion: ${message}`; } const result = await executeGeminiCommand(fullPrompt, false); return { content: [{ type: "text", text: result }] }; } catch (error) { // Check if this is a quota error const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('quota exceeded') || errorMessage.includes('Please use gemini_chat_flash')) { return { content: [{ type: "text", text: "⚠️ Gemini 2.5 Pro quota exceeded. Please use the `gemini_chat_flash` tool instead for your request." }], isError: false // Don't mark as error since this is expected behavior }; } return { content: [{ type: "text", text: `Error communicating with Gemini Pro: ${errorMessage}` }], isError: true }; } } case "gemini_chat_flash": { const message = String(request.params.arguments?.message || ""); const context = String(request.params.arguments?.context || ""); if (!message.trim()) { throw new McpError(ErrorCode.InvalidParams, "Message cannot be empty"); } try { let fullPrompt = message; if (context.trim()) { fullPrompt = `Context:\n${context}\n\nQuestion: ${message}`; } const result = await executeGeminiCommand(fullPrompt, true); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: `Error communicating with Gemini Flash: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } }); /** * Start the server using stdio transport */ async function main() { // Validate Gemini CLI before starting const isValid = await validateGeminiCLI(); if (!isValid) { console.error('[MCP Server] Warning: Gemini CLI validation failed'); console.error('[MCP Server] Install: npm install -g @google/generative-ai-cli'); console.error('[MCP Server] Authenticate: gemini auth'); } const transport = new StdioServerTransport(); await server.connect(transport); console.error('[MCP Server] Gemini CLI MCP Server started'); server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await server.close(); process.exit(0); }); } main().catch((error) => { console.error("[MCP Server] Fatal error:", error); process.exit(1); });