@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
JavaScript
// 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);
});