@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.
- package/LICENSE +674 -0
- package/README.md +273 -0
- package/package.json +48 -0
- 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
|
+
});
|