@mixio-pro/kalaasetu-mcp 1.2.2 → 2.0.2-beta
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/fal-config.json +106 -0
- package/package.json +2 -1
- package/src/index.ts +5 -9
- package/src/tools/fal/config.ts +120 -23
- package/src/tools/fal/generate.ts +361 -103
- package/src/tools/fal/index.ts +2 -7
- package/src/tools/fal/models.ts +157 -32
- package/src/tools/gemini.ts +40 -2
- package/src/tools/get-status.ts +174 -0
- package/src/tools/image-to-video.ts +334 -119
- package/src/utils/llm-prompt-enhancer.ts +302 -0
- package/src/utils/prompt-enhancer-presets.ts +303 -0
- package/src/utils/prompt-enhancer.ts +186 -0
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate module for fal.ai MCP server.
|
|
3
|
-
* Provides tools for generating content
|
|
3
|
+
* Provides tools for generating content using fal.ai models with MCP progress streaming.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { safeToolExecute } from "../../utils/tool-wrapper";
|
|
8
|
+
import { resolveEnhancer } from "../../utils/prompt-enhancer-presets";
|
|
8
9
|
import {
|
|
9
10
|
FAL_QUEUE_URL,
|
|
10
|
-
FAL_DIRECT_URL,
|
|
11
11
|
AUTHENTICATED_TIMEOUT,
|
|
12
12
|
getApiKey,
|
|
13
13
|
loadFalConfig,
|
|
14
|
-
type FalPresetConfig,
|
|
15
14
|
} from "./config";
|
|
16
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Helper to wait for a specified duration.
|
|
18
|
+
*/
|
|
19
|
+
async function wait(ms: number): Promise<void> {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* Make an authenticated request to fal.ai API.
|
|
19
25
|
*/
|
|
@@ -41,6 +47,12 @@ async function authenticatedRequest(
|
|
|
41
47
|
|
|
42
48
|
if (!response.ok) {
|
|
43
49
|
const errorText = await response.text();
|
|
50
|
+
if (response.status === 404) {
|
|
51
|
+
return {
|
|
52
|
+
status: "IN_PROGRESS_WAITING",
|
|
53
|
+
detail: "Request not yet available in queue.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
44
56
|
throw new Error(`[${response.status}] API error: ${errorText}`);
|
|
45
57
|
}
|
|
46
58
|
|
|
@@ -58,20 +70,38 @@ function sanitizeParameters(
|
|
|
58
70
|
);
|
|
59
71
|
}
|
|
60
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Progress reporter interface for MCP context compatibility.
|
|
75
|
+
*/
|
|
76
|
+
interface ProgressContext {
|
|
77
|
+
reportProgress?: (progress: {
|
|
78
|
+
progress: number;
|
|
79
|
+
total: number;
|
|
80
|
+
}) => Promise<void>;
|
|
81
|
+
streamContent?: (
|
|
82
|
+
content: { type: "text"; text: string } | { type: "text"; text: string }[]
|
|
83
|
+
) => Promise<void>;
|
|
84
|
+
log?: {
|
|
85
|
+
info: (message: string, data?: any) => void;
|
|
86
|
+
debug: (message: string, data?: any) => void;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
export const falGenerate = {
|
|
62
91
|
name: "fal_generate",
|
|
63
92
|
description:
|
|
64
93
|
"The primary tool for generating AI content (images, videos, etc.) using fal.ai. " +
|
|
65
|
-
"This tool
|
|
94
|
+
"This tool handles polling internally and streams progress updates to the client. " +
|
|
95
|
+
"If the generation takes too long (timeout or error), it returns a 'resume_id' that you can use to resume polling. " +
|
|
66
96
|
"Use 'fal_list_presets' to discover available intents and names. " +
|
|
67
97
|
"PREREQUISITE: If using local files as parameters, you MUST upload them first using 'fal_upload_file' and use the resulting CDN URL. " +
|
|
68
|
-
"If a task is expected to take longer than 10-20 seconds, set 'queue: true' to receive status and result URLs instead of a direct result. " +
|
|
69
98
|
"ONLY USE WHEN WORKING WITH FAL MODELS/PRESETS.",
|
|
70
99
|
parameters: z.object({
|
|
71
100
|
preset_name: z
|
|
72
101
|
.string()
|
|
102
|
+
.optional()
|
|
73
103
|
.describe(
|
|
74
|
-
"The unique name of the generation preset (e.g., 'ltx_image_to_video'
|
|
104
|
+
"Required for new requests. The unique name of the generation preset (e.g., 'ltx_image_to_video'). Obtain this from 'fal_list_presets'."
|
|
75
105
|
),
|
|
76
106
|
parameters: z
|
|
77
107
|
.record(z.string(), z.any())
|
|
@@ -81,120 +111,348 @@ export const falGenerate = {
|
|
|
81
111
|
"These override the default values defined in the preset. " +
|
|
82
112
|
"NOTE: For image-to-video or video-to-video tasks, use 'fal_upload_file' first and pass the resulting CDN URL here."
|
|
83
113
|
),
|
|
84
|
-
|
|
85
|
-
.
|
|
114
|
+
resume_id: z
|
|
115
|
+
.string()
|
|
86
116
|
.optional()
|
|
87
|
-
.default(false)
|
|
88
117
|
.describe(
|
|
89
|
-
"
|
|
90
|
-
"
|
|
118
|
+
"If provided, the tool will resume polling for an existing request instead of starting a new one. " +
|
|
119
|
+
"Use the 'request_id' returned in an 'IN_PROGRESS' response or after a timeout error."
|
|
91
120
|
),
|
|
92
121
|
}),
|
|
93
|
-
timeoutMs:
|
|
94
|
-
execute: async (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
timeoutMs: 90000, // 90 seconds MCP timeout (internal timeout is 60s)
|
|
123
|
+
execute: async (
|
|
124
|
+
args: {
|
|
125
|
+
preset_name?: string;
|
|
126
|
+
parameters?: Record<string, any>;
|
|
127
|
+
resume_id?: string;
|
|
128
|
+
auto_enhance?: boolean;
|
|
129
|
+
},
|
|
130
|
+
context?: ProgressContext
|
|
131
|
+
) => {
|
|
99
132
|
return safeToolExecute(async () => {
|
|
133
|
+
let statusUrl: string;
|
|
134
|
+
let responseUrl: string;
|
|
135
|
+
let requestId: string;
|
|
100
136
|
const config = loadFalConfig();
|
|
101
|
-
const preset = config.presets.find(
|
|
102
|
-
(p) => p.presetName === args.preset_name
|
|
103
|
-
);
|
|
104
137
|
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
if (args.resume_id) {
|
|
139
|
+
// Check if resume_id is a full URL (new format) or legacy ID
|
|
140
|
+
if (args.resume_id.startsWith("http")) {
|
|
141
|
+
// NEW: resume_id IS the status/response URL
|
|
142
|
+
statusUrl = args.resume_id;
|
|
143
|
+
// Derive responseUrl by removing /status suffix if present
|
|
144
|
+
responseUrl = args.resume_id.replace(/\/status$/, "");
|
|
145
|
+
// Extract requestId from URL for logging
|
|
146
|
+
const urlParts = args.resume_id.split("/");
|
|
147
|
+
const lastPart = urlParts[urlParts.length - 1] || "";
|
|
148
|
+
requestId =
|
|
149
|
+
lastPart.replace("/status", "") ||
|
|
150
|
+
urlParts[urlParts.length - 2] ||
|
|
151
|
+
"unknown";
|
|
152
|
+
context?.log?.info(`Resuming with FAL URL: ${args.resume_id}`);
|
|
153
|
+
} else {
|
|
154
|
+
// LEGACY: Try to resolve model from preset_name or parse modelId::requestId
|
|
155
|
+
let modelIdFromPreset: string | undefined;
|
|
156
|
+
if (args.preset_name) {
|
|
157
|
+
const p = config.presets.find(
|
|
158
|
+
(x) => x.presetName === args.preset_name
|
|
159
|
+
);
|
|
160
|
+
if (p) {
|
|
161
|
+
modelIdFromPreset = p.modelId;
|
|
162
|
+
context?.log?.info(
|
|
163
|
+
`Using model from preset '${args.preset_name}': ${modelIdFromPreset}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (args.resume_id.includes("::")) {
|
|
169
|
+
const parts = args.resume_id.split("::");
|
|
170
|
+
const mId = parts[0];
|
|
171
|
+
const rId = parts[1] || "";
|
|
172
|
+
|
|
173
|
+
requestId = rId;
|
|
174
|
+
// Prefer ID from preset if available (explicit user intent), otherwise use ID from string
|
|
175
|
+
const effectiveModelId = modelIdFromPreset || mId;
|
|
176
|
+
|
|
177
|
+
statusUrl = `${FAL_QUEUE_URL}/${effectiveModelId}/requests/${rId}/status`;
|
|
178
|
+
responseUrl = `${FAL_QUEUE_URL}/${effectiveModelId}/requests/${rId}`;
|
|
179
|
+
context?.log?.info(
|
|
180
|
+
`Resuming polling for ${effectiveModelId} request: ${rId}`
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
// Legacy/Fallback for raw UUIDs
|
|
184
|
+
requestId = args.resume_id;
|
|
185
|
+
|
|
186
|
+
if (modelIdFromPreset) {
|
|
187
|
+
// Best case: User provided the preset name!
|
|
188
|
+
statusUrl = `${FAL_QUEUE_URL}/${modelIdFromPreset}/requests/${requestId}/status`;
|
|
189
|
+
responseUrl = `${FAL_QUEUE_URL}/${modelIdFromPreset}/requests/${requestId}`;
|
|
190
|
+
context?.log?.info(
|
|
191
|
+
`Resuming polling with explicit model: ${modelIdFromPreset}`
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
// Worst case: No preset, no model in ID. Try legacy generic URL
|
|
195
|
+
statusUrl = `${FAL_QUEUE_URL}/requests/${args.resume_id}/status`;
|
|
196
|
+
responseUrl = `${FAL_QUEUE_URL}/requests/${args.resume_id}`;
|
|
197
|
+
|
|
198
|
+
// Verify/Recovery: Check if generic URL works, if not try to guess model
|
|
199
|
+
// ... (Smart recovery logic below)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Verify/Recovery: Check if generic URL works, if not try to guess model
|
|
203
|
+
try {
|
|
204
|
+
await authenticatedRequest(statusUrl, "GET");
|
|
205
|
+
} catch (e: any) {
|
|
206
|
+
if (`${e}`.includes("405") || `${e}`.includes("404")) {
|
|
207
|
+
context?.log?.info(
|
|
208
|
+
"Generic status URL failed, attempting to recover model-specific URL..."
|
|
209
|
+
);
|
|
210
|
+
let found = false;
|
|
211
|
+
for (const p of config.presets) {
|
|
212
|
+
const testUrl = `${FAL_QUEUE_URL}/${p.modelId}/requests/${requestId}/status`;
|
|
213
|
+
try {
|
|
214
|
+
await authenticatedRequest(testUrl, "GET");
|
|
215
|
+
statusUrl = testUrl;
|
|
216
|
+
responseUrl = `${FAL_QUEUE_URL}/${p.modelId}/requests/${requestId}`;
|
|
217
|
+
context?.log?.info(
|
|
218
|
+
`Recovered session using model: ${p.modelId}`
|
|
219
|
+
);
|
|
220
|
+
found = true;
|
|
221
|
+
break;
|
|
222
|
+
} catch (ignore) {}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!found) {
|
|
226
|
+
context?.log?.info(
|
|
227
|
+
"Could not recover session URL. Assuming generic URL (might fail)."
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
context?.log?.info(
|
|
233
|
+
`Resuming polling for request: ${args.resume_id}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
} // Close the LEGACY else block (line 149)
|
|
237
|
+
} else {
|
|
238
|
+
if (!args.preset_name) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
"preset_name is required when starting a new request."
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
// ... (config loading and preset finding code remains same, omitted for brevity but preserved in tool)
|
|
244
|
+
const preset = config.presets.find(
|
|
245
|
+
(p) => p.presetName === args.preset_name
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (!preset) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Preset '${args.preset_name}' not found. Use fal_list_presets to see available options.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ... (API key checking and Prompt Enhancement logic remains same)
|
|
255
|
+
try {
|
|
256
|
+
const apiKey = getApiKey();
|
|
257
|
+
if (context?.streamContent) {
|
|
258
|
+
await context.streamContent({
|
|
259
|
+
type: "text" as const,
|
|
260
|
+
text: `[FAL] ✓ API key found (${apiKey.slice(
|
|
261
|
+
0,
|
|
262
|
+
8
|
|
263
|
+
)}...). Using preset: ${args.preset_name} → model: ${
|
|
264
|
+
preset.modelId
|
|
265
|
+
}`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
} catch (keyError: any) {
|
|
269
|
+
// ...
|
|
270
|
+
throw keyError;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const mergedParams = {
|
|
274
|
+
...(preset.defaultParams || {}),
|
|
275
|
+
...(args.parameters || {}),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// ... (Prompt enhancement logic preserved)
|
|
279
|
+
// Copying simplified logic block for replacement context matches
|
|
280
|
+
const shouldEnhance = args.auto_enhance !== false;
|
|
281
|
+
if (shouldEnhance && preset.promptEnhancer && mergedParams.prompt) {
|
|
282
|
+
const enhancerName =
|
|
283
|
+
typeof preset.promptEnhancer === "string"
|
|
284
|
+
? preset.promptEnhancer
|
|
285
|
+
: null;
|
|
286
|
+
if (enhancerName === "ltx2") {
|
|
287
|
+
// ... (LLM logic)
|
|
288
|
+
const { enhancePromptWithLLM, isLLMEnhancerAvailable } =
|
|
289
|
+
await import("../../utils/llm-prompt-enhancer");
|
|
290
|
+
if (isLLMEnhancerAvailable()) {
|
|
291
|
+
// ...
|
|
292
|
+
try {
|
|
293
|
+
mergedParams.prompt = await enhancePromptWithLLM(
|
|
294
|
+
mergedParams.prompt,
|
|
295
|
+
"ltx2"
|
|
296
|
+
);
|
|
297
|
+
// ...
|
|
298
|
+
} catch (err) {}
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
// ...
|
|
302
|
+
const enhancer = resolveEnhancer(preset.promptEnhancer);
|
|
303
|
+
if (enhancer.hasTransformations()) {
|
|
304
|
+
mergedParams.prompt = enhancer.enhance(mergedParams.prompt);
|
|
305
|
+
const negatives = enhancer.getNegativeElements();
|
|
306
|
+
if (negatives && !mergedParams.negative_prompt)
|
|
307
|
+
mergedParams.negative_prompt = negatives;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sanitizedParams = sanitizeParameters(mergedParams);
|
|
313
|
+
const url = `${FAL_QUEUE_URL}/${preset.modelId}`;
|
|
314
|
+
|
|
315
|
+
if (context?.streamContent) {
|
|
316
|
+
await context.streamContent({
|
|
317
|
+
type: "text" as const,
|
|
318
|
+
text: `[FAL] Submitting generation request to ${preset.modelId}...`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const queueRes = await authenticatedRequest(
|
|
323
|
+
url,
|
|
324
|
+
"POST",
|
|
325
|
+
sanitizedParams
|
|
108
326
|
);
|
|
327
|
+
|
|
328
|
+
if (!queueRes.request_id && !queueRes.status_url) {
|
|
329
|
+
return JSON.stringify(queueRes);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
requestId =
|
|
333
|
+
queueRes.request_id || queueRes.status_url?.split("/").pop() || "";
|
|
334
|
+
|
|
335
|
+
if (!requestId) {
|
|
336
|
+
throw new Error("Could not extract request ID from response");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Prefer explicit status/response URLs from API if available
|
|
340
|
+
statusUrl =
|
|
341
|
+
queueRes.status_url ||
|
|
342
|
+
`${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}/status`;
|
|
343
|
+
responseUrl =
|
|
344
|
+
queueRes.response_url ||
|
|
345
|
+
`${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}`;
|
|
346
|
+
|
|
347
|
+
// Stream the FULL status URL for reliable resuming
|
|
348
|
+
if (context?.streamContent) {
|
|
349
|
+
await context.streamContent({
|
|
350
|
+
type: "text" as const,
|
|
351
|
+
text: `[FAL] Generation started. resume_id: ${statusUrl} (use this URL to check status)`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
109
354
|
}
|
|
110
355
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
356
|
+
// Stream message for resume calls
|
|
357
|
+
if (args.resume_id && context?.streamContent) {
|
|
358
|
+
await context.streamContent({
|
|
359
|
+
type: "text" as const,
|
|
360
|
+
text: `[FAL] Resuming status check for job: ${requestId}`,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
116
363
|
|
|
117
|
-
const
|
|
364
|
+
const startTime = Date.now();
|
|
365
|
+
const MAX_POLL_TIME = 60000; // 60 seconds internal timeout - then return resume_id
|
|
366
|
+
let pollCount = 0;
|
|
367
|
+
const POLL_INTERVAL = 3000;
|
|
118
368
|
|
|
119
|
-
|
|
120
|
-
|
|
369
|
+
while (Date.now() - startTime < MAX_POLL_TIME) {
|
|
370
|
+
pollCount++;
|
|
371
|
+
let res;
|
|
372
|
+
try {
|
|
373
|
+
res = await authenticatedRequest(statusUrl, "GET");
|
|
374
|
+
} catch (e: any) {
|
|
375
|
+
if (`${e}`.includes("405")) {
|
|
376
|
+
context?.log?.info(
|
|
377
|
+
`Status check 405 on ${statusUrl}, trying fallback to responseUrl...`
|
|
378
|
+
);
|
|
379
|
+
// Try checking the request root URL instead of /status
|
|
380
|
+
res = await authenticatedRequest(responseUrl, "GET");
|
|
381
|
+
// If successful, update statusUrl to match for future polls
|
|
382
|
+
statusUrl = responseUrl;
|
|
383
|
+
} else {
|
|
384
|
+
throw e;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
121
387
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
388
|
+
// Update URLs if the polling response provides better ones
|
|
389
|
+
// This fixes 405 errors if our constructed URL was slightly off
|
|
390
|
+
if (res.status_url) statusUrl = res.status_url;
|
|
391
|
+
if (res.response_url) responseUrl = res.response_url;
|
|
125
392
|
|
|
126
|
-
|
|
393
|
+
// Stream progress to MCP client
|
|
394
|
+
if (context?.reportProgress) {
|
|
395
|
+
// ... (progress logic)
|
|
396
|
+
const queuePosition = res.queue_position ?? 0;
|
|
397
|
+
const elapsed = Date.now() - startTime;
|
|
398
|
+
const progressPercent = Math.min(
|
|
399
|
+
Math.round((elapsed / MAX_POLL_TIME) * 100),
|
|
400
|
+
99
|
|
401
|
+
);
|
|
402
|
+
await context.reportProgress({
|
|
403
|
+
progress: progressPercent,
|
|
404
|
+
total: 100,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
127
407
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
408
|
+
if (context?.streamContent && pollCount % 5 === 0) {
|
|
409
|
+
// ...
|
|
410
|
+
await context.streamContent({
|
|
411
|
+
type: "text" as const,
|
|
412
|
+
text: `[FAL] Still processing... (${Math.round(
|
|
413
|
+
(Date.now() - startTime) / 1000
|
|
414
|
+
)}s elapsed, status: ${res.status})`,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
132
417
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"Only call this after 'fal_get_status' indicates that the request status is 'COMPLETED'. " +
|
|
142
|
-
"ONLY USE WHEN WORKING WITH FAL MODELS/PRESETS.",
|
|
143
|
-
parameters: z.object({
|
|
144
|
-
url: z
|
|
145
|
-
.string()
|
|
146
|
-
.describe(
|
|
147
|
-
"The 'response_url' provided by a queued 'fal_generate' or 'fal_get_status' call."
|
|
148
|
-
),
|
|
149
|
-
}),
|
|
150
|
-
timeoutMs: 300000,
|
|
151
|
-
execute: async (args: { url: string }) => {
|
|
152
|
-
const result = await authenticatedRequest(args.url, "GET");
|
|
153
|
-
return JSON.stringify(result);
|
|
154
|
-
},
|
|
155
|
-
};
|
|
418
|
+
if (res.status === "COMPLETED") {
|
|
419
|
+
if (context?.reportProgress) {
|
|
420
|
+
await context.reportProgress({ progress: 100, total: 100 });
|
|
421
|
+
}
|
|
422
|
+
// responseUrl is now guaranteed to be correct/fresh from polling
|
|
423
|
+
const finalResult = await authenticatedRequest(responseUrl, "GET");
|
|
424
|
+
return JSON.stringify(finalResult);
|
|
425
|
+
}
|
|
156
426
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
description:
|
|
163
|
-
"Check the current progress or status of an asynchronous fal.ai request. " +
|
|
164
|
-
"PREREQUISITE: This tool can ONLY be used with request 'status_url's obtained from a queued 'fal_generate' call. " +
|
|
165
|
-
"Use this for polling until the status becomes 'COMPLETED', then use 'fal_get_result' for the final output. " +
|
|
166
|
-
"ONLY USE WHEN WORKING WITH FAL MODELS/PRESETS.",
|
|
167
|
-
parameters: z.object({
|
|
168
|
-
url: z
|
|
169
|
-
.string()
|
|
170
|
-
.describe("The 'status_url' provided by a queued 'fal_generate' call."),
|
|
171
|
-
}),
|
|
172
|
-
timeoutMs: 300000,
|
|
173
|
-
execute: async (args: { url: string }) => {
|
|
174
|
-
const result = await authenticatedRequest(args.url, "GET");
|
|
175
|
-
return JSON.stringify(result);
|
|
176
|
-
},
|
|
177
|
-
};
|
|
427
|
+
if (res.status === "FAILED") {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`Generation failed: ${JSON.stringify(res.error || res)}`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
178
432
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
433
|
+
await wait(POLL_INTERVAL);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Timeout - return composite resume_id
|
|
437
|
+
// We need to know modelId here. If we started new, we have 'preset'
|
|
438
|
+
// If we resumed, we parsed 'mId' or used raw.
|
|
439
|
+
const currentModelId =
|
|
440
|
+
args.resume_id && args.resume_id.includes("::")
|
|
441
|
+
? args.resume_id.split("::")[0]
|
|
442
|
+
: args.preset_name
|
|
443
|
+
? config.presets.find((p) => p.presetName === args.preset_name)
|
|
444
|
+
?.modelId
|
|
445
|
+
: "unknown";
|
|
446
|
+
|
|
447
|
+
return JSON.stringify({
|
|
448
|
+
status: "IN_PROGRESS",
|
|
449
|
+
request_id: requestId,
|
|
450
|
+
resume_id: statusUrl, // Use the FULL URL for reliable resume
|
|
451
|
+
status_url: statusUrl,
|
|
452
|
+
response_url: responseUrl,
|
|
453
|
+
message:
|
|
454
|
+
"The generation is still in progress. Call this tool again with resume_id (the URL) to continue polling.",
|
|
455
|
+
});
|
|
456
|
+
}, "fal_generate");
|
|
199
457
|
},
|
|
200
458
|
};
|
package/src/tools/fal/index.ts
CHANGED
|
@@ -3,12 +3,7 @@
|
|
|
3
3
|
* Export all fal.ai tools for registration with the MCP server.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export { falListPresets, falGetPresetDetails
|
|
7
|
-
export {
|
|
8
|
-
falGenerate,
|
|
9
|
-
falGetResult,
|
|
10
|
-
falGetStatus,
|
|
11
|
-
falCancelRequest,
|
|
12
|
-
} from "./generate";
|
|
6
|
+
export { falListPresets, falGetPresetDetails } from "./models";
|
|
7
|
+
export { falGenerate } from "./generate";
|
|
13
8
|
export { falUploadFile } from "./storage";
|
|
14
9
|
export { SERVER_NAME, SERVER_DESCRIPTION, SERVER_VERSION } from "./config";
|