UNPKG

levisnkyyyy-images-mcp

Version:

Model Context Protocol server for AI image and video generation using LiteLLM and fal.ai

814 lines 37.6 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const client_1 = require("@fal-ai/client"); const FAL_API_KEY = process.env.FAL_API_KEY || ""; const FS_PREFIX = process.env.FS_PREFIX || ""; // Helper function to remove file extension from image/video name function removeFileExtension(filename) { // Remove common image and video file extensions return filename.replace(/\.(png|jpg|jpeg|gif|bmp|webp|svg|tiff|tif|ico|mp4|avi|mov|wmv|flv|mkv|webm|m4v|mpg|mpeg)$/i, ''); } // Configure fal client with API key if (FAL_API_KEY) { client_1.fal.config({ credentials: FAL_API_KEY, }); } const server = new index_js_1.Server({ name: "images-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, }); server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => { return { tools: [ { name: "nano_banana_pro_image_generation", description: "Generate an image using fal.ai's Nano Banana Pro model", inputSchema: { type: "object", properties: { prompt: { type: "string", description: "The text prompt to generate an image from", }, project_folder: { type: "string", description: "Path to the folder where generated images will be saved", }, image_name: { type: "string", description: "Base filename for the generated image(s) (without extension)", }, num_images: { type: "number", description: "Number of images to generate", minimum: 1, maximum: 4, default: 1, }, aspect_ratio: { type: "string", description: "Aspect ratio of the generated image", enum: ["21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16"], default: "1:1", }, output_format: { type: "string", description: "Output format of the generated image", enum: ["jpeg", "png", "webp"], default: "png", }, resolution: { type: "string", description: "Resolution of the generated image", enum: ["1K", "2K", "4K"], default: "1K", }, }, required: ["prompt", "project_folder", "image_name"], }, }, { name: "nano_banana_pro_image_edit", description: "Edit an existing image using fal.ai's Nano Banana Pro model", inputSchema: { type: "object", properties: { image_path: { type: "string", description: "Full path to the image file to edit", }, prompt: { type: "string", description: "The text prompt describing how to edit the image", }, project_folder: { type: "string", description: "Path to the folder where edited images will be saved", }, image_name: { type: "string", description: "Base filename for the edited image(s) (without extension)", }, num_images: { type: "number", description: "Number of images to generate", minimum: 1, maximum: 4, default: 1, }, aspect_ratio: { type: "string", description: "Aspect ratio of the generated image", enum: ["auto", "21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16"], default: "auto", }, output_format: { type: "string", description: "Output format of the generated image", enum: ["jpeg", "png", "webp"], default: "png", }, resolution: { type: "string", description: "Resolution of the generated image", enum: ["1K", "2K", "4K"], default: "1K", }, }, required: ["image_path", "prompt", "project_folder", "image_name"], }, }, { name: "veo31_video_generation", description: "Generate a video using Google's Veo 3.1 model", inputSchema: { type: "object", properties: { prompt: { type: "string", description: "The text prompt to generate a video from", }, project_folder: { type: "string", description: "Path to the folder where generated video will be saved", }, video_name: { type: "string", description: "Base filename for the generated video (without extension)", }, aspect_ratio: { type: "string", description: "Aspect ratio of the video", enum: ["16:9", "9:16"], default: "16:9", }, duration: { type: "string", description: "Duration of the video", enum: ["4s", "6s", "8s"], default: "8s", }, resolution: { type: "string", description: "Resolution of the video", enum: ["720p", "1080p"], default: "720p", }, generate_audio: { type: "boolean", description: "Whether to generate audio for the video (50% less credits if false)", default: true, }, enhance_prompt: { type: "boolean", description: "Whether to auto-improve prompt quality", default: true, }, negative_prompt: { type: "string", description: "Describe unwanted elements to avoid", }, seed: { type: "number", description: "Seed for reproducibility", }, }, required: ["prompt", "project_folder", "video_name"], }, }, { name: "veo31_image_to_video", description: "Generate a video from an image using Google's Veo 3.1 model", inputSchema: { type: "object", properties: { image_path: { type: "string", description: "Full path to the image file to animate (720p+ resolution, 16:9 or 9:16 aspect ratio)", }, prompt: { type: "string", description: "The text prompt describing the video generation from the image", }, project_folder: { type: "string", description: "Path to the folder where generated video will be saved", }, video_name: { type: "string", description: "Base filename for the generated video (without extension)", }, aspect_ratio: { type: "string", description: "Aspect ratio of the video", enum: ["auto", "16:9", "9:16"], default: "auto", }, duration: { type: "string", description: "Duration of the video", enum: ["4s", "6s", "8s"], default: "8s", }, resolution: { type: "string", description: "Resolution of the video", enum: ["720p", "1080p"], default: "720p", }, generate_audio: { type: "boolean", description: "Whether to generate audio for the video (50% less credits if false)", default: true, }, }, required: ["image_path", "prompt", "project_folder", "video_name"], }, }, { name: "veo31_reference_to_video", description: "Generate a video with consistent subject appearance using reference images and Google's Veo 3.1 model", inputSchema: { type: "object", properties: { image_paths: { type: "array", items: { type: "string" }, description: "Paths to reference images for consistent subject appearance (max 8MB each)", }, prompt: { type: "string", description: "The text prompt describing the video (include action, style, camera motion, ambiance)", }, project_folder: { type: "string", description: "Path to the folder where generated video will be saved", }, video_name: { type: "string", description: "Base filename for the generated video (without extension)", }, duration: { type: "string", description: "Duration of the video", enum: ["8s"], default: "8s", }, resolution: { type: "string", description: "Resolution of the video", enum: ["720p", "1080p"], default: "720p", }, generate_audio: { type: "boolean", description: "Whether to generate audio for the video (50% less credits if false)", default: true, }, }, required: ["image_paths", "prompt", "project_folder", "video_name"], }, }, { name: "kling_video_to_video", description: "Transform a video using Kling O1 video-to-video with reference images. Use @Image1, @Image2 in prompt to reference images.", inputSchema: { type: "object", properties: { video_path: { type: "string", description: "Full path to the video file to transform (.mp4/.mov, 3-10 seconds, 720-2160px, max 200MB)", }, prompt: { type: "string", description: "The text prompt describing the transformation. Use @Image1, @Image2, etc. to reference images.", }, project_folder: { type: "string", description: "Path to the folder where generated video will be saved", }, video_name: { type: "string", description: "Base filename for the generated video (without extension)", }, image_paths: { type: "array", items: { type: "string" }, description: "Optional paths to reference images for style/appearance (max 4). Reference in prompt as @Image1, @Image2, etc.", }, keep_audio: { type: "boolean", description: "Whether to preserve original video audio", default: false, }, aspect_ratio: { type: "string", description: "Output frame aspect ratio", enum: ["auto", "16:9", "9:16", "1:1"], default: "auto", }, duration: { type: "string", description: "Duration of the video in seconds", enum: ["5", "10"], default: "5", }, }, required: ["video_path", "prompt", "project_folder", "video_name"], }, }, ], }; }); server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { const toolName = request.params.name; if (toolName === "nano_banana_pro_image_generation") { const { prompt, project_folder, image_name: rawImageName, num_images = 1, aspect_ratio = "1:1", output_format = "png", resolution = "1K" } = request.params.arguments; const image_name = removeFileExtension(rawImageName); try { if (!FAL_API_KEY) { throw new Error("FAL_API_KEY environment variable is not set. Please set it to use fal.ai models."); } const result = await client_1.fal.run("fal-ai/nano-banana-pro", { input: { prompt, num_images, aspect_ratio, output_format, resolution, }, }); // Extract image data from result let images = []; if (result.data?.images) { images = result.data.images; } else if (result.images) { images = result.images; } else if (result.image) { images = [result.image]; } else if (result.url) { images = [{ url: result.url }]; } else { throw new Error(`Unexpected response structure from Nano Banana Pro: ${JSON.stringify(result)}`); } // Save each generated image const savedImages = []; const ext = output_format === 'jpeg' ? 'jpg' : output_format; for (let i = 0; i < images.length; i++) { const imageData = images[i]; let imageUrl; if (typeof imageData === 'string') { imageUrl = imageData; } else if (imageData.url) { imageUrl = imageData.url; } else { throw new Error(`No URL found in image data at index ${i}: ${JSON.stringify(imageData)}`); } const imgResponse = await fetch(imageUrl); const buffer = await imgResponse.arrayBuffer(); const filename = images.length > 1 ? `${image_name}_${i + 1}.${ext}` : `${image_name}.${ext}`; const folder = FS_PREFIX ? path_1.default.join(FS_PREFIX, project_folder) : project_folder; const filepath = path_1.default.join(folder, filename); await promises_1.default.access(folder, promises_1.default.constants.W_OK); await promises_1.default.writeFile(filepath, Buffer.from(buffer)); savedImages.push(filename); } return { content: [ { type: "text", text: `Successfully generated and saved ${images.length} image(s) with Nano Banana Pro using prompt: "${prompt}"`, }, { type: "text", text: `Saved to: ${savedImages.join(', ')}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error generating image with Nano Banana Pro: ${error instanceof Error ? error.message : String(error)}`, }, { type: "text", text: `Stack trace: ${error instanceof Error ? error.stack : 'No stack trace available'}`, }, ], isError: true, }; } } else if (toolName === "nano_banana_pro_image_edit") { const { image_path, prompt, project_folder, image_name: rawImageName, num_images = 1, aspect_ratio = "auto", output_format = "png", resolution = "1K" } = request.params.arguments; const image_name = removeFileExtension(rawImageName); try { if (!FAL_API_KEY) { throw new Error("FAL_API_KEY environment variable is not set. Please set it to use fal.ai models."); } // Read the image file and convert to base64 const fullImagePath = FS_PREFIX ? path_1.default.join(FS_PREFIX, image_path) : image_path; const imageBuffer = await promises_1.default.readFile(fullImagePath); const ext = path_1.default.extname(image_path).toLowerCase(); const mimeType = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg'; const imageBase64 = `data:${mimeType};base64,${imageBuffer.toString('base64')}`; const result = await client_1.fal.run("fal-ai/nano-banana-pro/edit", { input: { prompt, image_urls: [imageBase64], num_images, aspect_ratio, output_format, resolution, }, }); // Extract image data from result let images = []; if (result.data?.images) { images = result.data.images; } else if (result.images) { images = result.images; } else if (result.image) { images = [result.image]; } else if (result.url) { images = [{ url: result.url }]; } else { throw new Error(`Unexpected response structure from Nano Banana Pro Edit: ${JSON.stringify(result)}`); } // Save each edited image const savedImages = []; const outputExt = output_format === 'jpeg' ? 'jpg' : output_format; for (let i = 0; i < images.length; i++) { const imageData = images[i]; let imageUrl; if (typeof imageData === 'string') { imageUrl = imageData; } else if (imageData.url) { imageUrl = imageData.url; } else { throw new Error(`No URL found in image data at index ${i}: ${JSON.stringify(imageData)}`); } const imgResponse = await fetch(imageUrl); const buffer = await imgResponse.arrayBuffer(); const filename = images.length > 1 ? `${image_name}_${i + 1}.${outputExt}` : `${image_name}.${outputExt}`; const folder = FS_PREFIX ? path_1.default.join(FS_PREFIX, project_folder) : project_folder; const filepath = path_1.default.join(folder, filename); await promises_1.default.access(folder, promises_1.default.constants.W_OK); await promises_1.default.writeFile(filepath, Buffer.from(buffer)); savedImages.push(filename); } return { content: [ { type: "text", text: `Successfully edited and saved ${images.length} image(s) with Nano Banana Pro using prompt: "${prompt}"`, }, { type: "text", text: `Saved to: ${savedImages.join(', ')}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error editing image with Nano Banana Pro: ${error instanceof Error ? error.message : String(error)}`, }, { type: "text", text: `Stack trace: ${error instanceof Error ? error.stack : 'No stack trace available'}`, }, ], isError: true, }; } } else if (toolName === "veo31_video_generation") { const { prompt, project_folder, video_name: rawVideoName, aspect_ratio = "16:9", duration = "8s", resolution = "720p", generate_audio = true, enhance_prompt = true, negative_prompt, seed } = request.params.arguments; const video_name = removeFileExtension(rawVideoName); try { if (!FAL_API_KEY) { throw new Error("FAL_API_KEY environment variable is not set. Please set it to use fal.ai models."); } const input = { prompt, aspect_ratio, duration, resolution, generate_audio, enhance_prompt, }; if (negative_prompt) { input.negative_prompt = negative_prompt; } if (seed !== undefined) { input.seed = seed; } const result = await client_1.fal.run("fal-ai/veo3.1", { input, }); // Extract video URL from result const videoUrl = result.data?.video?.url || result.video?.url || result.url; if (!videoUrl) { throw new Error(`No video URL found in response ${JSON.stringify(result)}`); } // Fetch the video from URL const videoResponse = await fetch(videoUrl); const buffer = await videoResponse.arrayBuffer(); // Determine filename const filename = `${video_name}.mp4`; const folder = FS_PREFIX ? path_1.default.join(FS_PREFIX, project_folder) : project_folder; const filepath = path_1.default.join(folder, filename); await promises_1.default.access(folder, promises_1.default.constants.W_OK); // Save the video await promises_1.default.writeFile(filepath, Buffer.from(buffer)); return { content: [ { type: "text", text: `Successfully generated and saved video with Veo 3.1 using prompt: "${prompt}"`, }, { type: "text", text: `Saved to: ${filename}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error generating video with Veo 3.1: ${error instanceof Error ? error.message : String(error)}`, }, { type: "text", text: `Stack trace: ${error instanceof Error ? error.stack : 'No stack trace available'}`, }, ], isError: true, }; } } else if (toolName === "veo31_image_to_video") { const { image_path, prompt, project_folder, video_name: rawVideoName, aspect_ratio = "auto", duration = "8s", resolution = "720p", generate_audio = true } = request.params.arguments; const video_name = removeFileExtension(rawVideoName); try { if (!FAL_API_KEY) { throw new Error("FAL_API_KEY environment variable is not set. Please set it to use fal.ai models."); } // Read the image file and convert to base64 const fullImagePath = FS_PREFIX ? path_1.default.join(FS_PREFIX, image_path) : image_path; const imageBuffer = await promises_1.default.readFile(fullImagePath); const ext = path_1.default.extname(image_path).toLowerCase(); const mimeType = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg'; const imageBase64 = `data:${mimeType};base64,${imageBuffer.toString('base64')}`; const result = await client_1.fal.run("fal-ai/veo3.1/image-to-video", { input: { image_url: imageBase64, prompt, aspect_ratio, duration, resolution, generate_audio, }, }); // Extract video URL from result const videoUrl = result.data?.video?.url || result.video?.url || result.url; if (!videoUrl) { throw new Error(`No video URL found in response ${JSON.stringify(result)}`); } // Fetch the video from URL const videoResponse = await fetch(videoUrl); const buffer = await videoResponse.arrayBuffer(); // Determine filename const filename = `${video_name}.mp4`; const folder = FS_PREFIX ? path_1.default.join(FS_PREFIX, project_folder) : project_folder; const filepath = path_1.default.join(folder, filename); await promises_1.default.access(folder, promises_1.default.constants.W_OK); // Save the video await promises_1.default.writeFile(filepath, Buffer.from(buffer)); return { content: [ { type: "text", text: `Successfully generated video from image using Veo 3.1 with prompt: "${prompt}"`, }, { type: "text", text: `Saved to: ${filename}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error generating video with Veo 3.1 image-to-video: ${error instanceof Error ? error.message : String(error)}`, }, { type: "text", text: `Stack trace: ${error instanceof Error ? error.stack : 'No stack trace available'}`, }, ], isError: true, }; } } else if (toolName === "veo31_reference_to_video") { const { image_paths, prompt, project_folder, video_name: rawVideoName, duration = "8s", resolution = "720p", generate_audio = true } = request.params.arguments; const video_name = removeFileExtension(rawVideoName); try { if (!FAL_API_KEY) { throw new Error("FAL_API_KEY environment variable is not set. Please set it to use fal.ai models."); } // Read reference images and convert to base64 const imageUrls = []; for (const imagePath of image_paths) { const fullImagePath = FS_PREFIX ? path_1.default.join(FS_PREFIX, imagePath) : imagePath; const imageBuffer = await promises_1.default.readFile(fullImagePath); const ext = path_1.default.extname(imagePath).toLowerCase(); const mimeType = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg'; imageUrls.push(`data:${mimeType};base64,${imageBuffer.toString('base64')}`); } const result = await client_1.fal.run("fal-ai/veo3.1/reference-to-video", { input: { image_urls: imageUrls, prompt, duration, resolution, generate_audio, }, }); // Extract video URL from result const videoUrl = result.data?.video?.url || result.video?.url || result.url; if (!videoUrl) { throw new Error(`No video URL found in response ${JSON.stringify(result)}`); } // Fetch the video from URL const videoResponse = await fetch(videoUrl); const buffer = await videoResponse.arrayBuffer(); // Determine filename const filename = `${video_name}.mp4`; const folder = FS_PREFIX ? path_1.default.join(FS_PREFIX, project_folder) : project_folder; const filepath = path_1.default.join(folder, filename); await promises_1.default.access(folder, promises_1.default.constants.W_OK); // Save the video await promises_1.default.writeFile(filepath, Buffer.from(buffer)); return { content: [ { type: "text", text: `Successfully generated video from reference images using Veo 3.1 with prompt: "${prompt}"`, }, { type: "text", text: `Saved to: ${filename}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error generating video with Veo 3.1 reference-to-video: ${error instanceof Error ? error.message : String(error)}`, }, { type: "text", text: `Stack trace: ${error instanceof Error ? error.stack : 'No stack trace available'}`, }, ], isError: true, }; } } else if (toolName === "kling_video_to_video") { const { video_path, prompt, project_folder, video_name: rawVideoName, image_paths = [], keep_audio = false, aspect_ratio = "auto", duration = "5" } = request.params.arguments; const video_name = removeFileExtension(rawVideoName); try { if (!FAL_API_KEY) { throw new Error("FAL_API_KEY environment variable is not set. Please set it to use fal.ai models."); } // Read the video file and convert to base64 const fullVideoPath = FS_PREFIX ? path_1.default.join(FS_PREFIX, video_path) : video_path; const videoBuffer = await promises_1.default.readFile(fullVideoPath); const videoBase64 = `data:video/mp4;base64,${videoBuffer.toString('base64')}`; // Read reference images if provided const imageUrls = []; for (const imagePath of image_paths) { const fullImagePath = FS_PREFIX ? path_1.default.join(FS_PREFIX, imagePath) : imagePath; const imageBuffer = await promises_1.default.readFile(fullImagePath); const ext = path_1.default.extname(imagePath).toLowerCase(); const mimeType = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg'; imageUrls.push(`data:${mimeType};base64,${imageBuffer.toString('base64')}`); } const input = { video_url: videoBase64, prompt, aspect_ratio, duration, }; if (keep_audio) { input.keep_audio = keep_audio; } if (imageUrls.length > 0) { input.image_urls = imageUrls; } const result = await client_1.fal.run("fal-ai/kling-video/o1/video-to-video/reference", { input, }); // Extract video URL from result const resultVideoUrl = result.data?.video?.url || result.video?.url || result.url; if (!resultVideoUrl) { throw new Error(`No video URL found in response ${JSON.stringify(result)}`); } // Fetch the video from URL const videoResponse = await fetch(resultVideoUrl); const buffer = await videoResponse.arrayBuffer(); // Determine filename const filename = `${video_name}.mp4`; const folder = FS_PREFIX ? path_1.default.join(FS_PREFIX, project_folder) : project_folder; const filepath = path_1.default.join(folder, filename); await promises_1.default.access(folder, promises_1.default.constants.W_OK); // Save the video await promises_1.default.writeFile(filepath, Buffer.from(buffer)); return { content: [ { type: "text", text: `Successfully transformed video using Kling O1 video-to-video with prompt: "${prompt}"`, }, { type: "text", text: `Saved to: ${filename}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error transforming video with Kling video-to-video: ${error instanceof Error ? error.message : String(error)}`, }, { type: "text", text: `Stack trace: ${error instanceof Error ? error.stack : 'No stack trace available'}`, }, ], isError: true, }; } } else { throw new Error(`Unknown tool: ${toolName}`); } }); async function main() { const transport = new stdio_js_1.StdioServerTransport(); await server.connect(transport); console.error("Images MCP server running on stdio"); } main().catch((error) => { console.error("Server error:", error); process.exit(1); }); //# sourceMappingURL=index.js.map