UNPKG

mcp-kling

Version:

🎬 The FIRST MCP server for Kling AI video generation! Generate, extend, lip-sync AI videos, virtual try-on, and manage your account directly from Claude.

344 lines (343 loc) • 13.8 kB
import axios from 'axios'; import * as jose from 'jose'; import { promises as fs } from 'fs'; import { createWriteStream } from 'fs'; import path from 'path'; import { homedir } from 'os'; import { uploadFromUrl } from './file-uploader.js'; export default class KlingClient { accessKey; secretKey; axiosInstance; constructor(accessKey, secretKey) { this.accessKey = accessKey; this.secretKey = secretKey; this.axiosInstance = axios.create({ baseURL: 'https://api-singapore.klingai.com', timeout: 30000, headers: { 'Content-Type': 'application/json', } }); // Add request interceptor to generate fresh JWT for each request this.axiosInstance.interceptors.request.use(async (config) => { const jwt = await this.generateJWT(); config.headers['Authorization'] = `Bearer ${jwt}`; return config; }); } async generateJWT() { const secret = new TextEncoder().encode(this.secretKey); const jwt = await new jose.SignJWT({ iss: this.accessKey, exp: Math.floor(Date.now() / 1000) + (30 * 60), // 30 minutes nbf: Math.floor(Date.now() / 1000) - 5 // 5 seconds ago }) .setProtectedHeader({ alg: 'HS256', typ: 'JWT' }) .sign(secret); return jwt; } async processImageUrl(url) { if (!url) return undefined; if (url.startsWith('file://') || !url.startsWith('http')) { try { const uploadedUrl = await uploadFromUrl(url); console.log(`Uploaded file to: ${uploadedUrl}`); return uploadedUrl; } catch (uploadError) { throw new Error(`Failed to upload file: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`); } } return url; } async generateVideo(request) { const path = '/v1/videos/text2video'; // Process any image URLs const ref_image_url = await this.processImageUrl(request.ref_image_url); const body = { prompt: request.prompt, negative_prompt: request.negative_prompt || '', cfg_scale: request.cfg_scale || 0.8, aspect_ratio: request.aspect_ratio || '16:9', duration: request.duration || '5', model_name: request.model_name || 'kling-v2-master', // V2-master is default ...(request.image_url && { image_url: request.image_url }), ...(request.image_tail_url && { image_tail_url: request.image_tail_url }), ...(ref_image_url && { ref_image_url }), ...(request.ref_image_weight && { ref_image_weight: request.ref_image_weight }), ...(request.camera_control && { camera_control: request.camera_control }), ...(request.callback_url && { callback_url: request.callback_url }), ...(request.external_task_id && { external_task_id: request.external_task_id }), }; try { const response = await this.axiosInstance.post(path, body); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async generateImageToVideo(request) { const path = '/v1/videos/image2video'; if (!request.image_url) { throw new Error('image_url is required for image-to-video generation'); } // Process the image URL const imageUrl = await this.processImageUrl(request.image_url); const body = { image: imageUrl, // API uses 'image' not 'image_url' prompt: request.prompt, negative_prompt: request.negative_prompt || '', cfg_scale: request.cfg_scale || 0.8, duration: request.duration || '5', aspect_ratio: request.aspect_ratio || '16:9', model_name: request.model_name || 'kling-v2-master', // V2-master is default }; try { const response = await this.axiosInstance.post(path, body); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async getTaskStatus(taskId) { const path = `/v1/videos/text2video/${taskId}`; try { const response = await this.axiosInstance.get(path); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async extendVideo(request) { const path = '/v1/video/extension'; const body = { task_id: request.task_id, prompt: request.prompt, duration: request.duration || '5', mode: request.mode || 'standard', model_name: request.model_name || 'kling-v2-master', // V2-master is default }; try { const response = await this.axiosInstance.post(path, body); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async createLipsync(request) { const path = '/v1/videos/lip-sync'; // Process video URL const video_url = await this.processImageUrl(request.video_url); const input = { video_url: video_url, }; if (request.audio_url) { input.mode = 'audio2video'; input.audio_type = 'url'; input.audio_url = request.audio_url; } else if (request.tts_text) { input.mode = 'text2video'; input.text = request.tts_text; input.voice_id = request.tts_voice || 'male-magnetic'; input.voice_language = 'en'; input.voice_speed = request.tts_speed || 1.0; } else { throw new Error('Either audio_url or tts_text must be provided'); } const body = { input }; try { const response = await this.axiosInstance.post(path, body); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async downloadVideo(videoUrl, downloadPath) { const defaultPath = path.join(homedir(), 'Desktop'); const downloadDir = downloadPath || defaultPath; // Create directory if it doesn't exist await fs.mkdir(downloadDir, { recursive: true }); // Generate filename from URL and timestamp const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `kling-video-${timestamp}.mp4`; const filepath = path.join(downloadDir, filename); // Download the video const response = await axios.get(videoUrl, { responseType: 'stream', }); const writer = createWriteStream(filepath); response.data.pipe(writer); return new Promise((resolve, reject) => { writer.on('finish', () => resolve(filepath)); writer.on('error', reject); }); } async applyVideoEffect(request) { const path = '/v1/videos/effects'; // Validate image count based on effect type const dualCharacterEffects = ['hug', 'kiss', 'heart_gesture']; const singleImageEffects = ['squish', 'expansion', 'fuzzyfuzzy', 'bloombloom', 'dizzydizzy']; if (dualCharacterEffects.includes(request.effect_scene) && request.image_urls.length !== 2) { throw new Error(`Effect "${request.effect_scene}" requires exactly 2 images`); } if (singleImageEffects.includes(request.effect_scene) && request.image_urls.length !== 1) { throw new Error(`Effect "${request.effect_scene}" requires exactly 1 image`); } // Process all image URLs const processedImageUrls = await Promise.all(request.image_urls.map(url => this.processImageUrl(url))); const body = { input: { image_urls: processedImageUrls.filter(url => url !== undefined), effect_scene: request.effect_scene, duration: request.duration || '5', } }; // Always add model_name body.input.model_name = request.model_name || 'kling-v2-master'; try { const response = await this.axiosInstance.post(path, body); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async generateImage(request) { const path = '/v1/images/generation'; // Process reference image URL if provided const ref_image_url = await this.processImageUrl(request.ref_image_url); const body = { prompt: request.prompt, negative_prompt: request.negative_prompt || '', aspect_ratio: request.aspect_ratio || '1:1', num_images: request.num_images || 1, ...(ref_image_url && { ref_image_url }), ...(request.ref_image_weight && { ref_image_weight: request.ref_image_weight }), }; // Always add model_name body.model_name = request.model_name || 'kling-v2-master'; try { const response = await this.axiosInstance.post(path, body); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async getImageTaskStatus(taskId) { const path = `/v1/image/generation/${taskId}`; try { const response = await this.axiosInstance.get(path); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async virtualTryOn(request) { const path = '/v1/virtual-try-on'; if (request.cloth_image_urls.length === 0) { throw new Error('At least one clothing image URL is required'); } if (request.cloth_image_urls.length > 5) { throw new Error('Maximum 5 clothing items allowed per request'); } // Process all image URLs const person_image_url = await this.processImageUrl(request.person_image_url); const cloth_image_urls = await Promise.all(request.cloth_image_urls.map(url => this.processImageUrl(url))); const body = { model_name: request.model_name || 'kolors-virtual-try-on-v1.5', person_image_url: person_image_url, cloth_image_urls: cloth_image_urls.filter(url => url !== undefined), }; try { const response = await this.axiosInstance.post(path, body); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async getResourcePackages() { const path = '/v1/account/packages'; try { const response = await this.axiosInstance.get(path); return response.data.data.resource_packages || []; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async getAccountBalance() { const path = '/v1/account/balance'; try { const response = await this.axiosInstance.get(path); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } async listTasks(params) { const path = '/v1/task/list'; const queryParams = { page: params?.page || 1, page_size: params?.page_size || 10, ...(params?.status && { status: params.status }), ...(params?.start_time && { start_time: params.start_time }), ...(params?.end_time && { end_time: params.end_time }), }; try { const response = await this.axiosInstance.get(path, { params: queryParams }); return response.data.data; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Kling API error: ${error.response?.data?.message || error.message}`); } throw error; } } }