@danwahl/gemini-cli-mcp 0.1.3 → 0.1.5
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 +1 -1
- package/dist/index.js +59 -207
- package/dist/lib.js +149 -0
- package/dist/lib.test.js +145 -0
- package/package.json +6 -7
- package/cli.js +0 -4
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ npm test # run unit tests
|
|
|
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,216 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
|
|
7
|
-
//#region src/index.ts
|
|
6
|
+
import { runGemini, extractStructuredOutput } from "./lib.js";
|
|
8
7
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
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
8
|
const server = new McpServer({
|
|
168
|
-
|
|
169
|
-
|
|
9
|
+
name: "gemini-cli-mcp",
|
|
10
|
+
version,
|
|
170
11
|
});
|
|
171
12
|
server.registerTool("cli", {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
13
|
+
description: "Send a task or question to Gemini CLI and return the response. " +
|
|
14
|
+
"Gemini runs headlessly with full tool access (file read/write, web search, shell commands). " +
|
|
15
|
+
"Use this to delegate tasks that benefit from Gemini's capabilities or to get a second opinion.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
prompt: z
|
|
18
|
+
.string()
|
|
19
|
+
.describe("The task or question to send to Gemini CLI"),
|
|
20
|
+
cwd: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe("Absolute path to the working directory for Gemini to operate in"),
|
|
23
|
+
model: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Model to use. Omit to use Gemini CLI's default (auto). " +
|
|
27
|
+
"Official aliases: \"auto\" (default routing), \"pro\" (complex reasoning), " +
|
|
28
|
+
"\"flash\" (fast, balanced), \"flash-lite\" (fastest). " +
|
|
29
|
+
"Or pass a concrete model name like \"gemini-2.5-pro\"."),
|
|
30
|
+
sessionId: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Resume a previous Gemini session by ID. The session ID is returned in the structured output of each call."),
|
|
34
|
+
},
|
|
35
|
+
outputSchema: {
|
|
36
|
+
sessionId: z.string().nullable().describe("Gemini CLI session ID"),
|
|
37
|
+
response: z.string().describe("Gemini's text response"),
|
|
38
|
+
models: z.record(z.string(), z.number()).describe("Model name → total tokens used"),
|
|
39
|
+
tools: z.record(z.string(), z.number()).describe("Tool name → call count"),
|
|
40
|
+
},
|
|
41
|
+
annotations: {
|
|
42
|
+
readOnlyHint: false,
|
|
43
|
+
openWorldHint: true,
|
|
44
|
+
},
|
|
189
45
|
}, async ({ prompt, cwd, model, sessionId }) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
};
|
|
46
|
+
const timeoutMs = 120_000;
|
|
47
|
+
const result = await runGemini(prompt, cwd, model, timeoutMs, sessionId);
|
|
48
|
+
if (result.isError) {
|
|
49
|
+
return {
|
|
50
|
+
isError: true,
|
|
51
|
+
content: [{ type: "text", text: result.errorMessage ?? "Unknown error" }],
|
|
52
|
+
structuredContent: { sessionId: null, response: "", models: {}, tools: {} },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const structured = extractStructuredOutput(result.output);
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: structured.response }],
|
|
58
|
+
structuredContent: structured,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
async function runServer() {
|
|
62
|
+
const transport = new StdioServerTransport();
|
|
63
|
+
await server.connect(transport);
|
|
64
|
+
}
|
|
65
|
+
runServer().catch((error) => {
|
|
66
|
+
console.error("Fatal error:", error);
|
|
67
|
+
process.exit(1);
|
|
213
68
|
});
|
|
214
|
-
|
|
215
|
-
//#endregion
|
|
216
|
-
export { buildGeminiArgs, extractStructuredOutput, parseGeminiOutput, runGemini, server };
|
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.5",
|
|
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