Skip to main content
Glama
index.ts16.2 kB
// Suppress all Node.js warnings (including deprecation) (process as any).emitWarning = () => { }; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { OpenAI, AzureOpenAI, toFile } from "openai"; import fs from "fs"; import path from "path"; // Function to load environment variables from a file const loadEnvFile = (filePath: string) => { try { const envConfig = fs.readFileSync(filePath, "utf8"); envConfig.split("\n").forEach((line) => { const trimmedLine = line.trim(); if (trimmedLine && !trimmedLine.startsWith("#")) { const [key, ...valueParts] = trimmedLine.split("="); const value = valueParts.join("=").trim(); if (key) { // Remove surrounding quotes if present process.env[key.trim()] = value.startsWith("'") && value.endsWith("'") || value.startsWith("\"") && value.endsWith("\"") ? value.slice(1, -1) : value; } } }); console.log(`Loaded environment variables from ${filePath}`); } catch (error) { console.warn(`Warning: Could not read environment file at ${filePath}:`, error); } }; // Parse command line arguments for --env-file const cmdArgs = process.argv.slice(2); const envFileArgIndex = cmdArgs.findIndex(arg => arg === "--env-file"); if (envFileArgIndex !== -1 && cmdArgs[envFileArgIndex + 1]) { console.log("Loading environment variables from file:", cmdArgs[envFileArgIndex + 1]); const envFilePath = cmdArgs[envFileArgIndex + 1]; loadEnvFile(envFilePath); } else { console.log("No environment file provided"); } (async () => { const server = new McpServer({ name: "openai-gpt-image-mcp", version: "1.0.0" }, { capabilities: { tools: { listChanged: false } } }); // Zod schema for create-image tool input const createImageSchema = z.object({ prompt: z.string().max(32000), background: z.enum(["transparent", "opaque", "auto"]).optional(), model: z.literal("gpt-image-1").default("gpt-image-1"), moderation: z.enum(["auto", "low"]).optional(), n: z.number().int().min(1).max(10).optional(), output_compression: z.number().int().min(0).max(100).optional(), output_format: z.enum(["png", "jpeg", "webp"]).optional(), quality: z.enum(["auto", "high", "medium", "low"]).optional(), size: z.enum(["1024x1024", "1536x1024", "1024x1536", "auto"]).optional(), user: z.string().optional(), output: z.enum(["base64", "file_output"]).default("base64"), file_output: z.string().optional().refine( (val) => { if (!val) return true; // Check for Unix/Linux/macOS absolute paths if (val.startsWith("/")) return true; // Check for Windows absolute paths (C:/, D:\, etc.) if (/^[a-zA-Z]:[/\\]/.test(val)) return true; return false; }, { message: "file_output must be an absolute path" } ).describe("Absolute path to save the image file, including the desired file extension (e.g., /path/to/image.png). If multiple images are generated (n > 1), an index will be appended (e.g., /path/to/image_1.png)."), }).refine( (data) => { if (data.output !== "file_output") return true; if (typeof data.file_output !== "string") return false; // Check for Unix/Linux/macOS absolute paths if (data.file_output.startsWith("/")) return true; // Check for Windows absolute paths (C:/, D:\, etc.) if (/^[a-zA-Z]:[/\\]/.test(data.file_output)) return true; return false; }, { message: "file_output must be an absolute path when output is 'file_output'", path: ["file_output"] } ); // Use ._def.schema.shape to get the raw shape for server.tool due to Zod refinements server.tool( "create-image", (createImageSchema as any)._def.schema.shape, async (args, _extra) => { // If AZURE_OPENAI_API_KEY is defined, use the AzureOpenAI class const openai = process.env.AZURE_OPENAI_API_KEY ? new AzureOpenAI() : new OpenAI(); // Only allow gpt-image-1 const { prompt, background, model = "gpt-image-1", moderation, n, output_compression, output_format, quality, size, user, output = "base64", file_output: file_outputRaw, } = args; const file_output: string | undefined = file_outputRaw; // Enforce: if background is 'transparent', output_format must be 'png' or 'webp' if (background === "transparent" && output_format && !["png", "webp"].includes(output_format)) { throw new Error("If background is 'transparent', output_format must be 'png' or 'webp'"); } // Only include output_compression if output_format is webp or jpeg const imageParams: any = { prompt, model, ...(background ? { background } : {}), ...(moderation ? { moderation } : {}), ...(n ? { n } : {}), ...(output_format ? { output_format } : {}), ...(quality ? { quality } : {}), ...(size ? { size } : {}), ...(user ? { user } : {}), }; if ( typeof output_compression !== "undefined" && output_format && ["webp", "jpeg"].includes(output_format) ) { imageParams.output_compression = output_compression; } const result = await openai.images.generate(imageParams); // gpt-image-1 always returns base64 images in data[].b64_json const images = (result.data ?? []).map((img: any) => ({ b64: img.b64_json, mimeType: output_format === "jpeg" ? "image/jpeg" : output_format === "webp" ? "image/webp" : "image/png", ext: output_format === "jpeg" ? "jpg" : output_format === "webp" ? "webp" : "png", })); // Auto-switch to file_output if total base64 size exceeds 1MB const MAX_RESPONSE_SIZE = 1048576; // 1MB const totalBase64Size = images.reduce((sum, img) => sum + Buffer.byteLength(img.b64, "base64"), 0); let effectiveOutput = output; let effectiveFileOutput = file_output; if (output === "base64" && totalBase64Size > MAX_RESPONSE_SIZE) { effectiveOutput = "file_output"; if (!file_output) { // Use /tmp or MCP_HF_WORK_DIR if set const tmpDir = process.env.MCP_HF_WORK_DIR || "/tmp"; const unique = Date.now(); effectiveFileOutput = path.join(tmpDir, `openai_image_${unique}.${images[0]?.ext ?? "png"}`); } } if (effectiveOutput === "file_output") { const fs = await import("fs/promises"); const path = await import("path"); // If multiple images, append index to filename const basePath = effectiveFileOutput!; const responses = []; for (let i = 0; i < images.length; i++) { const img = images[i]; let filePath = basePath; if (images.length > 1) { const parsed = path.parse(basePath); filePath = path.join(parsed.dir, `${parsed.name}_${i + 1}.${img.ext ?? "png"}`); } else { // Ensure correct extension const parsed = path.parse(basePath); filePath = path.join(parsed.dir, `${parsed.name}.${img.ext ?? "png"}`); } await fs.writeFile(filePath, Buffer.from(img.b64, "base64")); responses.push({ type: "text", text: `Image saved to: file://${filePath}` }); } return { content: responses }; } else { // Default: base64 return { content: images.map((img) => ({ type: "image", data: img.b64, mimeType: img.mimeType, })), }; } } ); // Zod schema for edit-image tool input (gpt-image-1 only) const absolutePathCheck = (val: string | undefined) => { if (!val) return true; // Check for Unix/Linux/macOS absolute paths if (val.startsWith("/")) return true; // Check for Windows absolute paths (C:/, D:\, etc.) if (/^[a-zA-Z]:[/\\]/.test(val)) return true; return false; }; const base64Check = (val: string | undefined) => !!val && (/^([A-Za-z0-9+/=\r\n]+)$/.test(val) || val.startsWith("data:image/")); const imageInputSchema = z.string().refine( (val) => absolutePathCheck(val) || base64Check(val), { message: "Must be an absolute path or a base64-encoded string (optionally as a data URL)" } ).describe("Absolute path to an image file (png, jpg, webp < 25MB) or a base64-encoded image string."); // Base schema without refinement for server.tool signature const editImageBaseSchema = z.object({ image: z.string().describe("Absolute image path or base64 string to edit."), prompt: z.string().max(32000).describe("A text description of the desired edit. Max 32000 chars."), mask: z.string().optional().describe("Optional absolute path or base64 string for a mask image (png < 4MB, same dimensions as the first image). Fully transparent areas indicate where to edit."), model: z.literal("gpt-image-1").default("gpt-image-1"), n: z.number().int().min(1).max(10).optional().describe("Number of images to generate (1-10)."), quality: z.enum(["auto", "high", "medium", "low"]).optional().describe("Quality (high, medium, low) - only for gpt-image-1."), size: z.enum(["1024x1024", "1536x1024", "1024x1536", "auto"]).optional().describe("Size of the generated images."), user: z.string().optional().describe("Optional user identifier for OpenAI monitoring."), output: z.enum(["base64", "file_output"]).default("base64").describe("Output format: base64 or file path."), file_output: z.string().refine(absolutePathCheck, { message: "Path must be absolute" }).optional() .describe("Absolute path to save the output image file, including the desired file extension (e.g., /path/to/image.png). If n > 1, an index is appended."), }); // Full schema with refinement for validation inside the handler const editImageSchema = editImageBaseSchema.refine( (data) => { if (data.output !== "file_output") return true; if (typeof data.file_output !== "string") return false; return absolutePathCheck(data.file_output); }, { message: "file_output must be an absolute path when output is 'file_output'", path: ["file_output"] } ); // Edit Image Tool (gpt-image-1 only) server.tool( "edit-image", editImageBaseSchema.shape, // <-- Use the base schema shape here async (args, _extra) => { // Validate arguments using the full schema with refinements const validatedArgs = editImageSchema.parse(args); // Explicitly validate image and mask inputs here if (!absolutePathCheck(validatedArgs.image) && !base64Check(validatedArgs.image)) { throw new Error("Invalid 'image' input: Must be an absolute path or a base64-encoded string."); } if (validatedArgs.mask && !absolutePathCheck(validatedArgs.mask) && !base64Check(validatedArgs.mask)) { throw new Error("Invalid 'mask' input: Must be an absolute path or a base64-encoded string."); } const openai = process.env.AZURE_OPENAI_API_KEY ? new AzureOpenAI() : new OpenAI(); const { image: imageInput, prompt, mask: maskInput, model = "gpt-image-1", n, quality, size, user, output = "base64", file_output: file_outputRaw, } = validatedArgs; // <-- Use validatedArgs here const file_output: string | undefined = file_outputRaw; // Helper to convert input (path or base64) to toFile async function inputToFile(input: string, idx = 0) { if (absolutePathCheck(input)) { // File path: infer mime type from extension const ext = input.split('.').pop()?.toLowerCase(); let mime = "image/png"; if (ext === "jpg" || ext === "jpeg") mime = "image/jpeg"; else if (ext === "webp") mime = "image/webp"; else if (ext === "png") mime = "image/png"; // else default to png return await toFile(fs.createReadStream(input), undefined, { type: mime }); } else { // Base64 or data URL let base64 = input; let mime = "image/png"; if (input.startsWith("data:image/")) { // data URL const match = input.match(/^data:(image\/\w+);base64,(.*)$/); if (match) { mime = match[1]; base64 = match[2]; } } const buffer = Buffer.from(base64, "base64"); return await toFile(buffer, `input_${idx}.${mime.split("/")[1] || "png"}`, { type: mime }); } } // Prepare image input const imageFile = await inputToFile(imageInput, 0); // Prepare mask input const maskFile = maskInput ? await inputToFile(maskInput, 1) : undefined; // Construct parameters for OpenAI API const editParams: any = { image: imageFile, prompt, model, // Always gpt-image-1 ...(maskFile ? { mask: maskFile } : {}), ...(n ? { n } : {}), ...(quality ? { quality } : {}), ...(size ? { size } : {}), ...(user ? { user } : {}), // response_format is not applicable for gpt-image-1 (always b64_json) }; const result = await openai.images.edit(editParams); // gpt-image-1 always returns base64 images in data[].b64_json // We need to determine the output mime type and extension based on input/defaults // Since OpenAI doesn't return this for edits, we'll default to png const images = (result.data ?? []).map((img: any) => ({ b64: img.b64_json, mimeType: "image/png", ext: "png", })); // Auto-switch to file_output if total base64 size exceeds 1MB const MAX_RESPONSE_SIZE = 1048576; // 1MB const totalBase64Size = images.reduce((sum, img) => sum + Buffer.byteLength(img.b64, "base64"), 0); let effectiveOutput = output; let effectiveFileOutput = file_output; if (output === "base64" && totalBase64Size > MAX_RESPONSE_SIZE) { effectiveOutput = "file_output"; if (!file_output) { // Use /tmp or MCP_HF_WORK_DIR if set const tmpDir = process.env.MCP_HF_WORK_DIR || "/tmp"; const unique = Date.now(); effectiveFileOutput = path.join(tmpDir, `openai_image_edit_${unique}.png`); } } if (effectiveOutput === "file_output") { if (!effectiveFileOutput) { throw new Error("file_output path is required when output is 'file_output'"); } // Use fs/promises and path (already imported) const basePath = effectiveFileOutput!; const responses = []; for (let i = 0; i < images.length; i++) { const img = images[i]; let filePath = basePath; if (images.length > 1) { const parsed = path.parse(basePath); // Append index before the original extension if it exists, otherwise just append index and .png const ext = parsed.ext || `.${img.ext}`; filePath = path.join(parsed.dir, `${parsed.name}_${i + 1}${ext}`); } else { // Ensure the extension from the path is used, or default to .png const parsed = path.parse(basePath); const ext = parsed.ext || `.${img.ext}`; filePath = path.join(parsed.dir, `${parsed.name}${ext}`); } await fs.promises.writeFile(filePath, Buffer.from(img.b64, "base64")); // Workaround: Return file path as text responses.push({ type: "text", text: `Image saved to: file://${filePath}` }); } return { content: responses }; } else { // Default: base64 return { content: images.map((img) => ({ type: "image", data: img.b64, mimeType: img.mimeType, // Should be image/png })), }; } } ); const transport = new StdioServerTransport(); await server.connect(transport); })();

Latest Blog Posts

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/SureScaleAI/openai-gpt-image-mcp'

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