@brenoxp/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 +21 -0
- package/README.md +104 -0
- package/dist/__tests__/frontmatter.test.d.ts +1 -0
- package/dist/__tests__/frontmatter.test.js +82 -0
- package/dist/__tests__/gemini-executor.test.d.ts +1 -0
- package/dist/__tests__/gemini-executor.test.js +131 -0
- package/dist/__tests__/gemini-with-agent-md.test.d.ts +1 -0
- package/dist/__tests__/gemini-with-agent-md.test.js +85 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +23 -0
- package/dist/tools/gemini-with-agent-md.d.ts +9 -0
- package/dist/tools/gemini-with-agent-md.js +46 -0
- package/dist/tools/gemini.d.ts +8 -0
- package/dist/tools/gemini.js +37 -0
- package/dist/utils/frontmatter.d.ts +2 -0
- package/dist/utils/frontmatter.js +44 -0
- package/dist/utils/gemini-executor.d.ts +9 -0
- package/dist/utils/gemini-executor.js +45 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Breno Pinto
|
|
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,104 @@
|
|
|
1
|
+
# gemini-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that wraps [Gemini CLI](https://github.com/google-gemini/gemini-cli) for use with Claude Code and other MCP clients.
|
|
4
|
+
|
|
5
|
+
Use Gemini's large context window (1M tokens) and web search from within Claude Code.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed and authenticated
|
|
10
|
+
- Node.js 18+ or Bun
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
### Via npm (recommended)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @brenoxp/gemini-mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### From source
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/brenoxp/gemini-mcp.git
|
|
24
|
+
cd gemini-mcp
|
|
25
|
+
npm install
|
|
26
|
+
npm run build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage with Claude Code
|
|
30
|
+
|
|
31
|
+
Add to your Claude Code MCP configuration:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# If installed globally via npm
|
|
35
|
+
claude mcp add gemini-mcp -- gemini-mcp
|
|
36
|
+
|
|
37
|
+
# Or run directly with npx
|
|
38
|
+
claude mcp add gemini-mcp -- npx @brenoxp/gemini-mcp
|
|
39
|
+
|
|
40
|
+
# Or from source
|
|
41
|
+
claude mcp add gemini-mcp -- node /path/to/gemini-mcp/dist/index.js
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Tools
|
|
45
|
+
|
|
46
|
+
### gemini
|
|
47
|
+
|
|
48
|
+
Send a prompt directly to Gemini CLI.
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
- `prompt` (required): The prompt to send
|
|
52
|
+
- `model`: Gemini model to use (default: `gemini-2.5-flash`)
|
|
53
|
+
- `allowed_tools`: Comma-separated tools Gemini can use (e.g., `web_search,shell`)
|
|
54
|
+
|
|
55
|
+
### gemini_with_agent_md
|
|
56
|
+
|
|
57
|
+
Send a prompt with an agent.md file as context. Useful for delegating tasks that need specific instructions.
|
|
58
|
+
|
|
59
|
+
Parameters:
|
|
60
|
+
- `agent_md_path` (required): Absolute path to agent.md file
|
|
61
|
+
- `prompt` (required): The prompt to send
|
|
62
|
+
- `model`: Override model (otherwise inferred from frontmatter)
|
|
63
|
+
- `allowed_tools`: Comma-separated tools Gemini can use
|
|
64
|
+
|
|
65
|
+
Model mapping from Claude agent.md frontmatter:
|
|
66
|
+
- `haiku` -> `gemini-2.5-flash-lite`
|
|
67
|
+
- `sonnet` -> `gemini-2.5-flash`
|
|
68
|
+
- `opus` -> `gemini-2.5-pro`
|
|
69
|
+
|
|
70
|
+
The tool automatically strips frontmatter from the agent.md before passing to Gemini.
|
|
71
|
+
|
|
72
|
+
## Example prompts in Claude Code
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Use the gemini tool to summarize this 500-page PDF: /path/to/large.pdf
|
|
76
|
+
|
|
77
|
+
Use gemini with web_search to find the latest React 19 features
|
|
78
|
+
|
|
79
|
+
Use gemini_with_agent_md with my research agent to investigate this topic
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Why use this?
|
|
83
|
+
|
|
84
|
+
- Gemini has 1M token context - process huge files Claude can't handle in one shot
|
|
85
|
+
- Web search built into Gemini CLI
|
|
86
|
+
- Delegate cheap/simple tasks to Gemini, save Claude tokens for complex reasoning
|
|
87
|
+
- Run agents defined in agent.md files through Gemini
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Run directly with Bun
|
|
93
|
+
bun run dev
|
|
94
|
+
|
|
95
|
+
# Type check
|
|
96
|
+
npm run typecheck
|
|
97
|
+
|
|
98
|
+
# Build for distribution
|
|
99
|
+
npm run build
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { extractModelFromFrontmatter, stripFrontmatter } from "../utils/frontmatter.js";
|
|
3
|
+
describe("extractModelFromFrontmatter", () => {
|
|
4
|
+
it("extracts haiku model", () => {
|
|
5
|
+
const content = `---
|
|
6
|
+
model: haiku
|
|
7
|
+
---
|
|
8
|
+
# Agent`;
|
|
9
|
+
expect(extractModelFromFrontmatter(content)).toBe("gemini-2.5-flash-lite");
|
|
10
|
+
});
|
|
11
|
+
it("extracts sonnet model", () => {
|
|
12
|
+
const content = `---
|
|
13
|
+
model: sonnet
|
|
14
|
+
---
|
|
15
|
+
# Agent`;
|
|
16
|
+
expect(extractModelFromFrontmatter(content)).toBe("gemini-2.5-flash");
|
|
17
|
+
});
|
|
18
|
+
it("extracts opus model", () => {
|
|
19
|
+
const content = `---
|
|
20
|
+
model: opus
|
|
21
|
+
---
|
|
22
|
+
# Agent`;
|
|
23
|
+
expect(extractModelFromFrontmatter(content)).toBe("gemini-2.5-pro");
|
|
24
|
+
});
|
|
25
|
+
it("returns null for unknown model", () => {
|
|
26
|
+
const content = `---
|
|
27
|
+
model: gpt-4
|
|
28
|
+
---
|
|
29
|
+
# Agent`;
|
|
30
|
+
expect(extractModelFromFrontmatter(content)).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
it("returns null when no frontmatter", () => {
|
|
33
|
+
const content = `# Agent
|
|
34
|
+
Some content`;
|
|
35
|
+
expect(extractModelFromFrontmatter(content)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it("returns null when no model in frontmatter", () => {
|
|
38
|
+
const content = `---
|
|
39
|
+
title: My Agent
|
|
40
|
+
---
|
|
41
|
+
# Agent`;
|
|
42
|
+
expect(extractModelFromFrontmatter(content)).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
it("handles model with extra whitespace", () => {
|
|
45
|
+
const content = `---
|
|
46
|
+
model: haiku
|
|
47
|
+
---
|
|
48
|
+
# Agent`;
|
|
49
|
+
expect(extractModelFromFrontmatter(content)).toBe("gemini-2.5-flash-lite");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe("stripFrontmatter", () => {
|
|
53
|
+
it("removes frontmatter", () => {
|
|
54
|
+
const content = `---
|
|
55
|
+
model: sonnet
|
|
56
|
+
title: My Agent
|
|
57
|
+
---
|
|
58
|
+
# Agent
|
|
59
|
+
Content here`;
|
|
60
|
+
expect(stripFrontmatter(content)).toBe("# Agent\nContent here");
|
|
61
|
+
});
|
|
62
|
+
it("preserves content without frontmatter", () => {
|
|
63
|
+
const content = `# Agent
|
|
64
|
+
Content here`;
|
|
65
|
+
expect(stripFrontmatter(content)).toBe(content);
|
|
66
|
+
});
|
|
67
|
+
it("handles empty content", () => {
|
|
68
|
+
expect(stripFrontmatter("")).toBe("");
|
|
69
|
+
});
|
|
70
|
+
it("handles frontmatter only", () => {
|
|
71
|
+
const content = `---
|
|
72
|
+
model: haiku
|
|
73
|
+
---`;
|
|
74
|
+
expect(stripFrontmatter(content)).toBe("");
|
|
75
|
+
});
|
|
76
|
+
it("handles unclosed frontmatter", () => {
|
|
77
|
+
const content = `---
|
|
78
|
+
model: haiku
|
|
79
|
+
# Agent`;
|
|
80
|
+
expect(stripFrontmatter(content)).toBe(content);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { EventEmitter } from "events";
|
|
4
|
+
import { executeGemini, DEFAULT_GEMINI_MODEL, DEFAULT_TOOLS } from "../utils/gemini-executor.js";
|
|
5
|
+
vi.mock("child_process", () => ({
|
|
6
|
+
spawn: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
const mockSpawn = spawn;
|
|
9
|
+
function createMockProcess(exitCode = 0, stdout = "", stderr = "") {
|
|
10
|
+
const proc = new EventEmitter();
|
|
11
|
+
const stdinChunks = [];
|
|
12
|
+
proc.stdin = {
|
|
13
|
+
write: vi.fn((chunk) => stdinChunks.push(chunk)),
|
|
14
|
+
end: vi.fn(),
|
|
15
|
+
_chunks: stdinChunks,
|
|
16
|
+
};
|
|
17
|
+
proc.stdout = new EventEmitter();
|
|
18
|
+
proc.stderr = new EventEmitter();
|
|
19
|
+
setImmediate(() => {
|
|
20
|
+
proc.stdout?.emit("data", Buffer.from(stdout));
|
|
21
|
+
proc.stderr?.emit("data", Buffer.from(stderr));
|
|
22
|
+
proc.emit("close", exitCode);
|
|
23
|
+
});
|
|
24
|
+
return proc;
|
|
25
|
+
}
|
|
26
|
+
describe("executeGemini", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
it("spawns gemini with correct base args", async () => {
|
|
31
|
+
const mockProc = createMockProcess(0, '{"response": "test"}');
|
|
32
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
33
|
+
await executeGemini({ prompt: "hello" });
|
|
34
|
+
expect(mockSpawn).toHaveBeenCalledWith("gemini", expect.arrayContaining([
|
|
35
|
+
"-s",
|
|
36
|
+
"--model", DEFAULT_GEMINI_MODEL,
|
|
37
|
+
"--output-format", "json",
|
|
38
|
+
"-p", "hello",
|
|
39
|
+
]), expect.any(Object));
|
|
40
|
+
});
|
|
41
|
+
it("uses custom model when provided", async () => {
|
|
42
|
+
const mockProc = createMockProcess(0, '{"response": "test"}');
|
|
43
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
44
|
+
await executeGemini({ prompt: "hello", model: "gemini-2.5-pro" });
|
|
45
|
+
expect(mockSpawn).toHaveBeenCalledWith("gemini", expect.arrayContaining(["--model", "gemini-2.5-pro"]), expect.any(Object));
|
|
46
|
+
});
|
|
47
|
+
it("includes default tools in allowed-tools", async () => {
|
|
48
|
+
const mockProc = createMockProcess(0, '{"response": "test"}');
|
|
49
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
50
|
+
await executeGemini({ prompt: "hello" });
|
|
51
|
+
const args = mockSpawn.mock.calls[0][1];
|
|
52
|
+
const toolsIndex = args.indexOf("--allowed-tools");
|
|
53
|
+
const toolsValue = args[toolsIndex + 1];
|
|
54
|
+
for (const tool of DEFAULT_TOOLS) {
|
|
55
|
+
expect(toolsValue).toContain(tool);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
it("merges user tools with default tools", async () => {
|
|
59
|
+
const mockProc = createMockProcess(0, '{"response": "test"}');
|
|
60
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
61
|
+
await executeGemini({ prompt: "hello", allowedTools: "perplexity_search,shell" });
|
|
62
|
+
const args = mockSpawn.mock.calls[0][1];
|
|
63
|
+
const toolsIndex = args.indexOf("--allowed-tools");
|
|
64
|
+
const toolsValue = args[toolsIndex + 1];
|
|
65
|
+
expect(toolsValue).toContain("read_file");
|
|
66
|
+
expect(toolsValue).toContain("write_file");
|
|
67
|
+
expect(toolsValue).toContain("perplexity_search");
|
|
68
|
+
expect(toolsValue).toContain("shell");
|
|
69
|
+
});
|
|
70
|
+
it("deduplicates tools", async () => {
|
|
71
|
+
const mockProc = createMockProcess(0, '{"response": "test"}');
|
|
72
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
73
|
+
await executeGemini({ prompt: "hello", allowedTools: "read_file,shell" });
|
|
74
|
+
const args = mockSpawn.mock.calls[0][1];
|
|
75
|
+
const toolsIndex = args.indexOf("--allowed-tools");
|
|
76
|
+
const toolsValue = args[toolsIndex + 1];
|
|
77
|
+
const tools = toolsValue.split(",");
|
|
78
|
+
const readFileCount = tools.filter((t) => t === "read_file").length;
|
|
79
|
+
expect(readFileCount).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
it("pipes content to stdin when pipeContent provided", async () => {
|
|
82
|
+
const mockProc = createMockProcess(0, '{"response": "test"}');
|
|
83
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
84
|
+
const agentContent = "# Agent\nDo stuff";
|
|
85
|
+
await executeGemini({ prompt: "hello", pipeContent: agentContent });
|
|
86
|
+
expect(mockSpawn).toHaveBeenCalledWith("gemini", expect.any(Array), { stdio: ["pipe", "pipe", "pipe"] });
|
|
87
|
+
expect(mockProc.stdin?.write).toHaveBeenCalledWith(agentContent);
|
|
88
|
+
expect(mockProc.stdin?.end).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
it("sets stdin to ignore when no pipeContent", async () => {
|
|
91
|
+
const mockProc = createMockProcess(0, '{"response": "test"}');
|
|
92
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
93
|
+
await executeGemini({ prompt: "hello" });
|
|
94
|
+
expect(mockSpawn).toHaveBeenCalledWith("gemini", expect.any(Array), { stdio: ["ignore", "pipe", "pipe"] });
|
|
95
|
+
});
|
|
96
|
+
it("extracts response from JSON output", async () => {
|
|
97
|
+
const mockProc = createMockProcess(0, '{"response": "the answer"}');
|
|
98
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
99
|
+
const result = await executeGemini({ prompt: "hello" });
|
|
100
|
+
expect(result).toBe("the answer");
|
|
101
|
+
});
|
|
102
|
+
it("returns raw stdout when JSON has no response field", async () => {
|
|
103
|
+
const mockProc = createMockProcess(0, '{"other": "data"}');
|
|
104
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
105
|
+
const result = await executeGemini({ prompt: "hello" });
|
|
106
|
+
expect(result).toBe('{"other": "data"}');
|
|
107
|
+
});
|
|
108
|
+
it("returns raw stdout when output is not JSON", async () => {
|
|
109
|
+
const mockProc = createMockProcess(0, "plain text output");
|
|
110
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
111
|
+
const result = await executeGemini({ prompt: "hello" });
|
|
112
|
+
expect(result).toBe("plain text output");
|
|
113
|
+
});
|
|
114
|
+
it("rejects on non-zero exit code", async () => {
|
|
115
|
+
const mockProc = createMockProcess(1, "", "some error");
|
|
116
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
117
|
+
expect(executeGemini({ prompt: "hello" })).rejects.toThrow("gemini exited with code 1: some error");
|
|
118
|
+
});
|
|
119
|
+
it("rejects on spawn error", async () => {
|
|
120
|
+
const proc = new EventEmitter();
|
|
121
|
+
proc.stdin = null;
|
|
122
|
+
proc.stdout = new EventEmitter();
|
|
123
|
+
proc.stderr = new EventEmitter();
|
|
124
|
+
mockSpawn.mockReturnValue(proc);
|
|
125
|
+
const promise = executeGemini({ prompt: "hello" });
|
|
126
|
+
setImmediate(() => {
|
|
127
|
+
proc.emit("error", new Error("ENOENT"));
|
|
128
|
+
});
|
|
129
|
+
expect(promise).rejects.toThrow("Failed to spawn gemini: ENOENT");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { extractModelFromFrontmatter, stripFrontmatter } from "../utils/frontmatter.js";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const fixturesDir = path.join(__dirname, "fixtures");
|
|
8
|
+
async function loadFixture(name) {
|
|
9
|
+
return readFile(path.join(fixturesDir, name), "utf-8");
|
|
10
|
+
}
|
|
11
|
+
describe("gemini_with_agent_md with fixtures", () => {
|
|
12
|
+
describe("model extraction from fixture files", () => {
|
|
13
|
+
it("extracts haiku model from agent-haiku.md", async () => {
|
|
14
|
+
const content = await loadFixture("agent-haiku.md");
|
|
15
|
+
expect(extractModelFromFrontmatter(content)).toBe("gemini-2.5-flash-lite");
|
|
16
|
+
});
|
|
17
|
+
it("extracts sonnet model from agent-sonnet.md", async () => {
|
|
18
|
+
const content = await loadFixture("agent-sonnet.md");
|
|
19
|
+
expect(extractModelFromFrontmatter(content)).toBe("gemini-2.5-flash");
|
|
20
|
+
});
|
|
21
|
+
it("extracts opus model from agent-opus.md", async () => {
|
|
22
|
+
const content = await loadFixture("agent-opus.md");
|
|
23
|
+
expect(extractModelFromFrontmatter(content)).toBe("gemini-2.5-pro");
|
|
24
|
+
});
|
|
25
|
+
it("returns null for agent-no-model.md", async () => {
|
|
26
|
+
const content = await loadFixture("agent-no-model.md");
|
|
27
|
+
expect(extractModelFromFrontmatter(content)).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
it("returns null for agent-no-frontmatter.md", async () => {
|
|
30
|
+
const content = await loadFixture("agent-no-frontmatter.md");
|
|
31
|
+
expect(extractModelFromFrontmatter(content)).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("frontmatter stripping from fixture files", () => {
|
|
35
|
+
it("strips frontmatter from agent-haiku.md", async () => {
|
|
36
|
+
const content = await loadFixture("agent-haiku.md");
|
|
37
|
+
const stripped = stripFrontmatter(content);
|
|
38
|
+
expect(stripped).not.toContain("---");
|
|
39
|
+
expect(stripped).not.toContain("model: haiku");
|
|
40
|
+
expect(stripped).toContain("# Test Agent (Haiku)");
|
|
41
|
+
});
|
|
42
|
+
it("strips frontmatter from agent-no-model.md", async () => {
|
|
43
|
+
const content = await loadFixture("agent-no-model.md");
|
|
44
|
+
const stripped = stripFrontmatter(content);
|
|
45
|
+
expect(stripped).not.toContain("---");
|
|
46
|
+
expect(stripped).not.toContain("title:");
|
|
47
|
+
expect(stripped).toContain("# Test Agent (No Model)");
|
|
48
|
+
});
|
|
49
|
+
it("preserves content from agent-no-frontmatter.md", async () => {
|
|
50
|
+
const content = await loadFixture("agent-no-frontmatter.md");
|
|
51
|
+
const stripped = stripFrontmatter(content);
|
|
52
|
+
expect(stripped).toBe(content);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("model resolution logic", () => {
|
|
56
|
+
const DEFAULT_GEMINI_MODEL = "gemini-2.5-flash";
|
|
57
|
+
function resolveModel(explicitModel, inferredModel) {
|
|
58
|
+
return explicitModel ?? inferredModel ?? DEFAULT_GEMINI_MODEL;
|
|
59
|
+
}
|
|
60
|
+
it("uses explicit model when provided", async () => {
|
|
61
|
+
const content = await loadFixture("agent-haiku.md");
|
|
62
|
+
const inferred = extractModelFromFrontmatter(content);
|
|
63
|
+
const result = resolveModel("gemini-2.5-pro", inferred);
|
|
64
|
+
expect(result).toBe("gemini-2.5-pro");
|
|
65
|
+
});
|
|
66
|
+
it("falls back to inferred model when no explicit model", async () => {
|
|
67
|
+
const content = await loadFixture("agent-opus.md");
|
|
68
|
+
const inferred = extractModelFromFrontmatter(content);
|
|
69
|
+
const result = resolveModel(undefined, inferred);
|
|
70
|
+
expect(result).toBe("gemini-2.5-pro");
|
|
71
|
+
});
|
|
72
|
+
it("falls back to default when no explicit or inferred model", async () => {
|
|
73
|
+
const content = await loadFixture("agent-no-model.md");
|
|
74
|
+
const inferred = extractModelFromFrontmatter(content);
|
|
75
|
+
const result = resolveModel(undefined, inferred);
|
|
76
|
+
expect(result).toBe(DEFAULT_GEMINI_MODEL);
|
|
77
|
+
});
|
|
78
|
+
it("falls back to default for file without frontmatter", async () => {
|
|
79
|
+
const content = await loadFixture("agent-no-frontmatter.md");
|
|
80
|
+
const inferred = extractModelFromFrontmatter(content);
|
|
81
|
+
const result = resolveModel(undefined, inferred);
|
|
82
|
+
expect(result).toBe(DEFAULT_GEMINI_MODEL);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { registerGeminiTool } from "./tools/gemini.js";
|
|
4
|
+
import { registerGeminiWithAgentMdTool } from "./tools/gemini-with-agent-md.js";
|
|
5
|
+
export class GeminiMCPServer {
|
|
6
|
+
server;
|
|
7
|
+
constructor() {
|
|
8
|
+
this.server = new McpServer({
|
|
9
|
+
name: "gemini-mcp",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
});
|
|
12
|
+
this.setupTools();
|
|
13
|
+
}
|
|
14
|
+
setupTools() {
|
|
15
|
+
registerGeminiTool(this.server);
|
|
16
|
+
registerGeminiWithAgentMdTool(this.server);
|
|
17
|
+
}
|
|
18
|
+
async run() {
|
|
19
|
+
const transport = new StdioServerTransport();
|
|
20
|
+
await this.server.connect(transport);
|
|
21
|
+
console.error("Gemini MCP server running on stdio");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export declare const geminiWithAgentMdInputSchema: {
|
|
4
|
+
agent_md_path: z.ZodString;
|
|
5
|
+
prompt: z.ZodString;
|
|
6
|
+
model: z.ZodOptional<z.ZodString>;
|
|
7
|
+
allowed_tools: z.ZodOptional<z.ZodString>;
|
|
8
|
+
};
|
|
9
|
+
export declare function registerGeminiWithAgentMdTool(server: McpServer): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { executeGemini, DEFAULT_GEMINI_MODEL } from "../utils/gemini-executor.js";
|
|
4
|
+
import { extractModelFromFrontmatter, stripFrontmatter } from "../utils/frontmatter.js";
|
|
5
|
+
export const geminiWithAgentMdInputSchema = {
|
|
6
|
+
agent_md_path: z.string().describe("Absolute path to the agent.md file"),
|
|
7
|
+
prompt: z.string().describe("The prompt to send to Gemini"),
|
|
8
|
+
model: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Gemini model to use. If not specified, infers from agent.md frontmatter (haiku->gemini-2.5-flash-lite, sonnet->gemini-2.5-flash, opus->gemini-2.5-pro). Falls back to gemini-2.5-flash."),
|
|
12
|
+
allowed_tools: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Comma-separated list of tools Gemini can use (e.g., "perplexity_search,shell")'),
|
|
16
|
+
};
|
|
17
|
+
export function registerGeminiWithAgentMdTool(server) {
|
|
18
|
+
server.registerTool("gemini_with_agent_md", {
|
|
19
|
+
title: "Gemini CLI with Agent MD",
|
|
20
|
+
description: "Send a prompt to Gemini CLI with an agent.md file as context. Strips Claude-specific formatting and infers model from frontmatter.",
|
|
21
|
+
inputSchema: geminiWithAgentMdInputSchema,
|
|
22
|
+
}, async ({ agent_md_path, prompt, model, allowed_tools }) => {
|
|
23
|
+
try {
|
|
24
|
+
const content = await readFile(agent_md_path, "utf-8");
|
|
25
|
+
const cleaned = stripFrontmatter(content);
|
|
26
|
+
const inferredModel = extractModelFromFrontmatter(content);
|
|
27
|
+
const finalModel = model ?? inferredModel ?? DEFAULT_GEMINI_MODEL;
|
|
28
|
+
console.error(`Piping cleaned agent.md (${cleaned.length} chars), model: ${finalModel}`);
|
|
29
|
+
const result = await executeGemini({
|
|
30
|
+
prompt,
|
|
31
|
+
model: finalModel,
|
|
32
|
+
allowedTools: allowed_tools,
|
|
33
|
+
pipeContent: cleaned,
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: result }],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export declare const geminiInputSchema: {
|
|
4
|
+
prompt: z.ZodString;
|
|
5
|
+
model: z.ZodOptional<z.ZodString>;
|
|
6
|
+
allowed_tools: z.ZodOptional<z.ZodString>;
|
|
7
|
+
};
|
|
8
|
+
export declare function registerGeminiTool(server: McpServer): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { executeGemini, DEFAULT_GEMINI_MODEL } from "../utils/gemini-executor.js";
|
|
3
|
+
export const geminiInputSchema = {
|
|
4
|
+
prompt: z.string().describe("The prompt to send to Gemini"),
|
|
5
|
+
model: z
|
|
6
|
+
.string()
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("Gemini model to use (default: gemini-2.5-flash)"),
|
|
9
|
+
allowed_tools: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Comma-separated list of tools Gemini can use (e.g., "perplexity_search,shell")'),
|
|
13
|
+
};
|
|
14
|
+
export function registerGeminiTool(server) {
|
|
15
|
+
server.registerTool("gemini", {
|
|
16
|
+
title: "Gemini CLI",
|
|
17
|
+
description: "Send a prompt to Gemini CLI. Optionally specify model and allowed tools.",
|
|
18
|
+
inputSchema: geminiInputSchema,
|
|
19
|
+
}, async ({ prompt, model, allowed_tools }) => {
|
|
20
|
+
try {
|
|
21
|
+
const result = await executeGemini({
|
|
22
|
+
prompt,
|
|
23
|
+
model: model ?? DEFAULT_GEMINI_MODEL,
|
|
24
|
+
allowedTools: allowed_tools,
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text: result }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const CLAUDE_TO_GEMINI_MODEL = {
|
|
2
|
+
haiku: "gemini-2.5-flash-lite",
|
|
3
|
+
sonnet: "gemini-2.5-flash",
|
|
4
|
+
opus: "gemini-2.5-pro",
|
|
5
|
+
};
|
|
6
|
+
export function extractModelFromFrontmatter(content) {
|
|
7
|
+
const lines = content.split("\n");
|
|
8
|
+
let inFrontmatter = false;
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
if (line.trim() === "---") {
|
|
11
|
+
if (!inFrontmatter) {
|
|
12
|
+
inFrontmatter = true;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
if (inFrontmatter) {
|
|
18
|
+
const match = line.match(/^model:\s*(.+)$/);
|
|
19
|
+
if (match) {
|
|
20
|
+
const claudeModel = match[1].trim();
|
|
21
|
+
return CLAUDE_TO_GEMINI_MODEL[claudeModel] ?? null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export function stripFrontmatter(content) {
|
|
28
|
+
const lines = content.split("\n");
|
|
29
|
+
let inFrontmatter = false;
|
|
30
|
+
let frontmatterEnd = 0;
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
if (lines[i].trim() === "---") {
|
|
33
|
+
if (!inFrontmatter) {
|
|
34
|
+
inFrontmatter = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
frontmatterEnd = i + 1;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return lines.slice(frontmatterEnd).join("\n");
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const DEFAULT_GEMINI_MODEL = "gemini-2.5-flash";
|
|
2
|
+
export declare const DEFAULT_TOOLS: string[];
|
|
3
|
+
export interface ExecuteGeminiOptions {
|
|
4
|
+
prompt: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
allowedTools?: string;
|
|
7
|
+
pipeContent?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function executeGemini(options: ExecuteGeminiOptions): Promise<string>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
export const DEFAULT_GEMINI_MODEL = "gemini-2.5-flash";
|
|
3
|
+
export const DEFAULT_TOOLS = ["read_file", "write_file"];
|
|
4
|
+
export async function executeGemini(options) {
|
|
5
|
+
const { prompt, model = DEFAULT_GEMINI_MODEL, allowedTools, pipeContent } = options;
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const args = ["-s", "--model", model, "--output-format", "json"];
|
|
8
|
+
const userTools = allowedTools ? allowedTools.split(",").map(t => t.trim()) : [];
|
|
9
|
+
const allTools = [...new Set([...DEFAULT_TOOLS, ...userTools])];
|
|
10
|
+
args.push("--allowed-tools", allTools.join(","));
|
|
11
|
+
args.push("-p", prompt);
|
|
12
|
+
console.error(`Executing: gemini ${args.join(" ")}`);
|
|
13
|
+
const proc = spawn("gemini", args, {
|
|
14
|
+
stdio: [pipeContent ? "pipe" : "ignore", "pipe", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
if (pipeContent && proc.stdin) {
|
|
17
|
+
proc.stdin.write(pipeContent);
|
|
18
|
+
proc.stdin.end();
|
|
19
|
+
}
|
|
20
|
+
let stdout = "";
|
|
21
|
+
let stderr = "";
|
|
22
|
+
proc.stdout?.on("data", (data) => {
|
|
23
|
+
stdout += data.toString();
|
|
24
|
+
});
|
|
25
|
+
proc.stderr?.on("data", (data) => {
|
|
26
|
+
stderr += data.toString();
|
|
27
|
+
});
|
|
28
|
+
proc.on("close", (code) => {
|
|
29
|
+
if (code !== 0) {
|
|
30
|
+
reject(new Error(`gemini exited with code ${code}: ${stderr}`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const json = JSON.parse(stdout);
|
|
35
|
+
resolve(json.response ?? stdout);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
resolve(stdout);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
proc.on("error", (err) => {
|
|
42
|
+
reject(new Error(`Failed to spawn gemini: ${err.message}`));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brenoxp/gemini-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server that wraps Gemini CLI for use with Claude Code and other MCP clients",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"gemini-mcp": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"dev": "npx tsx src/index.ts",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"gemini",
|
|
28
|
+
"claude",
|
|
29
|
+
"ai",
|
|
30
|
+
"llm",
|
|
31
|
+
"model-context-protocol",
|
|
32
|
+
"claude-code",
|
|
33
|
+
"gemini-cli"
|
|
34
|
+
],
|
|
35
|
+
"author": "Breno Pinto",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/brenoxp/gemini-mcp.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/brenoxp/gemini-mcp/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/brenoxp/gemini-mcp#readme",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
47
|
+
"zod": "^4.3.5"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.0.0",
|
|
51
|
+
"typescript": "^5.7.0",
|
|
52
|
+
"vitest": "^4.0.16"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|