#
tokens: 9880/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | dist/
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | .env
3 | /dist
4 | .DS_Store
5 | /dist
6 | /build
7 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # openai-gpt-image-mcp
  2 | 
  3 | <p align="center">
  4 |   <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>
  5 |   <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>
  6 |   <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>
  7 |   <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>
  8 |   <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>
  9 | </p>
 10 | 
 11 | ---
 12 | 
 13 | A Model Context Protocol (MCP) tool server for OpenAI's GPT-4o/gpt-image-1 image generation and editing APIs.
 14 | 
 15 | - **Generate images** from text prompts using OpenAI's latest models.
 16 | - **Edit images** (inpainting, outpainting, compositing) with advanced prompt control.
 17 | - **Supports**: Claude Desktop, Cursor, VSCode, Windsurf, and any MCP-compatible client.
 18 | 
 19 | ---
 20 | 
 21 | ## ✨ Features
 22 | 
 23 | - **create-image**: Generate images from a prompt, with advanced options (size, quality, background, etc).
 24 | - **edit-image**: Edit or extend images using a prompt and optional mask, supporting both file paths and base64 input.
 25 | - **File output**: Save generated images directly to disk, or receive as base64.
 26 | 
 27 | ---
 28 | 
 29 | ## 🚀 Installation
 30 | 
 31 | ```sh
 32 | git clone https://github.com/SureScaleAI/openai-gpt-image-mcp.git
 33 | cd openai-gpt-image-mcp
 34 | yarn install
 35 | yarn build
 36 | ```
 37 | 
 38 | ---
 39 | 
 40 | ## 🔑 Configuration
 41 | 
 42 | Add to Claude Desktop or VSCode (including Cursor/Windsurf) config:
 43 | 
 44 | ```json
 45 | {
 46 |   "mcpServers": {
 47 |     "openai-gpt-image-mcp": {
 48 |       "command": "node",
 49 |       "args": ["/absolute/path/to/dist/index.js"],
 50 |       "env": { "OPENAI_API_KEY": "sk-..." }
 51 |     }
 52 |   }
 53 | }
 54 | ```
 55 | 
 56 | Also supports Azure deployments:
 57 | 
 58 | ```json
 59 | {
 60 |   "mcpServers": {
 61 |     "openai-gpt-image-mcp": {
 62 |       "command": "node",
 63 |       "args": ["/absolute/path/to/dist/index.js"],
 64 |       "env": { 
 65 |         "AZURE_OPENAI_API_KEY": "sk-...",
 66 |         "AZURE_OPENAI_ENDPOINT": "my.endpoint.com",
 67 |         "OPENAI_API_VERSION": "2024-12-01-preview"
 68 |       }
 69 |     }
 70 |   }
 71 | }
 72 | ```
 73 | 
 74 | Also supports supplying an environment files:
 75 | 
 76 | ```json
 77 | {
 78 |   "mcpServers": {
 79 |     "openai-gpt-image-mcp": {
 80 |       "command": "node",
 81 |       "args": ["/absolute/path/to/dist/index.js", "--env-file", "./deployment/.env"]
 82 |     }
 83 |   }
 84 | }
 85 | ```
 86 | 
 87 | ---
 88 | 
 89 | ## ⚡ Advanced
 90 | 
 91 | - For `create-image`, set `n` to generate up to 10 images at once.
 92 | - For `edit-image`, provide a mask image (file path or base64) to control where edits are applied.
 93 | - Provide an environment file with `--env-file path/to/file/.env`
 94 | - See `src/index.ts` for all options.
 95 | 
 96 | ---
 97 | 
 98 | ## 🧑‍💻 Development
 99 | 
