@mindstone-engineering/mcp-server-kling 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Kling authentication module.
3
+ *
4
+ * Manages access key + secret key lifecycle — env vars on startup, runtime
5
+ * update via configure tool, and JWT generation with caching.
6
+ *
7
+ * Kling uses JWT signed with HS256 (access key as issuer, secret key as
8
+ * signing key). Tokens are valid for 30 minutes and cached until near expiry.
9
+ */
10
+ /**
11
+ * Returns the current access key.
12
+ */
13
+ export declare function getAccessKey(): string;
14
+ /**
15
+ * Returns the current secret key.
16
+ */
17
+ export declare function getSecretKey(): string;
18
+ /**
19
+ * Returns true if both access key and secret key are configured.
20
+ */
21
+ export declare function isConfigured(): boolean;
22
+ /**
23
+ * Update the API keys at runtime (e.g. after configure_kling_api_keys).
24
+ * Clears the JWT cache to force re-generation with new credentials.
25
+ */
26
+ export declare function setApiKeys(newAccessKey: string, newSecretKey: string): void;
27
+ /**
28
+ * Generate a JWT token for Kling API authentication.
29
+ * Tokens are valid for 30 minutes; cached and reused until near expiry (60s buffer).
30
+ */
31
+ export declare function getJwtToken(): Promise<string>;
32
+ /**
33
+ * Clear the cached JWT token. Used when credentials are updated.
34
+ */
35
+ export declare function clearTokenCache(): void;
36
+ //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Kling authentication module.
3
+ *
4
+ * Manages access key + secret key lifecycle — env vars on startup, runtime
5
+ * update via configure tool, and JWT generation with caching.
6
+ *
7
+ * Kling uses JWT signed with HS256 (access key as issuer, secret key as
8
+ * signing key). Tokens are valid for 30 minutes and cached until near expiry.
9
+ */
10
+ import { SignJWT } from 'jose';
11
+ let accessKey = process.env.KLING_ACCESS_KEY || '';
12
+ let secretKey = process.env.KLING_SECRET_KEY || '';
13
+ /** JWT token cache (refreshed when expired) */
14
+ let cachedToken = null;
15
+ /**
16
+ * Returns the current access key.
17
+ */
18
+ export function getAccessKey() {
19
+ return accessKey;
20
+ }
21
+ /**
22
+ * Returns the current secret key.
23
+ */
24
+ export function getSecretKey() {
25
+ return secretKey;
26
+ }
27
+ /**
28
+ * Returns true if both access key and secret key are configured.
29
+ */
30
+ export function isConfigured() {
31
+ return accessKey.length > 0 && secretKey.length > 0;
32
+ }
33
+ /**
34
+ * Update the API keys at runtime (e.g. after configure_kling_api_keys).
35
+ * Clears the JWT cache to force re-generation with new credentials.
36
+ */
37
+ export function setApiKeys(newAccessKey, newSecretKey) {
38
+ accessKey = newAccessKey;
39
+ secretKey = newSecretKey;
40
+ cachedToken = null;
41
+ }
42
+ /**
43
+ * Generate a JWT token for Kling API authentication.
44
+ * Tokens are valid for 30 minutes; cached and reused until near expiry (60s buffer).
45
+ */
46
+ export async function getJwtToken() {
47
+ if (!accessKey || !secretKey) {
48
+ throw new Error('KLING_ACCESS_KEY and KLING_SECRET_KEY must be set');
49
+ }
50
+ // Return cached token if still valid (with 60s buffer)
51
+ const now = Math.floor(Date.now() / 1000);
52
+ if (cachedToken && cachedToken.expiresAt > now + 60) {
53
+ return cachedToken.jwt;
54
+ }
55
+ const secret = new TextEncoder().encode(secretKey);
56
+ const expiresAt = now + 1800; // 30 minutes
57
+ const jwt = await new SignJWT({
58
+ iss: accessKey,
59
+ exp: expiresAt,
60
+ nbf: now - 5, // Valid from 5 seconds ago (clock skew buffer)
61
+ })
62
+ .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
63
+ .sign(secret);
64
+ cachedToken = { jwt, expiresAt };
65
+ return jwt;
66
+ }
67
+ /**
68
+ * Clear the cached JWT token. Used when credentials are updated.
69
+ */
70
+ export function clearTokenCache() {
71
+ cachedToken = null;
72
+ }
73
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Path to bridge state file, supporting both current and legacy env vars.
3
+ */
4
+ export declare const BRIDGE_STATE_PATH: string;
5
+ /**
6
+ * Send a request to the host app bridge.
7
+ *
8
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
9
+ * that handles credential management and other cross-process operations.
10
+ */
11
+ export declare const bridgeRequest: (urlPath: string, body: Record<string, unknown>) => Promise<{
12
+ success: boolean;
13
+ warning?: string;
14
+ error?: string;
15
+ }>;
16
+ //# sourceMappingURL=bridge.d.ts.map
package/dist/bridge.js ADDED
@@ -0,0 +1,43 @@
1
+ import * as fs from 'fs';
2
+ import { REQUEST_TIMEOUT_MS } from './types.js';
3
+ /**
4
+ * Path to bridge state file, supporting both current and legacy env vars.
5
+ */
6
+ export const BRIDGE_STATE_PATH = process.env.MCP_HOST_BRIDGE_STATE || process.env.MINDSTONE_REBEL_BRIDGE_STATE || '';
7
+ const loadBridgeState = () => {
8
+ if (!BRIDGE_STATE_PATH)
9
+ return null;
10
+ try {
11
+ const raw = fs.readFileSync(BRIDGE_STATE_PATH, 'utf8');
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ };
18
+ /**
19
+ * Send a request to the host app bridge.
20
+ *
21
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
22
+ * that handles credential management and other cross-process operations.
23
+ */
24
+ export const bridgeRequest = async (urlPath, body) => {
25
+ const bridge = loadBridgeState();
26
+ if (!bridge) {
27
+ return { success: false, error: 'Bridge not available' };
28
+ }
29
+ const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
30
+ method: 'POST',
31
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Authorization: `Bearer ${bridge.token}`,
35
+ },
36
+ body: JSON.stringify(body),
37
+ });
38
+ if (response.status === 401 || response.status === 403) {
39
+ return { success: false, error: `Bridge returned ${response.status}: unauthorized. Check host app authentication.` };
40
+ }
41
+ return response.json();
42
+ };
43
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Kling API HTTP client.
3
+ *
4
+ * Centralises JWT Bearer auth injection, error handling, rate-limit
5
+ * messaging, and timeout handling for all Kling API calls.
6
+ */
7
+ /**
8
+ * Make an authenticated request to the Kling API.
9
+ *
10
+ * @param path API path relative to base, e.g. `/videos/text2video`
11
+ * @param options Additional fetch options
12
+ * @returns Parsed response data (unwrapped from Kling's { code, message, data } envelope)
13
+ */
14
+ export declare function klingFetch<T>(path: string, options?: RequestInit): Promise<T>;
15
+ //# sourceMappingURL=client.d.ts.map
package/dist/client.js ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Kling API HTTP client.
3
+ *
4
+ * Centralises JWT Bearer auth injection, error handling, rate-limit
5
+ * messaging, and timeout handling for all Kling API calls.
6
+ */
7
+ import { isConfigured, getJwtToken } from './auth.js';
8
+ import { KlingError, KLING_API_BASE, REQUEST_TIMEOUT_MS, } from './types.js';
9
+ /**
10
+ * Get user-friendly resolution for common Kling error codes.
11
+ */
12
+ function getErrorResolution(code, message) {
13
+ const msg = message?.toLowerCase() || '';
14
+ // Kling-specific error codes
15
+ if (code >= 1000 && code <= 1004) {
16
+ return 'Authentication failed. Check your Kling API credentials in Settings. Get keys from https://app.klingai.com/global/dev/api-key';
17
+ }
18
+ if (code === 1102) {
19
+ return 'Insufficient credits. Purchase more at https://app.klingai.com/global/dev/billing';
20
+ }
21
+ if (code === 1201) {
22
+ return 'Invalid request parameters. Check the prompt, model, and other settings.';
23
+ }
24
+ // Fallback to message-based matching
25
+ if (msg.includes('auth') || msg.includes('token')) {
26
+ return 'Check your Kling API credentials in Settings. Get keys from https://app.klingai.com/global/dev/api-key';
27
+ }
28
+ if (msg.includes('insufficient') ||
29
+ msg.includes('balance') ||
30
+ msg.includes('credit') ||
31
+ msg.includes('not enough')) {
32
+ return 'Insufficient credits. Purchase more at https://app.klingai.com/global/dev/billing';
33
+ }
34
+ if (msg.includes('content') || msg.includes('policy') || msg.includes('moderation')) {
35
+ return 'Content policy violation. Try a different prompt without sensitive content.';
36
+ }
37
+ return 'Please try again. If the issue persists, check the Kling AI status page.';
38
+ }
39
+ /**
40
+ * Make an authenticated request to the Kling API.
41
+ *
42
+ * @param path API path relative to base, e.g. `/videos/text2video`
43
+ * @param options Additional fetch options
44
+ * @returns Parsed response data (unwrapped from Kling's { code, message, data } envelope)
45
+ */
46
+ export async function klingFetch(path, options = {}) {
47
+ if (!isConfigured()) {
48
+ throw new KlingError('Kling API credentials not configured', 'AUTH_REQUIRED', 'Use configure_kling_api_keys to set your access key and secret key first.');
49
+ }
50
+ const jwt = await getJwtToken();
51
+ const url = `${KLING_API_BASE}${path}`;
52
+ let response;
53
+ try {
54
+ response = await fetch(url, {
55
+ ...options,
56
+ signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ Authorization: `Bearer ${jwt}`,
60
+ ...options.headers,
61
+ },
62
+ });
63
+ }
64
+ catch (error) {
65
+ if (error instanceof Error && error.name === 'TimeoutError') {
66
+ throw new KlingError('Request to Kling API timed out', 'TIMEOUT', 'The request took too long. Try again or check if the Kling API is available.');
67
+ }
68
+ throw error;
69
+ }
70
+ // Handle rate limiting
71
+ if (response.status === 429) {
72
+ let bodyText;
73
+ try {
74
+ bodyText = await response.text();
75
+ }
76
+ catch {
77
+ /* ignore */
78
+ }
79
+ let parsed;
80
+ try {
81
+ parsed = bodyText ? JSON.parse(bodyText) : undefined;
82
+ }
83
+ catch {
84
+ /* ignore */
85
+ }
86
+ if (parsed?.code && parsed.message) {
87
+ throw new KlingError(parsed.message, `KLING_${parsed.code}`, getErrorResolution(parsed.code, parsed.message));
88
+ }
89
+ const retryAfter = response.headers.get('Retry-After');
90
+ const waitTime = retryAfter ? `${retryAfter} seconds` : '30 seconds';
91
+ throw new KlingError(`Rate limited by Kling API. Please wait ${waitTime} before retrying.`, 'RATE_LIMITED', `Wait ${waitTime} and try again.`);
92
+ }
93
+ if (response.status === 401 || response.status === 403) {
94
+ throw new KlingError('Authentication failed', 'AUTH_FAILED', 'Your Kling API credentials are invalid or expired. Use configure_kling_api_keys to set new credentials.');
95
+ }
96
+ // Handle non-OK responses that may not be JSON
97
+ if (!response.ok) {
98
+ let bodyText;
99
+ try {
100
+ bodyText = await response.text();
101
+ }
102
+ catch {
103
+ bodyText = '';
104
+ }
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(bodyText);
108
+ }
109
+ catch {
110
+ /* not JSON */
111
+ }
112
+ if (parsed?.code !== undefined) {
113
+ throw new KlingError(parsed.message || `Kling API error (HTTP ${response.status})`, `KLING_${parsed.code}`, getErrorResolution(parsed.code, parsed.message));
114
+ }
115
+ throw new KlingError(`Kling API error (HTTP ${response.status})`, `HTTP_${response.status}`, response.status === 404
116
+ ? 'The API endpoint was not found. The Kling API may have changed.'
117
+ : 'Please try again. If the issue persists, check your API credentials.');
118
+ }
119
+ const data = (await response.json());
120
+ // Kling API returns code 0 for success
121
+ if (data.code !== 0) {
122
+ throw new KlingError(data.message || `Kling API error: ${data.code}`, `KLING_${data.code}`, getErrorResolution(data.code, data.message));
123
+ }
124
+ return data.data;
125
+ }
126
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Kling AI MCP Server
4
+ *
5
+ * Provides AI video generation via Kling AI's API through Model Context Protocol.
6
+ *
7
+ * Environment variables:
8
+ * - KLING_ACCESS_KEY: Kling API access key (required)
9
+ * - KLING_SECRET_KEY: Kling API secret key (required)
10
+ * - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
11
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Kling AI MCP Server
4
+ *
5
+ * Provides AI video generation via Kling AI's API through Model Context Protocol.
6
+ *
7
+ * Environment variables:
8
+ * - KLING_ACCESS_KEY: Kling API access key (required)
9
+ * - KLING_SECRET_KEY: Kling API secret key (required)
10
+ * - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
11
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
12
+ */
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { createServer } from './server.js';
15
+ async function main() {
16
+ const server = createServer();
17
+ const transport = new StdioServerTransport();
18
+ await server.connect(transport);
19
+ console.error('Kling MCP server running on stdio');
20
+ }
21
+ main().catch((error) => {
22
+ console.error('Fatal error:', error);
23
+ process.exit(1);
24
+ });
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
3
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,12 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerConfigureTools, registerVideoTools } from './tools/index.js';
3
+ export function createServer() {
4
+ const server = new McpServer({
5
+ name: 'kling-mcp-server',
6
+ version: '0.1.0',
7
+ });
8
+ registerConfigureTools(server);
9
+ registerVideoTools(server);
10
+ return server;
11
+ }
12
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerConfigureTools(server: McpServer): void;
3
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod';
2
+ import { setApiKeys } from '../auth.js';
3
+ import { bridgeRequest, BRIDGE_STATE_PATH } from '../bridge.js';
4
+ import { KlingError } from '../types.js';
5
+ import { withErrorHandling } from '../utils.js';
6
+ export function registerConfigureTools(server) {
7
+ server.registerTool('configure_kling_api_keys', {
8
+ description: 'Save Kling API credentials. Call this when the user provides their API keys.\n\n' +
9
+ 'WHEN TO USE:\n' +
10
+ '- User says "here are my Kling keys" or provides access_key/secret_key\n' +
11
+ '- You get an AUTH_REQUIRED error from other Kling tools\n' +
12
+ '- User wants to update/change their Kling credentials\n\n' +
13
+ 'WHERE TO GET KEYS:\n' +
14
+ 'Direct user to: https://app.klingai.com/global/dev/api-key\n' +
15
+ '1. Sign in to Kling AI\n' +
16
+ '2. Go to API Keys section\n' +
17
+ '3. Create new API key\n' +
18
+ '4. Copy BOTH the Access Key and Secret Key\n\n' +
19
+ 'IMPORTANT: Both keys are required. The Access Key identifies the account, the Secret Key signs requests.',
20
+ inputSchema: z.object({
21
+ access_key: z.string().min(1).describe('Kling API Access Key (identifies the account)'),
22
+ secret_key: z.string().min(1).describe('Kling API Secret Key (signs API requests)'),
23
+ }),
24
+ annotations: { readOnlyHint: false, destructiveHint: false },
25
+ }, withErrorHandling(async (args) => {
26
+ const trimmedAccessKey = args.access_key.trim();
27
+ const trimmedSecretKey = args.secret_key.trim();
28
+ // If bridge is available, persist via bridge
29
+ if (BRIDGE_STATE_PATH) {
30
+ try {
31
+ const result = await bridgeRequest('/bundled/kling/configure', {
32
+ accessKey: trimmedAccessKey,
33
+ secretKey: trimmedSecretKey,
34
+ });
35
+ if (result.success) {
36
+ setApiKeys(trimmedAccessKey, trimmedSecretKey);
37
+ const message = result.warning
38
+ ? `Kling API keys configured successfully. Note: ${result.warning}`
39
+ : 'Kling API keys configured successfully! You can now use generate_kling_video to create AI videos.';
40
+ return JSON.stringify({ ok: true, message });
41
+ }
42
+ // Bridge returned failure — surface as error, do NOT fall through
43
+ throw new KlingError(result.error || 'Bridge configuration failed', 'BRIDGE_ERROR', 'The host app bridge rejected the configuration request. Check the host app logs.');
44
+ }
45
+ catch (error) {
46
+ if (error instanceof KlingError)
47
+ throw error;
48
+ // Bridge request failed (network, timeout, etc.) — surface as error
49
+ throw new KlingError(`Bridge request failed: ${error instanceof Error ? error.message : String(error)}`, 'BRIDGE_ERROR', 'Could not reach the host app bridge. Ensure the host app is running.');
50
+ }
51
+ }
52
+ // No bridge configured — configure in-memory only
53
+ setApiKeys(trimmedAccessKey, trimmedSecretKey);
54
+ return JSON.stringify({
55
+ ok: true,
56
+ message: 'Kling API keys configured successfully! You can now use generate_kling_video to create AI videos.',
57
+ });
58
+ }));
59
+ }
60
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1,3 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerVideoTools } from './video.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerVideoTools } from './video.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerVideoTools(server: McpServer): void;
3
+ //# sourceMappingURL=video.d.ts.map
@@ -0,0 +1,215 @@
1
+ import { z } from 'zod';
2
+ import { klingFetch } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ const MODEL_ENUM = [
5
+ 'kling-v2-6',
6
+ 'kling-v2-5-turbo',
7
+ 'kling-v2-master',
8
+ 'kling-v2-1-master',
9
+ 'kling-v1-6',
10
+ 'kling-v1',
11
+ ];
12
+ export function registerVideoTools(server) {
13
+ // ─── generate_kling_video ──────────────────────────────────────
14
+ server.registerTool('generate_kling_video', {
15
+ description: 'Create an AI-generated video from a text description.\n\n' +
16
+ 'WORKFLOW:\n' +
17
+ '1. Call this tool with your prompt → returns task_id immediately\n' +
18
+ '2. Wait 30 seconds, then call check_kling_task(task_id, task_type="text2video")\n' +
19
+ '3. Repeat step 2 until status is "succeed" (typically 2-5 minutes total)\n' +
20
+ '4. When complete, you\'ll get a video URL (valid for 30 days)\n\n' +
21
+ 'MODELS (newest to oldest):\n' +
22
+ '- kling-v2-6 (default): Latest model with best quality and native audio support\n' +
23
+ '- kling-v2-5-turbo: Faster generation, still high quality\n' +
24
+ '- kling-v2-master: High quality model\n' +
25
+ '- kling-v2-1-master: Previous generation master\n' +
26
+ '- kling-v1-6: Good balance of speed and quality\n' +
27
+ '- kling-v1: Original model\n\n' +
28
+ 'COSTS: ~100 credits for 5s standard, ~200 for 5s pro.',
29
+ inputSchema: z.object({
30
+ prompt: z
31
+ .string()
32
+ .min(1)
33
+ .describe('Detailed description of the video. Include: subject, action/motion, camera angle, style, lighting, mood. Max 2500 chars.'),
34
+ negative_prompt: z
35
+ .string()
36
+ .optional()
37
+ .describe('Things to avoid: "blurry, distorted faces, text, watermark, low quality"'),
38
+ model: z.enum(MODEL_ENUM).optional().describe('Model version. Default: kling-v2-6'),
39
+ aspect_ratio: z
40
+ .enum(['16:9', '9:16', '1:1'])
41
+ .optional()
42
+ .describe('16:9=landscape, 9:16=portrait, 1:1=square. Default: 16:9'),
43
+ duration: z
44
+ .enum(['5', '10'])
45
+ .optional()
46
+ .describe('Video length in seconds. Default: 5'),
47
+ mode: z
48
+ .enum(['std', 'pro'])
49
+ .optional()
50
+ .describe('std=standard (faster), pro=professional (higher quality). Default: std'),
51
+ }),
52
+ annotations: { readOnlyHint: false, destructiveHint: false },
53
+ }, withErrorHandling(async (args) => {
54
+ const model = args.model || 'kling-v2-6';
55
+ const body = {
56
+ prompt: args.prompt,
57
+ model_name: model,
58
+ aspect_ratio: args.aspect_ratio || '16:9',
59
+ duration: args.duration || '5',
60
+ mode: args.mode || 'std',
61
+ };
62
+ if (args.negative_prompt) {
63
+ body.negative_prompt = args.negative_prompt;
64
+ }
65
+ const result = await klingFetch('/videos/text2video', {
66
+ method: 'POST',
67
+ body: JSON.stringify(body),
68
+ });
69
+ return JSON.stringify({
70
+ ok: true,
71
+ task_id: result.task_id,
72
+ task_type: 'text2video',
73
+ status: 'submitted',
74
+ message: `Video generation started. Use check_kling_task with task_id "${result.task_id}" and task_type "text2video" to poll for completion (typically 2-5 minutes).`,
75
+ nextPollSeconds: 30,
76
+ });
77
+ }));
78
+ // ─── generate_kling_image_to_video ─────────────────────────────
79
+ server.registerTool('generate_kling_image_to_video', {
80
+ description: 'Animate a still image into a video using AI.\n\n' +
81
+ 'WORKFLOW:\n' +
82
+ '1. You need a PUBLIC image URL (must start with https://)\n' +
83
+ '2. Call this tool with the image URL and a motion prompt → returns task_id\n' +
84
+ '3. Wait 30 seconds, then call check_kling_task(task_id, task_type="image2video")\n' +
85
+ '4. Repeat step 3 until status is "succeed" (typically 2-5 minutes)\n\n' +
86
+ 'IMAGE REQUIREMENTS:\n' +
87
+ '- Must be publicly accessible (https:// URL)\n' +
88
+ '- High resolution works best\n\n' +
89
+ 'IMPORTANT: If you have a local image, you must first upload it to a hosting service to get a public URL.',
90
+ inputSchema: z.object({
91
+ image_url: z
92
+ .string()
93
+ .url()
94
+ .describe('Public HTTPS URL of the image to animate.'),
95
+ prompt: z
96
+ .string()
97
+ .min(1)
98
+ .describe('Describe the desired motion/animation: camera movement, subject movement.'),
99
+ negative_prompt: z
100
+ .string()
101
+ .optional()
102
+ .describe('Motion to avoid: "jerky movement, face distortion, unnatural motion"'),
103
+ model: z.enum(MODEL_ENUM).optional().describe('Model version. Default: kling-v2-6'),
104
+ duration: z.enum(['5', '10']).optional().describe('Video length in seconds. Default: 5'),
105
+ mode: z
106
+ .enum(['std', 'pro'])
107
+ .optional()
108
+ .describe('std=standard, pro=professional quality. Default: std'),
109
+ }),
110
+ annotations: { readOnlyHint: false, destructiveHint: false },
111
+ }, withErrorHandling(async (args) => {
112
+ // Validate HTTPS URL
113
+ if (!args.image_url.startsWith('https://')) {
114
+ return JSON.stringify({
115
+ ok: false,
116
+ error: 'Image URL must use HTTPS',
117
+ code: 'INVALID_URL',
118
+ resolution: 'Kling requires publicly accessible HTTPS URLs. Upload your image to a hosting service first.',
119
+ });
120
+ }
121
+ const model = args.model || 'kling-v2-6';
122
+ const body = {
123
+ image: args.image_url,
124
+ prompt: args.prompt,
125
+ model_name: model,
126
+ duration: args.duration || '5',
127
+ mode: args.mode || 'std',
128
+ };
129
+ if (args.negative_prompt) {
130
+ body.negative_prompt = args.negative_prompt;
131
+ }
132
+ const result = await klingFetch('/videos/image2video', {
133
+ method: 'POST',
134
+ body: JSON.stringify(body),
135
+ });
136
+ return JSON.stringify({
137
+ ok: true,
138
+ task_id: result.task_id,
139
+ task_type: 'image2video',
140
+ status: 'submitted',
141
+ message: `Image-to-video generation started. Use check_kling_task with task_id "${result.task_id}" and task_type "image2video" to poll for completion.`,
142
+ nextPollSeconds: 30,
143
+ });
144
+ }));
145
+ // ─── check_kling_task ──────────────────────────────────────────
146
+ server.registerTool('check_kling_task', {
147
+ description: 'Check if a Kling video generation task is complete.\n\n' +
148
+ 'WHEN TO CALL:\n' +
149
+ '- After generate_kling_video or generate_kling_image_to_video returns a task_id\n' +
150
+ '- Wait ~30 seconds between checks\n' +
151
+ '- Keep polling until status is "succeed" or "failed"\n\n' +
152
+ 'RESPONSE STATUS VALUES:\n' +
153
+ '- "submitted" → Task received, generation starting\n' +
154
+ '- "processing" → Video being generated (wait and poll again)\n' +
155
+ '- "succeed" → DONE! Response includes video.url\n' +
156
+ '- "failed" → Error occurred, check the error message\n\n' +
157
+ 'VIDEO URL: Valid for 30 days after generation.',
158
+ inputSchema: z.object({
159
+ task_id: z
160
+ .string()
161
+ .min(1)
162
+ .describe('The task_id returned by generate_kling_video or generate_kling_image_to_video'),
163
+ task_type: z
164
+ .enum(['text2video', 'image2video'])
165
+ .optional()
166
+ .describe('Use "text2video" for generate_kling_video, "image2video" for generate_kling_image_to_video. Default: text2video'),
167
+ }),
168
+ annotations: { readOnlyHint: true },
169
+ }, withErrorHandling(async (args) => {
170
+ const taskType = args.task_type || 'text2video';
171
+ const altType = taskType === 'text2video' ? 'image2video' : 'text2video';
172
+ let result;
173
+ try {
174
+ result = await klingFetch(`/videos/${taskType}/${args.task_id}`);
175
+ }
176
+ catch (error) {
177
+ // If task not found with the given type, try the alternative type
178
+ if (error instanceof Error &&
179
+ 'code' in error &&
180
+ (error.code === 'HTTP_404' ||
181
+ error.code === 'KLING_1201')) {
182
+ result = await klingFetch(`/videos/${altType}/${args.task_id}`);
183
+ }
184
+ else {
185
+ throw error;
186
+ }
187
+ }
188
+ const response = {
189
+ ok: true,
190
+ task_id: result.task_id,
191
+ status: result.task_status,
192
+ };
193
+ if (result.task_status_msg) {
194
+ response.message = result.task_status_msg;
195
+ }
196
+ if (result.task_status === 'processing') {
197
+ response.nextPollSeconds = 20;
198
+ response.hint = 'Still processing. Poll again in 20 seconds.';
199
+ }
200
+ else if (result.task_status === 'succeed' && result.task_result?.videos?.length) {
201
+ const video = result.task_result.videos[0];
202
+ response.video = {
203
+ url: video.url,
204
+ duration: video.duration,
205
+ };
206
+ response.hint = 'Video generation complete! URL is valid for 30 days.';
207
+ }
208
+ else if (result.task_status === 'failed') {
209
+ response.ok = false;
210
+ response.resolution = 'Generation failed. Try a different prompt or check your credits.';
211
+ }
212
+ return JSON.stringify(response);
213
+ }));
214
+ }
215
+ //# sourceMappingURL=video.js.map
@@ -0,0 +1,36 @@
1
+ export declare const REQUEST_TIMEOUT_MS = 30000;
2
+ export declare const KLING_API_BASE = "https://api-singapore.klingai.com/v1";
3
+ export interface BridgeState {
4
+ port: number;
5
+ token: string;
6
+ }
7
+ export declare class KlingError extends Error {
8
+ readonly code: string;
9
+ readonly resolution: string;
10
+ constructor(message: string, code: string, resolution: string);
11
+ }
12
+ /**
13
+ * Kling API response wrapper.
14
+ * All responses return code 0 for success.
15
+ */
16
+ export interface KlingApiResponse<T> {
17
+ code: number;
18
+ message: string;
19
+ data: T;
20
+ }
21
+ export interface VideoGenerationResponse {
22
+ task_id: string;
23
+ }
24
+ export interface TaskStatusResponse {
25
+ task_id: string;
26
+ task_status: 'submitted' | 'processing' | 'succeed' | 'failed';
27
+ task_status_msg?: string;
28
+ task_result?: {
29
+ videos?: Array<{
30
+ url: string;
31
+ duration: string;
32
+ aspect_ratio?: string;
33
+ }>;
34
+ };
35
+ }
36
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ export const REQUEST_TIMEOUT_MS = 30_000;
2
+ export const KLING_API_BASE = 'https://api-singapore.klingai.com/v1';
3
+ export class KlingError extends Error {
4
+ code;
5
+ resolution;
6
+ constructor(message, code, resolution) {
7
+ super(message);
8
+ this.code = code;
9
+ this.resolution = resolution;
10
+ this.name = 'KlingError';
11
+ }
12
+ }
13
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,14 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ type ToolHandler<T> = (args: T, extra: unknown) => Promise<CallToolResult>;
3
+ /**
4
+ * Wraps a tool handler with standard error handling.
5
+ *
6
+ * - On success: returns the string result as a text content block.
7
+ * - On KlingError: returns a structured JSON error with code and resolution.
8
+ * - On unknown error: returns a generic error message.
9
+ *
10
+ * Secrets are never exposed in error messages.
11
+ */
12
+ export declare function withErrorHandling<T>(fn: (args: T, extra: unknown) => Promise<string>): ToolHandler<T>;
13
+ export {};
14
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,42 @@
1
+ import { KlingError } from './types.js';
2
+ /**
3
+ * Wraps a tool handler with standard error handling.
4
+ *
5
+ * - On success: returns the string result as a text content block.
6
+ * - On KlingError: returns a structured JSON error with code and resolution.
7
+ * - On unknown error: returns a generic error message.
8
+ *
9
+ * Secrets are never exposed in error messages.
10
+ */
11
+ export function withErrorHandling(fn) {
12
+ return async (args, extra) => {
13
+ try {
14
+ const result = await fn(args, extra);
15
+ return { content: [{ type: 'text', text: result }] };
16
+ }
17
+ catch (error) {
18
+ if (error instanceof KlingError) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify({
24
+ ok: false,
25
+ error: error.message,
26
+ code: error.code,
27
+ resolution: error.resolution,
28
+ }),
29
+ },
30
+ ],
31
+ isError: true,
32
+ };
33
+ }
34
+ const errorMessage = error instanceof Error ? error.message : String(error);
35
+ return {
36
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: errorMessage }) }],
37
+ isError: true,
38
+ };
39
+ }
40
+ };
41
+ }
42
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@mindstone-engineering/mcp-server-kling",
3
+ "version": "0.1.0",
4
+ "description": "Kling AI video generation MCP server for Model Context Protocol hosts",
5
+ "license": "FSL-1.1-MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-server-kling": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "!dist/**/*.map"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/nspr-io/mcp-servers.git",
17
+ "directory": "connectors/kling"
18
+ },
19
+ "homepage": "https://github.com/nspr-io/mcp-servers/tree/main/connectors/kling",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc && shx chmod +x dist/index.js",
25
+ "prepare": "npm run build",
26
+ "watch": "tsc --watch",
27
+ "start": "node dist/index.js",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:coverage": "vitest run --coverage"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.26.0",
34
+ "jose": "^5.2.0",
35
+ "zod": "^3.23.0"
36
+ },
37
+ "devDependencies": {
38
+ "@mindstone-engineering/mcp-test-harness": "file:../../test-harness",
39
+ "@types/node": "^22",
40
+ "@vitest/coverage-v8": "^4.1.3",
41
+ "msw": "^2.13.2",
42
+ "shx": "^0.3.4",
43
+ "typescript": "^5.8.2",
44
+ "vitest": "^4.1.3"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ }
49
+ }