@mixio-pro/kalaasetu-mcp 2.3.24 → 2.3.26

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": "2.3.24",
3
+ "version": "2.3.26",
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",
@@ -11,6 +11,10 @@ import {
11
11
  extractPrimitiveArgs,
12
12
  } from "../../utils/tool-wrapper";
13
13
  import { sanitizeResponse } from "../../utils/sanitize";
14
+ import {
15
+ encodeTrackingContext,
16
+ decodeTrackingContext,
17
+ } from "../../utils/endpoint-encoder";
14
18
  import {
15
19
  FAL_QUEUE_URL,
16
20
  AUTHENTICATED_TIMEOUT,
@@ -124,6 +128,12 @@ function buildZodSchema(
124
128
 
125
129
  // Always add resume_endpoint for resumable operations
126
130
  zodSchema = zodSchema.extend({
131
+ output_path: z
132
+ .string()
133
+ .optional()
134
+ .describe(
135
+ "Optional local path/filename to save the final generated asset when status is checked via get_generation_status.",
136
+ ),
127
137
  resume_endpoint: z
128
138
  .string()
129
139
  .optional()
@@ -167,21 +177,41 @@ export function createToolFromPreset(preset: FalPresetConfig) {
167
177
 
168
178
  // Handle resume flow
169
179
  if (args.resume_endpoint) {
170
- if (args.resume_endpoint.startsWith("http")) {
171
- statusUrl = args.resume_endpoint;
172
- responseUrl = args.resume_endpoint.replace(/\/status$/, "");
173
- const urlParts = args.resume_endpoint.split("/");
180
+ const resumeParts = args.resume_endpoint.split("||");
181
+ const maybeTracking = resumeParts[resumeParts.length - 1];
182
+ const decodedTracking =
183
+ maybeTracking &&
184
+ !maybeTracking.startsWith("http") &&
185
+ !maybeTracking.includes("/")
186
+ ? (() => {
187
+ try {
188
+ return decodeTrackingContext(maybeTracking);
189
+ } catch {
190
+ return null;
191
+ }
192
+ })()
193
+ : null;
194
+
195
+ const resumeEndpointWithoutTracking =
196
+ decodedTracking?.toolName && resumeParts.length > 1
197
+ ? resumeParts.slice(0, -1).join("||")
198
+ : args.resume_endpoint;
199
+
200
+ if (resumeEndpointWithoutTracking.startsWith("http")) {
201
+ statusUrl = resumeEndpointWithoutTracking;
202
+ responseUrl = resumeEndpointWithoutTracking.replace(/\/status$/, "");
203
+ const urlParts = resumeEndpointWithoutTracking.split("/");
174
204
  const lastPart = urlParts[urlParts.length - 1] || "";
175
205
  requestId =
176
206
  lastPart.replace("/status", "") ||
177
207
  urlParts[urlParts.length - 2] ||
178
208
  "unknown";
179
209
  context?.log?.info(
180
- `Resuming with FAL URL: ${args.resume_endpoint}`,
210
+ `Resuming with FAL URL: ${resumeEndpointWithoutTracking}`,
181
211
  );
182
212
  } else {
183
213
  // Legacy UUID format - reconstruct URL
184
- requestId = args.resume_endpoint;
214
+ requestId = resumeEndpointWithoutTracking;
185
215
  statusUrl = `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}/status`;
186
216
  responseUrl = `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}`;
187
217
  context?.log?.info(
@@ -204,7 +234,7 @@ export function createToolFromPreset(preset: FalPresetConfig) {
204
234
 
205
235
  // Build parameters: input_schema defaults → defaultParams → user args
206
236
  // Extract only the model parameters (exclude our internal fields)
207
- const { resume_endpoint, ...userParams } = args;
237
+ const { resume_endpoint, output_path, ...userParams } = args;
208
238
 
209
239
  // Start with defaults from input_schema
210
240
  const schemaDefaults: Record<string, any> = {};
@@ -390,7 +420,11 @@ export function createToolFromPreset(preset: FalPresetConfig) {
390
420
  return JSON.stringify({
391
421
  status: "IN_PROGRESS",
392
422
  request_id: requestId,
393
- resume_endpoint: statusUrl,
423
+ resume_endpoint: `${statusUrl}||${encodeTrackingContext({
424
+ toolName,
425
+ toolArgs: extractPrimitiveArgs(args),
426
+ requestId: (context as any)?.requestId,
427
+ })}`,
394
428
  status_url: statusUrl,
395
429
  response_url: responseUrl,
396
430
  message:
@@ -170,7 +170,7 @@ export const generateImage = {
170
170
 
171
171
  const numRefImages = args.reference_images?.length || 0;
172
172
  const MODELS =
173
- numRefImages >= 3
173
+ numRefImages >= 2
174
174
  ? [
175
175
  "gemini-3-pro-image-preview",
176
176
  "gemini-3.1-flash-image-preview",
@@ -8,6 +8,8 @@ import { safeToolExecute, extractPrimitiveArgs } from "../utils/tool-wrapper";
8
8
  import { logger } from "../utils/logger";
9
9
  import { getGoogleAccessToken } from "../utils/google-auth";
10
10
  import { sanitizeResponse } from "../utils/sanitize";
11
+ import { getStorage } from "../storage";
12
+ import * as path from "path";
11
13
 
12
14
  function getFalKey(): string {
13
15
  const falKey = process.env.FAL_KEY;
@@ -72,6 +74,146 @@ export async function fetchFalResult(responseUrl: string): Promise<any> {
72
74
  return await response.json();
73
75
  }
74
76
 
77
+ function getFalResultUrl(statusUrl: string): string {
78
+ return statusUrl.replace(/\/status$/, "");
79
+ }
80
+
81
+ function inferFileExtensionFromUrl(url: string): string {
82
+ const normalizedUrl = url.split("?")[0]?.toLowerCase() || "";
83
+ if (normalizedUrl.endsWith(".mp4")) return "mp4";
84
+ if (normalizedUrl.endsWith(".mov")) return "mov";
85
+ if (normalizedUrl.endsWith(".webm")) return "webm";
86
+ if (normalizedUrl.endsWith(".jpg") || normalizedUrl.endsWith(".jpeg"))
87
+ return "jpg";
88
+ if (normalizedUrl.endsWith(".webp")) return "webp";
89
+ if (normalizedUrl.endsWith(".png")) return "png";
90
+ return "bin";
91
+ }
92
+
93
+ function inferMimeTypeFromExtension(ext: string): string {
94
+ switch (ext) {
95
+ case "mp4":
96
+ return "video/mp4";
97
+ case "mov":
98
+ return "video/quicktime";
99
+ case "webm":
100
+ return "video/webm";
101
+ case "jpg":
102
+ return "image/jpeg";
103
+ case "webp":
104
+ return "image/webp";
105
+ case "png":
106
+ return "image/png";
107
+ default:
108
+ return "application/octet-stream";
109
+ }
110
+ }
111
+
112
+ function outputPathWithIndex(outputPath: string, index: number): string {
113
+ if (index === 0) {
114
+ return outputPath;
115
+ }
116
+ const extension = path.extname(outputPath);
117
+ if (!extension) {
118
+ return `${outputPath}_${index}`;
119
+ }
120
+ const base = outputPath.slice(0, -extension.length);
121
+ return `${base}_${index}${extension}`;
122
+ }
123
+
124
+ function extractOutputPathFromTrackingContext(
125
+ trackingContext: any,
126
+ ): string | undefined {
127
+ if (!trackingContext || typeof trackingContext !== "object") {
128
+ return undefined;
129
+ }
130
+
131
+ const topLevelOutputPath = trackingContext?.toolArgs?.output_path;
132
+ if (typeof topLevelOutputPath === "string" && topLevelOutputPath.length > 0) {
133
+ return topLevelOutputPath;
134
+ }
135
+
136
+ const nestedOutputPath = trackingContext?.toolArgs?.parameters?.output_path;
137
+ if (typeof nestedOutputPath === "string" && nestedOutputPath.length > 0) {
138
+ return nestedOutputPath;
139
+ }
140
+
141
+ return undefined;
142
+ }
143
+
144
+ async function saveFalUrls(
145
+ result: any,
146
+ outputPath?: string,
147
+ ): Promise<{
148
+ savedVideos: Array<{ url: string; filename: string; mimeType: string }>;
149
+ savedImages: Array<{ url: string; filename: string; mimeType: string }>;
150
+ }> {
151
+ const storage = getStorage();
152
+ const { generateTimestampedFilename } = await import("../utils/filename");
153
+
154
+ const savedVideos: Array<{ url: string; filename: string; mimeType: string }> =
155
+ [];
156
+ const savedImages: Array<{ url: string; filename: string; mimeType: string }> =
157
+ [];
158
+
159
+ const videoCandidates = Array.isArray(result?.videos) ? result.videos : [];
160
+ const imageCandidates = Array.isArray(result?.images) ? result.images : [];
161
+
162
+ const extractUrl = (item: any): string | undefined => {
163
+ if (typeof item === "string") return item;
164
+ if (item && typeof item.url === "string") return item.url;
165
+ return undefined;
166
+ };
167
+
168
+ for (let i = 0; i < videoCandidates.length; i++) {
169
+ const sourceUrl = extractUrl(videoCandidates[i]);
170
+ if (!sourceUrl || !sourceUrl.startsWith("http")) continue;
171
+
172
+ const response = await fetch(sourceUrl);
173
+ if (!response.ok) continue;
174
+
175
+ const buffer = Buffer.from(await response.arrayBuffer());
176
+ const ext = inferFileExtensionFromUrl(sourceUrl);
177
+ const filePath = outputPath
178
+ ? outputPathWithIndex(outputPath, i)
179
+ : generateTimestampedFilename(`fal_video_output_${i}.${ext === "bin" ? "mp4" : ext}`);
180
+
181
+ const url = await storage.writeFile(filePath, buffer);
182
+ savedVideos.push({
183
+ url,
184
+ filename: filePath,
185
+ mimeType: inferMimeTypeFromExtension(ext === "bin" ? "mp4" : ext),
186
+ });
187
+ }
188
+
189
+ for (let i = 0; i < imageCandidates.length; i++) {
190
+ const sourceUrl = extractUrl(imageCandidates[i]);
191
+ if (!sourceUrl || !sourceUrl.startsWith("http")) continue;
192
+
193
+ const response = await fetch(sourceUrl);
194
+ if (!response.ok) continue;
195
+
196
+ const buffer = Buffer.from(await response.arrayBuffer());
197
+ const ext = inferFileExtensionFromUrl(sourceUrl);
198
+ const defaultOutputPath = generateTimestampedFilename(
199
+ `fal_image_output_${i}.${ext === "bin" ? "png" : ext}`,
200
+ );
201
+
202
+ const filePath = outputPath
203
+ ? outputPathWithIndex(outputPath, i)
204
+ : defaultOutputPath;
205
+
206
+ const url = await storage.writeFile(filePath, buffer);
207
+ savedImages.push({
208
+ url,
209
+ filename: filePath,
210
+ mimeType: inferMimeTypeFromExtension(ext === "bin" ? "png" : ext),
211
+ });
212
+ }
213
+
214
+ return { savedVideos, savedImages };
215
+ }
216
+
75
217
  export async function checkVertexStatus(resumeEndpoint: string): Promise<any> {
76
218
  const accessToken = await getGoogleAccessToken();
77
219
 
@@ -244,7 +386,32 @@ export const getGenerationStatus = {
244
386
  let result: any;
245
387
 
246
388
  if (detectedSource === "fal") {
247
- result = await checkFalStatus(originalEndpoint);
389
+ const falStatusResult = await checkFalStatus(originalEndpoint);
390
+ const falStatus = falStatusResult?.status;
391
+
392
+ if (falStatus === "COMPLETED") {
393
+ const falResultUrl = getFalResultUrl(originalEndpoint);
394
+ const falFinalResult = await fetchFalResult(falResultUrl);
395
+ const falOutputPath = extractOutputPathFromTrackingContext(trackingContext);
396
+ const { savedVideos, savedImages } = await saveFalUrls(
397
+ falFinalResult,
398
+ falOutputPath,
399
+ );
400
+
401
+ if (savedVideos.length > 0) {
402
+ falFinalResult.saved_videos = savedVideos;
403
+ }
404
+ if (savedImages.length > 0) {
405
+ falFinalResult.saved_images = savedImages;
406
+ }
407
+
408
+ result = {
409
+ ...falFinalResult,
410
+ status: "COMPLETED",
411
+ };
412
+ } else {
413
+ result = falStatusResult;
414
+ }
248
415
  } else {
249
416
  result = await checkVertexStatus(originalEndpoint);
250
417
  }
@@ -277,7 +444,9 @@ export const getGenerationStatus = {
277
444
  result: safeResult,
278
445
  message:
279
446
  status === "COMPLETED"
280
- ? "Generation completed! Check 'result.response.saved_videos' for the video URLs."
447
+ ? detectedSource === "fal"
448
+ ? "Generation completed! Check 'result.saved_videos' or 'result.saved_images' for downloaded asset URLs."
449
+ : "Generation completed! Check 'result.response.saved_videos' for the video URLs."
281
450
  : status === "FAILED"
282
451
  ? "Generation failed. Check the 'result' field for error details."
283
452
  : "Generation is still in progress. Call this tool again with the same resume_endpoint to check later.",