100 | - TypeScript source: `src/index.ts`
101 | - Build: `yarn build`
102 | - Run: `node dist/index.js`
103 | 
104 | ---
105 | 
106 | ## 📝 License
107 | 
108 | MIT
109 | 
110 | ---
111 | 
112 | ## 🩺 Troubleshooting
113 | 
114 | - Make sure your `OPENAI_API_KEY` is valid and has image API access.
115 | - 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.
116 | - File paths must be absolute.
117 |   - **Unix/macOS/Linux**: Starting with `/` (e.g., `/path/to/image.png`)
118 |   - **Windows**: Drive letter followed by `:` (e.g., `C:/path/to/image.png` or `C:\path\to\image.png`)
119 | - For file output, ensure the directory is writable.
120 | - If you see errors about file types, check your image file extensions and formats.
121 | 
122 | ---
123 | 
124 | ## ⚠️ Limitations & Large File Handling
125 | 
126 | - **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.
127 | - **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`.
128 | - **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.
129 | - **Environment Variable:**
130 |   - `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`
131 | - **Best Practice:** For large or production images, always use file output and ensure your client is configured to handle file paths.
132 | 
133 | ---
134 | 
135 | ## 📚 References
136 | 
137 | - [OpenAI Images API Documentation](https://platform.openai.com/docs/api-reference/images)
138 | 
139 | ---
140 | 
141 | ## 🙏 Credits
142 | 
143 | - Built with [@modelcontextprotocol/sdk](https://www.npmjs.com/package/@modelcontextprotocol/sdk)
144 | - Uses [openai](https://www.npmjs.com/package/openai) Node.js SDK 
145 | - Built by [SureScale.ai](https://surescale.ai)
146 | - Contributions from [Axle Research and Technology](https://axleinfo.com/)
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "openai-gpt-image-mcp",
 3 |   "version": "1.0.0",
 4 |   "main": "index.js",
 5 |   "license": "MIT",
 6 |   "devDependencies": {
 7 |     "@types/node": "^22.15.2",
 8 |     "typescript": "^5.8.3"
 9 |   },
10 |   "dependencies": {
11 |     "@modelcontextprotocol/sdk": "^1.10.2",
12 |     "openai": "^4.96.0"
13 |   },
14 |   "scripts": {
15 |     "build": "tsc"
16 |   }
17 | }
18 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "compilerOptions": {
  3 |     /* Visit https://aka.ms/tsconfig to read more about this file */
  4 | 
  5 |     /* Projects */
  6 |     // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
  7 |     // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
  8 |     // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
  9 |     // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
 10 |     // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
 11 |     // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
 12 | 
 13 |     /* Language and Environment */
 14 |     "target": "es2020",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
 15 |     // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
 16 |     // "jsx": "preserve",                                /* Specify what JSX code is generated. */
 17 |     // "libReplacement": true,                           /* Enable lib replacement. */
 18 |     // "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
 19 |     // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
 20 |     // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
 21 |     // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
 22 |     // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
 23 |     // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
 24 |     // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
 25 |     // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
 26 |     // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
 27 | 
 28 |     /* Modules */
 29 |     "module": "commonjs",                                /* Specify what module code is generated. */
 30 |     "rootDir": "src",                                    /* Specify the root folder within your source files. */
 31 |     // "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
 32 |     // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
 33 |     // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
 34 |     // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
 35 |     // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
 36 |     // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
 37 |     // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
 38 |     // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
 39 |     // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
 40 |     // "rewriteRelativeImportExtensions": true,          /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
 41 |     // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
 42 |     // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
 43 |     // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
 44 |     // "noUncheckedSideEffectImports": true,             /* Check side effect imports. */
 45 |     // "resolveJsonModule": true,                        /* Enable importing .json files. */
 46 |     // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
 47 |     // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
 48 | 
 49 |     /* JavaScript Support */
 50 |     // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
 51 |     // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
 52 |     // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
 53 | 
 54 |     /* Emit */
 55 |     // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
 56 |     // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
 57 |     // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
 58 |     // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
 59 |     // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
 60 |     // "noEmit": true,                                   /* Disable emitting files from a compilation. */
 61 |     // "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. */
 62 |     "outDir": "dist",                                    /* Specify an output folder for all emitted files. */
 63 |     // "removeComments": true,                           /* Disable emitting comments. */
 64 |     // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
 65 |     // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
 66 |     // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
 67 |     // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
 68 |     // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
 69 |     // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
 70 |     // "newLine": "crlf",                                /* Set the newline character for emitting files. */
 71 |     // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
 72 |     // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
 73 |     // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
 74 |     // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
 75 |     // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
 76 | 
 77 |     /* Interop Constraints */
 78 |     // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
 79 |     // "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. */
 80 |     // "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
 81 |     // "erasableSyntaxOnly": true,                       /* Do not allow runtime constructs that are not part of ECMAScript. */
 82 |     // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
 83 |     "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
 84 |     // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
 85 |     "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
 86 | 
 87 |     /* Type Checking */
 88 |     "strict": false,                                      /* Enable all strict type-checking options. */
 89 |     // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
 90 |     // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
 91 |     // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
 92 |     // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
 93 |     // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
 94 |     // "strictBuiltinIteratorReturn": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
 95 |     // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
 96 |     // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
 97 |     // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
 98 |     // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
 99 |     // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
100 |     // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
101 |     // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
102 |     // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
103 |     // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
104 |     // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
105 |     // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
106 |     // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
107 |     // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
108 | 
109 |     /* Completeness */
110 |     // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
111 |     "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
112 |   }
113 | }
114 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Suppress all Node.js warnings (including deprecation)
  2 | (process as any).emitWarning = () => { };
  3 | 
  4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  5 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  6 | import { z } from "zod";
  7 | import { OpenAI, AzureOpenAI, toFile } from "openai";
  8 | import fs from "fs";
  9 | import path from "path";
 10 | 
 11 | // Function to load environment variables from a file
 12 | const loadEnvFile = (filePath: string) => {
 13 |   try {
 14 |     const envConfig = fs.readFileSync(filePath, "utf8");
 15 |     envConfig.split("\n").forEach((line) => {
 16 |       const trimmedLine = line.trim();
 17 |       if (trimmedLine && !trimmedLine.startsWith("#")) {
 18 |         const [key, ...valueParts] = trimmedLine.split("=");
 19 |         const value = valueParts.join("=").trim();
 20 |         if (key) {
 21 |           // Remove surrounding quotes if present
 22 |           process.env[key.trim()] = value.startsWith("'") && value.endsWith("'") || value.startsWith("\"") && value.endsWith("\"")
 23 |             ? value.slice(1, -1)
 24 |             : value;
 25 |         }
 26 |       }
 27 |     });
 28 |     console.log(`Loaded environment variables from ${filePath}`);
 29 |   } catch (error) {
 30 |     console.warn(`Warning: Could not read environment file at ${filePath}:`, error);
 31 |   }
 32 | };
 33 | 
 34 | // Parse command line arguments for --env-file
 35 | const cmdArgs = process.argv.slice(2);
 36 | const envFileArgIndex = cmdArgs.findIndex(arg => arg === "--env-file");
 37 | if (envFileArgIndex !== -1 && cmdArgs[envFileArgIndex + 1]) {
 38 |   console.log("Loading environment variables from file:", cmdArgs[envFileArgIndex + 1]);
 39 |   const envFilePath = cmdArgs[envFileArgIndex + 1];
 40 |   loadEnvFile(envFilePath);
 41 | } else {
 42 |   console.log("No environment file provided");
 43 | }
 44 | 
 45 | (async () => {
 46 |   const server = new McpServer({
 47 |     name: "openai-gpt-image-mcp",
 48 |     version: "1.0.0"
 49 |   }, {
 50 |     capabilities: {
 51 |       tools: { listChanged: false }
 52 |     }
 53 |   });
 54 | 
 55 |   // Zod schema for create-image tool input
 56 |   const createImageSchema = z.object({
 57 |     prompt: z.string().max(32000),
 58 |     background: z.enum(["transparent", "opaque", "auto"]).optional(),
 59 |     model: z.literal("gpt-image-1").default("gpt-image-1"),
 60 |     moderation: z.enum(["auto", "low"]).optional(),
 61 |     n: z.number().int().min(1).max(10).optional(),
 62 |     output_compression: z.number().int().min(0).max(100).optional(),
 63 |     output_format: z.enum(["png", "jpeg", "webp"]).optional(),
 64 |     quality: z.enum(["auto", "high", "medium", "low"]).optional(),
 65 |     size: z.enum(["1024x1024", "1536x1024", "1024x1536", "auto"]).optional(),
 66 |     user: z.string().optional(),
 67 |     output: z.enum(["base64", "file_output"]).default("base64"),
 68 |     file_output: z.string().optional().refine(
 69 |       (val) => {
 70 |         if (!val) return true;
 71 |         // Check for Unix/Linux/macOS absolute paths
 72 |         if (val.startsWith("/")) return true;
 73 |         // Check for Windows absolute paths (C:/, D:\, etc.)
 74 |         if (/^[a-zA-Z]:[/\\]/.test(val)) return true;
 75 |         return false;
 76 |       },
 77 |       { message: "file_output must be an absolute path" }
 78 |     ).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)."),
 79 |   }).refine(
 80 |     (data) => {
 81 |       if (data.output !== "file_output") return true;
 82 |       if (typeof data.file_output !== "string") return false;
 83 |       // Check for Unix/Linux/macOS absolute paths
 84 |       if (data.file_output.startsWith("/")) return true;
 85 |       // Check for Windows absolute paths (C:/, D:\, etc.)
 86 |       if (/^[a-zA-Z]:[/\\]/.test(data.file_output)) return true;
 87 |       return false;
 88 |     },
 89 |     { message: "file_output must be an absolute path when output is 'file_output'", path: ["file_output"] }
 90 |   );
 91 | 
 92 |   // Use ._def.schema.shape to get the raw shape for server.tool due to Zod refinements
 93 |   server.tool(
 94 |     "create-image",
 95 |     (createImageSchema as any)._def.schema.shape,
 96 |     async (args, _extra) => {
 97 |       // If AZURE_OPENAI_API_KEY is defined, use the AzureOpenAI class
 98 |       const openai = process.env.AZURE_OPENAI_API_KEY ? new AzureOpenAI() : new OpenAI();
 99 | 
100 |       // Only allow gpt-image-1
101 |       const {
102 |         prompt,
103 |         background,
104 |         model = "gpt-image-1",
105 |         moderation,
106 |         n,
107 |         output_compression,
108 |         output_format,
109 |         quality,
110 |         size,
111 |         user,
112 |         output = "base64",
113 |         file_output: file_outputRaw,
114 |       } = args;
115 |       const file_output: string | undefined = file_outputRaw;
116 | 
117 |       // Enforce: if background is 'transparent', output_format must be 'png' or 'webp'
118 |       if (background === "transparent" && output_format && !["png", "webp"].includes(output_format)) {
119 |         throw new Error("If background is 'transparent', output_format must be 'png' or 'webp'");
120 |       }
121 | 
122 |       // Only include output_compression if output_format is webp or jpeg
123 |       const imageParams: any = {
124 |         prompt,
125 |         model,
126 |         ...(background ? { background } : {}),
127 |         ...(moderation ? { moderation } : {}),
128 |         ...(n ? { n } : {}),
129 |         ...(output_format ? { output_format } : {}),
130 |         ...(quality ? { quality } : {}),
131 |         ...(size ? { size } : {}),
132 |         ...(user ? { user } : {}),
133 |       };
134 |       if (
135 |         typeof output_compression !== "undefined" &&
136 |         output_format &&
137 |         ["webp", "jpeg"].includes(output_format)
138 |       ) {
139 |         imageParams.output_compression = output_compression;
140 |       }
141 | 
142 |       const result = await openai.images.generate(imageParams);
143 | 
144 |       // gpt-image-1 always returns base64 images in data[].b64_json
145 |       const images = (result.data ?? []).map((img: any) => ({
146 |         b64: img.b64_json,
147 |         mimeType: output_format === "jpeg" ? "image/jpeg" : output_format === "webp" ? "image/webp" : "image/png",
148 |         ext: output_format === "jpeg" ? "jpg" : output_format === "webp" ? "webp" : "png",
149 |       }));
150 | 
151 |       // Auto-switch to file_output if total base64 size exceeds 1MB
152 |       const MAX_RESPONSE_SIZE = 1048576; // 1MB
153 |       const totalBase64Size = images.reduce((sum, img) => sum + Buffer.byteLength(img.b64, "base64"), 0);
154 |       let effectiveOutput = output;
155 |       let effectiveFileOutput = file_output;
156 |       if (output === "base64" && totalBase64Size > MAX_RESPONSE_SIZE) {
157 |         effectiveOutput = "file_output";
158 |         if (!file_output) {
159 |           // Use /tmp or MCP_HF_WORK_DIR if set
160 |           const tmpDir = process.env.MCP_HF_WORK_DIR || "/tmp";
161 |           const unique = Date.now();
162 |           effectiveFileOutput = path.join(tmpDir, `openai_image_${unique}.${images[0]?.ext ?? "png"}`);
163 |         }
164 |       }
165 | 
166 |       if (effectiveOutput === "file_output") {
167 |         const fs = await import("fs/promises");
168 |         const path = await import("path");
169 |         // If multiple images, append index to filename
170 |         const basePath = effectiveFileOutput!;
171 |         const responses = [];
172 |         for (let i = 0; i < images.length; i++) {
173 |           const img = images[i];
174 |           let filePath = basePath;
175 |           if (images.length > 1) {
176 |             const parsed = path.parse(basePath);
177 |             filePath = path.join(parsed.dir, `${parsed.name}_${i + 1}.${img.ext ?? "png"}`);
178 |           } else {
179 |             // Ensure correct extension
180 |             const parsed = path.parse(basePath);
181 |             filePath = path.join(parsed.dir, `${parsed.name}.${img.ext ?? "png"}`);
182 |           }
183 |           await fs.writeFile(filePath, Buffer.from(img.b64, "base64"));
184 |           responses.push({ type: "text", text: `Image saved to: file://${filePath}` });
185 |         }
186 |         return { content: responses };
187 |       } else {
188 |         // Default: base64
189 |         return {
190 |           content: images.map((img) => ({
191 |             type: "image",
192 |             data: img.b64,
193 |             mimeType: img.mimeType,
194 |           })),
195 |         };
196 |       }
197 |     }
198 |   );
199 | 
200 |   // Zod schema for edit-image tool input (gpt-image-1 only)
201 |   const absolutePathCheck = (val: string | undefined) => {
202 |     if (!val) return true;
203 |     // Check for Unix/Linux/macOS absolute paths
204 |     if (val.startsWith("/")) return true;
205 |     // Check for Windows absolute paths (C:/, D:\, etc.)
206 |     if (/^[a-zA-Z]:[/\\]/.test(val)) return true;
207 |     return false;
208 |   };
209 |   const base64Check = (val: string | undefined) => !!val && (/^([A-Za-z0-9+/=\r\n]+)$/.test(val) || val.startsWith("data:image/"));
210 |   const imageInputSchema = z.string().refine(
211 |     (val) => absolutePathCheck(val) || base64Check(val),
212 |     { message: "Must be an absolute path or a base64-encoded string (optionally as a data URL)" }
213 |   ).describe("Absolute path to an image file (png, jpg, webp < 25MB) or a base64-encoded image string.");
214 | 
215 |   // Base schema without refinement for server.tool signature
216 |   const editImageBaseSchema = z.object({
217 |     image: z.string().describe("Absolute image path or base64 string to edit."),
218 |     prompt: z.string().max(32000).describe("A text description of the desired edit. Max 32000 chars."),
219 |     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."),
220 |     model: z.literal("gpt-image-1").default("gpt-image-1"),
221 |     n: z.number().int().min(1).max(10).optional().describe("Number of images to generate (1-10)."),
222 |     quality: z.enum(["auto", "high", "medium", "low"]).optional().describe("Quality (high, medium, low) - only for gpt-image-1."),
223 |     size: z.enum(["1024x1024", "1536x1024", "1024x1536", "auto"]).optional().describe("Size of the generated images."),
224 |     user: z.string().optional().describe("Optional user identifier for OpenAI monitoring."),
225 |     output: z.enum(["base64", "file_output"]).default("base64").describe("Output format: base64 or file path."),
226 |     file_output: z.string().refine(absolutePathCheck, { message: "Path must be absolute" }).optional()
227 |       .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."),
228 |   });
229 | 
230 |   // Full schema with refinement for validation inside the handler
231 |   const editImageSchema = editImageBaseSchema.refine(
232 |     (data) => {
233 |       if (data.output !== "file_output") return true;
234 |       if (typeof data.file_output !== "string") return false;
235 |       return absolutePathCheck(data.file_output);
236 |     },
237 |     { message: "file_output must be an absolute path when output is 'file_output'", path: ["file_output"] }
238 |   );
239 | 
240 |   // Edit Image Tool (gpt-image-1 only)
241 |   server.tool(
242 |     "edit-image",
243 |     editImageBaseSchema.shape, // <-- Use the base schema shape here
244 |     async (args, _extra) => {
245 |       // Validate arguments using the full schema with refinements
246 |       const validatedArgs = editImageSchema.parse(args);
247 | 
248 |       // Explicitly validate image and mask inputs here
249 |       if (!absolutePathCheck(validatedArgs.image) && !base64Check(validatedArgs.image)) {
250 |         throw new Error("Invalid 'image' input: Must be an absolute path or a base64-encoded string.");
251 |       }
252 |       if (validatedArgs.mask && !absolutePathCheck(validatedArgs.mask) && !base64Check(validatedArgs.mask)) {
253 |         throw new Error("Invalid 'mask' input: Must be an absolute path or a base64-encoded string.");
254 |       }
255 | 
256 |       const openai = process.env.AZURE_OPENAI_API_KEY ? new AzureOpenAI() : new OpenAI();
257 |       const {
258 |         image: imageInput,
259 |         prompt,
260 |         mask: maskInput,
261 |         model = "gpt-image-1",
262 |         n,
263 |         quality,
264 |         size,
265 |         user,
266 |         output = "base64",
267 |         file_output: file_outputRaw,
268 |       } = validatedArgs; // <-- Use validatedArgs here
269 |       const file_output: string | undefined = file_outputRaw;
270 | 
271 |       // Helper to convert input (path or base64) to toFile
272 |       async function inputToFile(input: string, idx = 0) {
273 |         if (absolutePathCheck(input)) {
274 |           // File path: infer mime type from extension
275 |           const ext = input.split('.').pop()?.toLowerCase();
276 |           let mime = "image/png";
277 |           if (ext === "jpg" || ext === "jpeg") mime = "image/jpeg";
278 |           else if (ext === "webp") mime = "image/webp";
279 |           else if (ext === "png") mime = "image/png";
280 |           // else default to png
281 |           return await toFile(fs.createReadStream(input), undefined, { type: mime });
282 |         } else {
283 |           // Base64 or data URL
284 |           let base64 = input;
285 |           let mime = "image/png";
286 |           if (input.startsWith("data:image/")) {
287 |             // data URL
288 |             const match = input.match(/^data:(image\/\w+);base64,(.*)$/);
289 |             if (match) {
290 |               mime = match[1];
291 |               base64 = match[2];
292 |             }
293 |           }
294 |           const buffer = Buffer.from(base64, "base64");
295 |           return await toFile(buffer, `input_${idx}.${mime.split("/")[1] || "png"}`, { type: mime });
296 |         }
297 |       }
298 | 
299 |       // Prepare image input
300 |       const imageFile = await inputToFile(imageInput, 0);
301 | 
302 |       // Prepare mask input
303 |       const maskFile = maskInput ? await inputToFile(maskInput, 1) : undefined;
304 | 
305 |       // Construct parameters for OpenAI API
306 |       const editParams: any = {
307 |         image: imageFile,
308 |         prompt,
309 |         model, // Always gpt-image-1
310 |         ...(maskFile ? { mask: maskFile } : {}),
311 |         ...(n ? { n } : {}),
312 |         ...(quality ? { quality } : {}),
313 |         ...(size ? { size } : {}),
314 |         ...(user ? { user } : {}),
315 |         // response_format is not applicable for gpt-image-1 (always b64_json)
316 |       };
317 | 
318 |       const result = await openai.images.edit(editParams);
319 | 
320 |       // gpt-image-1 always returns base64 images in data[].b64_json
321 |       // We need to determine the output mime type and extension based on input/defaults
322 |       // Since OpenAI doesn't return this for edits, we'll default to png
323 |       const images = (result.data ?? []).map((img: any) => ({
324 |         b64: img.b64_json,
325 |         mimeType: "image/png",
326 |         ext: "png",
327 |       }));
328 | 
329 |       // Auto-switch to file_output if total base64 size exceeds 1MB
330 |       const MAX_RESPONSE_SIZE = 1048576; // 1MB
331 |       const totalBase64Size = images.reduce((sum, img) => sum + Buffer.byteLength(img.b64, "base64"), 0);
332 |       let effectiveOutput = output;
333 |       let effectiveFileOutput = file_output;
334 |       if (output === "base64" && totalBase64Size > MAX_RESPONSE_SIZE) {
335 |         effectiveOutput = "file_output";
336 |         if (!file_output) {
337 |           // Use /tmp or MCP_HF_WORK_DIR if set
338 |           const tmpDir = process.env.MCP_HF_WORK_DIR || "/tmp";
339 |           const unique = Date.now();
340 |           effectiveFileOutput = path.join(tmpDir, `openai_image_edit_${unique}.png`);
341 |         }
342 |       }
343 | 
344 |       if (effectiveOutput === "file_output") {
345 |         if (!effectiveFileOutput) {
346 |           throw new Error("file_output path is required when output is 'file_output'");
347 |         }
348 |         // Use fs/promises and path (already imported)
349 |         const basePath = effectiveFileOutput!;
350 |         const responses = [];
351 |         for (let i = 0; i < images.length; i++) {
352 |           const img = images[i];
353 |           let filePath = basePath;
354 |           if (images.length > 1) {
355 |             const parsed = path.parse(basePath);
356 |             // Append index before the original extension if it exists, otherwise just append index and .png
357 |             const ext = parsed.ext || `.${img.ext}`;
358 |             filePath = path.join(parsed.dir, `${parsed.name}_${i + 1}${ext}`);
359 |           } else {
360 |             // Ensure the extension from the path is used, or default to .png
361 |             const parsed = path.parse(basePath);
362 |             const ext = parsed.ext || `.${img.ext}`;
363 |             filePath = path.join(parsed.dir, `${parsed.name}${ext}`);
364 |           }
365 |           await fs.promises.writeFile(filePath, Buffer.from(img.b64, "base64"));
366 |           // Workaround: Return file path as text
367 |           responses.push({ type: "text", text: `Image saved to: file://${filePath}` });
368 |         }
369 |         return { content: responses };
370 |       } else {
371 |         // Default: base64
372 |         return {
373 |           content: images.map((img) => ({
374 |             type: "image",
375 |             data: img.b64,
376 |             mimeType: img.mimeType, // Should be image/png
377 |           })),
378 |         };
379 |       }
380 |     }
381 |   );
382 | 
383 |   const transport = new StdioServerTransport();
384 |   await server.connect(transport);
385 | })(); 
```