@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
|
+
* URL Context Tool - Analyze web pages by URL
|
|
3
|
+
*
|
|
4
|
+
* The URL context tool lets you provide URLs for Gemini to analyze.
|
|
5
|
+
* Useful for:
|
|
6
|
+
* - Extract data from web pages (prices, names, key findings)
|
|
7
|
+
* - Compare documents from multiple URLs
|
|
8
|
+
* - Synthesize content from several sources
|
|
9
|
+
* - Analyze code from GitHub or documentation sites
|
|
10
|
+
*
|
|
11
|
+
* Can be combined with Google Search for powerful workflows.
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
/**
|
|
15
|
+
* Register URL context tools with the MCP server
|
|
16
|
+
*/
|
|
17
|
+
export declare function registerUrlContextTool(server: McpServer): void;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Context Tool - Analyze web pages by URL
|
|
3
|
+
*
|
|
4
|
+
* The URL context tool lets you provide URLs for Gemini to analyze.
|
|
5
|
+
* Useful for:
|
|
6
|
+
* - Extract data from web pages (prices, names, key findings)
|
|
7
|
+
* - Compare documents from multiple URLs
|
|
8
|
+
* - Synthesize content from several sources
|
|
9
|
+
* - Analyze code from GitHub or documentation sites
|
|
10
|
+
*
|
|
11
|
+
* Can be combined with Google Search for powerful workflows.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { GoogleGenAI } from '@google/genai';
|
|
15
|
+
import { logger } from '../utils/logger.js';
|
|
16
|
+
/**
|
|
17
|
+
* Register URL context tools with the MCP server
|
|
18
|
+
*/
|
|
19
|
+
export function registerUrlContextTool(server) {
|
|
20
|
+
server.tool('gemini-analyze-url', {
|
|
21
|
+
urls: z
|
|
22
|
+
.array(z.string())
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(20)
|
|
25
|
+
.describe('URLs to analyze (1-20 URLs)'),
|
|
26
|
+
question: z
|
|
27
|
+
.string()
|
|
28
|
+
.describe('Question about the URLs or task to perform'),
|
|
29
|
+
useGoogleSearch: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.default(false)
|
|
32
|
+
.describe('Also use Google Search to find additional context'),
|
|
33
|
+
}, async ({ urls, question, useGoogleSearch }) => {
|
|
34
|
+
logger.info(`URL context analysis: ${urls.length} URLs`);
|
|
35
|
+
try {
|
|
36
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
37
|
+
if (!apiKey) {
|
|
38
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
39
|
+
}
|
|
40
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
41
|
+
const model = process.env.GEMINI_PRO_MODEL || 'gemini-3-pro-preview';
|
|
42
|
+
// Build the prompt with URLs
|
|
43
|
+
const urlList = urls.map((url, i) => `${i + 1}. ${url}`).join('\n');
|
|
44
|
+
const prompt = `${question}\n\nURLs to analyze:\n${urlList}`;
|
|
45
|
+
// Build tools config
|
|
46
|
+
const tools = [{ urlContext: {} }];
|
|
47
|
+
if (useGoogleSearch) {
|
|
48
|
+
tools.push({ googleSearch: {} });
|
|
49
|
+
}
|
|
50
|
+
// Execute
|
|
51
|
+
const response = await genAI.models.generateContent({
|
|
52
|
+
model,
|
|
53
|
+
contents: prompt,
|
|
54
|
+
config: {
|
|
55
|
+
tools,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const candidate = response.candidates?.[0];
|
|
59
|
+
if (!candidate) {
|
|
60
|
+
throw new Error('No response from URL analysis');
|
|
61
|
+
}
|
|
62
|
+
let responseText = response.text || '';
|
|
63
|
+
// Add URL retrieval status if available
|
|
64
|
+
const urlContextMetadata = candidate.urlContextMetadata;
|
|
65
|
+
if (urlContextMetadata?.urlMetadata) {
|
|
66
|
+
responseText += '\n\n---\n**URL Retrieval Status:**\n';
|
|
67
|
+
for (const meta of urlContextMetadata.urlMetadata) {
|
|
68
|
+
const status = meta.urlRetrievalStatus === 'URL_RETRIEVAL_STATUS_SUCCESS'
|
|
69
|
+
? '✓'
|
|
70
|
+
: '✗';
|
|
71
|
+
responseText += `${status} ${meta.retrievedUrl}\n`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
logger.info('URL context analysis completed');
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: responseText,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
86
|
+
logger.error(`Error in URL analysis: ${errorMessage}`);
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: `Error analyzing URLs: ${errorMessage}`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
isError: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// Convenience tool for comparing content from multiple URLs
|
|
99
|
+
server.tool('gemini-compare-urls', {
|
|
100
|
+
url1: z.string().describe('First URL to compare'),
|
|
101
|
+
url2: z.string().describe('Second URL to compare'),
|
|
102
|
+
aspect: z
|
|
103
|
+
.string()
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Specific aspect to compare (e.g., "pricing", "features", "ingredients")'),
|
|
106
|
+
}, async ({ url1, url2, aspect }) => {
|
|
107
|
+
logger.info(`URL comparison: ${url1} vs ${url2}`);
|
|
108
|
+
try {
|
|
109
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
110
|
+
if (!apiKey) {
|
|
111
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
112
|
+
}
|
|
113
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
114
|
+
const model = process.env.GEMINI_FLASH_MODEL || 'gemini-3-flash-preview';
|
|
115
|
+
const prompt = aspect
|
|
116
|
+
? `Compare the ${aspect} from these two URLs:\n1. ${url1}\n2. ${url2}\n\nProvide a detailed comparison highlighting differences and similarities.`
|
|
117
|
+
: `Compare the content from these two URLs:\n1. ${url1}\n2. ${url2}\n\nProvide a comprehensive comparison of key information from both sources.`;
|
|
118
|
+
const response = await genAI.models.generateContent({
|
|
119
|
+
model,
|
|
120
|
+
contents: prompt,
|
|
121
|
+
config: {
|
|
122
|
+
tools: [{ urlContext: {} }],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
logger.info('URL comparison completed');
|
|
126
|
+
return {
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: 'text',
|
|
130
|
+
text: response.text || 'Unable to compare URLs.',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
137
|
+
logger.error(`Error in URL comparison: ${errorMessage}`);
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: 'text',
|
|
142
|
+
text: `Error comparing URLs: ${errorMessage}`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
isError: true,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
// Tool for extracting specific data from URLs
|
|
150
|
+
server.tool('gemini-extract-from-url', {
|
|
151
|
+
url: z.string().describe('URL to extract data from'),
|
|
152
|
+
dataType: z
|
|
153
|
+
.enum(['prices', 'contacts', 'dates', 'products', 'links', 'custom'])
|
|
154
|
+
.describe('Type of data to extract'),
|
|
155
|
+
customFields: z
|
|
156
|
+
.string()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe('For custom extraction: comma-separated fields to extract'),
|
|
159
|
+
}, async ({ url, dataType, customFields }) => {
|
|
160
|
+
logger.info(`URL extraction: ${dataType} from ${url}`);
|
|
161
|
+
try {
|
|
162
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
163
|
+
if (!apiKey) {
|
|
164
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
165
|
+
}
|
|
166
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
167
|
+
const model = process.env.GEMINI_FLASH_MODEL || 'gemini-3-flash-preview';
|
|
168
|
+
// Build prompt based on data type
|
|
169
|
+
let prompt;
|
|
170
|
+
switch (dataType) {
|
|
171
|
+
case 'prices':
|
|
172
|
+
prompt = `Extract all prices and pricing information from this URL: ${url}\n\nReturn as a structured list with item name and price.`;
|
|
173
|
+
break;
|
|
174
|
+
case 'contacts':
|
|
175
|
+
prompt = `Extract all contact information (emails, phone numbers, addresses, social media) from this URL: ${url}`;
|
|
176
|
+
break;
|
|
177
|
+
case 'dates':
|
|
178
|
+
prompt = `Extract all dates, times, and scheduling information from this URL: ${url}`;
|
|
179
|
+
break;
|
|
180
|
+
case 'products':
|
|
181
|
+
prompt = `Extract all product names, descriptions, and details from this URL: ${url}`;
|
|
182
|
+
break;
|
|
183
|
+
case 'links':
|
|
184
|
+
prompt = `Extract all important links from this URL: ${url}\n\nCategorize them by purpose.`;
|
|
185
|
+
break;
|
|
186
|
+
case 'custom':
|
|
187
|
+
if (!customFields) {
|
|
188
|
+
throw new Error('customFields required for custom extraction');
|
|
189
|
+
}
|
|
190
|
+
prompt = `Extract the following information from this URL: ${url}\n\nFields to extract: ${customFields}`;
|
|
191
|
+
break;
|
|
192
|
+
default:
|
|
193
|
+
prompt = `Extract key information from this URL: ${url}`;
|
|
194
|
+
}
|
|
195
|
+
const response = await genAI.models.generateContent({
|
|
196
|
+
model,
|
|
197
|
+
contents: prompt,
|
|
198
|
+
config: {
|
|
199
|
+
tools: [{ urlContext: {} }],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
logger.info('URL extraction completed');
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: response.text || 'No data extracted.',
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
214
|
+
logger.error(`Error in URL extraction: ${errorMessage}`);
|
|
215
|
+
return {
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: 'text',
|
|
219
|
+
text: `Error extracting from URL: ${errorMessage}`,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
isError: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Generation Tool - Generate videos using Gemini's Veo model
|
|
3
|
+
*
|
|
4
|
+
* This tool generates videos from text descriptions. Video generation is async,
|
|
5
|
+
* so we provide tools to start generation and check status.
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
/**
|
|
9
|
+
* Register video generation tools with the MCP server
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerVideoGenTool(server: McpServer): void;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Generation Tool - Generate videos using Gemini's Veo model
|
|
3
|
+
*
|
|
4
|
+
* This tool generates videos from text descriptions. Video generation is async,
|
|
5
|
+
* so we provide tools to start generation and check status.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { startVideoGeneration, checkVideoStatus, getOutputDir, } from '../gemini-client.js';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
/**
|
|
11
|
+
* Register video generation tools with the MCP server
|
|
12
|
+
*/
|
|
13
|
+
export function registerVideoGenTool(server) {
|
|
14
|
+
// Start video generation
|
|
15
|
+
server.tool('gemini-generate-video', {
|
|
16
|
+
prompt: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe('Description of the video to generate (be detailed!)'),
|
|
19
|
+
aspectRatio: z
|
|
20
|
+
.enum(['16:9', '9:16'])
|
|
21
|
+
.default('16:9')
|
|
22
|
+
.describe('Aspect ratio (16:9 for landscape, 9:16 for portrait/mobile)'),
|
|
23
|
+
negativePrompt: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Things to avoid in the video (e.g., "text, watermarks, blurry")'),
|
|
27
|
+
}, async ({ prompt, aspectRatio, negativePrompt }) => {
|
|
28
|
+
logger.info(`Starting video generation: ${prompt.substring(0, 50)}...`);
|
|
29
|
+
try {
|
|
30
|
+
const result = await startVideoGeneration(prompt, {
|
|
31
|
+
aspectRatio,
|
|
32
|
+
negativePrompt,
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: `Video generation started!
|
|
39
|
+
|
|
40
|
+
**Operation ID:** \`${result.operationName}\`
|
|
41
|
+
**Status:** ${result.status}
|
|
42
|
+
|
|
43
|
+
Video generation takes 1-5 minutes. Use the \`gemini-check-video\` tool with the operation ID above to check progress and download when complete.
|
|
44
|
+
|
|
45
|
+
**Tip:** Save the operation ID - you'll need it to check status and retrieve the video.`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
52
|
+
logger.error(`Error starting video generation: ${errorMessage}`);
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: 'text',
|
|
57
|
+
text: `Error starting video generation: ${errorMessage}`,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// Check video generation status
|
|
65
|
+
server.tool('gemini-check-video', {
|
|
66
|
+
operationId: z
|
|
67
|
+
.string()
|
|
68
|
+
.describe('The operation ID returned when starting video generation'),
|
|
69
|
+
}, async ({ operationId }) => {
|
|
70
|
+
logger.info(`Checking video status: ${operationId}`);
|
|
71
|
+
try {
|
|
72
|
+
const result = await checkVideoStatus(operationId);
|
|
73
|
+
if (result.status === 'completed') {
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: `Video generation complete!
|
|
79
|
+
|
|
80
|
+
**Status:** ${result.status}
|
|
81
|
+
${result.filePath ? `**Saved to:** ${result.filePath}` : ''}
|
|
82
|
+
${result.videoUri ? `**Video URI:** ${result.videoUri}` : ''}
|
|
83
|
+
**Output directory:** ${getOutputDir()}
|
|
84
|
+
|
|
85
|
+
The video has been downloaded and saved to disk.`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
else if (result.status === 'failed') {
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: 'text',
|
|
95
|
+
text: `Video generation failed.
|
|
96
|
+
|
|
97
|
+
**Status:** ${result.status}
|
|
98
|
+
**Error:** ${result.error || 'Unknown error'}
|
|
99
|
+
|
|
100
|
+
Please try again with a different prompt.`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
isError: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: `Video still generating...
|
|
112
|
+
|
|
113
|
+
**Status:** ${result.status}
|
|
114
|
+
**Operation ID:** ${result.operationName}
|
|
115
|
+
|
|
116
|
+
Please check again in 30-60 seconds using the \`gemini-check-video\` tool.`,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
124
|
+
logger.error(`Error checking video status: ${errorMessage}`);
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: `Error checking video status: ${errorMessage}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouTube Analysis Tool - Analyze YouTube videos directly by URL
|
|
3
|
+
*
|
|
4
|
+
* Gemini can process YouTube videos natively, enabling:
|
|
5
|
+
* - Video summarization
|
|
6
|
+
* - Q&A about video content
|
|
7
|
+
* - Timestamp-based analysis
|
|
8
|
+
* - Audio and visual understanding
|
|
9
|
+
*
|
|
10
|
+
* Supports clipping intervals to analyze specific portions of videos.
|
|
11
|
+
*/
|
|
12
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
+
/**
|
|
14
|
+
* Register YouTube analysis tools with the MCP server
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerYouTubeTool(server: McpServer): void;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouTube Analysis Tool - Analyze YouTube videos directly by URL
|
|
3
|
+
*
|
|
4
|
+
* Gemini can process YouTube videos natively, enabling:
|
|
5
|
+
* - Video summarization
|
|
6
|
+
* - Q&A about video content
|
|
7
|
+
* - Timestamp-based analysis
|
|
8
|
+
* - Audio and visual understanding
|
|
9
|
+
*
|
|
10
|
+
* Supports clipping intervals to analyze specific portions of videos.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { GoogleGenAI } from '@google/genai';
|
|
14
|
+
import { logger } from '../utils/logger.js';
|
|
15
|
+
/**
|
|
16
|
+
* Parse time string to seconds (supports formats like "1m30s", "90s", "1:30", "90")
|
|
17
|
+
*/
|
|
18
|
+
function parseTimeToSeconds(time) {
|
|
19
|
+
// Handle MM:SS format
|
|
20
|
+
if (time.includes(':')) {
|
|
21
|
+
const [mins, secs] = time.split(':').map(Number);
|
|
22
|
+
return `${mins * 60 + secs}s`;
|
|
23
|
+
}
|
|
24
|
+
// Handle XmYs format
|
|
25
|
+
const minMatch = time.match(/(\d+)m/);
|
|
26
|
+
const secMatch = time.match(/(\d+)s/);
|
|
27
|
+
const mins = minMatch ? parseInt(minMatch[1]) : 0;
|
|
28
|
+
const secs = secMatch ? parseInt(secMatch[1]) : 0;
|
|
29
|
+
if (mins > 0 || secMatch) {
|
|
30
|
+
return `${mins * 60 + secs}s`;
|
|
31
|
+
}
|
|
32
|
+
// Handle plain number (assume seconds)
|
|
33
|
+
const num = parseInt(time);
|
|
34
|
+
if (!isNaN(num)) {
|
|
35
|
+
return `${num}s`;
|
|
36
|
+
}
|
|
37
|
+
return time;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Register YouTube analysis tools with the MCP server
|
|
41
|
+
*/
|
|
42
|
+
export function registerYouTubeTool(server) {
|
|
43
|
+
server.tool('gemini-youtube', {
|
|
44
|
+
url: z
|
|
45
|
+
.string()
|
|
46
|
+
.describe('YouTube video URL (e.g., https://www.youtube.com/watch?v=...)'),
|
|
47
|
+
question: z
|
|
48
|
+
.string()
|
|
49
|
+
.describe('Question about the video or task to perform (e.g., "Summarize this video", "What happens at 2:30?")'),
|
|
50
|
+
startTime: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Start time for analysis (e.g., "1m30s", "90", "1:30"). Optional.'),
|
|
54
|
+
endTime: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('End time for analysis (e.g., "5m00s", "300", "5:00"). Optional.'),
|
|
58
|
+
}, async ({ url, question, startTime, endTime }) => {
|
|
59
|
+
logger.info(`YouTube analysis: ${url.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
|
+
// Validate YouTube URL
|
|
66
|
+
if (!url.includes('youtube.com') && !url.includes('youtu.be')) {
|
|
67
|
+
throw new Error('Invalid YouTube URL. Please provide a valid YouTube video link.');
|
|
68
|
+
}
|
|
69
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
70
|
+
const model = process.env.GEMINI_PRO_MODEL || 'gemini-3-pro-preview';
|
|
71
|
+
// Build the video part with optional clipping
|
|
72
|
+
const videoPart = {
|
|
73
|
+
fileData: {
|
|
74
|
+
fileUri: url,
|
|
75
|
+
mimeType: 'video/*',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
// Add video metadata for clipping if provided
|
|
79
|
+
if (startTime || endTime) {
|
|
80
|
+
const videoMetadata = {};
|
|
81
|
+
if (startTime) {
|
|
82
|
+
videoMetadata.startOffset = parseTimeToSeconds(startTime);
|
|
83
|
+
}
|
|
84
|
+
if (endTime) {
|
|
85
|
+
videoMetadata.endOffset = parseTimeToSeconds(endTime);
|
|
86
|
+
}
|
|
87
|
+
videoPart.videoMetadata = videoMetadata;
|
|
88
|
+
}
|
|
89
|
+
// Build contents
|
|
90
|
+
const contents = [
|
|
91
|
+
{
|
|
92
|
+
role: 'user',
|
|
93
|
+
parts: [videoPart, { text: question }],
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
// Execute
|
|
97
|
+
const response = await genAI.models.generateContent({
|
|
98
|
+
model,
|
|
99
|
+
contents,
|
|
100
|
+
});
|
|
101
|
+
const responseText = response.text || '';
|
|
102
|
+
// Build response with context
|
|
103
|
+
let resultText = responseText;
|
|
104
|
+
if (startTime || endTime) {
|
|
105
|
+
const clipInfo = [];
|
|
106
|
+
if (startTime)
|
|
107
|
+
clipInfo.push(`from ${startTime}`);
|
|
108
|
+
if (endTime)
|
|
109
|
+
clipInfo.push(`to ${endTime}`);
|
|
110
|
+
resultText = `*Analyzed video clip ${clipInfo.join(' ')}*\n\n${responseText}`;
|
|
111
|
+
}
|
|
112
|
+
logger.info('YouTube analysis completed successfully');
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: 'text',
|
|
117
|
+
text: resultText,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
124
|
+
logger.error(`Error in YouTube analysis: ${errorMessage}`);
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: `Error analyzing YouTube video: ${errorMessage}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// Convenience tool for YouTube summarization
|
|
137
|
+
server.tool('gemini-youtube-summary', {
|
|
138
|
+
url: z.string().describe('YouTube video URL'),
|
|
139
|
+
style: z
|
|
140
|
+
.enum(['brief', 'detailed', 'bullet-points', 'chapters'])
|
|
141
|
+
.default('brief')
|
|
142
|
+
.describe('Summary style'),
|
|
143
|
+
}, async ({ url, style }) => {
|
|
144
|
+
logger.info(`YouTube summary: ${url.substring(0, 50)}...`);
|
|
145
|
+
try {
|
|
146
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
147
|
+
if (!apiKey) {
|
|
148
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
149
|
+
}
|
|
150
|
+
if (!url.includes('youtube.com') && !url.includes('youtu.be')) {
|
|
151
|
+
throw new Error('Invalid YouTube URL');
|
|
152
|
+
}
|
|
153
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
154
|
+
const model = process.env.GEMINI_FLASH_MODEL || 'gemini-3-flash-preview';
|
|
155
|
+
// Build prompt based on style
|
|
156
|
+
let prompt;
|
|
157
|
+
switch (style) {
|
|
158
|
+
case 'brief':
|
|
159
|
+
prompt = 'Summarize this video in 2-3 sentences.';
|
|
160
|
+
break;
|
|
161
|
+
case 'detailed':
|
|
162
|
+
prompt =
|
|
163
|
+
'Provide a detailed summary of this video, covering all main points and key takeaways. Include relevant timestamps for important moments.';
|
|
164
|
+
break;
|
|
165
|
+
case 'bullet-points':
|
|
166
|
+
prompt =
|
|
167
|
+
'Summarize this video as a bullet-point list of key points and takeaways.';
|
|
168
|
+
break;
|
|
169
|
+
case 'chapters':
|
|
170
|
+
prompt =
|
|
171
|
+
'Create a chapter breakdown of this video with timestamps and descriptions for each section.';
|
|
172
|
+
break;
|
|
173
|
+
default:
|
|
174
|
+
prompt = 'Summarize this video.';
|
|
175
|
+
}
|
|
176
|
+
const contents = [
|
|
177
|
+
{
|
|
178
|
+
role: 'user',
|
|
179
|
+
parts: [
|
|
180
|
+
{
|
|
181
|
+
fileData: {
|
|
182
|
+
fileUri: url,
|
|
183
|
+
mimeType: 'video/*',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{ text: prompt },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
const response = await genAI.models.generateContent({
|
|
191
|
+
model,
|
|
192
|
+
contents,
|
|
193
|
+
});
|
|
194
|
+
logger.info('YouTube summary completed successfully');
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: response.text || 'Unable to generate summary.',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
206
|
+
logger.error(`Error in YouTube summary: ${errorMessage}`);
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: `Error summarizing YouTube video: ${errorMessage}`,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Utility - Provides logging capabilities with different verbosity levels
|
|
3
|
+
*/
|
|
4
|
+
export type LogLevel = "quiet" | "normal" | "verbose";
|
|
5
|
+
interface Logger {
|
|
6
|
+
error: (message: string, ...args: any[]) => void;
|
|
7
|
+
warn: (message: string, ...args: any[]) => void;
|
|
8
|
+
info: (message: string, ...args: any[]) => void;
|
|
9
|
+
debug: (message: string, ...args: any[]) => void;
|
|
10
|
+
prompt: (prompt: string) => void;
|
|
11
|
+
response: (response: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Sets up the logger with the specified verbosity level
|
|
15
|
+
*/
|
|
16
|
+
export declare function setupLogger(level: LogLevel): void;
|
|
17
|
+
/**
|
|
18
|
+
* Returns whether full prompts and responses should be logged
|
|
19
|
+
*/
|
|
20
|
+
export declare function shouldLogFullMessages(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Returns whether info messages should be logged
|
|
23
|
+
*/
|
|
24
|
+
export declare function shouldLogInfo(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Returns whether debug messages should be logged
|
|
27
|
+
*/
|
|
28
|
+
export declare function shouldLogDebug(): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The logger instance
|
|
31
|
+
*/
|
|
32
|
+
export declare const logger: Logger;
|
|
33
|
+
export {};
|