@mixio-pro/kalaasetu-mcp 1.0.15 → 1.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mixio-pro/kalaasetu-mcp",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "A powerful Model Context Protocol server providing AI tools for content generation and analysis",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",
@@ -18,9 +18,6 @@ export class GCSStorageProvider implements StorageProvider {
18
18
  }
19
19
 
20
20
  async init(): Promise<void> {
21
- console.log(
22
- `Initializing GCS Storage Provider with bucket: ${this.bucket}`
23
- );
24
21
  // Verify we can get credentials
25
22
  try {
26
23
  await this.auth.getClient();
@@ -16,32 +16,23 @@ const ai = new GoogleGenAI({
16
16
  apiKey: process.env.GEMINI_API_KEY || "",
17
17
  });
18
18
 
19
+ import { ensureLocalFile } from "../utils/url-file";
20
+
19
21
  async function fileToGenerativePart(filePath: string) {
20
- const storage = getStorage();
22
+ const fileResult = await ensureLocalFile(filePath);
21
23
 
22
- // Check if file exists
23
- const exists = await storage.exists(filePath);
24
- if (!exists) {
25
- // Try to provide more helpful error information
26
- const isAbsolute = path.isAbsolute(filePath);
27
- const resolvedPath = isAbsolute
28
- ? filePath
29
- : path.resolve(process.cwd(), filePath);
30
- throw new Error(
31
- `File not found: ${filePath}\n` +
32
- `Resolved path: ${resolvedPath}\n` +
33
- `Is absolute: ${isAbsolute}\n` +
34
- `CWD: ${process.cwd()}`
35
- );
24
+ try {
25
+ const storage = getStorage();
26
+ const imageBytes = await storage.readFile(fileResult.path);
27
+ return {
28
+ inlineData: {
29
+ data: Buffer.from(imageBytes).toString("base64"),
30
+ mimeType: "image/jpeg",
31
+ },
32
+ };
33
+ } finally {
34
+ fileResult.cleanup();
36
35
  }
37
-
38
- const imageBytes = await storage.readFile(filePath);
39
- return {
40
- inlineData: {
41
- data: Buffer.from(imageBytes).toString("base64"),
42
- mimeType: "image/jpeg",
43
- },
44
- };
45
36
  }
46
37
 
47
38
  // Helper function to save WAV file
@@ -165,29 +156,18 @@ async function processVideoInput(
165
156
  },
166
157
  };
167
158
  } else {
168
- // Local file processing - use File Upload API
169
- const storage = getStorage();
159
+ // Local file processing or non-YouTube URL - use File Upload API
160
+ // ensureLocalFile handles downloading URLs if needed
161
+ const fileResult = await ensureLocalFile(input);
170
162
 
171
- // Check if file exists
172
- const exists = await storage.exists(input);
173
- if (!exists) {
174
- // Try to provide more helpful error information
175
- const isAbsolute = path.isAbsolute(input);
176
- const resolvedPath = isAbsolute
177
- ? input
178
- : path.resolve(process.cwd(), input);
179
- throw new Error(
180
- `Video file not found: ${input}\n` +
181
- `Resolved path: ${resolvedPath}\n` +
182
- `Is absolute: ${isAbsolute}\n` +
183
- `CWD: ${process.cwd()}`
184
- );
163
+ try {
164
+ // Upload file to Gemini API
165
+ // We pass the local path (temp or original)
166
+ const uploadedFile = await uploadFileToGemini(fileResult.path);
167
+ return uploadedFile;
168
+ } finally {
169
+ fileResult.cleanup();
185
170
  }
186
-
187
- // Upload file to Gemini API
188
- const uploadedFile = await uploadFileToGemini(input);
189
-
190
- return uploadedFile;
191
171
  }
192
172
  }
193
173
 
@@ -15,27 +15,67 @@ function fileToBase64(filePath: string): { data: string; mimeType: string } {
15
15
  const data = Buffer.from(buf).toString("base64");
16
16
  // Detect mime type from extension
17
17
  const ext = path.extname(filePath).toLowerCase();
18
- const mimeType = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' :
19
- ext === '.png' ? 'image/png' :
20
- ext === '.webp' ? 'image/webp' : 'image/png';
18
+ const mimeType =
19
+ ext === ".jpg" || ext === ".jpeg"
20
+ ? "image/jpeg"
21
+ : ext === ".png"
22
+ ? "image/png"
23
+ : ext === ".webp"
24
+ ? "image/webp"
25
+ : "image/png";
21
26
  return { data, mimeType };
22
27
  }
23
28
 
24
29
  export const imageToVideo = {
25
30
  name: "imageToVideo",
26
- description: "Generate videos from an image as starting first frame using Vertex Veo models (predictLongRunning + fetchPredictOperation).",
31
+ description:
32
+ "Generate videos from an image as starting first frame using Vertex Veo models (predictLongRunning + fetchPredictOperation).",
27
33
  parameters: z.object({
28
34
  prompt: z.string().describe("Text description for the video"),
29
- image_path: z.string().optional().describe("Path to source image for image-to-video generation"),
30
- aspect_ratio: z.string().optional().describe("Video aspect ratio: '16:9' or '9:16' (default: '9:16')"),
31
- duration_seconds: z.number().optional().describe("Video duration in seconds: 4, 6, or 8 (default: 6)"),
32
- resolution: z.string().optional().describe("Video resolution: '720p' or '1080p' (default: '720p')"),
33
- negative_prompt: z.string().optional().describe("Text describing what not to include in the video"),
34
- person_generation: z.string().optional().describe("Controls generation of people: 'allow_adult' (default for image-to-video) or 'allow_all'"),
35
- reference_images: z.array(z.string()).optional().describe("Additional image paths for reference (max 3)"),
36
- output_path: z.string().optional().describe("Output MP4 file path (if multiple predictions, index suffix is added)"),
37
- gemini_api_key: z.string().optional().describe("Gemini API key (uses GEMINI_API_KEY env var if not provided)"),
38
- model_id: z.string().optional().describe("Model ID (default: veo-2.0-generate-001)"),
35
+ image_path: z
36
+ .string()
37
+ .optional()
38
+ .describe("Path to source image for image-to-video generation"),
39
+ aspect_ratio: z
40
+ .string()
41
+ .optional()
42
+ .describe("Video aspect ratio: '16:9' or '9:16' (default: '9:16')"),
43
+ duration_seconds: z
44
+ .number()
45
+ .optional()
46
+ .describe("Video duration in seconds: 4, 6, or 8 (default: 6)"),
47
+ resolution: z
48
+ .string()
49
+ .optional()
50
+ .describe("Video resolution: '720p' or '1080p' (default: '720p')"),
51
+ negative_prompt: z
52
+ .string()
53
+ .optional()
54
+ .describe("Text describing what not to include in the video"),
55
+ person_generation: z
56
+ .string()
57
+ .optional()
58
+ .describe(
59
+ "Controls generation of people: 'allow_adult' (default for image-to-video) or 'allow_all'"
60
+ ),
61
+ reference_images: z
62
+ .array(z.string())
63
+ .optional()
64
+ .describe("Additional image paths for reference (max 3)"),
65
+ output_path: z
66
+ .string()
67
+ .optional()
68
+ .describe(
69
+ "Output MP4 file path (if multiple predictions, index suffix is added)"
70
+ ),
71
+ gemini_api_key: z
72
+ .string()
73
+ .optional()
74
+ .describe("Gemini API key (uses GEMINI_API_KEY env var if not provided)"),
75
+ model_id: z
76
+ .string()
77
+ .optional()
78
+ .describe("Model ID (default: veo-2.0-generate-001)"),
39
79
  }),
40
80
  execute: async (args: {
41
81
  prompt: string;
@@ -52,47 +92,49 @@ export const imageToVideo = {
52
92
  }) => {
53
93
  const apiKey = args.gemini_api_key || process.env.GEMINI_API_KEY;
54
94
  if (!apiKey) {
55
- throw new Error("Gemini API key is required. Set GEMINI_API_KEY environment variable or pass gemini_api_key parameter. Get one at https://aistudio.google.com/app/apikey");
95
+ throw new Error(
96
+ "Gemini API key is required. Set GEMINI_API_KEY environment variable or pass gemini_api_key parameter. Get one at https://aistudio.google.com/app/apikey"
97
+ );
56
98
  }
57
99
 
58
100
  const model = args.model_id || "veo-2.0-generate-001";
59
-
101
+
60
102
  // Initialize Google GenAI client
61
103
  const genai = new GoogleGenAI({ apiKey });
62
104
 
63
105
  // Build config for video generation
64
106
  const config: any = {};
65
-
107
+
66
108
  if (args.duration_seconds !== undefined) {
67
109
  config.duration_seconds = args.duration_seconds;
68
110
  } else {
69
111
  config.duration_seconds = 6; // default
70
112
  }
71
-
113
+
72
114
  if (args.aspect_ratio) {
73
115
  config.aspect_ratio = args.aspect_ratio;
74
116
  }
75
117
 
76
118
  try {
77
119
  // Start video generation operation
78
- console.log(`Starting video generation with model: ${model}`);
120
+ console.error(`Starting video generation with model: ${model}`);
79
121
  let operation = await genai.models.generateVideos({
80
122
  model,
81
123
  prompt: args.prompt,
82
124
  config,
83
125
  });
84
126
 
85
- console.log("Operation started, waiting for completion...");
86
-
127
+ console.error("Operation started, waiting for completion...");
128
+
87
129
  // Poll until operation is complete (max 10 minutes)
88
130
  let tries = 0;
89
131
  const maxTries = 60; // 10 minutes with 10s intervals
90
-
132
+
91
133
  while (!operation.done && tries < maxTries) {
92
134
  await wait(10000); // Wait 10 seconds
93
135
  tries++;
94
- console.log(`Polling attempt ${tries}/${maxTries}...`);
95
-
136
+ console.error(`Polling attempt ${tries}/${maxTries}...`);
137
+
96
138
  operation = await genai.operations.getVideosOperation({
97
139
  operation: operation,
98
140
  });
@@ -102,60 +144,75 @@ export const imageToVideo = {
102
144
  throw new Error("Video generation timed out after 10 minutes");
103
145
  }
104
146
 
105
- console.log("Operation completed!");
106
- console.log("Full Response:", JSON.stringify(operation.response, null, 2));
147
+ console.error("Operation completed!");
148
+ console.error(
149
+ "Full Response:",
150
+ JSON.stringify(operation.response, null, 2)
151
+ );
107
152
 
108
153
  // Extract generated videos from response
109
154
  const generatedVideos = operation.response?.generatedVideos || [];
110
-
155
+
111
156
  if (!generatedVideos || generatedVideos.length === 0) {
112
157
  const respStr = JSON.stringify(operation.response, null, 2);
113
- return `Video generation completed but no videos found in response.\n\nFull Response:\n${respStr.slice(0, 2000)}${respStr.length > 2000 ? '\n...(truncated)' : ''}`;
158
+ return `Video generation completed but no videos found in response.\n\nFull Response:\n${respStr.slice(
159
+ 0,
160
+ 2000
161
+ )}${respStr.length > 2000 ? "\n...(truncated)" : ""}`;
114
162
  }
115
163
 
116
164
  // Download and save videos
117
165
  const outputs: string[] = [];
118
-
166
+
119
167
  for (let i = 0; i < generatedVideos.length; i++) {
120
168
  const generatedVideo = generatedVideos[i];
121
169
  const videoUri = generatedVideo?.video?.uri;
122
-
170
+
123
171
  if (!videoUri) {
124
172
  console.warn(`Video ${i} has no URI`);
125
173
  continue;
126
174
  }
127
175
 
128
- console.log(`Downloading video ${i + 1}/${generatedVideos.length}...`);
129
-
176
+ console.error(
177
+ `Downloading video ${i + 1}/${generatedVideos.length}...`
178
+ );
179
+
130
180
  // Download video from URI
131
181
  const videoUrl = `${videoUri}&key=${apiKey}`;
132
182
  const response = await fetch(videoUrl);
133
-
183
+
134
184
  if (!response.ok) {
135
- throw new Error(`Failed to download video: ${response.status} ${response.statusText}`);
185
+ throw new Error(
186
+ `Failed to download video: ${response.status} ${response.statusText}`
187
+ );
136
188
  }
137
-
189
+
138
190
  const buffer = await response.arrayBuffer();
139
-
191
+
140
192
  // Save video to file
141
193
  const filePath = args.output_path
142
- ? (i === 0 ? args.output_path : args.output_path.replace(/\.mp4$/i, `_${i}.mp4`))
143
- : `video_output_${Date.now()}${i === 0 ? '' : '_' + i}.mp4`;
194
+ ? i === 0
195
+ ? args.output_path
196
+ : args.output_path.replace(/\.mp4$/i, `_${i}.mp4`)
197
+ : `video_output_${Date.now()}${i === 0 ? "" : "_" + i}.mp4`;
144
198
  const absPath = path.resolve(filePath);
145
-
199
+
146
200
  fs.writeFileSync(absPath, Buffer.from(buffer));
147
201
  outputs.push(absPath);
148
- console.log(`Saved video to: ${absPath}`);
202
+ console.error(`Saved video to: ${absPath}`);
149
203
  }
150
204
 
151
205
  if (outputs.length > 0) {
152
- return `Video(s) saved successfully:\n${outputs.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
206
+ return `Video(s) saved successfully:\n${outputs
207
+ .map((p, i) => `${i + 1}. ${p}`)
208
+ .join("\n")}`;
153
209
  }
154
210
 
155
211
  return "Video generation completed but no videos were saved.";
156
-
157
212
  } catch (error: any) {
158
- throw new Error(`Video generation failed: ${error.message || JSON.stringify(error)}`);
213
+ throw new Error(
214
+ `Video generation failed: ${error.message || JSON.stringify(error)}`
215
+ );
159
216
  }
160
217
  },
161
218
  };
@@ -11,32 +11,23 @@ async function wait(ms: number): Promise<void> {
11
11
  return new Promise((resolve) => setTimeout(resolve, ms));
12
12
  }
13
13
 
14
+ import { ensureLocalFile } from "../utils/url-file";
15
+
14
16
  async function fileToBase64(
15
17
  filePath: string
16
18
  ): Promise<{ data: string; mimeType: string }> {
17
- const storage = getStorage();
19
+ const fileResult = await ensureLocalFile(filePath);
18
20
 
19
- // Check if file exists
20
- const exists = await storage.exists(filePath);
21
- if (!exists) {
22
- // Try to provide more helpful error information
23
- const isAbsolute = path.isAbsolute(filePath);
24
- const resolvedPath = isAbsolute
25
- ? filePath
26
- : path.resolve(process.cwd(), filePath);
27
- throw new Error(
28
- `File not found: ${filePath}\n` +
29
- `Resolved path: ${resolvedPath}\n` +
30
- `Is absolute: ${isAbsolute}\n` +
31
- `CWD: ${process.cwd()}`
32
- );
21
+ try {
22
+ const storage = getStorage();
23
+ const buf = await storage.readFile(fileResult.path);
24
+ const data = Buffer.from(buf).toString("base64");
25
+ // Default to PNG if not sure, similar to existing code
26
+ const mimeType = "image/png";
27
+ return { data, mimeType };
28
+ } finally {
29
+ fileResult.cleanup();
33
30
  }
34
-
35
- const buf = await storage.readFile(filePath);
36
- const data = Buffer.from(buf).toString("base64");
37
- // Default to PNG if not sure, similar to existing code
38
- const mimeType = "image/png";
39
- return { data, mimeType };
40
31
  }
41
32
 
42
33
  export const imageToVideo = {
@@ -8,14 +8,16 @@ export async function callFalModel(
8
8
  const { falKey, logs = true } = options;
9
9
  const key = falKey || process.env.FAL_KEY;
10
10
  if (!key) {
11
- throw new Error("FAL_KEY is required. Provide it via fal_key parameter or FAL_KEY environment variable.");
11
+ throw new Error(
12
+ "FAL_KEY is required. Provide it via fal_key parameter or FAL_KEY environment variable."
13
+ );
12
14
  }
13
15
 
14
16
  fal.config({
15
17
  credentials: key,
16
18
  });
17
19
 
18
- console.log(`[${modelName}] Submitting request to FAL AI...`);
20
+ console.error(`[${modelName}] Submitting request to FAL AI...`);
19
21
 
20
22
  try {
21
23
  const result = await fal.subscribe(modelName, {
@@ -23,23 +25,29 @@ export async function callFalModel(
23
25
  logs,
24
26
  onQueueUpdate: (update) => {
25
27
  if (update.status === "IN_PROGRESS") {
26
- console.log(`[${modelName}] Status: ${update.status}`);
28
+ console.error(`[${modelName}] Status: ${update.status}`);
27
29
  if (logs && "logs" in update && update.logs) {
28
30
  update.logs.forEach((log) => {
29
- console.log(`[${modelName}] ${log.message}`);
31
+ console.error(`[${modelName}] ${log.message}`);
30
32
  });
31
33
  }
32
34
  } else if (update.status === "IN_QUEUE") {
33
- console.log(`[${modelName}] Status: ${update.status} - Waiting in queue...`);
35
+ console.error(
36
+ `[${modelName}] Status: ${update.status} - Waiting in queue...`
37
+ );
34
38
  }
35
39
  },
36
40
  });
37
41
 
38
- console.log(`[${modelName}] Generation completed successfully`);
42
+ console.error(`[${modelName}] Generation completed successfully`);
39
43
 
40
44
  return result;
41
45
  } catch (error: any) {
42
46
  console.error(`[${modelName}] Error:`, error);
43
- throw new Error(`FAL AI ${modelName} generation failed: ${error.message || JSON.stringify(error)}`);
47
+ throw new Error(
48
+ `FAL AI ${modelName} generation failed: ${
49
+ error.message || JSON.stringify(error)
50
+ }`
51
+ );
44
52
  }
45
53
  }
@@ -0,0 +1,91 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import { getStorage } from "../storage";
5
+ import { generateTimestampedFilename } from "./filename";
6
+
7
+ export interface LocalFileResult {
8
+ path: string;
9
+ isTemp: boolean;
10
+ cleanup: () => void;
11
+ }
12
+
13
+ /**
14
+ * Ensures the input is a local file path.
15
+ * If input is a URL, downloads it to a temporary file.
16
+ * If input is a file path, returns it as is (resolving if necessary).
17
+ */
18
+ export async function ensureLocalFile(input: string): Promise<LocalFileResult> {
19
+ const isUrl = input.startsWith("http://") || input.startsWith("https://");
20
+
21
+ if (isUrl) {
22
+ const tempDir = os.tmpdir();
23
+ // Use a timestamped filename to avoid collisions
24
+ // Try to guess extension from URL or default to bin/tmp
25
+ const urlPath = new URL(input).pathname;
26
+ const ext = path.extname(urlPath) || ".tmp";
27
+ // We'll use a simple name since generateTimestampedFilename expects a file path
28
+ const safeName = `mcp_download_${Date.now()}${ext}`;
29
+ const tempFilePath = path.join(tempDir, safeName);
30
+
31
+ try {
32
+ const response = await fetch(input);
33
+ if (!response.ok) {
34
+ throw new Error(
35
+ `Failed to download URL: ${input} (${response.status} ${response.statusText})`
36
+ );
37
+ }
38
+
39
+ const arrayBuffer = await response.arrayBuffer();
40
+ const buffer = Buffer.from(arrayBuffer);
41
+ fs.writeFileSync(tempFilePath, buffer);
42
+
43
+ return {
44
+ path: tempFilePath,
45
+ isTemp: true,
46
+ cleanup: () => {
47
+ try {
48
+ if (fs.existsSync(tempFilePath)) {
49
+ fs.unlinkSync(tempFilePath);
50
+ }
51
+ } catch (e) {
52
+ console.error(`Failed to cleanup temp file ${tempFilePath}:`, e);
53
+ }
54
+ },
55
+ };
56
+ } catch (error: any) {
57
+ // If download failed, clean up if we created the file
58
+ try {
59
+ if (fs.existsSync(tempFilePath)) {
60
+ fs.unlinkSync(tempFilePath);
61
+ }
62
+ } catch {}
63
+ throw new Error(`Failed to process URL ${input}: ${error.message}`);
64
+ }
65
+ } else {
66
+ // It's a local file path (or at least we treat it as one)
67
+ // We verify existence using our storage abstraction or fs
68
+ const storage = getStorage();
69
+ const exists = await storage.exists(input);
70
+
71
+ if (!exists) {
72
+ // Try to provide more helpful error information
73
+ const isAbsolute = path.isAbsolute(input);
74
+ const resolvedPath = isAbsolute
75
+ ? input
76
+ : path.resolve(process.cwd(), input);
77
+ throw new Error(
78
+ `File not found: ${input}\n` +
79
+ `Resolved path: ${resolvedPath}\n` +
80
+ `Is absolute: ${isAbsolute}\n` +
81
+ `CWD: ${process.cwd()}`
82
+ );
83
+ }
84
+
85
+ return {
86
+ path: input,
87
+ isTemp: false,
88
+ cleanup: () => {}, // No-op for existing local files
89
+ };
90
+ }
91
+ }