@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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./dist/index.js"
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
+ }