@raihan0824/mcp-veo2
Version:
MCP server for generating videos with Google Veo2
657 lines • 24.6 kB
JavaScript
import { z } from 'zod';
import { GoogleGenAI } from '@google/genai';
import { veoClient } from '../services/veoClient.js';
import { log } from '../utils/logger.js';
import appConfig from '../config.js';
import fs from 'fs/promises';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import S3Service from '../services/s3Service.js';
// Initialize the Google Gen AI client for image generation
const ai = new GoogleGenAI({ apiKey: appConfig.GOOGLE_API_KEY });
// Define the storage directory for generated images
const IMAGE_STORAGE_DIR = path.join(appConfig.STORAGE_DIR, 'images');
// Initialize S3 service if enabled
let s3Service;
if (appConfig.USE_S3) {
s3Service = new S3Service();
log.info('S3 storage enabled for image storage');
}
else {
log.info('Local storage enabled for image storage');
}
// Ensure the image storage directory exists (still needed for metadata in local mode or temp files)
(async () => {
try {
await fs.mkdir(IMAGE_STORAGE_DIR, { recursive: true });
}
catch (error) {
log.fatal('Failed to create image storage directory:', error);
process.exit(1);
}
})();
/**
* Saves a generated image to disk or S3
*
* @param imageBytes The base64 encoded image data
* @param prompt The prompt used to generate the image
* @param mimeType The MIME type of the image
* @returns The filepath/URL and ID of the saved image
*/
async function saveGeneratedImage(imageBytes, prompt, mimeType = 'image/png') {
try {
// Generate a unique ID for the image
const id = uuidv4();
// Determine the file extension based on MIME type
let extension = '.png';
if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') {
extension = '.jpg';
}
else if (mimeType === 'image/webp') {
extension = '.webp';
}
// Convert base64 to buffer
const buffer = Buffer.from(imageBytes, 'base64');
let metadata;
if (s3Service && appConfig.USE_S3) {
// Upload to S3
const s3Key = `images/${id}${extension}`;
// Create a temporary file for S3 upload
const tempFilePath = path.resolve(IMAGE_STORAGE_DIR, `temp_${id}${extension}`);
await fs.writeFile(tempFilePath, buffer);
try {
// Upload to S3
const s3Url = await s3Service.uploadFile(tempFilePath, s3Key);
// Create metadata with S3 information
metadata = {
id,
createdAt: new Date().toISOString(),
prompt,
mimeType,
size: buffer.length,
s3Key,
imageUrl: s3Url
};
log.info(`Image uploaded to S3 successfully with ID: ${id}, URL: ${s3Url}`);
}
finally {
// Clean up temporary file
try {
await fs.unlink(tempFilePath);
}
catch (error) {
log.warn(`Failed to delete temporary file: ${tempFilePath}`, error);
}
}
}
else {
// Save locally
const filepath = path.resolve(IMAGE_STORAGE_DIR, `${id}${extension}`);
await fs.writeFile(filepath, buffer);
metadata = {
id,
createdAt: new Date().toISOString(),
prompt,
mimeType,
size: buffer.length,
filepath
};
log.info(`Image saved locally with ID: ${id}`);
}
// Save the metadata
const metadataPath = path.resolve(IMAGE_STORAGE_DIR, `${id}.json`);
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
return {
id,
filepath: metadata.filepath,
imageUrl: metadata.imageUrl,
metadata
};
}
catch (error) {
log.error('Error saving generated image:', error);
throw error;
}
}
// Define schemas for tool inputs
const AspectRatioSchema = z.enum(['16:9', '9:16']);
const PersonGenerationSchema = z.enum(['dont_allow', 'allow_adult']);
/**
* Tool for generating a video from a text prompt
*
* @param args The tool arguments
* @returns The tool result
*/
export async function generateVideoFromText(args) {
try {
log.info('Generating video from text prompt');
log.verbose('Text prompt parameters:', JSON.stringify(args));
// Convert string boolean parameters to actual booleans
const enhancePrompt = typeof args.enhancePrompt === 'string'
? args.enhancePrompt.toLowerCase() === 'true' || args.enhancePrompt === '1'
: args.enhancePrompt ?? false;
const includeFullData = typeof args.includeFullData === 'string'
? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1'
: args.includeFullData ?? false;
const autoDownload = typeof args.autoDownload === 'string'
? args.autoDownload.toLowerCase() === 'true' || args.autoDownload === '1'
: args.autoDownload ?? true;
// Create config object from individual parameters with defaults
const config = {
aspectRatio: args.aspectRatio || '16:9',
personGeneration: args.personGeneration || 'dont_allow',
numberOfVideos: args.numberOfVideos || 1,
durationSeconds: args.durationSeconds || 5,
enhancePrompt: enhancePrompt,
negativePrompt: args.negativePrompt || ''
};
// Options for video generation with defaults
const options = {
includeFullData: includeFullData,
autoDownload: autoDownload
};
// Generate the video
const result = await veoClient.generateFromText(args.prompt, config, options);
// Prepare response content
const responseContent = [];
// If includeFullData is true and we have video data, include it in the response
if (args.includeFullData && result.videoData) {
responseContent.push({
type: 'image', // Use 'image' type for video content since MCP doesn't have a 'video' type
mimeType: result.mimeType,
data: result.videoData
});
}
// Add text content with metadata
responseContent.push({
type: 'text',
text: JSON.stringify({
success: true,
message: 'Video generated successfully',
videoId: result.id,
resourceUri: `videos://${result.id}`,
filepath: result.filepath,
videoUrl: result.videoUrl,
metadata: result
}, null, 2)
});
// Return the result
return {
content: responseContent
};
}
catch (error) {
log.error('Error generating video from text:', error);
// Return the error
return {
isError: true,
content: [
{
type: 'text',
text: `Error generating video: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
/**
* Tool for generating a video from an image
*
* @param args The tool arguments
* @returns The tool result
*/
export async function generateVideoFromImage(args) {
try {
log.info('Generating video from image');
log.verbose('Image parameters:', JSON.stringify(args));
// Extract image data based on the type
let imageData;
let mimeType;
if (typeof args.image === 'string') {
// It's a URL or file path
imageData = args.image;
}
else {
// It's an ImageContent object
imageData = args.image.data;
mimeType = args.image.mimeType;
}
// Convert string boolean parameters to actual booleans
const enhancePrompt = typeof args.enhancePrompt === 'string'
? args.enhancePrompt.toLowerCase() === 'true' || args.enhancePrompt === '1'
: args.enhancePrompt ?? false;
const includeFullData = typeof args.includeFullData === 'string'
? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1'
: args.includeFullData ?? false;
const autoDownload = typeof args.autoDownload === 'string'
? args.autoDownload.toLowerCase() === 'true' || args.autoDownload === '1'
: args.autoDownload ?? true;
// Create config object from individual parameters with defaults
const config = {
aspectRatio: args.aspectRatio || '16:9',
numberOfVideos: args.numberOfVideos || 1,
durationSeconds: args.durationSeconds || 5,
enhancePrompt: enhancePrompt,
negativePrompt: args.negativePrompt || ''
};
// Options for video generation with defaults
const options = {
includeFullData: includeFullData,
autoDownload: autoDownload
};
// Generate the video
const result = await veoClient.generateFromImage(imageData, args.prompt, config, options, mimeType);
// Prepare response content
const responseContent = [];
// If includeFullData is true and we have video data, include it in the response
if (args.includeFullData && result.videoData) {
responseContent.push({
type: 'image', // Use 'image' type for video content since MCP doesn't have a 'video' type
mimeType: result.mimeType,
data: result.videoData
});
}
// Add text content with metadata
responseContent.push({
type: 'text',
text: JSON.stringify({
success: true,
message: 'Video generated successfully',
videoId: result.id,
resourceUri: `videos://${result.id}`,
filepath: result.filepath,
videoUrl: result.videoUrl,
metadata: result
}, null, 2)
});
// Return the result
return {
content: responseContent
};
}
catch (error) {
log.error('Error generating video from image:', error);
// Return the error
return {
isError: true,
content: [
{
type: 'text',
text: `Error generating video: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
/**
* Tool for generating an image from a text prompt
*
* @param args The tool arguments
* @returns The tool result with generated image
*/
export async function generateImage(args) {
try {
log.info('Generating image from text prompt');
log.verbose('Image generation parameters:', JSON.stringify(args));
// Create config object
const config = {
numberOfImages: args.numberOfImages || 1
};
// Generate the image using Imagen
const response = await ai.models.generateImages({
model: "imagen-3.0-generate-002",
prompt: args.prompt,
config: config,
});
if (!response.generatedImages || response.generatedImages.length === 0) {
throw new Error('No images generated in the response');
}
const generatedImage = response.generatedImages[0];
if (!generatedImage.image?.imageBytes) {
throw new Error('Generated image missing image bytes');
}
// Save the generated image to disk
const { id, filepath, imageUrl, metadata } = await saveGeneratedImage(generatedImage.image.imageBytes, args.prompt, 'image/png');
// Prepare response content
const responseContent = [];
// Convert includeFullData to boolean if it's a string
const includeFullData = typeof args.includeFullData === 'string'
? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1'
: args.includeFullData !== false;
// If includeFullData is true (default) or not specified, include the image data
if (includeFullData) {
responseContent.push({
type: 'image',
mimeType: 'image/png',
data: generatedImage.image.imageBytes
});
}
// Add text content with metadata
responseContent.push({
type: 'text',
text: JSON.stringify({
success: true,
message: 'Image generated successfully',
imageId: id,
resourceUri: `images://${id}`,
filepath: filepath,
imageUrl: imageUrl,
metadata: metadata
}, null, 2)
});
// Return the result
return {
content: responseContent
};
}
catch (error) {
log.error('Error generating image:', error);
// Return the error
return {
isError: true,
content: [
{
type: 'text',
text: `Error generating image: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
/**
* Tool for generating a video from a generated image
*
* @param args The tool arguments
* @returns The tool result
*/
export async function generateVideoFromGeneratedImage(args) {
try {
log.info('Generating video from generated image');
log.verbose('Image generation parameters:', JSON.stringify(args));
// Convert string boolean parameters to actual booleans
const enhancePrompt = typeof args.enhancePrompt === 'string'
? args.enhancePrompt.toLowerCase() === 'true' || args.enhancePrompt === '1'
: args.enhancePrompt ?? false;
const includeFullData = typeof args.includeFullData === 'string'
? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1'
: args.includeFullData ?? false;
const autoDownload = typeof args.autoDownload === 'string'
? args.autoDownload.toLowerCase() === 'true' || args.autoDownload === '1'
: args.autoDownload ?? true;
// Create image config with defaults
const imageConfig = {
numberOfImages: args.numberOfImages || 1
};
// Create video config with defaults
const videoConfig = {
aspectRatio: args.aspectRatio || '16:9',
personGeneration: args.personGeneration || 'dont_allow',
numberOfVideos: args.numberOfVideos || 1,
durationSeconds: args.durationSeconds || 5,
enhancePrompt: enhancePrompt,
negativePrompt: args.negativePrompt || ''
};
// Options for video generation with defaults
const options = {
includeFullData: includeFullData,
autoDownload: autoDownload
};
// First generate the image
const imageResponse = await ai.models.generateImages({
model: "imagen-3.0-generate-002",
prompt: args.prompt,
config: imageConfig,
});
if (!imageResponse.generatedImages || imageResponse.generatedImages.length === 0) {
throw new Error('No images generated in the response');
}
const generatedImage = imageResponse.generatedImages[0];
if (!generatedImage.image?.imageBytes) {
throw new Error('Generated image missing image bytes');
}
// Save the generated image to disk
const { id: imageId, filepath: imageFilepath } = await saveGeneratedImage(generatedImage.image.imageBytes, args.prompt, 'image/png');
// Use the generated image to create a video
const videoPrompt = args.videoPrompt || args.prompt;
const result = await veoClient.generateFromImage(generatedImage.image.imageBytes, videoPrompt, videoConfig, options, 'image/png');
// Prepare response content
const responseContent = [];
// Always include the generated image
responseContent.push({
type: 'image',
mimeType: 'image/png',
data: generatedImage.image.imageBytes
});
// If includeFullData is true and we have video data, include it in the response
if (args.includeFullData && result.videoData) {
responseContent.push({
type: 'image', // Use 'image' type for video content since MCP doesn't have a 'video' type
mimeType: result.mimeType,
data: result.videoData
});
}
// Add text content with metadata
responseContent.push({
type: 'text',
text: JSON.stringify({
success: true,
message: 'Video generated from image successfully',
videoId: result.id,
videoResourceUri: `videos://${result.id}`,
videoFilepath: result.filepath,
videoUrl: result.videoUrl,
imageId: imageId,
imageResourceUri: `images://${imageId}`,
imageFilepath: imageFilepath,
metadata: result
}, null, 2)
});
// Return the result
return {
content: responseContent
};
}
catch (error) {
log.error('Error generating video from generated image:', error);
// Return the error
return {
isError: true,
content: [
{
type: 'text',
text: `Error generating video from generated image: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
/**
* Gets image metadata by ID
*
* @param id The image ID
* @returns The image metadata
*/
async function getImageMetadata(id) {
try {
const metadataPath = path.resolve(IMAGE_STORAGE_DIR, `${id}.json`);
const metadataJson = await fs.readFile(metadataPath, 'utf-8');
return JSON.parse(metadataJson);
}
catch (error) {
log.error(`Error getting metadata for image ${id}:`, error);
throw new Error(`Image metadata not found: ${id}`);
}
}
/**
* Tool for getting an image by ID
*
* @param args The tool arguments
* @returns The tool result
*/
export async function getImage(args) {
try {
log.info(`Getting image with ID: ${args.id}`);
// Get the image metadata
const metadata = await getImageMetadata(args.id);
// Convert includeFullData to boolean if it's a string
const includeFullData = typeof args.includeFullData === 'string'
? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1'
: args.includeFullData !== false;
// Prepare response content
const responseContent = [];
// If includeFullData is true (default) or not specified, include the image data
if (includeFullData && metadata.filepath) {
try {
const imageData = await fs.readFile(metadata.filepath);
responseContent.push({
type: 'image',
mimeType: metadata.mimeType || 'image/png',
data: imageData.toString('base64')
});
}
catch (error) {
log.error(`Error reading image file ${metadata.filepath}:`, error);
// Continue without the image data
}
}
// Add text content with metadata
responseContent.push({
type: 'text',
text: JSON.stringify({
success: true,
message: 'Image retrieved successfully',
imageId: metadata.id,
resourceUri: `images://${metadata.id}`,
filepath: metadata.filepath,
prompt: metadata.prompt,
createdAt: metadata.createdAt,
mimeType: metadata.mimeType,
size: metadata.size
}, null, 2)
});
// Return the result
return {
content: responseContent
};
}
catch (error) {
log.error(`Error getting image:`, error);
// Return the error
return {
isError: true,
content: [
{
type: 'text',
text: `Error getting image: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
/**
* Tool for listing all generated images
*
* @returns The tool result
*/
export async function listGeneratedImages() {
try {
log.info('Listing all generated images');
// Get all files in the image storage directory
const files = await fs.readdir(IMAGE_STORAGE_DIR);
// Filter for JSON metadata files
const metadataFiles = files.filter(file => file.endsWith('.json'));
// Read and parse each metadata file
const imagesPromises = metadataFiles.map(async (file) => {
const filePath = path.resolve(IMAGE_STORAGE_DIR, file);
try {
const metadataJson = await fs.readFile(filePath, 'utf-8');
return JSON.parse(metadataJson);
}
catch (error) {
log.error(`Error reading image metadata file ${filePath}:`, error);
return null;
}
});
// Wait for all metadata to be read and filter out any null values
const images = (await Promise.all(imagesPromises)).filter(image => image !== null);
// Return the result
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
count: images.length,
images: images.map(image => ({
id: image.id,
createdAt: image.createdAt,
prompt: image.prompt,
resourceUri: `images://${image.id}`,
filepath: image.filepath,
mimeType: image.mimeType,
size: image.size
}))
}, null, 2)
}
]
};
}
catch (error) {
log.error('Error listing images:', error);
// Return the error
return {
isError: true,
content: [
{
type: 'text',
text: `Error listing images: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
/**
* Tool for listing all generated videos
*
* @returns The tool result
*/
export async function listGeneratedVideos() {
try {
// Get all videos
const videos = await veoClient.listVideos();
// Return the result
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
count: videos.length,
videos: videos.map(video => ({
id: video.id,
createdAt: video.createdAt,
prompt: video.prompt,
resourceUri: `videos://${video.id}`,
filepath: video.filepath,
videoUrl: video.videoUrl
}))
}, null, 2)
}
]
};
}
catch (error) {
log.error('Error listing videos:', error);
// Return the error
return {
isError: true,
content: [
{
type: 'text',
text: `Error listing videos: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
//# sourceMappingURL=generateVideo.js.map