Skip to main content
Glama

Flux Cloudflare MCP

by Hzzy2O
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import fetch from "node-fetch"; import { Buffer } from 'buffer'; import * as process from 'process'; import * as fs from 'fs'; import * as path from 'path'; // Configuration const CONFIG = { serverName: "flux-cloudflare-mcp", serverVersion: "0.0.1", pollingAttempts: 5, pollingInterval: 2000, // ms output: { baseFolder: "./output", allowedExtensions: [".png", ".jpg", ".jpeg"], defaultExtension: ".png" }, image: { defaultWidth: 512, defaultHeight: 512, maxWidth: 1024, maxHeight: 1024 } }; // Initialize MCP server const server = new McpServer({ name: CONFIG.serverName, version: CONFIG.serverVersion, }); // Environment validation function getFluxApiToken(): string { const token = process.env.FLUX_API_TOKEN; if (!token) { console.error( "Error: FLUX_API_TOKEN environment variable is required" ); process.exit(1); } return token; } function getFluxApiUrl(): string { const url = process.env.FLUX_API_URL; if (!url) { console.error( "Error: FLUX_API_URL environment variable is required" ); process.exit(1); } return url; } // Schema definitions const imageGenerationSchema = { prompt: z.string().min(1).describe("Prompt for generated image"), file_name: z.string().min(1).optional().describe("Name of the file to save the image"), save_folder: z.string().default(CONFIG.output.baseFolder).optional().describe("Folder path to save the image"), width: z.number().int().positive().max(CONFIG.image.maxWidth).optional().describe("Width of the generated image"), height: z.number().int().positive().max(CONFIG.image.maxHeight).optional().describe("Height of the generated image"), num_inference_steps: z .number() .int() .min(1) .max(4) .default(4) .describe( "Number of denoising steps. 4 is recommended, and lower number of steps produce lower quality outputs, faster." ), aspect_ratio: z .enum([ "1:1", "1:2", "3:2", "3:4", "16:9", "9:16" ]) .default("1:1") .describe("Aspect ratio for the generated image"), }; // Helper functions function handleError(error: unknown): never { if (error instanceof Error) { throw new McpError(ErrorCode.InternalError, error.message); } throw new McpError(ErrorCode.InternalError, String(error)); } // Validate and create save path function validateSavePath(folderPath: string): { isValid: boolean; errorMsg: string; savePath: string } { try { // Create absolute path const absolutePath = path.resolve(folderPath); // Check if directory exists, create if it doesn't if (!fs.existsSync(absolutePath)) { fs.mkdirSync(absolutePath, { recursive: true }); } // Check if we have write permissions try { fs.accessSync(absolutePath, fs.constants.W_OK); } catch (error) { return { isValid: false, errorMsg: `No write permission for directory: ${absolutePath}`, savePath: absolutePath }; } return { isValid: true, errorMsg: "", savePath: absolutePath }; } catch (error) { return { isValid: false, errorMsg: `Invalid save path: ${error instanceof Error ? error.message : String(error)}`, savePath: "" }; } } // Register tools server.tool( "generate_image", "Generate an image from a text prompt using Flux model", imageGenerationSchema, async (input) => { try { console.log(`Received generation request: ${input.prompt}`); // Parameter validation if (!input.prompt) { throw new Error("Prompt cannot be empty"); } // Set default dimensions if not provided const width = input.width || CONFIG.image.defaultWidth; const height = input.height || CONFIG.image.defaultHeight; // Validate dimensions if (width <= 0 || height <= 0 || width > CONFIG.image.maxWidth || height > CONFIG.image.maxHeight) { throw new Error( `Width and height must be greater than 0 and not exceed ${CONFIG.image.maxWidth}. ` + `Current values: width=${width}, height=${height}` ); } const token = getFluxApiToken(); const apiUrl = getFluxApiUrl(); // Format prompt with aspect ratio included in the prompt string const formattedPrompt = `${input.aspect_ratio} ${input.prompt}`; // Construct message for chat completions const messages = [ { role: "user", content: formattedPrompt, }, ]; // Prepare request to Cloudflare Worker const response = await fetch(`${apiUrl}/v1/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ messages, ...(input.num_inference_steps !== undefined && { num_steps: input.num_inference_steps }), stream: false, }), }); if (!response.ok) { const errorData = await response.text(); throw new Error(`API request failed: ${response.status} - ${errorData}`); } const data = await response.json(); // Extract image URL directly from the response const imageUrl = data.url; if (!imageUrl) { throw new Error("Could not extract image URL from response"); } // If file_name is not provided, just return the URL if (!input.file_name) { return { content: [ { type: "text", text: JSON.stringify({ success: true, error: null, url: imageUrl, images: [] }, null, 2), }, ], }; } // Fetch the actual image to get it as base64 const imageResponse = await fetch(imageUrl); if (!imageResponse.ok) { throw new Error(`Failed to fetch image: ${imageResponse.status}`); } const imageBuffer = await imageResponse.arrayBuffer(); const base64Image = Buffer.from(imageBuffer).toString('base64'); const imageData = Buffer.from(imageBuffer); // Validate save path const { isValid, errorMsg, savePath } = validateSavePath(input.save_folder || CONFIG.output.baseFolder); if (!isValid) { throw new Error(errorMsg); } // Ensure file name has correct extension let fileName = input.file_name; const fileExt = path.extname(fileName).toLowerCase(); if (!fileExt || !CONFIG.output.allowedExtensions.includes(fileExt)) { fileName = `${path.basename(fileName, path.extname(fileName))}${CONFIG.output.defaultExtension}`; } // Save the image to file const savedImages = []; try { const filePath = path.join(savePath, fileName); fs.writeFileSync(filePath, imageData); // Use relative path for response to avoid exposing container paths const relativePath = path.relative(process.cwd(), filePath); savedImages.push(relativePath); console.log(`Image saved: ${filePath}`); } catch (error) { if (error instanceof Error && error.message.includes('permission')) { console.error(`No permission to save image to: ${savePath}`); } else { console.error(`Failed to save image: ${error instanceof Error ? error.message : String(error)}`); } } if (savedImages.length === 0) { throw new Error( "All image saves failed. Please ensure:\n" + "1. Using absolute path (e.g., /Users/username/Documents/images)\n" + "2. Directory has write permissions\n" + "3. Sufficient disk space" ); } // Determine MIME type (assuming PNG for now, could be improved with content type detection) const mimeType = "image/png"; return { content: [ { type: "text", text: JSON.stringify({ success: true, error: null, url: imageUrl, images: savedImages }, null, 2), }, ], }; } catch (error) { console.error(`Failed to generate image: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error), images: [] }, null, 2), }, ], }; } } ); // Server initialization async function main() { try { const transport = new StdioServerTransport(); await server.connect(transport); console.error( `${CONFIG.serverName} v${CONFIG.serverVersion} running on stdio` ); } catch (error) { console.error( "Server initialization error:", error instanceof Error ? error.message : String(error) ); process.exit(1); } } main().catch((error: unknown) => { console.error( "Unhandled server error:", error instanceof Error ? error.message : String(error) ); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Hzzy2O/flux-cloudfare-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server