generate_video
Generate videos from text prompts, image inputs, or reference clips using AI models like Seedance 2.0, Happyhorse 1.0, or Veo 3.1. Set duration, resolution, and aspect ratio to control output.
Instructions
Generate a video using AI via MeiGen platform. Supports text-to-video, image-to-video (first/last frame), and reference-video continuation (Seedance 2.0 only — pass referenceVideo URL + referenceVideoDuration together, and prompt must explicitly say "extend / continue"). Available models include Seedance 2.0 (fast/pro tiers, 4-15s), Happyhorse 1.0 (cost-effective, 3-15s), and Veo 3.1 (fast/pro tiers, 4/6/8s, native audio). Pricing varies — seedance/happyhorse are per-second, veo is per-generation by tier × duration. See https://www.meigen.ai/model-comparison for the current schedule. With a reference video (seedance only), billable seconds = max(reference_duration + duration, min_billable[duration]); total often higher than direct generation. Generation typically takes 1–5 minutes (veo at 4k can take up to ~8 min).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| prompt | Yes | The video generation prompt. Describe motion, scene, and style — not just the still image. | |
| model | Yes | Video model ID. Use list_models to see available video models. Common (as of writing): "seedance-2-0" (multi-tier general purpose), "happyhorse-1.0" (cost-effective i2v/t2v), "veo-3.1" (Google Veo with two tiers, 4/6/8s, native audio). | |
| tier | No | Quality tier — only for models that support tiers. seedance-2-0 and veo-3.1 currently accept "fast" (default, cheaper) or "pro" (higher fidelity). Tiers may be added by the platform — call list_models to see what each model exposes. | |
| duration | No | Video duration in seconds. seedance-2-0 / happyhorse-1.0 currently accept ~3–15s (any integer in range). veo-3.1 accepts exactly 4, 6, or 8 (default 4) — other values will be rejected. Defaults to the model's default duration. Call list_models for the current allowed values per model. | |
| resolution | No | Output resolution. Common: "480p" / "720p" / "1080p" (model-dependent). Use list_models to see what each model supports. Higher resolutions cost more credits per second. | |
| aspectRatio | No | Aspect ratio: "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "auto", "adaptive" (model-dependent). Defaults to "auto" when omitted. | |
| firstFrame | No | Optional first-frame image to control where the video starts. Accepts public URL or local file path (auto-uploaded). Highly recommended for image-to-video; with no first frame the model does pure text-to-video. | |
| lastFrame | No | Optional last-frame image to also control where the video ends. Used by seedance-2-0 and veo-3.1; happyhorse-1.0 ignores this field. Accepts public URL or local file path. Requires firstFrame to also be provided — passing lastFrame alone is rejected. | |
| referenceVideo | No | Optional reference video URL for Seedance 2.0 "video continuation". Must be a publicly accessible HTTPS URL (typically a previous generation result `videoUrl`); local paths are not supported. Only seedance-2-0 accepts this — passing it with other models will fail. IMPORTANT — prompt requirement: to make the new clip semantically continue the reference, the `prompt` MUST explicitly say "extend" / "continue" (e.g. prefix with "Extend this video with the following plot:"). Without that, the model treats the video as visual reference only and the new clip may drift from a true continuation. Output behavior: the output is ONLY your `duration` seconds (4-15s) of new content — the reference video is NOT concatenated into the output. To get a single "original + new" clip the user must stitch them locally. Billing: credits are charged at the With-reference-video rate, with `billable_seconds = max(reference_duration + duration, min_billable[duration])`. Total cost is often higher than direct generation of the same output length. Always pass `referenceVideoDuration` alongside this field — omitting it causes underbilling and broken continuation behavior. | |
| referenceVideoDuration | No | Duration of the reference video in seconds (typically 2–15 — backend validates the current allowed range). REQUIRED whenever `referenceVideo` is set; if omitted the backend treats it as 0, leading to undercharged credits and misconfigured generation. Pass the actual duration of the clip at `referenceVideo`. |
Implementation Reference
- src/tools/generate-video.ts:134-267 (handler)The main handler function `registerGenerateVideo` that registers the 'generate_video' MCP tool. Contains the full logic: validates provider, resolves frame images, validates reference video params, calls the API via `apiClient.generateVideo()`, polls for completion, downloads the video, and returns the result.
export function registerGenerateVideo(server: McpServer, apiClient: MeiGenApiClient, config: MeiGenConfig) { server.tool( 'generate_video', 'Generate a video using AI via MeiGen platform. Supports text-to-video, image-to-video (first/last frame), and reference-video continuation (Seedance 2.0 only — pass `referenceVideo` URL + `referenceVideoDuration` together, and prompt must explicitly say "extend / continue"). Available models include Seedance 2.0 (fast/pro tiers, 4-15s), Happyhorse 1.0 (cost-effective, 3-15s), and Veo 3.1 (fast/pro tiers, 4/6/8s, native audio). Pricing varies — seedance/happyhorse are per-second, veo is per-generation by tier × duration. See https://www.meigen.ai/model-comparison for the current schedule. With a reference video (seedance only), billable seconds = max(reference_duration + duration, min_billable[duration]); total often higher than direct generation. Generation typically takes 1–5 minutes (veo at 4k can take up to ~8 min).', generateVideoSchema, { readOnlyHint: false, destructiveHint: true }, async ({ prompt, model, tier, duration, resolution, aspectRatio, firstFrame, lastFrame, referenceVideo, referenceVideoDuration }, extra) => { const providers = getAvailableProviders(config) if (!providers.includes('meigen')) { return { content: [{ type: 'text' as const, text: 'Video generation requires a MeiGen API token.\n\n1. Get one at https://www.meigen.ai (sign in → avatar → Settings → API Keys)\n2. Make the token available:\n - **On Claude Code**: run `/meigen:setup` and paste the token\n - **On other hosts**: export `MEIGEN_API_TOKEN=meigen_sk_...` in your shell, or add it to your MCP config\'s env block for the meigen server', }], isError: true, } } let generationId: string | undefined try { const refList: string[] = [] if (firstFrame) { refList.push(await resolveFrameImage(firstFrame, config, (msg) => notify(extra, msg), 'first frame')) } if (lastFrame) { if (!firstFrame) { // lastFrame 单独传无意义(vendor i2v 只看 firstFrameUrl + 可选 lastFrameUrl,需配对) throw new Error('lastFrame requires firstFrame to also be provided') } refList.push(await resolveFrameImage(lastFrame, config, (msg) => notify(extra, msg), 'last frame')) } const referenceImages = refList.length > 0 ? refList : undefined // 参考视频(续写)校验:必须是 https URL,且必须配对传 referenceVideoDuration // 漏传 duration 会被后端按 0 处理 → 计费偏低 + 续写效果异常(主项目 docs 已明示) if (referenceVideo) { if (isLocalPath(referenceVideo)) { throw new Error('referenceVideo must be a public HTTPS URL (local paths are not supported — upload the clip first or use a previous generation\'s videoUrl).') } const unsafe = unsafeReferenceUrlReason(referenceVideo) if (unsafe) { throw new Error(`referenceVideo URL rejected: ${unsafe}. URL: ${referenceVideo}`) } if (typeof referenceVideoDuration !== 'number') { throw new Error('referenceVideoDuration is required when referenceVideo is set (pass the clip duration in seconds, 2-15).') } } else if (typeof referenceVideoDuration === 'number') { throw new Error('referenceVideoDuration was passed without referenceVideo — drop it, or pass a referenceVideo URL.') } await sharedApiSemaphore.acquire() try { // 1. Submit const genResponse = await apiClient.generateVideo({ prompt, modelId: model, aspectRatio: aspectRatio || 'auto', resolution, duration, tier, referenceImages, referenceVideo, referenceVideoDuration, }) if (!genResponse.generationId) { throw new Error('No generation ID returned') } generationId = genResponse.generationId await notify(extra, 'Video generation submitted, waiting for result (typically 1–5 minutes)...') // 2. Poll — 视频比图片慢,超时设 8min const status = await apiClient.waitForGeneration( generationId, 480_000, async (elapsedMs) => { await notify(extra, `Still generating video... (${Math.round(elapsedMs / 1000)}s elapsed)`) }, ) if (status.status === 'failed') { throw new Error(status.error || 'Video generation failed') } // mediaType guard: 防止用户传 image model id 给 generate_video,导致拿 jpg 写成 .mp4 if (status.mediaType && status.mediaType !== 'video') { throw new Error(`This model returned ${status.mediaType}, not video. Use generate_image for image models, or call list_models to see video model IDs.`) } const videoUrl = status.videoUrl if (!videoUrl) { throw new Error('No video URL in completed generation') } await notify(extra, 'Downloading video...') const savedPath = await saveVideoLocally(videoUrl) const actualModel = genResponse.modelId || model addRecentGeneration({ prompt, provider: 'meigen', model: actualModel, aspectRatio }) const lines = [`Video generated successfully.`] lines.push(`- Provider: MeiGen (model: ${actualModel}${tier ? `, tier: ${tier}` : ''})`) if (typeof duration === 'number') lines.push(`- Duration: ${duration}s`) if (resolution) lines.push(`- Resolution: ${resolution}`) lines.push(`- Video URL: ${videoUrl}`) if (savedPath) lines.push(`- Saved to: ${savedPath}`) lines.push(`\nVideo URLs may expire — download or save the file if you need long-term access.`) return { content: [{ type: 'text' as const, text: lines.join('\n') }], } } finally { sharedApiSemaphore.release() } } catch (error) { const message = error instanceof Error ? error.message : String(error) const guidance = classifyError(message) // 超时特殊提示:任务可能仍在后台跑,提醒用户避免重复扣费 // 后端 pg_cron cleanup_orphan_generations 会在 ~15min 内对"从未启动"的孤儿任务自动退款 const timeoutHint = /timed out|timeout/i.test(message) && generationId ? `\n\nGeneration ID: ${generationId}. The job may still complete in the background — check https://www.meigen.ai before retrying. If the backend job never started, credits are automatically refunded within ~15 minutes.` : '' return { content: [{ type: 'text' as const, text: `Video generation failed: ${message}\n\n${guidance}${timeoutHint}`, }], isError: true, } } } ) - src/tools/generate-video.ts:108-132 (schema)The `generateVideoSchema` Zod schema defining all input parameters: prompt, model, tier, duration, resolution, aspectRatio, firstFrame, lastFrame, referenceVideo, and referenceVideoDuration.
export const generateVideoSchema = { prompt: z.string().trim().min(1, 'Prompt cannot be empty').describe('The video generation prompt. Describe motion, scene, and style — not just the still image.'), model: z.string().min(1).describe('Video model ID. Use list_models to see available video models. Common (as of writing): "seedance-2-0" (multi-tier general purpose), "happyhorse-1.0" (cost-effective i2v/t2v), "veo-3.1" (Google Veo with two tiers, 4/6/8s, native audio).'), tier: z.string().optional() .describe('Quality tier — only for models that support tiers. seedance-2-0 and veo-3.1 currently accept "fast" (default, cheaper) or "pro" (higher fidelity). Tiers may be added by the platform — call list_models to see what each model exposes.'), duration: z.number().int().positive().optional() .describe('Video duration in seconds. seedance-2-0 / happyhorse-1.0 currently accept ~3–15s (any integer in range). veo-3.1 accepts exactly 4, 6, or 8 (default 4) — other values will be rejected. Defaults to the model\'s default duration. Call list_models for the current allowed values per model.'), resolution: z.string().optional() .describe('Output resolution. Common: "480p" / "720p" / "1080p" (model-dependent). Use list_models to see what each model supports. Higher resolutions cost more credits per second.'), aspectRatio: z.string().optional() .describe('Aspect ratio: "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "auto", "adaptive" (model-dependent). Defaults to "auto" when omitted.'), firstFrame: z.string().optional() .describe('Optional first-frame image to control where the video starts. Accepts public URL or local file path (auto-uploaded). Highly recommended for image-to-video; with no first frame the model does pure text-to-video.'), lastFrame: z.string().optional() .describe('Optional last-frame image to also control where the video ends. Used by seedance-2-0 and veo-3.1; happyhorse-1.0 ignores this field. Accepts public URL or local file path. Requires firstFrame to also be provided — passing lastFrame alone is rejected.'), referenceVideo: z.string().optional() .describe( 'Optional reference video URL for Seedance 2.0 "video continuation". Must be a publicly accessible HTTPS URL (typically a previous generation result `videoUrl`); local paths are not supported. Only seedance-2-0 accepts this — passing it with other models will fail. ' + 'IMPORTANT — prompt requirement: to make the new clip semantically continue the reference, the `prompt` MUST explicitly say "extend" / "continue" (e.g. prefix with "Extend this video with the following plot:"). Without that, the model treats the video as visual reference only and the new clip may drift from a true continuation. ' + 'Output behavior: the output is ONLY your `duration` seconds (4-15s) of new content — the reference video is NOT concatenated into the output. To get a single "original + new" clip the user must stitch them locally. ' + 'Billing: credits are charged at the With-reference-video rate, with `billable_seconds = max(reference_duration + duration, min_billable[duration])`. Total cost is often higher than direct generation of the same output length. Always pass `referenceVideoDuration` alongside this field — omitting it causes underbilling and broken continuation behavior.' ), referenceVideoDuration: z.number().int().positive().optional() .describe('Duration of the reference video in seconds (typically 2–15 — backend validates the current allowed range). REQUIRED whenever `referenceVideo` is set; if omitted the backend treats it as 0, leading to undercharged credits and misconfigured generation. Pass the actual duration of the clip at `referenceVideo`.'), } - src/server.ts:276-278 (registration)Registration call: `registerGenerateVideo(server, apiClient, config)` wires the tool into the MCP server.
// Video generation (requires MeiGen Token) registerGenerateVideo(server, apiClient, config) - src/lib/meigen-api.ts:189-232 (helper)The `generateVideo` method on `MeiGenApiClient` that sends the POST request to `/api/generate/v2` with all video parameters.
/** Generate a video (requires API token) */ async generateVideo(params: { prompt: string modelId: string // 视频模型必须显式传(无后端默认) aspectRatio?: string resolution?: string duration?: number tier?: string referenceImages?: string[] referenceVideo?: string // 仅 Seedance 2.0:参考视频 URL(续写场景) referenceVideoDuration?: number // 参考视频时长(秒);传 referenceVideo 必须同时传 }): Promise<MeiGenGenerationResponse> { if (!this.apiToken) { throw new Error('MEIGEN_API_TOKEN is required for video generation via MeiGen') } const body: Record<string, unknown> = { modelId: params.modelId, prompt: params.prompt, aspectRatio: params.aspectRatio || 'auto', } if (params.resolution) body.resolution = params.resolution if (typeof params.duration === 'number') body.duration = params.duration if (params.tier) body.tier = params.tier if (params.referenceImages?.length) body.referenceImages = params.referenceImages if (params.referenceVideo) body.referenceVideo = params.referenceVideo if (typeof params.referenceVideoDuration === 'number') body.referenceVideoDuration = params.referenceVideoDuration const res = await fetch(`${this.baseUrl}/api/generate/v2`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }) const json = await res.json() as MeiGenGenerationResponse if (!res.ok || !json.success) { throw new Error(json.error || `Generation failed: ${res.status}`) } return json } - src/lib/generation-shared.ts:1-46 (helper)Shared utilities between generate_image and generate_video: the shared API semaphore (rate-limit protection) and `classifyError` for translating error messages into user guidance.
/** * Shared utilities between generate_image and generate_video tools. * Both hit the same backend endpoint (/api/generate/v2) and share the * same rate limit (12/min/user), so they must share the API semaphore. */ import { Semaphore } from './semaphore.js' // Shared API semaphore — image + video both submit to /api/generate/v2 and share // the backend rate limit (12 req/min/user). Splitting into two Semaphore(4) instances // would let MCP burst to 8 concurrent submits and trip 429. export const sharedApiSemaphore = new Semaphore(4) /** Translate a raw provider/network error message into actionable user guidance. */ export function classifyError(message: string): string { const lower = message.toLowerCase() if (lower.includes('safety') || lower.includes('policy') || lower.includes('flagged') || lower.includes('content_blocked') || lower.includes('moderation')) return 'The prompt may have triggered a content safety filter. Try rephrasing the prompt to avoid sensitive content.' if (lower.includes('credit') || lower.includes('insufficient') || message.includes('402')) return 'Insufficient credits. Daily free credits refresh each day, or view plans and top up at https://www.meigen.ai/model-comparison.' if (lower.includes('timed out') || lower.includes('timeout')) return 'Generation timed out. This can happen during high demand. You can try again — it may succeed on retry.' if (lower.includes('rate') && (lower.includes('limit') || message.includes('429'))) return 'Too many requests. Wait a moment and try again.' if (lower.includes('model') && (lower.includes('invalid') || lower.includes('inactive'))) return 'This model may be unavailable. Use list_models to check currently available models.' if (lower.includes('ratio') && lower.includes('not supported')) return 'This aspect ratio is not supported by the selected model. Use list_models to check supported ratios, or omit aspectRatio to let the server auto-infer.' if (lower.includes('token') && (lower.includes('invalid') || lower.includes('expired'))) return 'API token issue. On Claude Code, run /meigen:setup to reconfigure. On other hosts, check your MEIGEN_API_TOKEN env var or the env block for the meigen server in your MCP config.' if (lower.includes('econnrefused') || lower.includes('fetch failed') || lower.includes('network')) return 'Network connection issue. Check your internet connection and try again.' if (lower.includes('comfyui') || lower.includes('node_errors')) return 'ComfyUI workflow error. Use comfyui_workflow view to inspect the workflow, or try a different one.' return 'You can try again, or use a different prompt/model.' }