@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,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speech Generation Tool - Text-to-Speech with Gemini
|
|
3
|
+
*
|
|
4
|
+
* Generate high-quality speech from text using Gemini's native TTS.
|
|
5
|
+
* Features:
|
|
6
|
+
* - 30 voice options with different tones and styles
|
|
7
|
+
* - Multi-speaker support (up to 2 speakers)
|
|
8
|
+
* - Controllable style, accent, pace via natural language
|
|
9
|
+
* - 24 language support
|
|
10
|
+
*
|
|
11
|
+
* Output: WAV files saved to the output directory
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
/**
|
|
15
|
+
* Register speech generation tools with the MCP server
|
|
16
|
+
*/
|
|
17
|
+
export declare function registerSpeechTool(server: McpServer): void;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speech Generation Tool - Text-to-Speech with Gemini
|
|
3
|
+
*
|
|
4
|
+
* Generate high-quality speech from text using Gemini's native TTS.
|
|
5
|
+
* Features:
|
|
6
|
+
* - 30 voice options with different tones and styles
|
|
7
|
+
* - Multi-speaker support (up to 2 speakers)
|
|
8
|
+
* - Controllable style, accent, pace via natural language
|
|
9
|
+
* - 24 language support
|
|
10
|
+
*
|
|
11
|
+
* Output: WAV files saved to the output directory
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { GoogleGenAI, Modality } from '@google/genai';
|
|
15
|
+
import { logger } from '../utils/logger.js';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
// Get output directory for generated files
|
|
19
|
+
function getOutputDir() {
|
|
20
|
+
return (process.env.GEMINI_OUTPUT_DIR || path.join(process.cwd(), 'gemini-output'));
|
|
21
|
+
}
|
|
22
|
+
// Save PCM audio as WAV file
|
|
23
|
+
function saveWavFile(filename, pcmData, channels = 1, sampleRate = 24000, bitsPerSample = 16) {
|
|
24
|
+
const outputDir = getOutputDir();
|
|
25
|
+
if (!fs.existsSync(outputDir)) {
|
|
26
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
const filePath = path.join(outputDir, filename);
|
|
29
|
+
// Create WAV header
|
|
30
|
+
const byteRate = (sampleRate * channels * bitsPerSample) / 8;
|
|
31
|
+
const blockAlign = (channels * bitsPerSample) / 8;
|
|
32
|
+
const dataSize = pcmData.length;
|
|
33
|
+
const headerSize = 44;
|
|
34
|
+
const fileSize = headerSize + dataSize - 8;
|
|
35
|
+
const header = Buffer.alloc(headerSize);
|
|
36
|
+
// RIFF header
|
|
37
|
+
header.write('RIFF', 0);
|
|
38
|
+
header.writeUInt32LE(fileSize, 4);
|
|
39
|
+
header.write('WAVE', 8);
|
|
40
|
+
// fmt chunk
|
|
41
|
+
header.write('fmt ', 12);
|
|
42
|
+
header.writeUInt32LE(16, 16); // chunk size
|
|
43
|
+
header.writeUInt16LE(1, 20); // audio format (PCM)
|
|
44
|
+
header.writeUInt16LE(channels, 22);
|
|
45
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
46
|
+
header.writeUInt32LE(byteRate, 28);
|
|
47
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
48
|
+
header.writeUInt16LE(bitsPerSample, 34);
|
|
49
|
+
// data chunk
|
|
50
|
+
header.write('data', 36);
|
|
51
|
+
header.writeUInt32LE(dataSize, 40);
|
|
52
|
+
// Write file
|
|
53
|
+
fs.writeFileSync(filePath, Buffer.concat([header, pcmData]));
|
|
54
|
+
return filePath;
|
|
55
|
+
}
|
|
56
|
+
// Available voices
|
|
57
|
+
const VOICES = [
|
|
58
|
+
'Zephyr',
|
|
59
|
+
'Puck',
|
|
60
|
+
'Charon',
|
|
61
|
+
'Kore',
|
|
62
|
+
'Fenrir',
|
|
63
|
+
'Leda',
|
|
64
|
+
'Orus',
|
|
65
|
+
'Aoede',
|
|
66
|
+
'Callirrhoe',
|
|
67
|
+
'Autonoe',
|
|
68
|
+
'Enceladus',
|
|
69
|
+
'Iapetus',
|
|
70
|
+
'Umbriel',
|
|
71
|
+
'Algieba',
|
|
72
|
+
'Despina',
|
|
73
|
+
'Erinome',
|
|
74
|
+
'Algenib',
|
|
75
|
+
'Rasalgethi',
|
|
76
|
+
'Laomedeia',
|
|
77
|
+
'Achernar',
|
|
78
|
+
'Alnilam',
|
|
79
|
+
'Schedar',
|
|
80
|
+
'Gacrux',
|
|
81
|
+
'Pulcherrima',
|
|
82
|
+
'Achird',
|
|
83
|
+
'Zubenelgenubi',
|
|
84
|
+
'Vindemiatrix',
|
|
85
|
+
'Sadachbia',
|
|
86
|
+
'Sadaltager',
|
|
87
|
+
'Sulafat',
|
|
88
|
+
];
|
|
89
|
+
/**
|
|
90
|
+
* Register speech generation tools with the MCP server
|
|
91
|
+
*/
|
|
92
|
+
export function registerSpeechTool(server) {
|
|
93
|
+
// Single speaker TTS
|
|
94
|
+
server.tool('gemini-speak', {
|
|
95
|
+
text: z.string().describe('The text to convert to speech'),
|
|
96
|
+
voice: z
|
|
97
|
+
.enum(VOICES)
|
|
98
|
+
.default('Kore')
|
|
99
|
+
.describe('Voice to use. Popular: Kore (firm), Puck (upbeat), Zephyr (bright), Charon (informative), Aoede (breezy)'),
|
|
100
|
+
style: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Style instructions (e.g., "cheerfully", "in a spooky whisper", "with excitement")'),
|
|
104
|
+
}, async ({ text, voice, style }) => {
|
|
105
|
+
logger.info(`Speech generation: ${text.substring(0, 50)}...`);
|
|
106
|
+
try {
|
|
107
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
108
|
+
if (!apiKey) {
|
|
109
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
110
|
+
}
|
|
111
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
112
|
+
const model = 'gemini-2.5-flash-preview-tts';
|
|
113
|
+
// Build prompt with optional style
|
|
114
|
+
const prompt = style ? `Say ${style}: "${text}"` : text;
|
|
115
|
+
// Generate speech
|
|
116
|
+
const response = await genAI.models.generateContent({
|
|
117
|
+
model,
|
|
118
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
119
|
+
config: {
|
|
120
|
+
responseModalities: [Modality.AUDIO],
|
|
121
|
+
speechConfig: {
|
|
122
|
+
voiceConfig: {
|
|
123
|
+
prebuiltVoiceConfig: { voiceName: voice },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
// Extract audio data
|
|
129
|
+
const audioData = response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
|
|
130
|
+
if (!audioData) {
|
|
131
|
+
throw new Error('No audio data in response');
|
|
132
|
+
}
|
|
133
|
+
// Save as WAV
|
|
134
|
+
const audioBuffer = Buffer.from(audioData, 'base64');
|
|
135
|
+
const timestamp = Date.now();
|
|
136
|
+
const filename = `speech-${timestamp}.wav`;
|
|
137
|
+
const filePath = saveWavFile(filename, audioBuffer);
|
|
138
|
+
logger.info(`Speech saved to: ${filePath}`);
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: `Speech generated successfully!\n\n**Voice:** ${voice}\n**File:** ${filePath}\n**Duration:** ~${Math.round(audioBuffer.length / 48000)}s\n\nThe audio file has been saved and can be played with any audio player.`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
150
|
+
logger.error(`Error in speech generation: ${errorMessage}`);
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: `Error generating speech: ${errorMessage}`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
isError: true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// Multi-speaker TTS (for dialogues/podcasts)
|
|
163
|
+
server.tool('gemini-dialogue', {
|
|
164
|
+
script: z
|
|
165
|
+
.string()
|
|
166
|
+
.describe('The dialogue script with speaker names. Format: "Speaker1: line\\nSpeaker2: line"'),
|
|
167
|
+
speaker1: z.string().describe('Name of first speaker as used in script'),
|
|
168
|
+
speaker1Voice: z
|
|
169
|
+
.enum(VOICES)
|
|
170
|
+
.default('Kore')
|
|
171
|
+
.describe('Voice for speaker 1'),
|
|
172
|
+
speaker2: z.string().describe('Name of second speaker as used in script'),
|
|
173
|
+
speaker2Voice: z
|
|
174
|
+
.enum(VOICES)
|
|
175
|
+
.default('Puck')
|
|
176
|
+
.describe('Voice for speaker 2'),
|
|
177
|
+
style: z
|
|
178
|
+
.string()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe('Style instructions for the dialogue (e.g., "Make Speaker1 sound tired, Speaker2 excited")'),
|
|
181
|
+
}, async ({ script, speaker1, speaker1Voice, speaker2, speaker2Voice, style, }) => {
|
|
182
|
+
logger.info(`Dialogue generation: ${speaker1} & ${speaker2}`);
|
|
183
|
+
try {
|
|
184
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
185
|
+
if (!apiKey) {
|
|
186
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
187
|
+
}
|
|
188
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
189
|
+
const model = 'gemini-2.5-flash-preview-tts';
|
|
190
|
+
// Build prompt
|
|
191
|
+
let prompt = script;
|
|
192
|
+
if (style) {
|
|
193
|
+
prompt = `${style}\n\n${script}`;
|
|
194
|
+
}
|
|
195
|
+
// Generate multi-speaker speech
|
|
196
|
+
const response = await genAI.models.generateContent({
|
|
197
|
+
model,
|
|
198
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
199
|
+
config: {
|
|
200
|
+
responseModalities: [Modality.AUDIO],
|
|
201
|
+
speechConfig: {
|
|
202
|
+
multiSpeakerVoiceConfig: {
|
|
203
|
+
speakerVoiceConfigs: [
|
|
204
|
+
{
|
|
205
|
+
speaker: speaker1,
|
|
206
|
+
voiceConfig: {
|
|
207
|
+
prebuiltVoiceConfig: { voiceName: speaker1Voice },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
speaker: speaker2,
|
|
212
|
+
voiceConfig: {
|
|
213
|
+
prebuiltVoiceConfig: { voiceName: speaker2Voice },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
// Extract audio data
|
|
222
|
+
const audioData = response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
|
|
223
|
+
if (!audioData) {
|
|
224
|
+
throw new Error('No audio data in response');
|
|
225
|
+
}
|
|
226
|
+
// Save as WAV
|
|
227
|
+
const audioBuffer = Buffer.from(audioData, 'base64');
|
|
228
|
+
const timestamp = Date.now();
|
|
229
|
+
const filename = `dialogue-${timestamp}.wav`;
|
|
230
|
+
const filePath = saveWavFile(filename, audioBuffer);
|
|
231
|
+
logger.info(`Dialogue saved to: ${filePath}`);
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: 'text',
|
|
236
|
+
text: `Dialogue generated successfully!\n\n**Speakers:**\n- ${speaker1}: ${speaker1Voice}\n- ${speaker2}: ${speaker2Voice}\n\n**File:** ${filePath}\n**Duration:** ~${Math.round(audioBuffer.length / 48000)}s`,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
243
|
+
logger.error(`Error in dialogue generation: ${errorMessage}`);
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: 'text',
|
|
248
|
+
text: `Error generating dialogue: ${errorMessage}`,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
isError: true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// List available voices
|
|
256
|
+
server.tool('gemini-list-voices', {}, async () => {
|
|
257
|
+
const voiceDescriptions = {
|
|
258
|
+
Zephyr: 'Bright',
|
|
259
|
+
Puck: 'Upbeat',
|
|
260
|
+
Charon: 'Informative',
|
|
261
|
+
Kore: 'Firm',
|
|
262
|
+
Fenrir: 'Excitable',
|
|
263
|
+
Leda: 'Youthful',
|
|
264
|
+
Orus: 'Firm',
|
|
265
|
+
Aoede: 'Breezy',
|
|
266
|
+
Callirrhoe: 'Easy-going',
|
|
267
|
+
Autonoe: 'Bright',
|
|
268
|
+
Enceladus: 'Breathy',
|
|
269
|
+
Iapetus: 'Clear',
|
|
270
|
+
Umbriel: 'Easy-going',
|
|
271
|
+
Algieba: 'Smooth',
|
|
272
|
+
Despina: 'Smooth',
|
|
273
|
+
Erinome: 'Clear',
|
|
274
|
+
Algenib: 'Gravelly',
|
|
275
|
+
Rasalgethi: 'Informative',
|
|
276
|
+
Laomedeia: 'Upbeat',
|
|
277
|
+
Achernar: 'Soft',
|
|
278
|
+
Alnilam: 'Firm',
|
|
279
|
+
Schedar: 'Even',
|
|
280
|
+
Gacrux: 'Mature',
|
|
281
|
+
Pulcherrima: 'Forward',
|
|
282
|
+
Achird: 'Friendly',
|
|
283
|
+
Zubenelgenubi: 'Casual',
|
|
284
|
+
Vindemiatrix: 'Gentle',
|
|
285
|
+
Sadachbia: 'Lively',
|
|
286
|
+
Sadaltager: 'Knowledgeable',
|
|
287
|
+
Sulafat: 'Warm',
|
|
288
|
+
};
|
|
289
|
+
let text = '**Available Voices for Speech Generation:**\n\n';
|
|
290
|
+
for (const voice of VOICES) {
|
|
291
|
+
text += `- **${voice}** - ${voiceDescriptions[voice] || ''}\n`;
|
|
292
|
+
}
|
|
293
|
+
text +=
|
|
294
|
+
'\n*Use these voices with gemini-speak or gemini-dialogue tools.*';
|
|
295
|
+
return {
|
|
296
|
+
content: [
|
|
297
|
+
{
|
|
298
|
+
type: 'text',
|
|
299
|
+
text,
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Output Tool - Get JSON responses with schema validation
|
|
3
|
+
*
|
|
4
|
+
* This tool enables Gemini to generate responses that adhere to a provided JSON Schema.
|
|
5
|
+
* Useful for:
|
|
6
|
+
* - Data extraction from unstructured text
|
|
7
|
+
* - Structured classification
|
|
8
|
+
* - Generating data for APIs or databases
|
|
9
|
+
*
|
|
10
|
+
* Supports combining with Google Search for grounded structured data.
|
|
11
|
+
*/
|
|
12
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
+
/**
|
|
14
|
+
* Register structured output tools with the MCP server
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerStructuredTool(server: McpServer): void;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Output Tool - Get JSON responses with schema validation
|
|
3
|
+
*
|
|
4
|
+
* This tool enables Gemini to generate responses that adhere to a provided JSON Schema.
|
|
5
|
+
* Useful for:
|
|
6
|
+
* - Data extraction from unstructured text
|
|
7
|
+
* - Structured classification
|
|
8
|
+
* - Generating data for APIs or databases
|
|
9
|
+
*
|
|
10
|
+
* Supports combining with Google Search for grounded structured data.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { GoogleGenAI } from '@google/genai';
|
|
14
|
+
import { logger } from '../utils/logger.js';
|
|
15
|
+
/**
|
|
16
|
+
* Register structured output tools with the MCP server
|
|
17
|
+
*/
|
|
18
|
+
export function registerStructuredTool(server) {
|
|
19
|
+
server.tool('gemini-structured', {
|
|
20
|
+
prompt: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe('The prompt or data to process'),
|
|
23
|
+
schema: z
|
|
24
|
+
.string()
|
|
25
|
+
.describe('JSON Schema as a string. Example: {"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name"]}'),
|
|
26
|
+
useGoogleSearch: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.default(false)
|
|
29
|
+
.describe('Use Google Search to ground the response in real-world data'),
|
|
30
|
+
}, async ({ prompt, schema, useGoogleSearch }) => {
|
|
31
|
+
logger.info(`Structured output request: ${prompt.substring(0, 50)}...`);
|
|
32
|
+
try {
|
|
33
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
34
|
+
if (!apiKey) {
|
|
35
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
36
|
+
}
|
|
37
|
+
// Parse the schema
|
|
38
|
+
let jsonSchema;
|
|
39
|
+
try {
|
|
40
|
+
jsonSchema = JSON.parse(schema);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error('Invalid JSON schema provided. Please provide a valid JSON Schema string.');
|
|
44
|
+
}
|
|
45
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
46
|
+
const model = process.env.GEMINI_PRO_MODEL || 'gemini-3-pro-preview';
|
|
47
|
+
// Build config
|
|
48
|
+
const config = {
|
|
49
|
+
responseMimeType: 'application/json',
|
|
50
|
+
responseJsonSchema: jsonSchema,
|
|
51
|
+
};
|
|
52
|
+
// Add Google Search if requested
|
|
53
|
+
if (useGoogleSearch) {
|
|
54
|
+
config.tools = [{ googleSearch: {} }];
|
|
55
|
+
}
|
|
56
|
+
// Execute
|
|
57
|
+
const response = await genAI.models.generateContent({
|
|
58
|
+
model,
|
|
59
|
+
contents: prompt,
|
|
60
|
+
config,
|
|
61
|
+
});
|
|
62
|
+
const responseText = response.text || '';
|
|
63
|
+
// Validate that we got valid JSON
|
|
64
|
+
let parsedResponse;
|
|
65
|
+
try {
|
|
66
|
+
parsedResponse = JSON.parse(responseText);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error('Response was not valid JSON');
|
|
70
|
+
}
|
|
71
|
+
// Format nicely for display
|
|
72
|
+
const formattedJson = JSON.stringify(parsedResponse, null, 2);
|
|
73
|
+
logger.info('Structured output completed successfully');
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: `**Structured Response:**\n\`\`\`json\n${formattedJson}\n\`\`\``,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
85
|
+
logger.error(`Error in structured output: ${errorMessage}`);
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: `Error generating structured output: ${errorMessage}`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// Convenience tool for common extraction patterns
|
|
98
|
+
server.tool('gemini-extract', {
|
|
99
|
+
text: z.string().describe('The text to extract information from'),
|
|
100
|
+
extractType: z
|
|
101
|
+
.enum(['entities', 'facts', 'summary', 'keywords', 'sentiment', 'custom'])
|
|
102
|
+
.describe('What type of information to extract'),
|
|
103
|
+
customFields: z
|
|
104
|
+
.string()
|
|
105
|
+
.optional()
|
|
106
|
+
.describe('For custom extraction: comma-separated list of fields to extract. Example: "name, date, amount, description"'),
|
|
107
|
+
}, async ({ text, extractType, customFields }) => {
|
|
108
|
+
logger.info(`Extraction request: ${extractType}`);
|
|
109
|
+
try {
|
|
110
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
111
|
+
if (!apiKey) {
|
|
112
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
113
|
+
}
|
|
114
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
115
|
+
const model = process.env.GEMINI_FLASH_MODEL || 'gemini-3-flash-preview';
|
|
116
|
+
// Build schema based on extraction type
|
|
117
|
+
let schema;
|
|
118
|
+
let prompt;
|
|
119
|
+
switch (extractType) {
|
|
120
|
+
case 'entities':
|
|
121
|
+
schema = {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
people: { type: 'array', items: { type: 'string' }, description: 'Names of people mentioned' },
|
|
125
|
+
organizations: { type: 'array', items: { type: 'string' }, description: 'Organizations mentioned' },
|
|
126
|
+
locations: { type: 'array', items: { type: 'string' }, description: 'Locations mentioned' },
|
|
127
|
+
dates: { type: 'array', items: { type: 'string' }, description: 'Dates mentioned' },
|
|
128
|
+
amounts: { type: 'array', items: { type: 'string' }, description: 'Monetary amounts or quantities' },
|
|
129
|
+
},
|
|
130
|
+
required: ['people', 'organizations', 'locations', 'dates', 'amounts'],
|
|
131
|
+
};
|
|
132
|
+
prompt = `Extract all named entities from the following text:\n\n${text}`;
|
|
133
|
+
break;
|
|
134
|
+
case 'facts':
|
|
135
|
+
schema = {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
facts: {
|
|
139
|
+
type: 'array',
|
|
140
|
+
items: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
fact: { type: 'string', description: 'A factual statement' },
|
|
144
|
+
confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
|
|
145
|
+
},
|
|
146
|
+
required: ['fact', 'confidence'],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
required: ['facts'],
|
|
151
|
+
};
|
|
152
|
+
prompt = `Extract all factual statements from the following text:\n\n${text}`;
|
|
153
|
+
break;
|
|
154
|
+
case 'summary':
|
|
155
|
+
schema = {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
title: { type: 'string', description: 'A brief title' },
|
|
159
|
+
summary: { type: 'string', description: 'A concise summary' },
|
|
160
|
+
keyPoints: { type: 'array', items: { type: 'string' }, description: 'Key points' },
|
|
161
|
+
topics: { type: 'array', items: { type: 'string' }, description: 'Main topics covered' },
|
|
162
|
+
},
|
|
163
|
+
required: ['title', 'summary', 'keyPoints', 'topics'],
|
|
164
|
+
};
|
|
165
|
+
prompt = `Summarize the following text and extract key information:\n\n${text}`;
|
|
166
|
+
break;
|
|
167
|
+
case 'keywords':
|
|
168
|
+
schema = {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
keywords: { type: 'array', items: { type: 'string' }, description: 'Important keywords' },
|
|
172
|
+
phrases: { type: 'array', items: { type: 'string' }, description: 'Important phrases' },
|
|
173
|
+
categories: { type: 'array', items: { type: 'string' }, description: 'Topic categories' },
|
|
174
|
+
},
|
|
175
|
+
required: ['keywords', 'phrases', 'categories'],
|
|
176
|
+
};
|
|
177
|
+
prompt = `Extract keywords, important phrases, and topic categories from the following text:\n\n${text}`;
|
|
178
|
+
break;
|
|
179
|
+
case 'sentiment':
|
|
180
|
+
schema = {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
overallSentiment: { type: 'string', enum: ['positive', 'negative', 'neutral', 'mixed'] },
|
|
184
|
+
sentimentScore: { type: 'number', description: 'Score from -1 (negative) to 1 (positive)' },
|
|
185
|
+
emotions: { type: 'array', items: { type: 'string' }, description: 'Emotions detected' },
|
|
186
|
+
reasoning: { type: 'string', description: 'Brief explanation of the sentiment analysis' },
|
|
187
|
+
},
|
|
188
|
+
required: ['overallSentiment', 'sentimentScore', 'emotions', 'reasoning'],
|
|
189
|
+
};
|
|
190
|
+
prompt = `Analyze the sentiment of the following text:\n\n${text}`;
|
|
191
|
+
break;
|
|
192
|
+
case 'custom':
|
|
193
|
+
if (!customFields) {
|
|
194
|
+
throw new Error('customFields is required for custom extraction');
|
|
195
|
+
}
|
|
196
|
+
const fields = customFields.split(',').map((f) => f.trim());
|
|
197
|
+
const properties = {};
|
|
198
|
+
for (const field of fields) {
|
|
199
|
+
properties[field] = { type: 'string', description: `The ${field} extracted from the text` };
|
|
200
|
+
}
|
|
201
|
+
schema = {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties,
|
|
204
|
+
required: fields,
|
|
205
|
+
};
|
|
206
|
+
prompt = `Extract the following information from the text: ${customFields}\n\nText:\n${text}`;
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
throw new Error(`Unknown extraction type: ${extractType}`);
|
|
210
|
+
}
|
|
211
|
+
// Execute
|
|
212
|
+
const response = await genAI.models.generateContent({
|
|
213
|
+
model,
|
|
214
|
+
contents: prompt,
|
|
215
|
+
config: {
|
|
216
|
+
responseMimeType: 'application/json',
|
|
217
|
+
responseJsonSchema: schema,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
const responseText = response.text || '';
|
|
221
|
+
const parsedResponse = JSON.parse(responseText);
|
|
222
|
+
const formattedJson = JSON.stringify(parsedResponse, null, 2);
|
|
223
|
+
logger.info('Extraction completed successfully');
|
|
224
|
+
return {
|
|
225
|
+
content: [
|
|
226
|
+
{
|
|
227
|
+
type: 'text',
|
|
228
|
+
text: `**Extracted ${extractType}:**\n\`\`\`json\n${formattedJson}\n\`\`\``,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
235
|
+
logger.error(`Error in extraction: ${errorMessage}`);
|
|
236
|
+
return {
|
|
237
|
+
content: [
|
|
238
|
+
{
|
|
239
|
+
type: 'text',
|
|
240
|
+
text: `Error extracting data: ${errorMessage}`,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
isError: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarize Tool - Provides content summarization using Gemini models
|
|
3
|
+
*
|
|
4
|
+
* This tool allows summarizing long text content at different levels of detail.
|
|
5
|
+
*/
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
/**
|
|
8
|
+
* Register summarization tools with the MCP server
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerSummarizeTool(server: McpServer): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarize Tool - Provides content summarization using Gemini models
|
|
3
|
+
*
|
|
4
|
+
* This tool allows summarizing long text content at different levels of detail.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { generateWithGeminiFlash } from "../gemini-client.js";
|
|
8
|
+
/**
|
|
9
|
+
* Register summarization tools with the MCP server
|
|
10
|
+
*/
|
|
11
|
+
export function registerSummarizeTool(server) {
|
|
12
|
+
server.tool("gemini-summarize", {
|
|
13
|
+
content: z.string().describe("The content to summarize"),
|
|
14
|
+
length: z.enum(["brief", "moderate", "detailed"]).default("moderate").describe("The desired summary length"),
|
|
15
|
+
format: z.enum(["paragraph", "bullet-points", "outline"]).default("paragraph").describe("The output format")
|
|
16
|
+
}, async ({ content, length, format }) => {
|
|
17
|
+
console.log(`Summarizing content (${length}, ${format})`);
|
|
18
|
+
try {
|
|
19
|
+
// Configure length parameters
|
|
20
|
+
let wordCount;
|
|
21
|
+
switch (length) {
|
|
22
|
+
case "brief":
|
|
23
|
+
wordCount = "50-100 words";
|
|
24
|
+
break;
|
|
25
|
+
case "moderate":
|
|
26
|
+
wordCount = "150-250 words";
|
|
27
|
+
break;
|
|
28
|
+
case "detailed":
|
|
29
|
+
wordCount = "300-500 words";
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
wordCount = "150-250 words";
|
|
33
|
+
}
|
|
34
|
+
// Configure format instructions
|
|
35
|
+
let formatInstructions;
|
|
36
|
+
switch (format) {
|
|
37
|
+
case "bullet-points":
|
|
38
|
+
formatInstructions = "Use bullet points for each key idea or concept.";
|
|
39
|
+
break;
|
|
40
|
+
case "outline":
|
|
41
|
+
formatInstructions = "Create a structured outline with main points and sub-points.";
|
|
42
|
+
break;
|
|
43
|
+
case "paragraph":
|
|
44
|
+
default:
|
|
45
|
+
formatInstructions = "Use cohesive paragraphs with clear transitions.";
|
|
46
|
+
}
|
|
47
|
+
const prompt = `
|
|
48
|
+
Summarize the following content in ${wordCount}. ${formatInstructions}
|
|
49
|
+
|
|
50
|
+
Focus on capturing the most important information, main arguments, and key conclusions.
|
|
51
|
+
|
|
52
|
+
Content to summarize:
|
|
53
|
+
"""
|
|
54
|
+
${content}
|
|
55
|
+
"""
|
|
56
|
+
`;
|
|
57
|
+
const response = await generateWithGeminiFlash(prompt);
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: response
|
|
62
|
+
}]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
67
|
+
console.error(`Error summarizing content: ${errorMessage}`);
|
|
68
|
+
return {
|
|
69
|
+
content: [{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: `Error: ${errorMessage}`
|
|
72
|
+
}],
|
|
73
|
+
isError: true
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|