@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.
- package/LICENCE +21 -0
- package/README.md +418 -0
- package/dist/gemini-client.d.ts +120 -0
- package/dist/gemini-client.js +399 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +220 -0
- package/dist/tools/analyze.d.ts +10 -0
- package/dist/tools/analyze.js +96 -0
- package/dist/tools/brainstorm.d.ts +10 -0
- package/dist/tools/brainstorm.js +220 -0
- package/dist/tools/cache.d.ts +17 -0
- package/dist/tools/cache.js +286 -0
- package/dist/tools/code-exec.d.ts +17 -0
- package/dist/tools/code-exec.js +135 -0
- package/dist/tools/document.d.ts +16 -0
- package/dist/tools/document.js +333 -0
- package/dist/tools/image-edit.d.ts +16 -0
- package/dist/tools/image-edit.js +291 -0
- package/dist/tools/image-gen.d.ts +17 -0
- package/dist/tools/image-gen.js +148 -0
- package/dist/tools/query.d.ts +11 -0
- package/dist/tools/query.js +63 -0
- package/dist/tools/search.d.ts +15 -0
- package/dist/tools/search.js +128 -0
- package/dist/tools/speech.d.ts +17 -0
- package/dist/tools/speech.js +304 -0
- package/dist/tools/structured.d.ts +16 -0
- package/dist/tools/structured.js +247 -0
- package/dist/tools/summarize.d.ts +10 -0
- package/dist/tools/summarize.js +77 -0
- package/dist/tools/url-context.d.ts +17 -0
- package/dist/tools/url-context.js +226 -0
- package/dist/tools/video-gen.d.ts +11 -0
- package/dist/tools/video-gen.js +136 -0
- package/dist/tools/youtube.d.ts +16 -0
- package/dist/tools/youtube.js +218 -0
- package/dist/utils/logger.d.ts +33 -0
- package/dist/utils/logger.js +82 -0
- 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
|
+
}
|