@staticpayload/gemini-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +273 -0
  3. package/package.json +48 -0
  4. package/src/index.js +413 -0
package/src/index.js ADDED
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+ import { spawn, execFile } from 'node:child_process';
10
+ import { promisify } from 'node:util';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ // Configuration
15
+ const EXECUTION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
16
+ const MAX_BUFFER_SIZE = 50 * 1024 * 1024; // 50MB
17
+
18
+ // Track running processes for cleanup
19
+ const runningProcesses = new Map();
20
+ let processIdCounter = 0;
21
+
22
+ /**
23
+ * Log to stderr only (MCP uses stdout for protocol messages)
24
+ */
25
+ function log(message) {
26
+ process.stderr.write(`[gemini-mcp] ${message}\n`);
27
+ }
28
+
29
+ /**
30
+ * Discover the Gemini CLI binary path.
31
+ */
32
+ async function discoverGeminiCli() {
33
+ // Check for explicit override
34
+ if (process.env.GEMINI_CLI_PATH) {
35
+ return process.env.GEMINI_CLI_PATH;
36
+ }
37
+
38
+ // Try to find 'gemini' in PATH
39
+ try {
40
+ const { stdout } = await execFileAsync('which', ['gemini'], { timeout: 5000 });
41
+ const path = stdout.trim();
42
+ if (path) return path;
43
+ } catch {
44
+ // 'which' failed
45
+ }
46
+
47
+ // Try common installation paths
48
+ const commonPaths = [
49
+ '/usr/local/bin/gemini',
50
+ '/opt/homebrew/bin/gemini',
51
+ `${process.env.HOME}/.local/bin/gemini`,
52
+ `${process.env.HOME}/.npm-global/bin/gemini`,
53
+ `${process.env.HOME}/.nvm/versions/node/v20.19.6/bin/gemini`,
54
+ ];
55
+
56
+ for (const p of commonPaths) {
57
+ try {
58
+ await execFileAsync(p, ['--version'], { timeout: 5000 });
59
+ return p;
60
+ } catch {
61
+ // Continue
62
+ }
63
+ }
64
+
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Health check - verify Gemini CLI works
70
+ */
71
+ async function healthCheck(geminiPath) {
72
+ try {
73
+ const { stdout } = await execFileAsync(geminiPath, ['--version'], {
74
+ timeout: 10000,
75
+ env: process.env,
76
+ });
77
+ return { ok: true, version: stdout.trim() };
78
+ } catch (error) {
79
+ return { ok: false, error: error.message };
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Execute Gemini prompt with streaming
85
+ */
86
+ function executeGeminiPrompt(geminiPath, prompt, model) {
87
+ return new Promise((resolve) => {
88
+ const args = ['-p', prompt];
89
+ if (model) args.push('-m', model);
90
+
91
+ const proc = spawn(geminiPath, args, {
92
+ env: process.env,
93
+ stdio: ['pipe', 'pipe', 'pipe'],
94
+ });
95
+
96
+ const id = ++processIdCounter;
97
+ runningProcesses.set(id, proc);
98
+
99
+ let stdout = '';
100
+ let stderr = '';
101
+ let killed = false;
102
+
103
+ // Timeout handler
104
+ const timeout = setTimeout(() => {
105
+ killed = true;
106
+ proc.kill('SIGTERM');
107
+ }, EXECUTION_TIMEOUT_MS);
108
+
109
+ proc.stdout.on('data', (data) => {
110
+ stdout += data.toString();
111
+ });
112
+
113
+ proc.stderr.on('data', (data) => {
114
+ stderr += data.toString();
115
+ });
116
+
117
+ proc.on('close', (code) => {
118
+ clearTimeout(timeout);
119
+ runningProcesses.delete(id);
120
+
121
+ if (killed) {
122
+ resolve({ success: false, error: 'Execution timeout (5 minutes)', stderr });
123
+ } else if (code === 0) {
124
+ resolve({ success: true, text: stdout, stderr: stderr || null });
125
+ } else {
126
+ resolve({ success: false, error: `Exit code ${code}`, stderr, code });
127
+ }
128
+ });
129
+
130
+ proc.on('error', (error) => {
131
+ clearTimeout(timeout);
132
+ runningProcesses.delete(id);
133
+ resolve({ success: false, error: error.message, stderr });
134
+ });
135
+ });
136
+ }
137
+
138
+ /**
139
+ * List available Gemini models
140
+ */
141
+ async function executeGeminiModels(geminiPath) {
142
+ try {
143
+ const { stdout, stderr } = await execFileAsync(geminiPath, ['models', 'list'], {
144
+ env: process.env,
145
+ timeout: 30000,
146
+ maxBuffer: MAX_BUFFER_SIZE,
147
+ });
148
+
149
+ // Parse model list - each line is typically a model name
150
+ const lines = stdout.trim().split('\n').filter(Boolean);
151
+ const models = lines.map((line) => {
152
+ // Try to parse structured output if available
153
+ const trimmed = line.trim();
154
+ return { name: trimmed };
155
+ });
156
+
157
+ return { success: true, models, raw: stdout };
158
+ } catch (error) {
159
+ return { success: false, error: error.message, stderr: error.stderr };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Execute raw Gemini CLI command
165
+ */
166
+ function executeGeminiRaw(geminiPath, args) {
167
+ return new Promise((resolve) => {
168
+ // Basic safety: no shell injection possible since we use spawn with array args
169
+ const proc = spawn(geminiPath, args, {
170
+ env: process.env,
171
+ stdio: ['pipe', 'pipe', 'pipe'],
172
+ });
173
+
174
+ const id = ++processIdCounter;
175
+ runningProcesses.set(id, proc);
176
+
177
+ let stdout = '';
178
+ let stderr = '';
179
+ let killed = false;
180
+
181
+ const timeout = setTimeout(() => {
182
+ killed = true;
183
+ proc.kill('SIGTERM');
184
+ }, EXECUTION_TIMEOUT_MS);
185
+
186
+ proc.stdout.on('data', (data) => {
187
+ stdout += data.toString();
188
+ });
189
+
190
+ proc.stderr.on('data', (data) => {
191
+ stderr += data.toString();
192
+ });
193
+
194
+ proc.on('close', (code) => {
195
+ clearTimeout(timeout);
196
+ runningProcesses.delete(id);
197
+
198
+ if (killed) {
199
+ resolve({ success: false, error: 'Execution timeout', stdout, stderr, code: null });
200
+ } else {
201
+ resolve({ success: code === 0, stdout, stderr, code });
202
+ }
203
+ });
204
+
205
+ proc.on('error', (error) => {
206
+ clearTimeout(timeout);
207
+ runningProcesses.delete(id);
208
+ resolve({ success: false, error: error.message, stdout, stderr, code: null });
209
+ });
210
+ });
211
+ }
212
+
213
+ /**
214
+ * Kill all running processes
215
+ */
216
+ function killAllProcesses() {
217
+ for (const [id, proc] of runningProcesses) {
218
+ try {
219
+ proc.kill('SIGTERM');
220
+ } catch {
221
+ // Process may already be dead
222
+ }
223
+ runningProcesses.delete(id);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Tool definitions
229
+ */
230
+ const TOOLS = [
231
+ {
232
+ name: 'gemini_prompt',
233
+ description: 'Send a prompt to Google Gemini CLI. Uses the locally installed and authenticated Gemini CLI.',
234
+ inputSchema: {
235
+ type: 'object',
236
+ properties: {
237
+ prompt: {
238
+ type: 'string',
239
+ description: 'The prompt to send to Gemini',
240
+ },
241
+ model: {
242
+ type: 'string',
243
+ description: 'Optional model to use (e.g., "gemini-2.5-flash"). Uses CLI default if not specified.',
244
+ },
245
+ },
246
+ required: ['prompt'],
247
+ },
248
+ },
249
+ {
250
+ name: 'gemini_models',
251
+ description: 'List available Gemini models via the CLI.',
252
+ inputSchema: {
253
+ type: 'object',
254
+ properties: {},
255
+ required: [],
256
+ },
257
+ },
258
+ {
259
+ name: 'gemini_raw',
260
+ description: 'Execute raw Gemini CLI command with arbitrary arguments. Use with caution.',
261
+ inputSchema: {
262
+ type: 'object',
263
+ properties: {
264
+ args: {
265
+ type: 'array',
266
+ items: { type: 'string' },
267
+ description: 'Array of CLI arguments to pass to gemini command',
268
+ },
269
+ },
270
+ required: ['args'],
271
+ },
272
+ },
273
+ ];
274
+
275
+ /**
276
+ * Main server entry point
277
+ */
278
+ async function main() {
279
+ // Discover Gemini CLI
280
+ const geminiPath = await discoverGeminiCli();
281
+
282
+ if (!geminiPath) {
283
+ log('FATAL: Gemini CLI not found.');
284
+ log('Install: npm install -g @google/gemini-cli');
285
+ log('Or set GEMINI_CLI_PATH=/path/to/gemini');
286
+ process.exit(1);
287
+ }
288
+
289
+ // Health check
290
+ const health = await healthCheck(geminiPath);
291
+ if (!health.ok) {
292
+ log(`FATAL: Gemini CLI health check failed: ${health.error}`);
293
+ process.exit(1);
294
+ }
295
+
296
+ log(`Gemini CLI: ${geminiPath} (${health.version})`);
297
+
298
+ // Create MCP server
299
+ const server = new Server(
300
+ { name: 'gemini-cli-mcp', version: '1.0.0' },
301
+ { capabilities: { tools: {} } }
302
+ );
303
+
304
+ // Handle tools/list
305
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
306
+ return { tools: TOOLS };
307
+ });
308
+
309
+ // Handle tools/call
310
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
311
+ const { name, arguments: args } = request.params;
312
+
313
+ try {
314
+ switch (name) {
315
+ case 'gemini_prompt': {
316
+ const prompt = args?.prompt;
317
+ const model = args?.model;
318
+
319
+ if (!prompt || typeof prompt !== 'string') {
320
+ return {
321
+ content: [{ type: 'text', text: 'Error: prompt is required and must be a string' }],
322
+ isError: true,
323
+ };
324
+ }
325
+
326
+ const result = await executeGeminiPrompt(geminiPath, prompt, model);
327
+
328
+ if (result.success) {
329
+ return {
330
+ content: [{ type: 'text', text: result.text }],
331
+ isError: false,
332
+ };
333
+ }
334
+
335
+ return {
336
+ content: [{ type: 'text', text: `Error: ${result.error}\n${result.stderr || ''}` }],
337
+ isError: true,
338
+ };
339
+ }
340
+
341
+ case 'gemini_models': {
342
+ const result = await executeGeminiModels(geminiPath);
343
+
344
+ if (result.success) {
345
+ return {
346
+ content: [{ type: 'text', text: result.raw }],
347
+ isError: false,
348
+ };
349
+ }
350
+
351
+ return {
352
+ content: [{ type: 'text', text: `Error: ${result.error}` }],
353
+ isError: true,
354
+ };
355
+ }
356
+
357
+ case 'gemini_raw': {
358
+ const rawArgs = args?.args;
359
+
360
+ if (!Array.isArray(rawArgs)) {
361
+ return {
362
+ content: [{ type: 'text', text: 'Error: args must be an array of strings' }],
363
+ isError: true,
364
+ };
365
+ }
366
+
367
+ const result = await executeGeminiRaw(geminiPath, rawArgs);
368
+
369
+ const output = [
370
+ result.stdout ? `stdout:\n${result.stdout}` : '',
371
+ result.stderr ? `stderr:\n${result.stderr}` : '',
372
+ result.code !== null ? `exit code: ${result.code}` : '',
373
+ ].filter(Boolean).join('\n\n');
374
+
375
+ return {
376
+ content: [{ type: 'text', text: output || '(no output)' }],
377
+ isError: !result.success,
378
+ };
379
+ }
380
+
381
+ default:
382
+ return {
383
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
384
+ isError: true,
385
+ };
386
+ }
387
+ } catch (error) {
388
+ return {
389
+ content: [{ type: 'text', text: `Unexpected error: ${error.message}` }],
390
+ isError: true,
391
+ };
392
+ }
393
+ });
394
+
395
+ // Signal handling for graceful shutdown
396
+ const shutdown = () => {
397
+ log('Shutting down...');
398
+ killAllProcesses();
399
+ process.exit(0);
400
+ };
401
+
402
+ process.on('SIGINT', shutdown);
403
+ process.on('SIGTERM', shutdown);
404
+
405
+ // Connect via stdio transport
406
+ const transport = new StdioServerTransport();
407
+ await server.connect(transport);
408
+ }
409
+
410
+ main().catch((error) => {
411
+ log(`Fatal error: ${error.message}`);
412
+ process.exit(1);
413
+ });