#
tokens: 8123/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# 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);
})(); 
```