@rlabs-inc/gemini-mcp 0.5.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.
Files changed (39) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +418 -0
  3. package/dist/gemini-client.d.ts +120 -0
  4. package/dist/gemini-client.js +399 -0
  5. package/dist/index.d.ts +8 -0
  6. package/dist/index.js +220 -0
  7. package/dist/tools/analyze.d.ts +10 -0
  8. package/dist/tools/analyze.js +96 -0
  9. package/dist/tools/brainstorm.d.ts +10 -0
  10. package/dist/tools/brainstorm.js +220 -0
  11. package/dist/tools/cache.d.ts +17 -0
  12. package/dist/tools/cache.js +286 -0
  13. package/dist/tools/code-exec.d.ts +17 -0
  14. package/dist/tools/code-exec.js +135 -0
  15. package/dist/tools/document.d.ts +16 -0
  16. package/dist/tools/document.js +333 -0
  17. package/dist/tools/image-edit.d.ts +16 -0
  18. package/dist/tools/image-edit.js +291 -0
  19. package/dist/tools/image-gen.d.ts +17 -0
  20. package/dist/tools/image-gen.js +148 -0
  21. package/dist/tools/query.d.ts +11 -0
  22. package/dist/tools/query.js +63 -0
  23. package/dist/tools/search.d.ts +15 -0
  24. package/dist/tools/search.js +128 -0
  25. package/dist/tools/speech.d.ts +17 -0
  26. package/dist/tools/speech.js +304 -0
  27. package/dist/tools/structured.d.ts +16 -0
  28. package/dist/tools/structured.js +247 -0
  29. package/dist/tools/summarize.d.ts +10 -0
  30. package/dist/tools/summarize.js +77 -0
  31. package/dist/tools/url-context.d.ts +17 -0
  32. package/dist/tools/url-context.js +226 -0
  33. package/dist/tools/video-gen.d.ts +11 -0
  34. package/dist/tools/video-gen.js +136 -0
  35. package/dist/tools/youtube.d.ts +16 -0
  36. package/dist/tools/youtube.js +218 -0
  37. package/dist/utils/logger.d.ts +33 -0
  38. package/dist/utils/logger.js +82 -0
  39. package/package.json +48 -0
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Image Editing Tool - Multi-turn conversational image editing with Nano Banana Pro
3
+ *
4
+ * This tool enables iterative image refinement through conversation.
5
+ * Uses Gemini 3's chat sessions to maintain context and thought signatures.
6
+ *
7
+ * Workflow:
8
+ * 1. Start an edit session with an initial image generation
9
+ * 2. Continue refining with follow-up prompts ("make it warmer", "add more clouds")
10
+ * 3. Each response returns the updated image
11
+ */
12
+ import { z } from 'zod';
13
+ import { GoogleGenAI, Modality } from '@google/genai';
14
+ import { logger } from '../utils/logger.js';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ // Store active image editing sessions
18
+ // The SDK handles thought signatures automatically when using chat sessions
19
+ const activeEditSessions = new Map();
20
+ // Get output directory
21
+ function getOutputDir() {
22
+ return (process.env.GEMINI_OUTPUT_DIR || path.join(process.cwd(), 'gemini-output'));
23
+ }
24
+ // Save image to disk
25
+ function saveImage(base64, mimeType) {
26
+ const outputDir = getOutputDir();
27
+ if (!fs.existsSync(outputDir)) {
28
+ fs.mkdirSync(outputDir, { recursive: true });
29
+ }
30
+ const timestamp = Date.now();
31
+ const extension = mimeType.split('/')[1] || 'png';
32
+ const filename = `edit-${timestamp}.${extension}`;
33
+ const filePath = path.join(outputDir, filename);
34
+ const buffer = Buffer.from(base64, 'base64');
35
+ fs.writeFileSync(filePath, buffer);
36
+ return filePath;
37
+ }
38
+ /**
39
+ * Register image editing tools with the MCP server
40
+ */
41
+ export function registerImageEditTool(server) {
42
+ // Start a new image editing session
43
+ server.tool('gemini-start-image-edit', {
44
+ prompt: z
45
+ .string()
46
+ .describe('Initial prompt to generate the base image to edit'),
47
+ aspectRatio: z
48
+ .enum([
49
+ '1:1',
50
+ '2:3',
51
+ '3:2',
52
+ '3:4',
53
+ '4:3',
54
+ '4:5',
55
+ '5:4',
56
+ '9:16',
57
+ '16:9',
58
+ '21:9',
59
+ ])
60
+ .default('1:1')
61
+ .describe('Aspect ratio for the image'),
62
+ imageSize: z
63
+ .enum(['1K', '2K', '4K'])
64
+ .default('2K')
65
+ .describe('Resolution: 1K (fast), 2K (balanced), 4K (highest quality)'),
66
+ useGoogleSearch: z
67
+ .boolean()
68
+ .default(false)
69
+ .describe('Ground the image in real-world info via Google Search'),
70
+ }, async ({ prompt, aspectRatio, imageSize, useGoogleSearch }) => {
71
+ logger.info(`Starting image edit session: ${prompt.substring(0, 50)}...`);
72
+ try {
73
+ const apiKey = process.env.GEMINI_API_KEY;
74
+ if (!apiKey) {
75
+ throw new Error('GEMINI_API_KEY not set');
76
+ }
77
+ const genAI = new GoogleGenAI({ apiKey });
78
+ const imageModel = process.env.GEMINI_IMAGE_MODEL || 'gemini-3-pro-image-preview';
79
+ // Create a chat session for multi-turn editing
80
+ // The SDK handles thought signatures automatically in chat mode
81
+ const chatConfig = {
82
+ responseModalities: [Modality.TEXT, Modality.IMAGE],
83
+ imageConfig: {
84
+ aspectRatio,
85
+ imageSize,
86
+ },
87
+ };
88
+ if (useGoogleSearch) {
89
+ chatConfig.tools = [{ googleSearch: {} }];
90
+ }
91
+ const chat = genAI.chats.create({
92
+ model: imageModel,
93
+ config: chatConfig,
94
+ });
95
+ // Send the initial prompt
96
+ const response = await chat.sendMessage({ message: prompt });
97
+ // Extract image from response
98
+ const parts = response.candidates?.[0]?.content?.parts;
99
+ if (!parts) {
100
+ throw new Error('No parts in response');
101
+ }
102
+ let imageData;
103
+ let mimeType = 'image/png';
104
+ let description;
105
+ for (const part of parts) {
106
+ if (part.inlineData) {
107
+ const inlineData = part.inlineData;
108
+ imageData = inlineData.data;
109
+ mimeType = inlineData.mimeType || 'image/png';
110
+ }
111
+ else if (part.text) {
112
+ description = part.text;
113
+ }
114
+ }
115
+ if (!imageData) {
116
+ throw new Error('No image data in response');
117
+ }
118
+ // Generate session ID and store the chat
119
+ const sessionId = `edit-${Date.now()}-${Math.random().toString(36).substring(7)}`;
120
+ activeEditSessions.set(sessionId, {
121
+ chat,
122
+ lastImageBase64: imageData,
123
+ lastImageMimeType: mimeType,
124
+ });
125
+ // Save to disk
126
+ const filePath = saveImage(imageData, mimeType);
127
+ logger.info(`Image edit session started: ${sessionId}`);
128
+ return {
129
+ content: [
130
+ {
131
+ type: 'image',
132
+ data: imageData,
133
+ mimeType,
134
+ },
135
+ {
136
+ type: 'text',
137
+ text: `Image edit session started!\n\nSession ID: ${sessionId}\nSettings: ${imageSize}, ${aspectRatio}${useGoogleSearch ? ', with Google Search' : ''}\nSaved to: ${filePath}\n\nUse gemini-continue-image-edit with this session ID to make changes.${description ? `\n\nGemini's description: ${description}` : ''}`,
138
+ },
139
+ ],
140
+ };
141
+ }
142
+ catch (error) {
143
+ const errorMessage = error instanceof Error ? error.message : String(error);
144
+ logger.error(`Error starting image edit session: ${errorMessage}`);
145
+ return {
146
+ content: [
147
+ {
148
+ type: 'text',
149
+ text: `Error starting image edit session: ${errorMessage}`,
150
+ },
151
+ ],
152
+ isError: true,
153
+ };
154
+ }
155
+ });
156
+ // Continue editing in an existing session
157
+ server.tool('gemini-continue-image-edit', {
158
+ sessionId: z.string().describe('The session ID from gemini-start-image-edit'),
159
+ prompt: z
160
+ .string()
161
+ .describe('Edit instruction (e.g., "make it warmer", "add mountains in the background", "change to night time")'),
162
+ }, async ({ sessionId, prompt }) => {
163
+ logger.info(`Continuing image edit: ${prompt.substring(0, 50)}...`);
164
+ try {
165
+ const session = activeEditSessions.get(sessionId);
166
+ if (!session) {
167
+ return {
168
+ content: [
169
+ {
170
+ type: 'text',
171
+ text: `Session not found: ${sessionId}\n\nActive sessions may have expired. Start a new session with gemini-start-image-edit.`,
172
+ },
173
+ ],
174
+ isError: true,
175
+ };
176
+ }
177
+ // Send the edit instruction
178
+ const chat = session.chat;
179
+ const response = await chat.sendMessage({ message: prompt });
180
+ // Extract image from response
181
+ const parts = response.candidates?.[0]?.content?.parts;
182
+ if (!parts) {
183
+ throw new Error('No parts in response');
184
+ }
185
+ let imageData;
186
+ let mimeType = 'image/png';
187
+ let description;
188
+ for (const part of parts) {
189
+ if (part.inlineData) {
190
+ imageData = part.inlineData.data;
191
+ mimeType = part.inlineData.mimeType || 'image/png';
192
+ }
193
+ else if (part.text) {
194
+ description = part.text;
195
+ }
196
+ }
197
+ if (!imageData) {
198
+ // Sometimes the model responds with just text (explanation)
199
+ return {
200
+ content: [
201
+ {
202
+ type: 'text',
203
+ text: description || 'No image generated. Try a different edit instruction.',
204
+ },
205
+ ],
206
+ };
207
+ }
208
+ // Update session with new image
209
+ session.lastImageBase64 = imageData;
210
+ session.lastImageMimeType = mimeType;
211
+ // Save to disk
212
+ const filePath = saveImage(imageData, mimeType);
213
+ logger.info(`Image edit continued successfully`);
214
+ return {
215
+ content: [
216
+ {
217
+ type: 'image',
218
+ data: imageData,
219
+ mimeType,
220
+ },
221
+ {
222
+ type: 'text',
223
+ text: `Image updated!\n\nSession ID: ${sessionId}\nSaved to: ${filePath}\n\nContinue editing with more instructions or start a new session.${description ? `\n\nGemini's description: ${description}` : ''}`,
224
+ },
225
+ ],
226
+ };
227
+ }
228
+ catch (error) {
229
+ const errorMessage = error instanceof Error ? error.message : String(error);
230
+ logger.error(`Error continuing image edit: ${errorMessage}`);
231
+ return {
232
+ content: [
233
+ {
234
+ type: 'text',
235
+ text: `Error continuing image edit: ${errorMessage}`,
236
+ },
237
+ ],
238
+ isError: true,
239
+ };
240
+ }
241
+ });
242
+ // End/close an editing session
243
+ server.tool('gemini-end-image-edit', {
244
+ sessionId: z.string().describe('The session ID to close'),
245
+ }, async ({ sessionId }) => {
246
+ const session = activeEditSessions.get(sessionId);
247
+ if (!session) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: 'text',
252
+ text: `Session not found or already closed: ${sessionId}`,
253
+ },
254
+ ],
255
+ };
256
+ }
257
+ // Remove the session
258
+ activeEditSessions.delete(sessionId);
259
+ logger.info(`Image edit session closed: ${sessionId}`);
260
+ return {
261
+ content: [
262
+ {
263
+ type: 'text',
264
+ text: `Session ${sessionId} closed successfully.`,
265
+ },
266
+ ],
267
+ };
268
+ });
269
+ // List active editing sessions
270
+ server.tool('gemini-list-image-sessions', {}, async () => {
271
+ const sessions = Array.from(activeEditSessions.keys());
272
+ if (sessions.length === 0) {
273
+ return {
274
+ content: [
275
+ {
276
+ type: 'text',
277
+ text: 'No active image editing sessions.\n\nStart one with gemini-start-image-edit.',
278
+ },
279
+ ],
280
+ };
281
+ }
282
+ return {
283
+ content: [
284
+ {
285
+ type: 'text',
286
+ text: `Active image editing sessions:\n\n${sessions.map((id) => `• ${id}`).join('\n')}\n\nUse gemini-continue-image-edit with a session ID to continue editing.`,
287
+ },
288
+ ],
289
+ };
290
+ });
291
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Image Generation Tool - Generate images using Gemini's Nano Banana Pro model
3
+ *
4
+ * This tool generates actual images from text descriptions and returns them
5
+ * both as base64 (for Claude to view) and saves them to disk (for user access).
6
+ *
7
+ * Nano Banana Pro Features:
8
+ * - Up to 4K resolution
9
+ * - 10 aspect ratios
10
+ * - Google Search grounding for real-world accuracy
11
+ * - High-fidelity text rendering
12
+ */
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ /**
15
+ * Register image generation tools with the MCP server
16
+ */
17
+ export declare function registerImageGenTool(server: McpServer): void;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Image Generation Tool - Generate images using Gemini's Nano Banana Pro model
3
+ *
4
+ * This tool generates actual images from text descriptions and returns them
5
+ * both as base64 (for Claude to view) and saves them to disk (for user access).
6
+ *
7
+ * Nano Banana Pro Features:
8
+ * - Up to 4K resolution
9
+ * - 10 aspect ratios
10
+ * - Google Search grounding for real-world accuracy
11
+ * - High-fidelity text rendering
12
+ */
13
+ import { z } from 'zod';
14
+ import { generateImage, getOutputDir, } from '../gemini-client.js';
15
+ import { logger } from '../utils/logger.js';
16
+ /**
17
+ * Register image generation tools with the MCP server
18
+ */
19
+ export function registerImageGenTool(server) {
20
+ // Image generation tool with full Nano Banana Pro capabilities
21
+ server.tool('gemini-generate-image', {
22
+ prompt: z.string().describe('Description of the image to generate'),
23
+ style: z
24
+ .string()
25
+ .optional()
26
+ .describe('Art style (e.g., "photorealistic", "watercolor", "anime", "oil painting", "cyberpunk")'),
27
+ aspectRatio: z
28
+ .enum([
29
+ '1:1',
30
+ '2:3',
31
+ '3:2',
32
+ '3:4',
33
+ '4:3',
34
+ '4:5',
35
+ '5:4',
36
+ '9:16',
37
+ '16:9',
38
+ '21:9',
39
+ ])
40
+ .default('1:1')
41
+ .describe('Aspect ratio: 1:1 (square), 16:9 (widescreen), 9:16 (portrait/mobile), 21:9 (ultrawide), etc.'),
42
+ imageSize: z
43
+ .enum(['1K', '2K', '4K'])
44
+ .default('2K')
45
+ .describe('Resolution: 1K (fast), 2K (balanced, default), 4K (highest quality)'),
46
+ useGoogleSearch: z
47
+ .boolean()
48
+ .default(false)
49
+ .describe('Ground the image in real-world info via Google Search (useful for current events, real places, etc.)'),
50
+ }, async ({ prompt, style, aspectRatio, imageSize, useGoogleSearch }) => {
51
+ logger.info(`Generating ${imageSize} image: ${prompt.substring(0, 50)}...`);
52
+ try {
53
+ const result = await generateImage(prompt, {
54
+ aspectRatio: aspectRatio,
55
+ imageSize: imageSize,
56
+ style,
57
+ saveToFile: true,
58
+ useGoogleSearch,
59
+ });
60
+ // Return the image in MCP format - Claude will be able to SEE this!
61
+ const content = [
62
+ {
63
+ type: 'image',
64
+ data: result.base64,
65
+ mimeType: result.mimeType,
66
+ },
67
+ {
68
+ type: 'text',
69
+ text: `Image generated successfully!\n\nSettings: ${imageSize}, ${aspectRatio}${useGoogleSearch ? ', with Google Search grounding' : ''}\nSaved to: ${result.filePath}\nOutput directory: ${getOutputDir()}${result.description ? `\n\nGemini's description: ${result.description}` : ''}`,
70
+ },
71
+ ];
72
+ return { content };
73
+ }
74
+ catch (error) {
75
+ const errorMessage = error instanceof Error ? error.message : String(error);
76
+ logger.error(`Error generating image: ${errorMessage}`);
77
+ return {
78
+ content: [
79
+ {
80
+ type: 'text',
81
+ text: `Error generating image: ${errorMessage}`,
82
+ },
83
+ ],
84
+ isError: true,
85
+ };
86
+ }
87
+ });
88
+ // Legacy image prompt tool (for compatibility) - generates text prompts for other tools
89
+ server.tool('gemini-image-prompt', {
90
+ description: z
91
+ .string()
92
+ .describe('Description of the image to generate a prompt for'),
93
+ style: z.string().optional().describe('The artistic style for the image'),
94
+ mood: z
95
+ .string()
96
+ .optional()
97
+ .describe('The mood or atmosphere of the image'),
98
+ details: z
99
+ .string()
100
+ .optional()
101
+ .describe('Additional details to include'),
102
+ }, async ({ description, style, mood, details }) => {
103
+ logger.info(`Generating image prompt for: ${description}`);
104
+ try {
105
+ // Import the text generation function
106
+ const { generateWithGeminiPro } = await import('../gemini-client.js');
107
+ const prompt = `
108
+ You are an expert at creating detailed text-to-image prompts for generative AI art tools.
109
+ Based on the following description, create a highly detailed, structured prompt that would produce the best possible image.
110
+
111
+ Description: ${description}
112
+ ${style ? `Style: ${style}` : ''}
113
+ ${mood ? `Mood: ${mood}` : ''}
114
+ ${details ? `Additional details: ${details}` : ''}
115
+
116
+ Format your response as follows:
117
+ 1. A refined one-paragraph image prompt that's highly detailed and descriptive
118
+ 2. Key elements that should be emphasized
119
+ 3. Technical suggestions (like camera angle, lighting, etc.)
120
+ 4. Style references that would work well
121
+
122
+ Use detail-rich, vivid language that generative AI image models would respond well to.
123
+ `;
124
+ const response = await generateWithGeminiPro(prompt);
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: response,
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ catch (error) {
135
+ const errorMessage = error instanceof Error ? error.message : String(error);
136
+ logger.error(`Error generating image prompt: ${errorMessage}`);
137
+ return {
138
+ content: [
139
+ {
140
+ type: 'text',
141
+ text: `Error: ${errorMessage}`,
142
+ },
143
+ ],
144
+ isError: true,
145
+ };
146
+ }
147
+ });
148
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Query Tool - Send direct queries to Gemini models
3
+ *
4
+ * This tool allows sending prompts directly to Gemini and receiving responses.
5
+ * Supports Gemini 3's thinking levels for controlling reasoning depth.
6
+ */
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ /**
9
+ * Register query tools with the MCP server
10
+ */
11
+ export declare function registerQueryTool(server: McpServer): void;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Query Tool - Send direct queries to Gemini models
3
+ *
4
+ * This tool allows sending prompts directly to Gemini and receiving responses.
5
+ * Supports Gemini 3's thinking levels for controlling reasoning depth.
6
+ */
7
+ import { z } from 'zod';
8
+ import { generateWithGeminiPro, generateWithGeminiFlash, } from '../gemini-client.js';
9
+ /**
10
+ * Register query tools with the MCP server
11
+ */
12
+ export function registerQueryTool(server) {
13
+ // Standard query tool using Pro model with thinking level support
14
+ server.tool('gemini-query', {
15
+ prompt: z.string().describe('The prompt to send to Gemini'),
16
+ model: z
17
+ .enum(['pro', 'flash'])
18
+ .default('pro')
19
+ .describe('The Gemini model to use (pro or flash)'),
20
+ thinkingLevel: z
21
+ .enum(['minimal', 'low', 'medium', 'high'])
22
+ .optional()
23
+ .describe('Reasoning depth: minimal/low for fast responses, medium/high for complex tasks. ' +
24
+ 'Pro supports low/high only. Flash supports all levels. Default is high.'),
25
+ }, async ({ prompt, model, thinkingLevel }) => {
26
+ console.log(`Querying Gemini ${model} model (thinking: ${thinkingLevel || 'default'}) with prompt: ${prompt.substring(0, 100)}...`);
27
+ try {
28
+ const options = thinkingLevel
29
+ ? { thinkingLevel: thinkingLevel }
30
+ : {};
31
+ const response = model === 'pro'
32
+ ? await generateWithGeminiPro(prompt, options)
33
+ : await generateWithGeminiFlash(prompt, options);
34
+ // Check for empty response to avoid potential MCP errors
35
+ if (!response || response.trim() === "") {
36
+ return {
37
+ content: [{
38
+ type: "text",
39
+ text: "Error: Received empty response from Gemini API"
40
+ }],
41
+ isError: true
42
+ };
43
+ }
44
+ return {
45
+ content: [{
46
+ type: "text",
47
+ text: response
48
+ }]
49
+ };
50
+ }
51
+ catch (error) {
52
+ const errorMessage = error instanceof Error ? error.message : String(error);
53
+ console.error(`Error querying Gemini: ${errorMessage}`);
54
+ return {
55
+ content: [{
56
+ type: "text",
57
+ text: `Error: ${errorMessage}`
58
+ }],
59
+ isError: true
60
+ };
61
+ }
62
+ });
63
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Google Search Grounding Tool - Real-time web information with citations
3
+ *
4
+ * This tool connects Gemini to Google Search for:
5
+ * - Accurate answers grounded in real-world information
6
+ * - Access to recent events and current topics
7
+ * - Verifiable sources with citations
8
+ *
9
+ * Returns responses with inline citations linked to source URLs.
10
+ */
11
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ /**
13
+ * Register Google Search grounding tools with the MCP server
14
+ */
15
+ export declare function registerSearchTool(server: McpServer): void;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Google Search Grounding Tool - Real-time web information with citations
3
+ *
4
+ * This tool connects Gemini to Google Search for:
5
+ * - Accurate answers grounded in real-world information
6
+ * - Access to recent events and current topics
7
+ * - Verifiable sources with citations
8
+ *
9
+ * Returns responses with inline citations linked to source URLs.
10
+ */
11
+ import { z } from 'zod';
12
+ import { GoogleGenAI } from '@google/genai';
13
+ import { logger } from '../utils/logger.js';
14
+ /**
15
+ * Add inline citations to text based on grounding metadata
16
+ */
17
+ function addCitations(text, supports, chunks) {
18
+ if (!supports || !chunks || supports.length === 0) {
19
+ return text;
20
+ }
21
+ // Sort supports by endIndex in descending order to avoid shifting issues
22
+ const sortedSupports = [...supports].sort((a, b) => (b.segment?.endIndex ?? 0) - (a.segment?.endIndex ?? 0));
23
+ let result = text;
24
+ for (const support of sortedSupports) {
25
+ const endIndex = support.segment?.endIndex;
26
+ if (endIndex === undefined || !support.groundingChunkIndices?.length) {
27
+ continue;
28
+ }
29
+ const citationLinks = support.groundingChunkIndices
30
+ .map((i) => {
31
+ const uri = chunks[i]?.web?.uri;
32
+ const title = chunks[i]?.web?.title;
33
+ if (uri) {
34
+ return `[${title || i + 1}](${uri})`;
35
+ }
36
+ return null;
37
+ })
38
+ .filter(Boolean);
39
+ if (citationLinks.length > 0) {
40
+ const citationString = ' ' + citationLinks.join(', ');
41
+ result = result.slice(0, endIndex) + citationString + result.slice(endIndex);
42
+ }
43
+ }
44
+ return result;
45
+ }
46
+ /**
47
+ * Register Google Search grounding tools with the MCP server
48
+ */
49
+ export function registerSearchTool(server) {
50
+ server.tool('gemini-search', {
51
+ query: z
52
+ .string()
53
+ .describe('The question or topic to search for. Gemini will use Google Search to find current information.'),
54
+ returnCitations: z
55
+ .boolean()
56
+ .default(true)
57
+ .describe('Include inline citations with source URLs'),
58
+ }, async ({ query, returnCitations }) => {
59
+ logger.info(`Google Search query: ${query.substring(0, 50)}...`);
60
+ try {
61
+ const apiKey = process.env.GEMINI_API_KEY;
62
+ if (!apiKey) {
63
+ throw new Error('GEMINI_API_KEY not set');
64
+ }
65
+ const genAI = new GoogleGenAI({ apiKey });
66
+ const model = process.env.GEMINI_PRO_MODEL || 'gemini-3-pro-preview';
67
+ // Execute with Google Search tool enabled
68
+ const response = await genAI.models.generateContent({
69
+ model,
70
+ contents: query,
71
+ config: {
72
+ tools: [{ googleSearch: {} }],
73
+ },
74
+ });
75
+ const candidate = response.candidates?.[0];
76
+ if (!candidate) {
77
+ throw new Error('No response from search');
78
+ }
79
+ let responseText = response.text || '';
80
+ const groundingMetadata = candidate.groundingMetadata;
81
+ // Build response with citations if requested
82
+ if (returnCitations && groundingMetadata) {
83
+ const supports = groundingMetadata.groundingSupports || [];
84
+ const chunks = groundingMetadata.groundingChunks || [];
85
+ if (supports.length > 0 && chunks.length > 0) {
86
+ responseText = addCitations(responseText, supports, chunks);
87
+ }
88
+ // Add sources section at the end
89
+ if (chunks.length > 0) {
90
+ responseText += '\n\n---\n**Sources:**\n';
91
+ const seenUrls = new Set();
92
+ for (const chunk of chunks) {
93
+ if (chunk.web?.uri && !seenUrls.has(chunk.web.uri)) {
94
+ seenUrls.add(chunk.web.uri);
95
+ responseText += `- [${chunk.web.title || 'Source'}](${chunk.web.uri})\n`;
96
+ }
97
+ }
98
+ }
99
+ // Add search queries used (for transparency)
100
+ if (groundingMetadata.webSearchQueries?.length) {
101
+ responseText += `\n*Searches performed: ${groundingMetadata.webSearchQueries.join(', ')}*`;
102
+ }
103
+ }
104
+ logger.info('Google Search completed successfully');
105
+ return {
106
+ content: [
107
+ {
108
+ type: 'text',
109
+ text: responseText,
110
+ },
111
+ ],
112
+ };
113
+ }
114
+ catch (error) {
115
+ const errorMessage = error instanceof Error ? error.message : String(error);
116
+ logger.error(`Error in Google Search: ${errorMessage}`);
117
+ return {
118
+ content: [
119
+ {
120
+ type: 'text',
121
+ text: `Error performing search: ${errorMessage}`,
122
+ },
123
+ ],
124
+ isError: true,
125
+ };
126
+ }
127
+ });
128
+ }