@danwahl/gemini-cli-mcp 0.1.4 → 0.1.6
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/README.md +5 -5
- package/dist/index.js +69 -211
- package/dist/lib.js +149 -0
- package/dist/lib.test.js +145 -0
- package/package.json +6 -7
- package/cli.js +0 -2
package/README.md
CHANGED
|
@@ -34,16 +34,16 @@ npm run build
|
|
|
34
34
|
**User install** (available across all projects):
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
claude mcp add
|
|
37
|
+
claude mcp add gemini-cli -s user -- npx -y @danwahl/gemini-cli-mcp
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
**Project install** (shared with your team via `.mcp.json`):
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
|
-
claude mcp add
|
|
43
|
+
claude mcp add gemini-cli -s project -- npx -y @danwahl/gemini-cli-mcp
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Or from source, replace `npx @danwahl/gemini-cli-mcp` with `node /absolute/path/to/gemini-cli-mcp/
|
|
46
|
+
Or from source, replace `npx -y @danwahl/gemini-cli-mcp` with `node /absolute/path/to/gemini-cli-mcp/dist/index.js`.
|
|
47
47
|
|
|
48
48
|
Verify with `claude mcp list`.
|
|
49
49
|
|
|
@@ -96,14 +96,14 @@ Gemini runs with `--approval-mode yolo`, giving it full tool access: read/write
|
|
|
96
96
|
## Development
|
|
97
97
|
|
|
98
98
|
```sh
|
|
99
|
-
npm run build # compile with
|
|
99
|
+
npm run build # compile with tsc
|
|
100
100
|
npm test # run unit tests
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
### Smoke test
|
|
104
104
|
|
|
105
105
|
```sh
|
|
106
|
-
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node
|
|
106
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node dist/index.js
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
## Design
|
package/dist/index.js
CHANGED
|
@@ -1,221 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
|
-
|
|
8
|
-
//#region src/lib.ts
|
|
9
|
-
function buildGeminiArgs(prompt, model, sessionId, outputFormat = "json") {
|
|
10
|
-
const args = [
|
|
11
|
-
"-p",
|
|
12
|
-
prompt,
|
|
13
|
-
"--output-format",
|
|
14
|
-
outputFormat,
|
|
15
|
-
"--approval-mode",
|
|
16
|
-
"yolo"
|
|
17
|
-
];
|
|
18
|
-
if (model) args.push("--model", model);
|
|
19
|
-
if (sessionId) args.push("--resume", sessionId);
|
|
20
|
-
return args;
|
|
21
|
-
}
|
|
22
|
-
function parseGeminiOutput(stdout) {
|
|
23
|
-
const trimmed = stdout.trim();
|
|
24
|
-
if (!trimmed) return {
|
|
25
|
-
sessionId: null,
|
|
26
|
-
response: ""
|
|
27
|
-
};
|
|
28
|
-
try {
|
|
29
|
-
const parsed = JSON.parse(trimmed);
|
|
30
|
-
if (parsed !== null && typeof parsed === "object" && "response" in parsed && typeof parsed.response === "string") {
|
|
31
|
-
const obj = parsed;
|
|
32
|
-
return {
|
|
33
|
-
sessionId: typeof obj.session_id === "string" ? obj.session_id : null,
|
|
34
|
-
response: obj.response,
|
|
35
|
-
stats: typeof obj.stats === "object" && obj.stats !== null ? obj.stats : void 0
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
sessionId: null,
|
|
40
|
-
response: trimmed
|
|
41
|
-
};
|
|
42
|
-
} catch {
|
|
43
|
-
return {
|
|
44
|
-
sessionId: null,
|
|
45
|
-
response: trimmed
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function extractStructuredOutput(output) {
|
|
50
|
-
const models = {};
|
|
51
|
-
const tools = {};
|
|
52
|
-
const stats = output.stats;
|
|
53
|
-
if (stats) {
|
|
54
|
-
if (stats.models && typeof stats.models === "object") {
|
|
55
|
-
for (const [name, modelStats] of Object.entries(stats.models)) if (modelStats && typeof modelStats === "object") {
|
|
56
|
-
const t = modelStats.tokens;
|
|
57
|
-
if (t && typeof t === "object" && typeof t.total === "number") models[name] = t.total;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (stats.tools && typeof stats.tools === "object") {
|
|
61
|
-
const byName = stats.tools.byName;
|
|
62
|
-
if (byName && typeof byName === "object") {
|
|
63
|
-
for (const [name, toolStats] of Object.entries(byName)) if (toolStats && typeof toolStats === "object") {
|
|
64
|
-
const count = toolStats.count;
|
|
65
|
-
if (typeof count === "number") tools[name] = count;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
sessionId: output.sessionId,
|
|
72
|
-
response: output.response,
|
|
73
|
-
models,
|
|
74
|
-
tools
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
function runGemini(prompt, cwd, model, timeoutMs, sessionId) {
|
|
78
|
-
if (!existsSync(cwd)) return Promise.resolve({
|
|
79
|
-
output: {
|
|
80
|
-
sessionId: null,
|
|
81
|
-
response: ""
|
|
82
|
-
},
|
|
83
|
-
isError: true,
|
|
84
|
-
errorMessage: `Working directory does not exist: ${cwd}`
|
|
85
|
-
});
|
|
86
|
-
return new Promise((resolve) => {
|
|
87
|
-
const args = buildGeminiArgs(prompt, model, sessionId);
|
|
88
|
-
let child;
|
|
89
|
-
try {
|
|
90
|
-
child = spawn("gemini", args, {
|
|
91
|
-
cwd,
|
|
92
|
-
env: process.env
|
|
93
|
-
});
|
|
94
|
-
} catch (err) {
|
|
95
|
-
resolve({
|
|
96
|
-
output: {
|
|
97
|
-
sessionId: null,
|
|
98
|
-
response: ""
|
|
99
|
-
},
|
|
100
|
-
isError: true,
|
|
101
|
-
errorMessage: `Failed to spawn gemini: ${String(err)}. Is gemini-cli installed? Try: npm install -g @google/gemini-cli`
|
|
102
|
-
});
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
let stdout = "";
|
|
106
|
-
let stderr = "";
|
|
107
|
-
let timedOut = false;
|
|
108
|
-
const timer = setTimeout(() => {
|
|
109
|
-
timedOut = true;
|
|
110
|
-
child.kill("SIGTERM");
|
|
111
|
-
setTimeout(() => {
|
|
112
|
-
try {
|
|
113
|
-
child.kill("SIGKILL");
|
|
114
|
-
} catch {}
|
|
115
|
-
}, 5e3);
|
|
116
|
-
}, timeoutMs);
|
|
117
|
-
child.stdout?.on("data", (chunk) => {
|
|
118
|
-
stdout += chunk.toString();
|
|
119
|
-
});
|
|
120
|
-
child.stderr?.on("data", (chunk) => {
|
|
121
|
-
stderr += chunk.toString();
|
|
122
|
-
});
|
|
123
|
-
child.on("error", (err) => {
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
const isNotFound = err.code === "ENOENT" || err.message.includes("ENOENT");
|
|
126
|
-
resolve({
|
|
127
|
-
output: {
|
|
128
|
-
sessionId: null,
|
|
129
|
-
response: ""
|
|
130
|
-
},
|
|
131
|
-
isError: true,
|
|
132
|
-
errorMessage: isNotFound ? `gemini binary not found. Install with: npm install -g @google/gemini-cli` : `Failed to spawn gemini: ${err.message}`
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
child.on("close", (code) => {
|
|
136
|
-
clearTimeout(timer);
|
|
137
|
-
if (timedOut) {
|
|
138
|
-
resolve({
|
|
139
|
-
output: {
|
|
140
|
-
sessionId: null,
|
|
141
|
-
response: ""
|
|
142
|
-
},
|
|
143
|
-
isError: true,
|
|
144
|
-
errorMessage: `gemini timed out after ${timeoutMs / 1e3}s`
|
|
145
|
-
});
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (code !== 0) {
|
|
149
|
-
const detail = stderr.trim() || stdout.trim() || `exit code ${code}`;
|
|
150
|
-
resolve({
|
|
151
|
-
output: {
|
|
152
|
-
sessionId: null,
|
|
153
|
-
response: ""
|
|
154
|
-
},
|
|
155
|
-
isError: true,
|
|
156
|
-
errorMessage: `gemini exited with code ${code}: ${detail}`
|
|
157
|
-
});
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
resolve({
|
|
161
|
-
output: parseGeminiOutput(stdout),
|
|
162
|
-
isError: false
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
//#endregion
|
|
169
|
-
//#region src/index.ts
|
|
6
|
+
import { runGemini, extractStructuredOutput } from "./lib.js";
|
|
170
7
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
171
8
|
const server = new McpServer({
|
|
172
|
-
|
|
173
|
-
|
|
9
|
+
name: "gemini-cli-mcp",
|
|
10
|
+
version,
|
|
174
11
|
});
|
|
175
12
|
server.registerTool("cli", {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
13
|
+
description: "Send a task to Gemini CLI and return the response. " +
|
|
14
|
+
"Gemini runs headlessly with full tool access (file read/write, web search, shell commands) " +
|
|
15
|
+
"and operates in the working directory you specify via `cwd`.\n\n" +
|
|
16
|
+
"When to use this tool:\n" +
|
|
17
|
+
"- Delegating rote coding tasks: boilerplate generation, repetitive refactors, bulk edits across many files\n" +
|
|
18
|
+
"- Getting a second opinion: code review, architecture feedback, sanity-checking an approach\n" +
|
|
19
|
+
"- Research and brainstorming: Gemini has web search and a large context window, useful for exploring options or summarizing docs\n" +
|
|
20
|
+
"- Large file analysis: processing files that would be expensive to handle directly\n" +
|
|
21
|
+
"- Parallel workstreams: offloading independent subtasks while you continue other work\n\n" +
|
|
22
|
+
"Session resumption: each response includes a `sessionId`. " +
|
|
23
|
+
"Pass it back via the `sessionId` parameter to continue a conversation " +
|
|
24
|
+
"without re-sending context — useful for multi-step tasks or follow-up questions.\n\n" +
|
|
25
|
+
"Keep prompts self-contained: include all necessary context in the `prompt` since " +
|
|
26
|
+
"Gemini has no access to your conversation history or MCP state.",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
prompt: z
|
|
29
|
+
.string()
|
|
30
|
+
.describe("The task or question to send to Gemini CLI"),
|
|
31
|
+
cwd: z
|
|
32
|
+
.string()
|
|
33
|
+
.describe("Absolute path to the working directory for Gemini to operate in"),
|
|
34
|
+
model: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Model to use. Omit to use Gemini CLI's default (auto). " +
|
|
38
|
+
"Official aliases: \"auto\" (default routing), \"pro\" (complex reasoning), " +
|
|
39
|
+
"\"flash\" (fast, balanced), \"flash-lite\" (fastest). " +
|
|
40
|
+
"Or pass a concrete model name like \"gemini-2.5-pro\"."),
|
|
41
|
+
sessionId: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Resume a previous Gemini session by ID. The session ID is returned in the structured output of each call."),
|
|
45
|
+
},
|
|
46
|
+
outputSchema: {
|
|
47
|
+
sessionId: z.string().nullable().describe("Gemini CLI session ID"),
|
|
48
|
+
response: z.string().describe("Gemini's text response"),
|
|
49
|
+
models: z.record(z.string(), z.number()).describe("Model name → total tokens used"),
|
|
50
|
+
tools: z.record(z.string(), z.number()).describe("Tool name → call count"),
|
|
51
|
+
},
|
|
52
|
+
annotations: {
|
|
53
|
+
readOnlyHint: false,
|
|
54
|
+
openWorldHint: true,
|
|
55
|
+
},
|
|
193
56
|
}, async ({ prompt, cwd, model, sessionId }) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
};
|
|
57
|
+
const timeoutMs = 120_000;
|
|
58
|
+
const result = await runGemini(prompt, cwd, model, timeoutMs, sessionId);
|
|
59
|
+
if (result.isError) {
|
|
60
|
+
return {
|
|
61
|
+
isError: true,
|
|
62
|
+
content: [{ type: "text", text: result.errorMessage ?? "Unknown error" }],
|
|
63
|
+
structuredContent: { sessionId: null, response: "", models: {}, tools: {} },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const structured = extractStructuredOutput(result.output);
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: structured.response }],
|
|
69
|
+
structuredContent: structured,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
async function runServer() {
|
|
73
|
+
const transport = new StdioServerTransport();
|
|
74
|
+
await server.connect(transport);
|
|
75
|
+
}
|
|
76
|
+
runServer().catch((error) => {
|
|
77
|
+
console.error("Fatal error:", error);
|
|
78
|
+
process.exit(1);
|
|
217
79
|
});
|
|
218
|
-
const transport = new StdioServerTransport();
|
|
219
|
-
await server.connect(transport);
|
|
220
|
-
|
|
221
|
-
//#endregion
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
export function buildGeminiArgs(prompt, model, sessionId, outputFormat = "json") {
|
|
4
|
+
const args = ["-p", prompt, "--output-format", outputFormat, "--approval-mode", "yolo"];
|
|
5
|
+
if (model) {
|
|
6
|
+
args.push("--model", model);
|
|
7
|
+
}
|
|
8
|
+
if (sessionId) {
|
|
9
|
+
args.push("--resume", sessionId);
|
|
10
|
+
}
|
|
11
|
+
return args;
|
|
12
|
+
}
|
|
13
|
+
export function parseGeminiOutput(stdout) {
|
|
14
|
+
const trimmed = stdout.trim();
|
|
15
|
+
if (!trimmed) {
|
|
16
|
+
return { sessionId: null, response: "" };
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(trimmed);
|
|
20
|
+
if (parsed !== null &&
|
|
21
|
+
typeof parsed === "object" &&
|
|
22
|
+
"response" in parsed &&
|
|
23
|
+
typeof parsed.response === "string") {
|
|
24
|
+
const obj = parsed;
|
|
25
|
+
return {
|
|
26
|
+
sessionId: typeof obj.session_id === "string" ? obj.session_id : null,
|
|
27
|
+
response: obj.response,
|
|
28
|
+
stats: typeof obj.stats === "object" && obj.stats !== null
|
|
29
|
+
? obj.stats
|
|
30
|
+
: undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { sessionId: null, response: trimmed };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { sessionId: null, response: trimmed };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function extractStructuredOutput(output) {
|
|
40
|
+
const models = {};
|
|
41
|
+
const tools = {};
|
|
42
|
+
const stats = output.stats;
|
|
43
|
+
if (stats) {
|
|
44
|
+
if (stats.models && typeof stats.models === "object") {
|
|
45
|
+
for (const [name, modelStats] of Object.entries(stats.models)) {
|
|
46
|
+
if (modelStats && typeof modelStats === "object") {
|
|
47
|
+
const t = modelStats.tokens;
|
|
48
|
+
if (t && typeof t === "object" && typeof t.total === "number") {
|
|
49
|
+
models[name] = t.total;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (stats.tools && typeof stats.tools === "object") {
|
|
55
|
+
const byName = stats.tools.byName;
|
|
56
|
+
if (byName && typeof byName === "object") {
|
|
57
|
+
for (const [name, toolStats] of Object.entries(byName)) {
|
|
58
|
+
if (toolStats && typeof toolStats === "object") {
|
|
59
|
+
const count = toolStats.count;
|
|
60
|
+
if (typeof count === "number")
|
|
61
|
+
tools[name] = count;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { sessionId: output.sessionId, response: output.response, models, tools };
|
|
68
|
+
}
|
|
69
|
+
export function runGemini(prompt, cwd, model, timeoutMs, sessionId) {
|
|
70
|
+
if (!existsSync(cwd)) {
|
|
71
|
+
return Promise.resolve({
|
|
72
|
+
output: { sessionId: null, response: "" },
|
|
73
|
+
isError: true,
|
|
74
|
+
errorMessage: `Working directory does not exist: ${cwd}`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const args = buildGeminiArgs(prompt, model, sessionId);
|
|
79
|
+
let child;
|
|
80
|
+
try {
|
|
81
|
+
child = spawn("gemini", args, { cwd, env: process.env });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
resolve({
|
|
85
|
+
output: { sessionId: null, response: "" },
|
|
86
|
+
isError: true,
|
|
87
|
+
errorMessage: `Failed to spawn gemini: ${String(err)}. Is gemini-cli installed? Try: npm install -g @google/gemini-cli`,
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let stdout = "";
|
|
92
|
+
let stderr = "";
|
|
93
|
+
let timedOut = false;
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
timedOut = true;
|
|
96
|
+
child.kill("SIGTERM");
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
try {
|
|
99
|
+
child.kill("SIGKILL");
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// already dead
|
|
103
|
+
}
|
|
104
|
+
}, 5000);
|
|
105
|
+
}, timeoutMs);
|
|
106
|
+
child.stdout?.on("data", (chunk) => {
|
|
107
|
+
stdout += chunk.toString();
|
|
108
|
+
});
|
|
109
|
+
child.stderr?.on("data", (chunk) => {
|
|
110
|
+
stderr += chunk.toString();
|
|
111
|
+
});
|
|
112
|
+
child.on("error", (err) => {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
const isNotFound = err.code === "ENOENT" ||
|
|
115
|
+
err.message.includes("ENOENT");
|
|
116
|
+
resolve({
|
|
117
|
+
output: { sessionId: null, response: "" },
|
|
118
|
+
isError: true,
|
|
119
|
+
errorMessage: isNotFound
|
|
120
|
+
? `gemini binary not found. Install with: npm install -g @google/gemini-cli`
|
|
121
|
+
: `Failed to spawn gemini: ${err.message}`,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
child.on("close", (code) => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
if (timedOut) {
|
|
127
|
+
resolve({
|
|
128
|
+
output: { sessionId: null, response: "" },
|
|
129
|
+
isError: true,
|
|
130
|
+
errorMessage: `gemini timed out after ${timeoutMs / 1000}s`,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (code !== 0) {
|
|
135
|
+
const detail = stderr.trim() || stdout.trim() || `exit code ${code}`;
|
|
136
|
+
resolve({
|
|
137
|
+
output: { sessionId: null, response: "" },
|
|
138
|
+
isError: true,
|
|
139
|
+
errorMessage: `gemini exited with code ${code}: ${detail}`,
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
resolve({
|
|
144
|
+
output: parseGeminiOutput(stdout),
|
|
145
|
+
isError: false,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
package/dist/lib.test.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { buildGeminiArgs, parseGeminiOutput, extractStructuredOutput, } from "./lib.ts";
|
|
4
|
+
describe("buildGeminiArgs", () => {
|
|
5
|
+
it("omits --model when not provided", () => {
|
|
6
|
+
const args = buildGeminiArgs("hello", undefined);
|
|
7
|
+
assert.ok(!args.includes("--model"));
|
|
8
|
+
});
|
|
9
|
+
it("includes --model when provided", () => {
|
|
10
|
+
const args = buildGeminiArgs("hello", "gemini-2.5-pro");
|
|
11
|
+
const idx = args.indexOf("--model");
|
|
12
|
+
assert.ok(idx !== -1);
|
|
13
|
+
assert.equal(args[idx + 1], "gemini-2.5-pro");
|
|
14
|
+
});
|
|
15
|
+
it("passes model string through unchanged", () => {
|
|
16
|
+
const args = buildGeminiArgs("hello", "gemini-1.5-flash-001");
|
|
17
|
+
assert.equal(args[args.indexOf("--model") + 1], "gemini-1.5-flash-001");
|
|
18
|
+
});
|
|
19
|
+
it("includes --resume when sessionId provided", () => {
|
|
20
|
+
const args = buildGeminiArgs("x", undefined, "my-session-id");
|
|
21
|
+
const idx = args.indexOf("--resume");
|
|
22
|
+
assert.ok(idx !== -1);
|
|
23
|
+
assert.equal(args[idx + 1], "my-session-id");
|
|
24
|
+
});
|
|
25
|
+
it("omits --resume when sessionId not provided", () => {
|
|
26
|
+
const args = buildGeminiArgs("x", undefined);
|
|
27
|
+
assert.ok(!args.includes("--resume"));
|
|
28
|
+
});
|
|
29
|
+
it("includes --approval-mode yolo", () => {
|
|
30
|
+
const args = buildGeminiArgs("x", undefined);
|
|
31
|
+
const idx = args.indexOf("--approval-mode");
|
|
32
|
+
assert.ok(idx !== -1);
|
|
33
|
+
assert.equal(args[idx + 1], "yolo");
|
|
34
|
+
});
|
|
35
|
+
it("includes --output-format json", () => {
|
|
36
|
+
const args = buildGeminiArgs("x", undefined);
|
|
37
|
+
const idx = args.indexOf("--output-format");
|
|
38
|
+
assert.ok(idx !== -1);
|
|
39
|
+
assert.equal(args[idx + 1], "json");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("parseGeminiOutput", () => {
|
|
43
|
+
it("parses valid JSON response", () => {
|
|
44
|
+
const result = parseGeminiOutput(JSON.stringify({ response: "Hello, world!" }));
|
|
45
|
+
assert.equal(result.response, "Hello, world!");
|
|
46
|
+
assert.equal(result.sessionId, null);
|
|
47
|
+
assert.equal(result.stats, undefined);
|
|
48
|
+
});
|
|
49
|
+
it("extracts session_id", () => {
|
|
50
|
+
const result = parseGeminiOutput(JSON.stringify({ session_id: "abc-123", response: "ok" }));
|
|
51
|
+
assert.equal(result.sessionId, "abc-123");
|
|
52
|
+
});
|
|
53
|
+
it("extracts stats field", () => {
|
|
54
|
+
const stats = { models: {}, tools: {} };
|
|
55
|
+
const result = parseGeminiOutput(JSON.stringify({ response: "ok", stats }));
|
|
56
|
+
assert.deepEqual(result.stats, stats);
|
|
57
|
+
});
|
|
58
|
+
it("handles missing stats gracefully", () => {
|
|
59
|
+
const result = parseGeminiOutput(JSON.stringify({ response: "ok" }));
|
|
60
|
+
assert.equal(result.stats, undefined);
|
|
61
|
+
});
|
|
62
|
+
it("returns raw stdout when JSON parsing fails", () => {
|
|
63
|
+
const raw = "this is not json";
|
|
64
|
+
const result = parseGeminiOutput(raw);
|
|
65
|
+
assert.equal(result.response, raw);
|
|
66
|
+
assert.equal(result.sessionId, null);
|
|
67
|
+
});
|
|
68
|
+
it("returns raw stdout for JSON without response field", () => {
|
|
69
|
+
const raw = JSON.stringify({ message: "unexpected shape" });
|
|
70
|
+
const result = parseGeminiOutput(raw);
|
|
71
|
+
assert.equal(result.response, raw);
|
|
72
|
+
});
|
|
73
|
+
it("handles empty stdout", () => {
|
|
74
|
+
const result = parseGeminiOutput("");
|
|
75
|
+
assert.equal(result.response, "");
|
|
76
|
+
});
|
|
77
|
+
it("trims surrounding whitespace before parsing", () => {
|
|
78
|
+
const result = parseGeminiOutput(" " + JSON.stringify({ response: "trimmed" }) + "\n");
|
|
79
|
+
assert.equal(result.response, "trimmed");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe("extractStructuredOutput", () => {
|
|
83
|
+
it("passes through sessionId and response", () => {
|
|
84
|
+
const output = { sessionId: "abc", response: "hello" };
|
|
85
|
+
const result = extractStructuredOutput(output);
|
|
86
|
+
assert.equal(result.sessionId, "abc");
|
|
87
|
+
assert.equal(result.response, "hello");
|
|
88
|
+
});
|
|
89
|
+
it("returns empty records when no stats", () => {
|
|
90
|
+
const output = { sessionId: null, response: "hi" };
|
|
91
|
+
const result = extractStructuredOutput(output);
|
|
92
|
+
assert.deepEqual(result.models, {});
|
|
93
|
+
assert.deepEqual(result.tools, {});
|
|
94
|
+
});
|
|
95
|
+
it("extracts model token totals", () => {
|
|
96
|
+
const output = {
|
|
97
|
+
sessionId: null,
|
|
98
|
+
response: "hi",
|
|
99
|
+
stats: {
|
|
100
|
+
models: {
|
|
101
|
+
"gemini-2.5-pro": { tokens: { total: 100 } },
|
|
102
|
+
"gemini-2.0-flash": { tokens: { total: 50 } },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
assert.deepEqual(extractStructuredOutput(output).models, {
|
|
107
|
+
"gemini-2.5-pro": 100,
|
|
108
|
+
"gemini-2.0-flash": 50,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
it("omits models with no token data", () => {
|
|
112
|
+
const output = {
|
|
113
|
+
sessionId: null,
|
|
114
|
+
response: "hi",
|
|
115
|
+
stats: { models: { "gemini-2.5-pro": {} } },
|
|
116
|
+
};
|
|
117
|
+
assert.deepEqual(extractStructuredOutput(output).models, {});
|
|
118
|
+
});
|
|
119
|
+
it("extracts tool call counts from byName", () => {
|
|
120
|
+
const output = {
|
|
121
|
+
sessionId: null,
|
|
122
|
+
response: "hi",
|
|
123
|
+
stats: {
|
|
124
|
+
tools: {
|
|
125
|
+
byName: {
|
|
126
|
+
list_directory: { count: 2 },
|
|
127
|
+
web_fetch: { count: 1 },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
assert.deepEqual(extractStructuredOutput(output).tools, {
|
|
133
|
+
list_directory: 2,
|
|
134
|
+
web_fetch: 1,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
it("returns empty tools when no tool calls", () => {
|
|
138
|
+
const output = {
|
|
139
|
+
sessionId: null,
|
|
140
|
+
response: "hi",
|
|
141
|
+
stats: { tools: { totalCalls: 0, byName: {} } },
|
|
142
|
+
};
|
|
143
|
+
assert.deepEqual(extractStructuredOutput(output).tools, {});
|
|
144
|
+
});
|
|
145
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danwahl/gemini-cli-mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6",
|
|
5
5
|
"description": "MCP server that exposes Gemini CLI as a single tool for Claude Code",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -10,15 +10,14 @@
|
|
|
10
10
|
},
|
|
11
11
|
"keywords": ["mcp", "gemini", "claude", "llm", "ai"],
|
|
12
12
|
"engines": { "node": ">=18" },
|
|
13
|
-
"bin": { "gemini-cli-mcp": "
|
|
13
|
+
"bin": { "gemini-cli-mcp": "dist/index.js" },
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "
|
|
16
|
-
"test": "node --import tsx/esm --test src/
|
|
15
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
16
|
+
"test": "node --import tsx/esm --test src/lib.test.ts",
|
|
17
17
|
"prepublishOnly": "npm run build"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
21
|
-
"cli.js"
|
|
20
|
+
"dist"
|
|
22
21
|
],
|
|
23
22
|
"dependencies": {
|
|
24
23
|
"@modelcontextprotocol/sdk": "^1.21.0",
|
|
@@ -26,7 +25,7 @@
|
|
|
26
25
|
},
|
|
27
26
|
"devDependencies": {
|
|
28
27
|
"@types/node": "^25.3.3",
|
|
29
|
-
"
|
|
28
|
+
"shx": "^0.3.4",
|
|
30
29
|
"tsx": "^4.21.0",
|
|
31
30
|
"typescript": "^5.8.0"
|
|
32
31
|
}
|
package/cli.js
DELETED