levisnkyyyy-images-mcp
Version:
Model Context Protocol server for AI image and video generation using LiteLLM and fal.ai
814 lines • 37.6 kB
JavaScript
#!/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