Skip to main content
Glama

Vidu MCP Server

by el-el-san
index.ts16 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import fetch from "node-fetch"; import * as fs from "fs"; import * as path from "path"; import * as dotenv from "dotenv"; // インターフェースを定義して型安全性を確保 interface StartResponse { task_id: string; state: string; model: string; images: string[]; prompt: string; duration: number; seed: number; resolution: string; bgm: boolean; movement_amplitude: string; created_at: string; } interface CreationItem { id: string; url: string; cover_url: string; } interface StatusResponse { state: string; err_code?: string; credits?: number; creations?: CreationItem[]; } interface UploadResponse { id: string; put_url: string; expires_at: string; } interface FinishResponse { uri: string; } // Load environment variables dotenv.config(); // Get API key from environment variable const VIDU_API_KEY = process.env.VIDU_API_KEY; if (!VIDU_API_KEY) { console.error("Error: VIDU_API_KEY environment variable is not set"); process.exit(1); } // Base URL for the Vidu API const VIDU_API_BASE_URL = "https://api.vidu.com"; // Create an MCP server const server = new McpServer({ name: "Vidu Video Generator", version: "1.0.0" }); // Tool for image-to-video conversion server.tool( "image-to-video", "Generate a video from an image using Vidu API", { image_url: z.string().url().describe("URL of the image to convert to video"), prompt: z.string().max(1500).optional().describe("Text prompt for video generation (max 1500 chars)"), duration: z.number().int().optional().describe("Duration of the output video in seconds (model-specific)"), model: z.enum(["viduq1", "vidu1.5", "vidu2.0"]).default("vidu2.0").describe("Model name for generation"), resolution: z.enum(["360p", "720p", "1080p"]).optional().describe("Resolution of the output video (model/duration-specific)"), movement_amplitude: z.enum(["auto", "small", "medium", "large"]).default("auto").describe("Movement amplitude of objects in the frame"), seed: z.number().int().optional().describe("Random seed for reproducibility"), bgm: z.boolean().optional().describe("Add background music (4s videos only)"), callback_url: z.string().url().optional().describe("Callback URL for async notifications") }, async ({ image_url, prompt, duration, model, resolution, movement_amplitude, seed, bgm, callback_url }) => { try { // Validate model-specific constraints let finalDuration = duration; let finalResolution = resolution; if (model === "viduq1") { // viduq1 only supports 5s duration and 1080p resolution finalDuration = 5; finalResolution = "1080p"; if (duration && duration !== 5) { console.warn(`Model viduq1 only supports 5s duration. Using 5s instead of ${duration}s.`); } if (resolution && resolution !== "1080p") { console.warn(`Model viduq1 only supports 1080p resolution. Using 1080p instead of ${resolution}.`); } } else { // vidu1.5 and vidu2.0 if (!duration || ![4, 8].includes(duration)) { finalDuration = 4; // Default to 4s } else { finalDuration = duration; } // Resolution constraints based on duration if (finalDuration === 4) { if (!resolution || !["360p", "720p", "1080p"].includes(resolution)) { finalResolution = "360p"; // Default for 4s } else { finalResolution = resolution; } } else if (finalDuration === 8) { finalResolution = "720p"; // Only option for 8s if (resolution && resolution !== "720p") { console.warn(`8s videos only support 720p resolution. Using 720p instead of ${resolution}.`); } } } // BGM validation const finalBgm = bgm === true && finalDuration === 4; if (bgm === true && finalDuration !== 4) { console.warn(`BGM is only supported for 4s videos. BGM will not be added for ${finalDuration}s video.`); } // Step 1: Start the generation task const startResponse = await fetch(`${VIDU_API_BASE_URL}/ent/v2/img2video`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Token ${VIDU_API_KEY}` }, body: JSON.stringify({ model, images: [image_url], prompt: prompt || "", duration: finalDuration, seed: seed !== undefined ? seed : Math.floor(Math.random() * 1000000), resolution: finalResolution, movement_amplitude, bgm: finalBgm, ...(callback_url && { callback_url }) }) }); if (!startResponse.ok) { const errorData = await startResponse.text(); return { isError: true, content: [ { type: "text", text: `Error starting video generation: ${errorData}` } ] }; } const startData = await startResponse.json() as StartResponse; const taskId = startData.task_id; // Step 2: Poll for completion let state = startData.state; let result: StatusResponse | null = null; // Add a message to indicate that we're processing let status = `Task created with ID: ${taskId}\nInitial state: ${state}\n`; status += "Waiting for processing to complete...\n"; // Maximum wait time: 5 minutes const maxPolls = 60; let pollCount = 0; while (state !== "success" && state !== "failed" && pollCount < maxPolls) { // Wait for 5 seconds before polling again await new Promise(resolve => setTimeout(resolve, 5000)); const statusResponse = await fetch(`${VIDU_API_BASE_URL}/ent/v2/tasks/${taskId}/creations`, { method: "GET", headers: { "Content-Type": "application/json", "Authorization": `Token ${VIDU_API_KEY}` } }); if (!statusResponse.ok) { const errorData = await statusResponse.text(); return { isError: true, content: [ { type: "text", text: `Error checking generation status: ${errorData}` } ] }; } const statusData = await statusResponse.json() as StatusResponse; state = statusData.state; pollCount++; status += `Current state: ${state}\n`; if (state === "success") { result = statusData; break; } else if (state === "failed") { return { isError: true, content: [ { type: "text", text: `Video generation failed: ${statusData.err_code || "Unknown error"}` } ] }; } } if (state !== "success") { return { isError: true, content: [ { type: "text", text: `Timed out waiting for video generation to complete. Last state: ${state}` } ] }; } // Format the successful result if (result && result.creations && result.creations.length > 0) { const videoUrl = result.creations[0].url; const coverUrl = result.creations[0].cover_url; const credits = result.credits; return { content: [ { type: "text", text: ` Video generation complete! Task ID: ${taskId} Status: ${state} Credits used: ${credits || 'N/A'} Video URL: ${videoUrl} Cover Image URL: ${coverUrl} Note: These URLs are valid for one hour. ` } ] }; } else { return { content: [ { type: "text", text: ` Video generation completed, but no download URLs were returned. Task ID: ${taskId} Status: ${state} ` } ] }; } } catch (error: any) { console.error("Error in image-to-video tool:", error); return { isError: true, content: [ { type: "text", text: `An unexpected error occurred: ${error.message}` } ] }; } } ); // Tool for checking generation status server.tool( "check-generation-status", "Check the status of a video generation task", { task_id: z.string().describe("Task ID returned by the image-to-video tool") }, async ({ task_id }) => { try { const statusResponse = await fetch(`${VIDU_API_BASE_URL}/ent/v2/tasks/${task_id}/creations`, { method: "GET", headers: { "Content-Type": "application/json", "Authorization": `Token ${VIDU_API_KEY}` } }); if (!statusResponse.ok) { const errorData = await statusResponse.text(); return { isError: true, content: [ { type: "text", text: `Error checking generation status: ${errorData}` } ] }; } const statusData = await statusResponse.json() as StatusResponse; if (statusData.state === "success") { if (statusData.creations && statusData.creations.length > 0) { const videoUrl = statusData.creations[0].url; const coverUrl = statusData.creations[0].cover_url; const credits = statusData.credits; return { content: [ { type: "text", text: ` Generation task complete! Task ID: ${task_id} Status: ${statusData.state} Credits used: ${credits || 'N/A'} Video URL: ${videoUrl} Cover Image URL: ${coverUrl} Note: These URLs are valid for one hour. ` } ] }; } else { return { content: [ { type: "text", text: ` Generation task complete but no download URLs available. Task ID: ${task_id} Status: ${statusData.state} ` } ] }; } } else if (statusData.state === "failed") { return { isError: true, content: [ { type: "text", text: `Generation task failed with error code: ${statusData.err_code || "Unknown error"}` } ] }; } else { return { content: [ { type: "text", text: ` Generation task is still in progress. Task ID: ${task_id} Current Status: ${statusData.state} You can check again later using the same task ID. ` } ] }; } } catch (error: any) { console.error("Error in check-generation-status tool:", error); return { isError: true, content: [ { type: "text", text: `An unexpected error occurred: ${error.message}` } ] }; } } ); // Tool for uploading images server.tool( "upload-image", "Upload an image to use with the Vidu API", { image_path: z.string().describe("Local path to the image file"), image_type: z.enum(["png", "webp", "jpeg", "jpg"]).describe("Image file type") }, async ({ image_path, image_type }) => { try { // Validate image path and existence if (!fs.existsSync(image_path)) { return { isError: true, content: [ { type: "text", text: `The specified image file does not exist: ${image_path}` } ] }; } // Validate file size (must be less than 10MB for upload) const stats = fs.statSync(image_path); const fileSizeInBytes = stats.size; const fileSizeInMB = fileSizeInBytes / (1024 * 1024); if (fileSizeInMB > 10) { return { isError: true, content: [ { type: "text", text: `File size exceeds the 10MB limit for image upload. Current size: ${fileSizeInMB.toFixed(2)}MB` } ] }; } // Step 1: Create upload link const createUploadResponse = await fetch(`${VIDU_API_BASE_URL}/tools/v2/files/uploads`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Token ${VIDU_API_KEY}` }, body: JSON.stringify({ scene: "vidu" }) }); if (!createUploadResponse.ok) { const errorData = await createUploadResponse.text(); return { isError: true, content: [ { type: "text", text: `Error creating upload link: ${errorData}` } ] }; } const uploadData = await createUploadResponse.json() as UploadResponse; const resourceId = uploadData.id; const putUrl = uploadData.put_url; // Step 2: Upload the image const imageBuffer = fs.readFileSync(image_path); const uploadResponse = await fetch(putUrl, { method: "PUT", headers: { "Content-Type": `image/${image_type}` }, body: imageBuffer }); if (!uploadResponse.ok) { return { isError: true, content: [ { type: "text", text: `Error uploading image: ${uploadResponse.statusText}` } ] }; } // Get the ETag from the response headers const etag = uploadResponse.headers.get("etag")?.replace(/"/g, ""); if (!etag) { return { isError: true, content: [ { type: "text", text: "Failed to get ETag from upload response" } ] }; } // Step 3: Complete the upload const finishUploadResponse = await fetch(`${VIDU_API_BASE_URL}/tools/v2/files/uploads/${resourceId}/finish`, { method: "PUT", headers: { "Content-Type": "application/json", "Authorization": `Token ${VIDU_API_KEY}` }, body: JSON.stringify({ etag }) }); if (!finishUploadResponse.ok) { const errorData = await finishUploadResponse.text(); return { isError: true, content: [ { type: "text", text: `Error finishing upload: ${errorData}` } ] }; } const finishData = await finishUploadResponse.json() as FinishResponse; const uri = finishData.uri; return { content: [ { type: "text", text: ` Image uploaded successfully! Resource ID: ${resourceId} URI: ${uri} You can use this URI as the image_url parameter for the image-to-video tool. ` } ] }; } catch (error: any) { console.error("Error in upload-image tool:", error); return { isError: true, content: [ { type: "text", text: `An unexpected error occurred: ${error.message}` } ] }; } } ); // Main function to start the server async function main() { try { // Connect to the server using stdio transport const transport = new StdioServerTransport(); await server.connect(transport); console.error("Vidu MCP Server started successfully"); } catch (error) { console.error("Error starting server:", error); process.exit(1); } } // Run the server main();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/el-el-san/vidu-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server