@mixio-pro/kalaasetu-mcp 2.1.0 → 2.1.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.
@@ -5,14 +5,13 @@
5
5
  * Each preset becomes a first-class MCP tool with its own Zod schema.
6
6
  */
7
7
 
8
- import jsonata from "jsonata";
9
- import { z } from "zod";
8
+ import { z, type ZodTypeAny } from "zod";
9
+ import { safeToolExecute } from "../../utils/tool-wrapper";
10
10
  import { resolveEnhancer } from "../../utils/prompt-enhancer-presets";
11
11
  import { sanitizeResponse } from "../../utils/sanitize";
12
- import { safeToolExecute } from "../../utils/tool-wrapper";
13
12
  import {
14
- AUTHENTICATED_TIMEOUT,
15
13
  FAL_QUEUE_URL,
14
+ AUTHENTICATED_TIMEOUT,
16
15
  getApiKey,
17
16
  loadFalConfig,
18
17
  type FalPresetConfig,
@@ -31,7 +30,7 @@ async function wait(ms: number): Promise<void> {
31
30
  async function authenticatedRequest(
32
31
  url: string,
33
32
  method: "GET" | "POST" | "PUT" = "GET",
34
- jsonData?: Record<string, any>
33
+ jsonData?: Record<string, any>,
35
34
  ): Promise<any> {
36
35
  const headers: Record<string, string> = {
37
36
  Authorization: `Key ${getApiKey()}`,
@@ -68,10 +67,10 @@ async function authenticatedRequest(
68
67
  * Sanitize parameters by removing null/undefined values.
69
68
  */
70
69
  function sanitizeParameters(
71
- parameters: Record<string, any>
70
+ parameters: Record<string, any>,
72
71
  ): Record<string, any> {
73
72
  return Object.fromEntries(
74
- Object.entries(parameters).filter(([_, v]) => v != null)
73
+ Object.entries(parameters).filter(([_, v]) => v != null),
75
74
  );
76
75
  }
77
76
 
@@ -84,7 +83,7 @@ interface ProgressContext {
84
83
  total: number;
85
84
  }) => Promise<void>;
86
85
  streamContent?: (
87
- content: { type: "text"; text: string } | { type: "text"; text: string }[]
86
+ content: { type: "text"; text: string } | { type: "text"; text: string }[],
88
87
  ) => Promise<void>;
89
88
  log?: {
90
89
  info: (message: string, data?: any) => void;
@@ -92,13 +91,12 @@ interface ProgressContext {
92
91
  };
93
92
  }
94
93
 
95
-
96
94
  /**
97
95
  * Build a Zod object schema from preset input_schema using built-in z.fromJSONSchema.
98
96
  */
99
97
  function buildZodSchema(
100
98
  inputSchema: Record<string, any> | undefined,
101
- defaultParams?: Record<string, any>
99
+ defaultParams?: Record<string, any>,
102
100
  ): z.ZodObject<any> {
103
101
  // Construct a properties object for JSON schema
104
102
  const properties: Record<string, any> = {};
@@ -129,13 +127,13 @@ function buildZodSchema(
129
127
  .optional()
130
128
  .describe(
131
129
  "If provided, resume polling for an existing request instead of starting a new one. " +
132
- "Use the 'resume_endpoint' returned in an 'IN_PROGRESS' response."
130
+ "Use the 'resume_endpoint' returned in an 'IN_PROGRESS' response.",
133
131
  ),
134
132
  auto_enhance: z
135
133
  .boolean()
136
134
  .default(true) // Our custom default
137
135
  .describe(
138
- "Whether to automatically enhance the prompt. Set to false to disable."
136
+ "Whether to automatically enhance the prompt. Set to false to disable.",
139
137
  ),
140
138
  });
141
139
 
@@ -148,7 +146,7 @@ function buildZodSchema(
148
146
  export function createToolFromPreset(preset: FalPresetConfig) {
149
147
  const zodSchema = buildZodSchema(
150
148
  preset.input_schema as Record<string, any>,
151
- preset.defaultParams
149
+ preset.defaultParams,
152
150
  );
153
151
 
154
152
  const toolName = preset.presetName.startsWith("fal_")
@@ -164,266 +162,245 @@ export function createToolFromPreset(preset: FalPresetConfig) {
164
162
  parameters: zodSchema,
165
163
  timeoutMs: 90000, // 90 seconds MCP timeout
166
164
 
167
- execute: async (
168
- args: Record<string, any>,
169
- context?: ProgressContext
170
- ) => {
171
- return safeToolExecute(async () => {
172
- let statusUrl: string;
173
- let responseUrl: string;
174
- let requestId: string;
175
-
176
- // Handle resume flow
177
- if (args.resume_endpoint) {
178
- if (args.resume_endpoint.startsWith("http")) {
179
- statusUrl = args.resume_endpoint;
180
- responseUrl = args.resume_endpoint.replace(/\/status$/, "");
181
- const urlParts = args.resume_endpoint.split("/");
182
- const lastPart = urlParts[urlParts.length - 1] || "";
183
- requestId =
184
- lastPart.replace("/status", "") ||
185
- urlParts[urlParts.length - 2] ||
186
- "unknown";
187
- context?.log?.info(`Resuming with FAL URL: ${args.resume_endpoint}`);
188
- } else {
189
- // Legacy UUID format - reconstruct URL
190
- requestId = args.resume_endpoint;
191
- statusUrl = `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}/status`;
192
- responseUrl = `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}`;
193
- context?.log?.info(
194
- `Resuming polling for ${preset.modelId} request: ${requestId}`
195
- );
196
- }
197
- } else {
198
- // New request - start generation
199
- try {
200
- const apiKey = getApiKey();
201
- if (context?.streamContent) {
202
- await context.streamContent({
203
- type: "text" as const,
204
- text: `[FAL] ✓ API key found (${apiKey.slice(0, 8)}...). Using model: ${preset.modelId}`,
205
- });
165
+ execute: async (args: Record<string, any>, context?: ProgressContext) => {
166
+ return safeToolExecute(
167
+ async () => {
168
+ let statusUrl: string;
169
+ let responseUrl: string;
170
+ let requestId: string;
171
+
172
+ // Handle resume flow
173
+ if (args.resume_endpoint) {
174
+ if (args.resume_endpoint.startsWith("http")) {
175
+ statusUrl = args.resume_endpoint;
176
+ responseUrl = args.resume_endpoint.replace(/\/status$/, "");
177
+ const urlParts = args.resume_endpoint.split("/");
178
+ const lastPart = urlParts[urlParts.length - 1] || "";
179
+ requestId =
180
+ lastPart.replace("/status", "") ||
181
+ urlParts[urlParts.length - 2] ||
182
+ "unknown";
183
+ context?.log?.info(
184
+ `Resuming with FAL URL: ${args.resume_endpoint}`,
185
+ );
186
+ } else {
187
+ // Legacy UUID format - reconstruct URL
188
+ requestId = args.resume_endpoint;
189
+ statusUrl = `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}/status`;
190
+ responseUrl = `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}`;
191
+ context?.log?.info(
192
+ `Resuming polling for ${preset.modelId} request: ${requestId}`,
193
+ );
206
194
  }
207
- } catch (keyError: any) {
208
- throw keyError;
209
- }
210
-
211
- // Build parameters: input_schema defaults → defaultParams → user args
212
- // Extract only the model parameters (exclude our internal fields)
213
- const { resume_endpoint, auto_enhance, ...userParams } = args;
214
-
215
- // Start with defaults from input_schema
216
- const schemaDefaults: Record<string, any> = {};
217
- if (preset.input_schema) {
218
- for (const [key, paramSchema] of Object.entries(preset.input_schema)) {
219
- if ((paramSchema as any).default !== undefined) {
220
- schemaDefaults[key] = (paramSchema as any).default;
195
+ } else {
196
+ // New request - start generation
197
+ try {
198
+ const apiKey = getApiKey();
199
+ if (context?.streamContent) {
200
+ await context.streamContent({
201
+ type: "text" as const,
202
+ text: `[FAL] ✓ API key found (${apiKey.slice(0, 8)}...). Using model: ${preset.modelId}`,
203
+ });
221
204
  }
205
+ } catch (keyError: any) {
206
+ throw keyError;
222
207
  }
223
- }
224
208
 
225
- // Merge: schema defaults → preset defaultParams → user params
226
- const mergedParams: Record<string, any> = {
227
- ...schemaDefaults,
228
- ...(preset.defaultParams || {}),
229
- ...userParams,
230
- };
231
-
232
- // Apply transformations if defined
233
- if (preset.transformers) {
234
- // 1. JSONata transformation
235
- if (preset.transformers.jsonata) {
236
- try {
237
- const expression = jsonata(preset.transformers.jsonata);
238
- const transformed = await expression.evaluate(mergedParams);
239
- if (transformed && typeof transformed === "object") {
240
- Object.assign(mergedParams, transformed);
209
+ // Build parameters: input_schema defaults → defaultParams → user args
210
+ // Extract only the model parameters (exclude our internal fields)
211
+ const { resume_endpoint, auto_enhance, ...userParams } = args;
212
+
213
+ // Start with defaults from input_schema
214
+ const schemaDefaults: Record<string, any> = {};
215
+ if (preset.input_schema) {
216
+ for (const [key, paramSchema] of Object.entries(
217
+ preset.input_schema,
218
+ )) {
219
+ if ((paramSchema as any).default !== undefined) {
220
+ schemaDefaults[key] = (paramSchema as any).default;
241
221
  }
242
- } catch (err) {
243
- context?.log?.info(`JSONata transformation failed: ${err}`);
244
222
  }
245
223
  }
246
224
 
247
- // 2. Type-based coercion
248
- if (preset.transformers.types) {
249
- for (const [key, type] of Object.entries(preset.transformers.types)) {
250
- if (mergedParams[key] !== undefined && mergedParams[key] !== null) {
251
- if (type === "number") {
252
- mergedParams[key] = Number(mergedParams[key]);
253
- } else if (type === "string") {
254
- mergedParams[key] = String(mergedParams[key]);
255
- } else if (type === "boolean") {
256
- mergedParams[key] = String(mergedParams[key]).toLowerCase() === "true";
225
+ // Merge: schema defaults → preset defaultParams → user params
226
+ const mergedParams = {
227
+ ...schemaDefaults,
228
+ ...(preset.defaultParams || {}),
229
+ ...userParams,
230
+ };
231
+
232
+ // Apply prompt enhancement if enabled
233
+ const shouldEnhance = auto_enhance !== false;
234
+ if (shouldEnhance && preset.promptEnhancer && mergedParams.prompt) {
235
+ const enhancerName =
236
+ typeof preset.promptEnhancer === "string"
237
+ ? preset.promptEnhancer
238
+ : null;
239
+
240
+ if (enhancerName === "ltx2") {
241
+ const { enhancePromptWithLLM, isLLMEnhancerAvailable } =
242
+ await import("../../utils/llm-prompt-enhancer");
243
+ if (isLLMEnhancerAvailable()) {
244
+ try {
245
+ const originalPrompt = mergedParams.prompt;
246
+ mergedParams.prompt = await enhancePromptWithLLM(
247
+ mergedParams.prompt,
248
+ "ltx2",
249
+ );
250
+ context?.log?.info(
251
+ `LLM-enhanced prompt: "${originalPrompt}" → "${mergedParams.prompt}"`,
252
+ );
253
+ } catch (err) {
254
+ context?.log?.info(
255
+ `LLM enhancement failed, using original prompt`,
256
+ );
257
+ }
258
+ }
259
+ } else if (preset.promptEnhancer) {
260
+ const enhancer = resolveEnhancer(preset.promptEnhancer);
261
+ if (enhancer.hasTransformations()) {
262
+ mergedParams.prompt = enhancer.enhance(mergedParams.prompt);
263
+ const negatives = enhancer.getNegativeElements();
264
+ if (negatives && !mergedParams.negative_prompt) {
265
+ mergedParams.negative_prompt = negatives;
257
266
  }
258
267
  }
259
268
  }
260
269
  }
261
- }
262
270
 
263
- // Apply prompt enhancement if enabled
264
- const shouldEnhance = auto_enhance !== false;
265
- if (shouldEnhance && preset.promptEnhancer && mergedParams.prompt) {
266
- const enhancerName =
267
- typeof preset.promptEnhancer === "string"
268
- ? preset.promptEnhancer
269
- : null;
270
-
271
- if (enhancerName === "ltx2") {
272
- const { enhancePromptWithLLM, isLLMEnhancerAvailable } =
273
- await import("../../utils/llm-prompt-enhancer");
274
- if (isLLMEnhancerAvailable()) {
275
- try {
276
- const originalPrompt = mergedParams.prompt;
277
- mergedParams.prompt = await enhancePromptWithLLM(
278
- mergedParams.prompt,
279
- "ltx2"
280
- );
281
- context?.log?.info(
282
- `LLM-enhanced prompt: "${originalPrompt}" → "${mergedParams.prompt}"`
283
- );
284
- } catch (err) {
285
- context?.log?.info(
286
- `LLM enhancement failed, using original prompt`
287
- );
288
- }
289
- }
290
- } else if (preset.promptEnhancer) {
291
- const enhancer = resolveEnhancer(preset.promptEnhancer);
292
- if (enhancer.hasTransformations()) {
293
- mergedParams.prompt = enhancer.enhance(mergedParams.prompt);
294
- const negatives = enhancer.getNegativeElements();
295
- if (negatives && !mergedParams.negative_prompt) {
296
- mergedParams.negative_prompt = negatives;
297
- }
298
- }
271
+ const sanitizedParams = sanitizeParameters(mergedParams);
272
+ const url = `${FAL_QUEUE_URL}/${preset.modelId}`;
273
+
274
+ if (context?.streamContent) {
275
+ await context.streamContent({
276
+ type: "text" as const,
277
+ text: `[FAL] Submitting generation request to ${preset.modelId}...`,
278
+ });
299
279
  }
300
- }
301
280
 
302
- const sanitizedParams = sanitizeParameters(mergedParams);
303
- const url = `${FAL_QUEUE_URL}/${preset.modelId}`;
281
+ const queueRes = await authenticatedRequest(
282
+ url,
283
+ "POST",
284
+ sanitizedParams,
285
+ );
304
286
 
305
- if (context?.streamContent) {
306
- await context.streamContent({
307
- type: "text" as const,
308
- text: `[FAL] Submitting generation request to ${preset.modelId}...`,
309
- });
310
- }
287
+ if (!queueRes.request_id && !queueRes.status_url) {
288
+ return JSON.stringify(sanitizeResponse(queueRes));
289
+ }
311
290
 
312
- const queueRes = await authenticatedRequest(
313
- url,
314
- "POST",
315
- sanitizedParams
316
- );
291
+ requestId =
292
+ queueRes.request_id ||
293
+ queueRes.status_url?.split("/").pop() ||
294
+ "";
317
295
 
318
- if (!queueRes.request_id && !queueRes.status_url) {
319
- return JSON.stringify(sanitizeResponse(queueRes));
320
- }
296
+ if (!requestId) {
297
+ throw new Error("Could not extract request ID from response");
298
+ }
321
299
 
322
- requestId =
323
- queueRes.request_id || queueRes.status_url?.split("/").pop() || "";
300
+ statusUrl =
301
+ queueRes.status_url ||
302
+ `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}/status`;
303
+ responseUrl =
304
+ queueRes.response_url ||
305
+ `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}`;
324
306
 
325
- if (!requestId) {
326
- throw new Error("Could not extract request ID from response");
307
+ if (context?.streamContent) {
308
+ await context.streamContent({
309
+ type: "text" as const,
310
+ text: `[FAL] Generation started. resume_endpoint: ${statusUrl}`,
311
+ });
312
+ }
327
313
  }
328
314
 
329
- statusUrl =
330
- queueRes.status_url ||
331
- `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}/status`;
332
- responseUrl =
333
- queueRes.response_url ||
334
- `${FAL_QUEUE_URL}/${preset.modelId}/requests/${requestId}`;
335
-
336
- if (context?.streamContent) {
315
+ // Stream message for resume calls
316
+ if (args.resume_endpoint && context?.streamContent) {
337
317
  await context.streamContent({
338
318
  type: "text" as const,
339
- text: `[FAL] Generation started. resume_endpoint: ${statusUrl}`,
319
+ text: `[FAL] Resuming status check for job: ${requestId}`,
340
320
  });
341
321
  }
342
- }
343
322
 
344
- // Stream message for resume calls
345
- if (args.resume_endpoint && context?.streamContent) {
346
- await context.streamContent({
347
- type: "text" as const,
348
- text: `[FAL] Resuming status check for job: ${requestId}`,
349
- });
350
- }
351
-
352
- // Poll for completion
353
- const startTime = Date.now();
354
- const MAX_POLL_TIME = 60000; // 60 seconds internal timeout
355
- let pollCount = 0;
356
- const POLL_INTERVAL = 3000;
357
-
358
- while (Date.now() - startTime < MAX_POLL_TIME) {
359
- pollCount++;
360
- let res;
361
- try {
362
- res = await authenticatedRequest(statusUrl, "GET");
363
- } catch (e: any) {
364
- if (`${e}`.includes("405")) {
365
- context?.log?.info(
366
- `Status check 405 on ${statusUrl}, trying fallback...`
367
- );
368
- res = await authenticatedRequest(responseUrl, "GET");
369
- statusUrl = responseUrl;
370
- } else {
371
- throw e;
323
+ // Poll for completion
324
+ const startTime = Date.now();
325
+ const MAX_POLL_TIME = 60000; // 60 seconds internal timeout
326
+ let pollCount = 0;
327
+ const POLL_INTERVAL = 3000;
328
+
329
+ while (Date.now() - startTime < MAX_POLL_TIME) {
330
+ pollCount++;
331
+ let res;
332
+ try {
333
+ res = await authenticatedRequest(statusUrl, "GET");
334
+ } catch (e: any) {
335
+ if (`${e}`.includes("405")) {
336
+ context?.log?.info(
337
+ `Status check 405 on ${statusUrl}, trying fallback...`,
338
+ );
339
+ res = await authenticatedRequest(responseUrl, "GET");
340
+ statusUrl = responseUrl;
341
+ } else {
342
+ throw e;
343
+ }
372
344
  }
373
- }
374
345
 
375
- if (res.status_url) statusUrl = res.status_url;
376
- if (res.response_url) responseUrl = res.response_url;
346
+ if (res.status_url) statusUrl = res.status_url;
347
+ if (res.response_url) responseUrl = res.response_url;
377
348
 
378
- if (context?.reportProgress) {
379
- const elapsed = Date.now() - startTime;
380
- const progressPercent = Math.min(
381
- Math.round((elapsed / MAX_POLL_TIME) * 100),
382
- 99
383
- );
384
- await context.reportProgress({
385
- progress: progressPercent,
386
- total: 100,
387
- });
388
- }
349
+ if (context?.reportProgress) {
350
+ const elapsed = Date.now() - startTime;
351
+ const progressPercent = Math.min(
352
+ Math.round((elapsed / MAX_POLL_TIME) * 100),
353
+ 99,
354
+ );
355
+ await context.reportProgress({
356
+ progress: progressPercent,
357
+ total: 100,
358
+ });
359
+ }
389
360
 
390
- if (context?.streamContent && pollCount % 5 === 0) {
391
- await context.streamContent({
392
- type: "text" as const,
393
- text: `[FAL] Still processing... (${Math.round(
394
- (Date.now() - startTime) / 1000
395
- )}s elapsed, status: ${res.status})`,
396
- });
397
- }
361
+ if (context?.streamContent && pollCount % 5 === 0) {
362
+ await context.streamContent({
363
+ type: "text" as const,
364
+ text: `[FAL] Still processing... (${Math.round(
365
+ (Date.now() - startTime) / 1000,
366
+ )}s elapsed, status: ${res.status})`,
367
+ });
368
+ }
398
369
 
399
- if (res.status === "COMPLETED") {
400
- if (context?.reportProgress) {
401
- await context.reportProgress({ progress: 100, total: 100 });
370
+ if (res.status === "COMPLETED") {
371
+ if (context?.reportProgress) {
372
+ await context.reportProgress({ progress: 100, total: 100 });
373
+ }
374
+ const finalResult = await authenticatedRequest(
375
+ responseUrl,
376
+ "GET",
377
+ );
378
+ return JSON.stringify(sanitizeResponse(finalResult));
402
379
  }
403
- const finalResult = await authenticatedRequest(responseUrl, "GET");
404
- return JSON.stringify(sanitizeResponse(finalResult));
405
- }
406
380
 
407
- if (res.status === "FAILED") {
408
- throw new Error(
409
- `Generation failed: ${JSON.stringify(res.error || res)}`
410
- );
381
+ if (res.status === "FAILED") {
382
+ throw new Error(
383
+ `Generation failed: ${JSON.stringify(res.error || res)}`,
384
+ );
385
+ }
386
+
387
+ await wait(POLL_INTERVAL);
411
388
  }
412
389
 
413
- await wait(POLL_INTERVAL);
414
- }
415
-
416
- // Timeout - return resume_endpoint
417
- return JSON.stringify({
418
- status: "IN_PROGRESS",
419
- request_id: requestId,
420
- resume_endpoint: statusUrl,
421
- status_url: statusUrl,
422
- response_url: responseUrl,
423
- message:
424
- "The generation is still in progress. Call this tool again with resume_endpoint to continue polling.",
425
- });
426
- }, preset.presetName);
390
+ // Timeout - return resume_endpoint
391
+ return JSON.stringify({
392
+ status: "IN_PROGRESS",
393
+ request_id: requestId,
394
+ resume_endpoint: statusUrl,
395
+ status_url: statusUrl,
396
+ response_url: responseUrl,
397
+ message:
398
+ "The generation is still in progress. Call this tool again with resume_endpoint to continue polling.",
399
+ });
400
+ },
401
+ preset.presetName,
402
+ { toolName: toolName },
403
+ );
427
404
  },
428
405
  };
429
406
  }