/**
* Utility functions for the fal.ai MCP server.
*
* This module provides helper functions for making API requests
* and handling authentication with the fal.ai service.
*/
export class FalAPIError extends Error {
constructor(
message: string,
public statusCode?: number,
public details?: Record<string, any>
) {
super(statusCode ? `[${statusCode}] ${message}` : message);
this.name = 'FalAPIError';
}
}
export interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT';
headers?: Record<string, string>;
body?: any;
timeout?: number;
}
/**
* Make an authenticated request to fal.ai API
*/
export async function authenticatedRequest(
url: string,
apiKey: string,
options: RequestOptions = {}
): Promise<any> {
const {
method = 'GET',
headers = {},
body,
timeout = 100000
} = options;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const fetchOptions: RequestInit = {
method,
headers: {
'Authorization': `Key ${apiKey}`,
'Content-Type': 'application/json',
...headers
},
signal: controller.signal
};
if (body && (method === 'POST' || method === 'PUT')) {
fetchOptions.body = JSON.stringify(body);
}
const response = await fetch(url, fetchOptions);
clearTimeout(timeoutId);
if (!response.ok) {
let errorDetails: any;
try {
errorDetails = await response.json();
} catch {
errorDetails = { message: await response.text() };
}
throw new FalAPIError(
`API error: ${JSON.stringify(errorDetails)}`,
response.status,
errorDetails
);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof FalAPIError) {
throw error;
}
throw new FalAPIError(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Make a non-authenticated request to fal.ai API
*/
export async function publicRequest(
url: string,
timeout: number = 30000
): Promise<any> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
let errorDetails: any;
try {
errorDetails = await response.json();
} catch {
errorDetails = { message: await response.text() };
}
throw new FalAPIError(
`API error: ${JSON.stringify(errorDetails)}`,
response.status,
errorDetails
);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof FalAPIError) {
throw error;
}
throw new FalAPIError(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Sanitize parameters for API requests
*/
export function sanitizeParameters(parameters: Record<string, any>): Record<string, any> {
const sanitized: Record<string, any> = {};
for (const [key, value] of Object.entries(parameters)) {
if (value !== null && value !== undefined) {
sanitized[key] = value;
}
}
return sanitized;
}
/**
* Upload a file to fal.ai storage
*/
export async function uploadFile(
apiKey: string,
filePath: string
): Promise<{ file_url: string; file_name: string; file_size: number; content_type: string }> {
const fs = await import('fs');
const path = await import('path');
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const fileName = path.basename(filePath);
const fileSize = fs.statSync(filePath).size;
// Guess content type based on extension
const ext = path.extname(filePath).toLowerCase();
const contentTypeMap: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.json': 'application/json'
};
const contentType = contentTypeMap[ext] || 'application/octet-stream';
const FAL_REST_URL = 'https://rest.alpha.fal.ai';
const initiateUrl = `${FAL_REST_URL}/storage/upload/initiate?storage_type=fal-cdn-v3`;
// Initiate upload
const initiateResponse = await authenticatedRequest(
initiateUrl,
apiKey,
{
method: 'POST',
body: {
content_type: contentType,
file_name: fileName
}
}
);
const { file_url, upload_url } = initiateResponse;
// Upload file content
const fileBuffer = fs.readFileSync(filePath);
const uploadResponse = await fetch(upload_url, {
method: 'PUT',
headers: {
'Content-Type': contentType
},
body: fileBuffer
});
if (!uploadResponse.ok) {
throw new FalAPIError(`File upload failed`, uploadResponse.status);
}
return {
file_url,
file_name: fileName,
file_size: fileSize,
content_type: contentType
};
}