@danwahl/gemini-cli-mcp 0.1.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 +21 -0
- package/README.md +111 -0
- package/cli.js +2 -0
- package/dist/index.js +214 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dan Wahl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# gemini-cli-mcp
|
|
2
|
+
|
|
3
|
+
A minimal MCP server that exposes [Gemini CLI](https://github.com/google-gemini/gemini-cli) as a single tool callable from Claude Code (or any MCP client).
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
Claude Code sends prompts to this server via MCP. The server spawns `gemini -p "..."` in headless mode and returns the response. Gemini inherits your Google OAuth session — no API key required.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Claude Code ──MCP/stdio──▶ gemini-cli-mcp ──spawn──▶ gemini -p "..." --output-format json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed and authenticated:
|
|
16
|
+
```sh
|
|
17
|
+
npm install -g @google/gemini-cli
|
|
18
|
+
gemini # complete the OAuth login flow
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### From source
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
git clone https://github.com/danwahl/gemini-cli-mcp
|
|
27
|
+
cd gemini-cli-mcp
|
|
28
|
+
npm install
|
|
29
|
+
npm run build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
If installed via npm:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
claude mcp add gemini npx @danwahl/gemini-cli-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or from source:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
claude mcp add gemini node /absolute/path/to/gemini-cli-mcp/cli.js
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Verify with `claude mcp list`.
|
|
47
|
+
|
|
48
|
+
## Tool: `cli`
|
|
49
|
+
|
|
50
|
+
| Parameter | Type | Required | Description |
|
|
51
|
+
|-------------|--------|----------|-------------|
|
|
52
|
+
| `prompt` | string | yes | Task or question to send to Gemini |
|
|
53
|
+
| `cwd` | string | yes | Absolute path to working directory |
|
|
54
|
+
| `model` | string | no | Model name or alias (see below). Omit to use Gemini CLI's default (`auto`). |
|
|
55
|
+
| `sessionId` | string | no | Resume a previous session. The session ID is returned in the structured output of each call. |
|
|
56
|
+
|
|
57
|
+
### Structured output
|
|
58
|
+
|
|
59
|
+
Each call returns structured content alongside the text response:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"sessionId": "e80096bd-...",
|
|
64
|
+
"response": "Gemini's answer...",
|
|
65
|
+
"models": {
|
|
66
|
+
"gemini-2.5-flash-lite": 1399,
|
|
67
|
+
"gemini-3-flash-preview": 18635
|
|
68
|
+
},
|
|
69
|
+
"tools": {
|
|
70
|
+
"list_directory": 2
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`models` maps model name → total tokens used. `tools` maps tool name → call count (only present when Gemini used tools).
|
|
76
|
+
|
|
77
|
+
### Model aliases
|
|
78
|
+
|
|
79
|
+
These are passed directly to the CLI, which resolves them:
|
|
80
|
+
|
|
81
|
+
| Alias | Description |
|
|
82
|
+
|--------------|-------------|
|
|
83
|
+
| `auto` | Default routing (pro or preview depending on settings) |
|
|
84
|
+
| `pro` | Complex reasoning tasks |
|
|
85
|
+
| `flash` | Fast, balanced — good for most tasks |
|
|
86
|
+
| `flash-lite` | Fastest, for simple tasks |
|
|
87
|
+
|
|
88
|
+
Or pass any concrete model name like `"gemini-2.5-pro"`.
|
|
89
|
+
|
|
90
|
+
### What Gemini can do
|
|
91
|
+
|
|
92
|
+
Gemini runs with `--approval-mode yolo`, giving it full tool access: read/write files, run shell commands, web search, and more. It operates in the `cwd` you specify.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
npm run build # compile with tsdown
|
|
98
|
+
npm test # run unit tests
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Smoke test
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node cli.js
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Design
|
|
108
|
+
|
|
109
|
+
One tool, no prompt wrappers. Claude Code is the orchestrator — it decides what to ask Gemini and how to phrase it. This server is a thin, reliable pipe between MCP and `gemini -p`.
|
|
110
|
+
|
|
111
|
+
See [CLAUDE.md](./CLAUDE.md) for project conventions.
|
package/cli.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region src/index.ts
|
|
8
|
+
function buildGeminiArgs(prompt, model, sessionId, outputFormat = "json") {
|
|
9
|
+
const args = [
|
|
10
|
+
"-p",
|
|
11
|
+
prompt,
|
|
12
|
+
"--output-format",
|
|
13
|
+
outputFormat,
|
|
14
|
+
"--approval-mode",
|
|
15
|
+
"yolo"
|
|
16
|
+
];
|
|
17
|
+
if (model) args.push("--model", model);
|
|
18
|
+
if (sessionId) args.push("--resume", sessionId);
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
function parseGeminiOutput(stdout) {
|
|
22
|
+
const trimmed = stdout.trim();
|
|
23
|
+
if (!trimmed) return {
|
|
24
|
+
sessionId: null,
|
|
25
|
+
response: ""
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(trimmed);
|
|
29
|
+
if (parsed !== null && typeof parsed === "object" && "response" in parsed && typeof parsed.response === "string") {
|
|
30
|
+
const obj = parsed;
|
|
31
|
+
return {
|
|
32
|
+
sessionId: typeof obj.session_id === "string" ? obj.session_id : null,
|
|
33
|
+
response: obj.response,
|
|
34
|
+
stats: typeof obj.stats === "object" && obj.stats !== null ? obj.stats : void 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
sessionId: null,
|
|
39
|
+
response: trimmed
|
|
40
|
+
};
|
|
41
|
+
} catch {
|
|
42
|
+
return {
|
|
43
|
+
sessionId: null,
|
|
44
|
+
response: trimmed
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function extractStructuredOutput(output) {
|
|
49
|
+
const models = {};
|
|
50
|
+
const tools = {};
|
|
51
|
+
const stats = output.stats;
|
|
52
|
+
if (stats) {
|
|
53
|
+
if (stats.models && typeof stats.models === "object") {
|
|
54
|
+
for (const [name, modelStats] of Object.entries(stats.models)) if (modelStats && typeof modelStats === "object") {
|
|
55
|
+
const t = modelStats.tokens;
|
|
56
|
+
if (t && typeof t === "object" && typeof t.total === "number") models[name] = t.total;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (stats.tools && typeof stats.tools === "object") {
|
|
60
|
+
const byName = stats.tools.byName;
|
|
61
|
+
if (byName && typeof byName === "object") {
|
|
62
|
+
for (const [name, toolStats] of Object.entries(byName)) if (toolStats && typeof toolStats === "object") {
|
|
63
|
+
const count = toolStats.count;
|
|
64
|
+
if (typeof count === "number") tools[name] = count;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
sessionId: output.sessionId,
|
|
71
|
+
response: output.response,
|
|
72
|
+
models,
|
|
73
|
+
tools
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function runGemini(prompt, cwd, model, timeoutMs, sessionId) {
|
|
77
|
+
if (!existsSync(cwd)) return Promise.resolve({
|
|
78
|
+
output: {
|
|
79
|
+
sessionId: null,
|
|
80
|
+
response: ""
|
|
81
|
+
},
|
|
82
|
+
isError: true,
|
|
83
|
+
errorMessage: `Working directory does not exist: ${cwd}`
|
|
84
|
+
});
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const args = buildGeminiArgs(prompt, model, sessionId);
|
|
87
|
+
let child;
|
|
88
|
+
try {
|
|
89
|
+
child = spawn("gemini", args, {
|
|
90
|
+
cwd,
|
|
91
|
+
env: process.env
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
resolve({
|
|
95
|
+
output: {
|
|
96
|
+
sessionId: null,
|
|
97
|
+
response: ""
|
|
98
|
+
},
|
|
99
|
+
isError: true,
|
|
100
|
+
errorMessage: `Failed to spawn gemini: ${String(err)}. Is gemini-cli installed? Try: npm install -g @google/gemini-cli`
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let stdout = "";
|
|
105
|
+
let stderr = "";
|
|
106
|
+
let timedOut = false;
|
|
107
|
+
const timer = setTimeout(() => {
|
|
108
|
+
timedOut = true;
|
|
109
|
+
child.kill("SIGTERM");
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
try {
|
|
112
|
+
child.kill("SIGKILL");
|
|
113
|
+
} catch {}
|
|
114
|
+
}, 5e3);
|
|
115
|
+
}, timeoutMs);
|
|
116
|
+
child.stdout?.on("data", (chunk) => {
|
|
117
|
+
stdout += chunk.toString();
|
|
118
|
+
});
|
|
119
|
+
child.stderr?.on("data", (chunk) => {
|
|
120
|
+
stderr += chunk.toString();
|
|
121
|
+
});
|
|
122
|
+
child.on("error", (err) => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
const isNotFound = err.code === "ENOENT" || err.message.includes("ENOENT");
|
|
125
|
+
resolve({
|
|
126
|
+
output: {
|
|
127
|
+
sessionId: null,
|
|
128
|
+
response: ""
|
|
129
|
+
},
|
|
130
|
+
isError: true,
|
|
131
|
+
errorMessage: isNotFound ? `gemini binary not found. Install with: npm install -g @google/gemini-cli` : `Failed to spawn gemini: ${err.message}`
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
child.on("close", (code) => {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
if (timedOut) {
|
|
137
|
+
resolve({
|
|
138
|
+
output: {
|
|
139
|
+
sessionId: null,
|
|
140
|
+
response: ""
|
|
141
|
+
},
|
|
142
|
+
isError: true,
|
|
143
|
+
errorMessage: `gemini timed out after ${timeoutMs / 1e3}s`
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (code !== 0) {
|
|
148
|
+
const detail = stderr.trim() || stdout.trim() || `exit code ${code}`;
|
|
149
|
+
resolve({
|
|
150
|
+
output: {
|
|
151
|
+
sessionId: null,
|
|
152
|
+
response: ""
|
|
153
|
+
},
|
|
154
|
+
isError: true,
|
|
155
|
+
errorMessage: `gemini exited with code ${code}: ${detail}`
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
resolve({
|
|
160
|
+
output: parseGeminiOutput(stdout),
|
|
161
|
+
isError: false
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const server = new McpServer({
|
|
167
|
+
name: "gemini-cli-mcp",
|
|
168
|
+
version: "0.1.0"
|
|
169
|
+
});
|
|
170
|
+
server.registerTool("cli", {
|
|
171
|
+
description: "Send a task or question to Gemini CLI and return the response. Gemini runs headlessly with full tool access (file read/write, web search, shell commands). Use this to delegate tasks that benefit from Gemini's capabilities or to get a second opinion.",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
prompt: z.string().describe("The task or question to send to Gemini CLI"),
|
|
174
|
+
cwd: z.string().describe("Absolute path to the working directory for Gemini to operate in"),
|
|
175
|
+
model: z.string().optional().describe("Model to use. Omit to use Gemini CLI's default (auto). Official aliases: \"auto\" (default routing), \"pro\" (complex reasoning), \"flash\" (fast, balanced), \"flash-lite\" (fastest). Or pass a concrete model name like \"gemini-2.5-pro\"."),
|
|
176
|
+
sessionId: z.string().optional().describe("Resume a previous Gemini session by ID. The session ID is returned in the structured output of each call.")
|
|
177
|
+
},
|
|
178
|
+
outputSchema: {
|
|
179
|
+
sessionId: z.string().nullable().describe("Gemini CLI session ID"),
|
|
180
|
+
response: z.string().describe("Gemini's text response"),
|
|
181
|
+
models: z.record(z.string(), z.number()).describe("Model name → total tokens used"),
|
|
182
|
+
tools: z.record(z.string(), z.number()).describe("Tool name → call count")
|
|
183
|
+
},
|
|
184
|
+
annotations: {
|
|
185
|
+
readOnlyHint: false,
|
|
186
|
+
openWorldHint: true
|
|
187
|
+
}
|
|
188
|
+
}, async ({ prompt, cwd, model, sessionId }) => {
|
|
189
|
+
const timeoutMs = 12e4;
|
|
190
|
+
const result = await runGemini(prompt, cwd, model, timeoutMs, sessionId);
|
|
191
|
+
if (result.isError) return {
|
|
192
|
+
isError: true,
|
|
193
|
+
content: [{
|
|
194
|
+
type: "text",
|
|
195
|
+
text: result.errorMessage ?? "Unknown error"
|
|
196
|
+
}]
|
|
197
|
+
};
|
|
198
|
+
const structured = extractStructuredOutput(result.output);
|
|
199
|
+
return {
|
|
200
|
+
content: [{
|
|
201
|
+
type: "text",
|
|
202
|
+
text: structured.response
|
|
203
|
+
}],
|
|
204
|
+
structuredContent: structured
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
const isMain = process.argv[1] !== void 0 && (import.meta.url === `file://${process.argv[1]}` || process.argv[1].endsWith("cli.js") || process.argv[1].endsWith("dist/index.js"));
|
|
208
|
+
if (isMain) {
|
|
209
|
+
const transport = new StdioServerTransport();
|
|
210
|
+
await server.connect(transport);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//#endregion
|
|
214
|
+
export { buildGeminiArgs, extractStructuredOutput, parseGeminiOutput, runGemini };
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@danwahl/gemini-cli-mcp",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "MCP server that exposes Gemini CLI as a single tool for Claude Code",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/danwahl/gemini-cli-mcp.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["mcp", "gemini", "claude", "llm", "ai"],
|
|
12
|
+
"engines": { "node": ">=18" },
|
|
13
|
+
"bin": "./cli.js",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsdown src/index.ts",
|
|
16
|
+
"test": "node --import tsx/esm --test src/index.test.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"cli.js"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.21.0",
|
|
25
|
+
"zod": "^3.24.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^25.3.3",
|
|
29
|
+
"tsdown": "^0.10.0",
|
|
30
|
+
"tsx": "^4.21.0",
|
|
31
|
+
"typescript": "^5.8.0"
|
|
32
|
+
}
|
|
33
|
+
}
|