@mixio-pro/kalaasetu-mcp 2.3.24 → 2.3.25
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 +1 -1
- package/src/tools/fal/dynamic-tools.ts +42 -8
- package/src/tools/get-status.ts +171 -2
package/package.json
CHANGED
|
@@ -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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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: ${
|
|
210
|
+
`Resuming with FAL URL: ${resumeEndpointWithoutTracking}`,
|
|
181
211
|
);
|
|
182
212
|
} else {
|
|
183
213
|
// Legacy UUID format - reconstruct URL
|
|
184
|
-
requestId =
|
|
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:
|
package/src/tools/get-status.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
?
|
|
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.",
|