@mixio-pro/kalaasetu-mcp 1.2.1 → 2.0.1-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 +0 -9
- package/src/tools/fal/config.ts +120 -23
- package/src/tools/fal/generate.ts +370 -84
- package/src/tools/fal/index.ts +2 -7
- package/src/tools/fal/models.ts +163 -29
- package/src/tools/fal/storage.ts +9 -2
- package/src/tools/gemini.ts +106 -26
- package/src/tools/image-to-video.ts +359 -129
- package/src/tools/perplexity.ts +61 -61
- package/src/tools/youtube.ts +8 -3
- 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
|
|
|
@@ -59,114 +71,388 @@ function sanitizeParameters(
|
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
/**
|
|
62
|
-
*
|
|
74
|
+
* Progress reporter interface for MCP context compatibility.
|
|
63
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
|
+
|
|
64
90
|
export const falGenerate = {
|
|
65
91
|
name: "fal_generate",
|
|
66
92
|
description:
|
|
67
|
-
"
|
|
93
|
+
"The primary tool for generating AI content (images, videos, etc.) using fal.ai. " +
|
|
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. " +
|
|
96
|
+
"Use 'fal_list_presets' to discover available intents and names. " +
|
|
97
|
+
"PREREQUISITE: If using local files as parameters, you MUST upload them first using 'fal_upload_file' and use the resulting CDN URL. " +
|
|
98
|
+
"ONLY USE WHEN WORKING WITH FAL MODELS/PRESETS.",
|
|
68
99
|
parameters: z.object({
|
|
69
100
|
preset_name: z
|
|
70
101
|
.string()
|
|
71
|
-
.
|
|
102
|
+
.optional()
|
|
103
|
+
.describe(
|
|
104
|
+
"Required for new requests. The unique name of the generation preset (e.g., 'ltx_image_to_video'). Obtain this from 'fal_list_presets'."
|
|
105
|
+
),
|
|
72
106
|
parameters: z
|
|
73
107
|
.record(z.string(), z.any())
|
|
74
108
|
.optional()
|
|
75
|
-
.describe(
|
|
76
|
-
|
|
77
|
-
|
|
109
|
+
.describe(
|
|
110
|
+
"A dictionary of model-specific parameters (e.g., { 'prompt': '...', 'image_url': '...' }). " +
|
|
111
|
+
"These override the default values defined in the preset. " +
|
|
112
|
+
"NOTE: For image-to-video or video-to-video tasks, use 'fal_upload_file' first and pass the resulting CDN URL here."
|
|
113
|
+
),
|
|
114
|
+
resume_id: z
|
|
115
|
+
.string()
|
|
78
116
|
.optional()
|
|
79
|
-
.default(false)
|
|
80
117
|
.describe(
|
|
81
|
-
"
|
|
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."
|
|
82
120
|
),
|
|
83
121
|
}),
|
|
84
|
-
timeoutMs:
|
|
85
|
-
execute: async (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
122
|
+
timeoutMs: 600000, // 10 minutes max
|
|
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
|
+
) => {
|
|
90
132
|
return safeToolExecute(async () => {
|
|
133
|
+
let statusUrl: string;
|
|
134
|
+
let responseUrl: string;
|
|
135
|
+
let requestId: string;
|
|
91
136
|
const config = loadFalConfig();
|
|
92
|
-
const preset = config.presets.find(
|
|
93
|
-
(p) => p.presetName === args.preset_name
|
|
94
|
-
);
|
|
95
137
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
99
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
|
|
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
|
+
}
|
|
100
354
|
}
|
|
101
355
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|
|
107
363
|
|
|
108
|
-
const
|
|
364
|
+
const startTime = Date.now();
|
|
365
|
+
const MAX_POLL_TIME = 600000; // 10 minutes
|
|
366
|
+
let pollCount = 0;
|
|
367
|
+
const POLL_INTERVAL = 3000;
|
|
109
368
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
387
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
116
392
|
|
|
117
|
-
|
|
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
|
+
}
|
|
118
407
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}
|
|
123
417
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}),
|
|
133
|
-
timeoutMs: 300000,
|
|
134
|
-
execute: async (args: { url: string }) => {
|
|
135
|
-
const result = await authenticatedRequest(args.url, "GET");
|
|
136
|
-
return JSON.stringify(result);
|
|
137
|
-
},
|
|
138
|
-
};
|
|
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
|
+
}
|
|
139
426
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
description: "Check the status of a queued fal.ai request.",
|
|
146
|
-
parameters: z.object({
|
|
147
|
-
url: z.string().describe("The status_url from a queued request"),
|
|
148
|
-
}),
|
|
149
|
-
timeoutMs: 300000,
|
|
150
|
-
execute: async (args: { url: string }) => {
|
|
151
|
-
const result = await authenticatedRequest(args.url, "GET");
|
|
152
|
-
return JSON.stringify(result);
|
|
153
|
-
},
|
|
154
|
-
};
|
|
427
|
+
if (res.status === "FAILED") {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`Generation failed: ${JSON.stringify(res.error || res)}`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
155
432
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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");
|
|
171
457
|
},
|
|
172
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";
|