# Directory Structure
```
├── .cursorignore
├── .gitignore
├── LICENSE
├── package.json
├── README.md
├── src
│ └── index.ts
├── test_cat_image_edited.png
├── test_cat_image.png
├── test_cat_with_moustache.png
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/.cursorignore:
--------------------------------------------------------------------------------
```
node_modules/
dist/
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
.env
/dist
.DS_Store
/dist
/build
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# openai-gpt-image-mcp
<p align="center">
<a href="https://www.npmjs.com/package/@modelcontextprotocol/sdk"><img src="https://img.shields.io/npm/v/@modelcontextprotocol/sdk?label=MCP%20SDK&color=blue" alt="MCP SDK"></a>
<a href="https://www.npmjs.com/package/openai"><img src="https://img.shields.io/npm/v/openai?label=OpenAI%20SDK&color=blueviolet" alt="OpenAI SDK"></a>
<a href="https://github.com/SureScaleAI/openai-gpt-image-mcp/blob/main/LICENSE"><img src="https://img.shields.io/github/license/SureScaleAI/openai-gpt-image-mcp?color=brightgreen" alt="License"></a>
<a href="https://github.com/SureScaleAI/openai-gpt-image-mcp/stargazers"><img src="https://img.shields.io/github/stars/SureScaleAI/openai-gpt-image-mcp?style=social" alt="GitHub stars"></a>
<a href="https://github.com/SureScaleAI/openai-gpt-image-mcp/actions"><img src="https://img.shields.io/github/actions/workflow/status/SureScaleAI/openai-gpt-image-mcp/main.yml?label=build&logo=github" alt="Build Status"></a>
</p>
---
A Model Context Protocol (MCP) tool server for OpenAI's GPT-4o/gpt-image-1 image generation and editing APIs.
- **Generate images** from text prompts using OpenAI's latest models.
- **Edit images** (inpainting, outpainting, compositing) with advanced prompt control.
- **Supports**: Claude Desktop, Cursor, VSCode, Windsurf, and any MCP-compatible client.
---
## ✨ Features
- **create-image**: Generate images from a prompt, with advanced options (size, quality, background, etc).
- **edit-image**: Edit or extend images using a prompt and optional mask, supporting both file paths and base64 input.
- **File output**: Save generated images directly to disk, or receive as base64.
---
## 🚀 Installation
```sh
git clone https://github.com/SureScaleAI/openai-gpt-image-mcp.git
cd openai-gpt-image-mcp
yarn install
yarn build
```
---
## 🔑 Configuration
Add to Claude Desktop or VSCode (including Cursor/Windsurf) config:
```json
{
"mcpServers": {
"openai-gpt-image-mcp": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": { "OPENAI_API_KEY": "sk-..." }
}
}
}
```
Also supports Azure deployments:
```json
{
"mcpServers": {
"openai-gpt-image-mcp": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"AZURE_OPENAI_API_KEY": "sk-...",
"AZURE_OPENAI_ENDPOINT": "my.endpoint.com",
"OPENAI_API_VERSION": "2024-12-01-preview"
}
}
}
}
```
Also supports supplying an environment files:
```json
{
"mcpServers": {
"openai-gpt-image-mcp": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js", "--env-file", "./deployment/.env"]
}
}
}
```
---
## ⚡ Advanced
- For `create-image`, set `n` to generate up to 10 images at once.
- For `edit-image`, provide a mask image (file path or base64) to control where edits are applied.
- Provide an environment file with `--env-file path/to/file/.env`
- See `src/index.ts` for all options.
---
## 🧑💻 Development
- TypeScript source: `src/index.ts`
- Build: `yarn build`
- Run: `node dist/index.js`
---
## 📝 License
MIT
---
## 🩺 Troubleshooting
- Make sure your `OPENAI_API_KEY` is valid and has image API access.
- You must have a [verified OpenAI organization](https://platform.openai.com/account/organization). After verifying, it can take 15–20 minutes for image API access to activate.
- File paths must be absolute.
- **Unix/macOS/Linux**: Starting with `/` (e.g., `/path/to/image.png`)
- **Windows**: Drive letter followed by `:` (e.g., `C:/path/to/image.png` or `C:\path\to\image.png`)
- For file output, ensure the directory is writable.
- If you see errors about file types, check your image file extensions and formats.
---
## ⚠️ Limitations & Large File Handling
- **1MB Payload Limit:** MCP clients (including Claude Desktop) have a hard 1MB limit for tool responses. Large images (especially high-res or multiple images) can easily exceed this limit if returned as base64.
- **Auto-Switch to File Output:** If the total image size exceeds 1MB, the tool will automatically save images to disk and return the file path(s) instead of base64. This ensures compatibility and prevents errors like `result exceeds maximum length of 1048576`.
- **Default File Location:** If you do not specify a `file_output` path, images will be saved to `/tmp` (or the directory set by the `MCP_HF_WORK_DIR` environment variable) with a unique filename.
- **Environment Variable:**
- `MCP_HF_WORK_DIR`: Set this to control where large images and file outputs are saved. Example: `export MCP_HF_WORK_DIR=/your/desired/dir`
- **Best Practice:** For large or production images, always use file output and ensure your client is configured to handle file paths.
---
## 📚 References
- [OpenAI Images API Documentation](https://platform.openai.com/docs/api-reference/images)
---
## 🙏 Credits
- Built with [@modelcontextprotocol/sdk](https://www.npmjs.com/package/@modelcontextprotocol/sdk)
- Uses [openai](https://www.npmjs.com/package/openai) Node.js SDK
- Built by [SureScale.ai](https://surescale.ai)
- Contributions from [Axle Research and Technology](https://axleinfo.com/)
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "openai-gpt-image-mcp",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"@types/node": "^22.15.2",
"typescript": "^5.8.3"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",
"openai": "^4.96.0"
},
"scripts": {
"build": "tsc"
}
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "src", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": false, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
// 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);
})();
```