@mixio-pro/kalaasetu-mcp 1.1.3 → 1.2.0
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/index.ts +10 -9
- package/src/test-context.ts +52 -0
- package/src/test-error-handling.ts +31 -0
- package/src/tools/fal/config.ts +95 -1
- package/src/tools/fal/generate.ts +48 -17
- package/src/tools/fal/index.ts +2 -2
- package/src/tools/fal/models.ts +73 -27
- package/src/tools/fal/storage.ts +62 -58
- package/src/tools/gemini.ts +263 -237
- package/src/tools/image-to-video.ts +199 -185
- package/src/tools/perplexity.ts +194 -154
- package/src/tools/youtube.ts +52 -33
- package/src/utils/tool-wrapper.ts +86 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { FastMCP } from "fastmcp";
|
|
3
|
+
import pkg from "../package.json";
|
|
3
4
|
import { geminiEditImage, geminiTextToImage } from "./tools/gemini";
|
|
4
5
|
import { imageToVideo } from "./tools/image-to-video";
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
falListPresets,
|
|
8
|
+
falGetPresetDetails,
|
|
9
|
+
falGetConfig,
|
|
10
|
+
falGenerate,
|
|
10
11
|
falGetResult,
|
|
11
12
|
falGetStatus,
|
|
12
13
|
falCancelRequest,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
|
|
16
17
|
const server = new FastMCP({
|
|
17
18
|
name: "Kalaasetu MCP Server",
|
|
18
|
-
version:
|
|
19
|
+
version: pkg.version as any,
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
// Gemini Image Tools
|
|
@@ -40,10 +41,10 @@ server.addTool(imageToVideo);
|
|
|
40
41
|
// server.addTool(perplexityVideos);
|
|
41
42
|
|
|
42
43
|
// Fal AI Tools
|
|
43
|
-
server.addTool(
|
|
44
|
-
server.addTool(
|
|
45
|
-
server.addTool(
|
|
46
|
-
server.addTool(
|
|
44
|
+
server.addTool(falGetConfig);
|
|
45
|
+
server.addTool(falListPresets);
|
|
46
|
+
server.addTool(falGetPresetDetails);
|
|
47
|
+
server.addTool(falGenerate);
|
|
47
48
|
server.addTool(falGetResult);
|
|
48
49
|
server.addTool(falGetStatus);
|
|
49
50
|
server.addTool(falCancelRequest);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { safeToolExecute } from "./utils/tool-wrapper";
|
|
3
|
+
|
|
4
|
+
async function runTest() {
|
|
5
|
+
console.log("Running Error Context Verification Test...");
|
|
6
|
+
|
|
7
|
+
// 1. Mock Schema Validation Error
|
|
8
|
+
const schema = z.object({
|
|
9
|
+
prompt: z.string(),
|
|
10
|
+
count: z.number().min(1).max(5),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const mockToolWithValidation = async (args: any) => {
|
|
14
|
+
// Simulate what happens in a real tool when Zod parsing fails inside the execute block
|
|
15
|
+
// OR if we manually parse and it fails
|
|
16
|
+
// Most tools currently don't re-parse inside execute because MCP handles it?
|
|
17
|
+
// Wait, the tools define `parameters` schema, but the `execute` function receives `args`.
|
|
18
|
+
// The MCP server framework usually does the validation BEFORE calling execute.
|
|
19
|
+
// BUT if we look at the code, some tools do manual checks or parsing.
|
|
20
|
+
|
|
21
|
+
// Let's simulate a manual validation failure inside execute, or a deep validation failure
|
|
22
|
+
try {
|
|
23
|
+
schema.parse(args);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return "success";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const zodResult = await safeToolExecute(
|
|
31
|
+
async () => mockToolWithValidation({ prompt: 123, count: 10 }),
|
|
32
|
+
"ZodTool"
|
|
33
|
+
);
|
|
34
|
+
console.log("\n--- Zod Error Result ---");
|
|
35
|
+
console.log(JSON.stringify(zodResult, null, 2));
|
|
36
|
+
|
|
37
|
+
// 2. Mock API Error with Status
|
|
38
|
+
const mockToolWithApiError = async () => {
|
|
39
|
+
// Simulate a fetch error structure
|
|
40
|
+
const err: any = new Error("API request failed");
|
|
41
|
+
err.status = 429;
|
|
42
|
+
err.statusText = "Too Many Requests";
|
|
43
|
+
err.responseBody = "Rate limit exceeded. Retry in 60s.";
|
|
44
|
+
throw err;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const apiResult = await safeToolExecute(mockToolWithApiError, "ApiTool");
|
|
48
|
+
console.log("\n--- API Error Result ---");
|
|
49
|
+
console.log(JSON.stringify(apiResult, null, 2));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
runTest();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { safeToolExecute } from "./utils/tool-wrapper";
|
|
2
|
+
|
|
3
|
+
async function runTest() {
|
|
4
|
+
console.log("Running Error Handling Verification Test...");
|
|
5
|
+
|
|
6
|
+
// Mock tool that throws an error
|
|
7
|
+
const mockTool = async () => {
|
|
8
|
+
throw new Error("Simulated tool failure");
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const result = await safeToolExecute(mockTool, "MockTool");
|
|
12
|
+
|
|
13
|
+
console.log("Result:", JSON.stringify(result, null, 2));
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
result.isError === true &&
|
|
17
|
+
result.content &&
|
|
18
|
+
result.content[0]?.text.includes(
|
|
19
|
+
"Tool execution failed in MockTool: Simulated tool failure"
|
|
20
|
+
)
|
|
21
|
+
) {
|
|
22
|
+
console.log(
|
|
23
|
+
"✅ Verification PASSED: Error was correctly caught and formatted."
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
console.error("❌ Verification FAILED: Unexpected result format.");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
runTest();
|
package/src/tools/fal/config.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Configuration module for the fal.ai MCP server.
|
|
3
2
|
* Provides centralized configuration settings for API endpoints, timeouts, and server metadata.
|
|
4
3
|
*/
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
5
6
|
|
|
6
7
|
// API URLs
|
|
7
8
|
export const FAL_BASE_URL = "https://fal.ai/api";
|
|
@@ -32,3 +33,96 @@ export function getApiKey(): string {
|
|
|
32
33
|
}
|
|
33
34
|
return apiKey;
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Interface for a FAL preset configuration.
|
|
39
|
+
*/
|
|
40
|
+
export interface FalPresetConfig {
|
|
41
|
+
presetName: string;
|
|
42
|
+
intent: string;
|
|
43
|
+
modelId: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
defaultParams?: Record<string, any>;
|
|
46
|
+
inputType?: "text" | "image" | "video" | "audio" | "custom";
|
|
47
|
+
outputType?: "image" | "video" | "audio" | "text" | "custom";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Interface for the FAL configuration file.
|
|
52
|
+
*/
|
|
53
|
+
export interface FalConfig {
|
|
54
|
+
presets: FalPresetConfig[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Default fallback presets in case no config file is provided.
|
|
59
|
+
*/
|
|
60
|
+
export const DEFAULT_PRESETS: FalPresetConfig[] = [
|
|
61
|
+
{
|
|
62
|
+
presetName: "cinematic_image",
|
|
63
|
+
intent: "Generate high-quality cinematic images from text prompts",
|
|
64
|
+
modelId: "fal-ai/flux/pro/v1.1",
|
|
65
|
+
description: "FLUX.1 [pro] v1.1 for state-of-the-art image generation",
|
|
66
|
+
defaultParams: {
|
|
67
|
+
image_size: "landscape_4_3",
|
|
68
|
+
},
|
|
69
|
+
inputType: "text",
|
|
70
|
+
outputType: "image",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
presetName: "cinematic_video",
|
|
74
|
+
intent: "Generate realistic cinematic video from text or image",
|
|
75
|
+
modelId: "fal-ai/luma-dream-machine",
|
|
76
|
+
description: "Luma Dream Machine for high-quality video generation",
|
|
77
|
+
inputType: "custom",
|
|
78
|
+
outputType: "video",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
presetName: "creative_image",
|
|
82
|
+
intent: "Generate creative or artistic images with high flexibility",
|
|
83
|
+
modelId: "fal-ai/stable-diffusion-v35-large",
|
|
84
|
+
description: "Stable Diffusion 3.5 Large for diverse image styles",
|
|
85
|
+
inputType: "text",
|
|
86
|
+
outputType: "image",
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load the FAL configuration from a JSON file.
|
|
92
|
+
* Defaults to DEFAULT_PRESETS if no path is provided or if the file cannot be read.
|
|
93
|
+
*/
|
|
94
|
+
export function loadFalConfig(): FalConfig {
|
|
95
|
+
const configPath = process.env.FAL_CONFIG_JSON_PATH;
|
|
96
|
+
|
|
97
|
+
if (!configPath) {
|
|
98
|
+
console.error(
|
|
99
|
+
"FAL_CONFIG_JSON_PATH not set. Using internal default presets."
|
|
100
|
+
);
|
|
101
|
+
return { presets: DEFAULT_PRESETS };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const absolutePath = path.isAbsolute(configPath)
|
|
106
|
+
? configPath
|
|
107
|
+
: path.join(process.cwd(), configPath);
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(absolutePath)) {
|
|
110
|
+
console.error(
|
|
111
|
+
`FAL config file not found at ${absolutePath}. Using default presets.`
|
|
112
|
+
);
|
|
113
|
+
return { presets: DEFAULT_PRESETS };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fileContent = fs.readFileSync(absolutePath, "utf-8");
|
|
117
|
+
const config = JSON.parse(fileContent) as FalConfig;
|
|
118
|
+
|
|
119
|
+
if (!config.presets || !Array.isArray(config.presets)) {
|
|
120
|
+
throw new Error("Invalid FAL config: 'presets' must be an array.");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return config;
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
console.error(`Error loading FAL config: ${error.message}`);
|
|
126
|
+
return { presets: DEFAULT_PRESETS };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import { safeToolExecute } from "../../utils/tool-wrapper";
|
|
7
8
|
import {
|
|
8
9
|
FAL_QUEUE_URL,
|
|
9
10
|
FAL_DIRECT_URL,
|
|
10
11
|
AUTHENTICATED_TIMEOUT,
|
|
11
12
|
getApiKey,
|
|
13
|
+
loadFalConfig,
|
|
14
|
+
type FalPresetConfig,
|
|
12
15
|
} from "./config";
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -56,17 +59,20 @@ function sanitizeParameters(
|
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/**
|
|
59
|
-
*
|
|
62
|
+
* Unified generation tool using presets defined in configuration.
|
|
60
63
|
*/
|
|
61
|
-
export const
|
|
62
|
-
name: "
|
|
64
|
+
export const falGenerate = {
|
|
65
|
+
name: "fal_generate",
|
|
63
66
|
description:
|
|
64
|
-
"Generate content using
|
|
67
|
+
"Generate content using a named preset and optional parameters. Use fal_list_presets to discover available intents and preset names.",
|
|
65
68
|
parameters: z.object({
|
|
66
|
-
|
|
69
|
+
preset_name: z
|
|
70
|
+
.string()
|
|
71
|
+
.describe("The name of the preset to use (e.g., 'cinematic_image')"),
|
|
67
72
|
parameters: z
|
|
68
73
|
.record(z.string(), z.any())
|
|
69
|
-
.
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Optional model-specific parameters to override defaults"),
|
|
70
76
|
queue: z
|
|
71
77
|
.boolean()
|
|
72
78
|
.optional()
|
|
@@ -75,23 +81,43 @@ export const falGenerateContent = {
|
|
|
75
81
|
"Whether to use the queuing system for long-running tasks. Default: false"
|
|
76
82
|
),
|
|
77
83
|
}),
|
|
84
|
+
timeoutMs: 300000,
|
|
78
85
|
execute: async (args: {
|
|
79
|
-
|
|
80
|
-
parameters
|
|
86
|
+
preset_name: string;
|
|
87
|
+
parameters?: Record<string, any>;
|
|
81
88
|
queue?: boolean;
|
|
82
89
|
}) => {
|
|
83
|
-
|
|
90
|
+
return safeToolExecute(async () => {
|
|
91
|
+
const config = loadFalConfig();
|
|
92
|
+
const preset = config.presets.find(
|
|
93
|
+
(p) => p.presetName === args.preset_name
|
|
94
|
+
);
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
if (!preset) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Preset '${args.preset_name}' not found. Use fal_list_presets to see available options.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
87
101
|
|
|
88
|
-
|
|
102
|
+
// Merge defaults from config with runtime overrides
|
|
103
|
+
const mergedParams = {
|
|
104
|
+
...(preset.defaultParams || {}),
|
|
105
|
+
...(args.parameters || {}),
|
|
106
|
+
};
|
|
89
107
|
|
|
90
|
-
|
|
108
|
+
const sanitizedParams = sanitizeParameters(mergedParams);
|
|
91
109
|
|
|
92
|
-
|
|
110
|
+
const baseUrl = args.queue ? FAL_QUEUE_URL : FAL_DIRECT_URL;
|
|
111
|
+
const url = `${baseUrl}/${preset.modelId}`;
|
|
93
112
|
|
|
94
|
-
|
|
113
|
+
console.error(
|
|
114
|
+
`[fal_generate] Using preset '${args.preset_name}' on model ${preset.modelId}...`
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const result = await authenticatedRequest(url, "POST", sanitizedParams);
|
|
118
|
+
|
|
119
|
+
return JSON.stringify(result);
|
|
120
|
+
}, "fal_generate");
|
|
95
121
|
},
|
|
96
122
|
};
|
|
97
123
|
|
|
@@ -104,6 +130,7 @@ export const falGetResult = {
|
|
|
104
130
|
parameters: z.object({
|
|
105
131
|
url: z.string().describe("The response_url from a queued request"),
|
|
106
132
|
}),
|
|
133
|
+
timeoutMs: 300000,
|
|
107
134
|
execute: async (args: { url: string }) => {
|
|
108
135
|
const result = await authenticatedRequest(args.url, "GET");
|
|
109
136
|
return JSON.stringify(result);
|
|
@@ -119,6 +146,7 @@ export const falGetStatus = {
|
|
|
119
146
|
parameters: z.object({
|
|
120
147
|
url: z.string().describe("The status_url from a queued request"),
|
|
121
148
|
}),
|
|
149
|
+
timeoutMs: 300000,
|
|
122
150
|
execute: async (args: { url: string }) => {
|
|
123
151
|
const result = await authenticatedRequest(args.url, "GET");
|
|
124
152
|
return JSON.stringify(result);
|
|
@@ -134,8 +162,11 @@ export const falCancelRequest = {
|
|
|
134
162
|
parameters: z.object({
|
|
135
163
|
url: z.string().describe("The cancel_url from a queued request"),
|
|
136
164
|
}),
|
|
165
|
+
timeoutMs: 300000,
|
|
137
166
|
execute: async (args: { url: string }) => {
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
return safeToolExecute(async () => {
|
|
168
|
+
const result = await authenticatedRequest(args.url, "PUT");
|
|
169
|
+
return JSON.stringify(result);
|
|
170
|
+
}, "fal_cancel_request");
|
|
140
171
|
},
|
|
141
172
|
};
|
package/src/tools/fal/index.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Export all fal.ai tools for registration with the MCP server.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export {
|
|
6
|
+
export { falListPresets, falGetPresetDetails, falGetConfig } from "./models";
|
|
7
7
|
export {
|
|
8
|
-
|
|
8
|
+
falGenerate,
|
|
9
9
|
falGetResult,
|
|
10
10
|
falGetStatus,
|
|
11
11
|
falCancelRequest,
|
package/src/tools/fal/models.ts
CHANGED
|
@@ -1,13 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Models module for fal.ai MCP server.
|
|
3
|
-
* Provides tools for
|
|
3
|
+
* Provides tools for retrieving information about configured fal.ai models.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { FAL_BASE_URL, DEFAULT_TIMEOUT } from "./config";
|
|
7
|
+
import { loadFalConfig, FAL_BASE_URL, DEFAULT_TIMEOUT } from "./config";
|
|
8
|
+
import { safeToolExecute } from "../../utils/tool-wrapper";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Tool to list available generation presets and their intents.
|
|
12
|
+
*/
|
|
13
|
+
export const falListPresets = {
|
|
14
|
+
name: "fal_list_presets",
|
|
15
|
+
description:
|
|
16
|
+
"List all available generation presets, including their intents, input types, and output types. Use this to find the right preset for a task.",
|
|
17
|
+
parameters: z.object({}),
|
|
18
|
+
timeoutMs: 30000,
|
|
19
|
+
execute: async () => {
|
|
20
|
+
return safeToolExecute(async () => {
|
|
21
|
+
const config = loadFalConfig();
|
|
22
|
+
const summary = config.presets.map((p) => ({
|
|
23
|
+
presetName: p.presetName,
|
|
24
|
+
intent: p.intent,
|
|
25
|
+
inputType: p.inputType,
|
|
26
|
+
outputType: p.outputType,
|
|
27
|
+
description: p.description,
|
|
28
|
+
}));
|
|
29
|
+
return JSON.stringify(summary, null, 2);
|
|
30
|
+
}, "fal_list_presets");
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Tool to get full details for a specific preset, including default parameters.
|
|
36
|
+
*/
|
|
37
|
+
export const falGetPresetDetails = {
|
|
38
|
+
name: "fal_get_preset_details",
|
|
39
|
+
description:
|
|
40
|
+
"Get full details for a specific generation preset, including its model ID and default parameters.",
|
|
41
|
+
parameters: z.object({
|
|
42
|
+
preset_name: z.string().describe("The name of the preset to inspect"),
|
|
43
|
+
}),
|
|
44
|
+
timeoutMs: 30000,
|
|
45
|
+
execute: async (args: { preset_name: string }) => {
|
|
46
|
+
return safeToolExecute(async () => {
|
|
47
|
+
const config = loadFalConfig();
|
|
48
|
+
const preset = config.presets.find(
|
|
49
|
+
(p) => p.presetName === args.preset_name
|
|
50
|
+
);
|
|
51
|
+
if (!preset) {
|
|
52
|
+
throw new Error(`Preset '${args.preset_name}' not found.`);
|
|
53
|
+
}
|
|
54
|
+
return JSON.stringify(preset, null, 2);
|
|
55
|
+
}, "fal_get_preset_details");
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Helper for making public API requests to fal.ai
|
|
11
61
|
*/
|
|
12
62
|
async function publicRequest(url: string): Promise<any> {
|
|
13
63
|
const response = await fetch(url, {
|
|
@@ -24,8 +74,24 @@ async function publicRequest(url: string): Promise<any> {
|
|
|
24
74
|
}
|
|
25
75
|
|
|
26
76
|
/**
|
|
27
|
-
*
|
|
77
|
+
* Tool to retrieve the current full FAL configuration.
|
|
28
78
|
*/
|
|
79
|
+
export const falGetConfig = {
|
|
80
|
+
name: "fal_get_config",
|
|
81
|
+
description: "Retrieve the full FAL configuration JSON file content.",
|
|
82
|
+
parameters: z.object({}),
|
|
83
|
+
timeoutMs: 30000,
|
|
84
|
+
execute: async () => {
|
|
85
|
+
return safeToolExecute(async () => {
|
|
86
|
+
const config = loadFalConfig();
|
|
87
|
+
return JSON.stringify(config, null, 2);
|
|
88
|
+
}, "fal_get_config");
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
// ORIGINAL UNRESTRICTED TOOLS - Commented out for reference
|
|
94
|
+
|
|
29
95
|
export const falListModels = {
|
|
30
96
|
name: "fal_list_models",
|
|
31
97
|
description:
|
|
@@ -40,6 +106,7 @@ export const falListModels = {
|
|
|
40
106
|
.optional()
|
|
41
107
|
.describe("The total number of models to retrieve per page"),
|
|
42
108
|
}),
|
|
109
|
+
timeoutMs: 300000,
|
|
43
110
|
execute: async (args: { page?: number; total?: number }) => {
|
|
44
111
|
let url = `${FAL_BASE_URL}/models`;
|
|
45
112
|
|
|
@@ -56,15 +123,13 @@ export const falListModels = {
|
|
|
56
123
|
},
|
|
57
124
|
};
|
|
58
125
|
|
|
59
|
-
/**
|
|
60
|
-
* Search for models on fal.ai based on keywords.
|
|
61
|
-
*/
|
|
62
126
|
export const falSearchModels = {
|
|
63
127
|
name: "fal_search_models",
|
|
64
128
|
description: "Search for models on fal.ai based on keywords.",
|
|
65
129
|
parameters: z.object({
|
|
66
130
|
keywords: z.string().describe("The search terms to find models"),
|
|
67
131
|
}),
|
|
132
|
+
timeoutMs: 300000,
|
|
68
133
|
execute: async (args: { keywords: string }) => {
|
|
69
134
|
const url = `${FAL_BASE_URL}/models?keywords=${encodeURIComponent(
|
|
70
135
|
args.keywords
|
|
@@ -73,23 +138,4 @@ export const falSearchModels = {
|
|
|
73
138
|
return JSON.stringify(result);
|
|
74
139
|
},
|
|
75
140
|
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get the OpenAPI schema for a specific model.
|
|
79
|
-
*/
|
|
80
|
-
export const falGetSchema = {
|
|
81
|
-
name: "fal_get_schema",
|
|
82
|
-
description: "Get the OpenAPI schema for a specific fal.ai model.",
|
|
83
|
-
parameters: z.object({
|
|
84
|
-
model_id: z
|
|
85
|
-
.string()
|
|
86
|
-
.describe('The ID of the model (e.g., "fal-ai/flux/dev")'),
|
|
87
|
-
}),
|
|
88
|
-
execute: async (args: { model_id: string }) => {
|
|
89
|
-
const url = `${FAL_BASE_URL}/openapi/queue/openapi.json?endpoint_id=${encodeURIComponent(
|
|
90
|
-
args.model_id
|
|
91
|
-
)}`;
|
|
92
|
-
const result = await publicRequest(url);
|
|
93
|
-
return JSON.stringify(result);
|
|
94
|
-
},
|
|
95
|
-
};
|
|
141
|
+
*/
|
package/src/tools/fal/storage.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import * as fs from "fs";
|
|
8
|
+
import { safeToolExecute } from "../../utils/tool-wrapper";
|
|
8
9
|
import * as path from "path";
|
|
9
10
|
import { FAL_REST_URL, AUTHENTICATED_TIMEOUT, getApiKey } from "./config";
|
|
10
11
|
|
|
@@ -41,76 +42,79 @@ export const falUploadFile = {
|
|
|
41
42
|
parameters: z.object({
|
|
42
43
|
path: z.string().describe("The absolute path to the file to upload"),
|
|
43
44
|
}),
|
|
45
|
+
timeoutMs: 300000,
|
|
44
46
|
execute: async (args: { path: string }) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
return safeToolExecute(async () => {
|
|
48
|
+
// Validate file exists
|
|
49
|
+
if (!fs.existsSync(args.path)) {
|
|
50
|
+
throw new Error(`File not found: ${args.path}`);
|
|
51
|
+
}
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const filename = path.basename(args.path);
|
|
54
|
+
const fileSize = fs.statSync(args.path).size;
|
|
55
|
+
const contentType = getMimeType(args.path);
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
// Step 1: Initiate upload
|
|
58
|
+
const initiateUrl = `${FAL_REST_URL}/storage/upload/initiate?storage_type=fal-cdn-v3`;
|
|
59
|
+
const initiatePayload = {
|
|
60
|
+
content_type: contentType,
|
|
61
|
+
file_name: filename,
|
|
62
|
+
};
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
console.error(`[fal_upload] Initiating upload for ${filename}...`);
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
const initiateResponse = await fetch(initiateUrl, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
Authorization: `Key ${getApiKey()}`,
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify(initiatePayload),
|
|
73
|
+
signal: AbortSignal.timeout(AUTHENTICATED_TIMEOUT),
|
|
74
|
+
});
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
if (!initiateResponse.ok) {
|
|
77
|
+
const errorText = await initiateResponse.text();
|
|
78
|
+
throw new Error(
|
|
79
|
+
`[${initiateResponse.status}] Failed to initiate upload: ${errorText}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const initiateData = (await initiateResponse.json()) as {
|
|
84
|
+
file_url: string;
|
|
85
|
+
upload_url: string;
|
|
86
|
+
};
|
|
87
|
+
const { file_url, upload_url } = initiateData;
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
// Step 2: Upload file content
|
|
90
|
+
console.error(`[fal_upload] Uploading file content...`);
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
const fileContent = fs.readFileSync(args.path);
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
const uploadResponse = await fetch(upload_url, {
|
|
95
|
+
method: "PUT",
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": contentType,
|
|
98
|
+
},
|
|
99
|
+
body: fileContent,
|
|
100
|
+
signal: AbortSignal.timeout(AUTHENTICATED_TIMEOUT),
|
|
101
|
+
});
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
if (!uploadResponse.ok) {
|
|
104
|
+
const errorText = await uploadResponse.text();
|
|
105
|
+
throw new Error(
|
|
106
|
+
`[${uploadResponse.status}] Failed to upload file: ${errorText}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
console.error(`[fal_upload] Upload completed successfully`);
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
return JSON.stringify({
|
|
113
|
+
file_url,
|
|
114
|
+
file_name: filename,
|
|
115
|
+
file_size: fileSize,
|
|
116
|
+
content_type: contentType,
|
|
117
|
+
});
|
|
118
|
+
}, "fal_upload_file");
|
|
115
119
|
},
|
|
116
120
|
};
|