@felixpy/openai-image 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +202 -2
- package/dist/index.js +119 -0
- package/package.json +54 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felix Yang (felixpy)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,203 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<h1>openai-image</h1>
|
|
4
|
+
|
|
5
|
+
<p>Generate and edit images with OpenAI from any MCP client.</p>
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<a href="https://www.npmjs.com/package/@felixpy/openai-image"><img alt="npm version" src="https://img.shields.io/npm/v/@felixpy/openai-image"></a>
|
|
9
|
+
<a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/github/license/felixpy/openai-image"></a>
|
|
10
|
+
<a href="https://github.com/felixpy/openai-image/actions/workflows/ci.yml"><img alt="CI status" src="https://img.shields.io/github/actions/workflow/status/felixpy/openai-image/ci.yml?branch=main&label=CI"></a>
|
|
11
|
+
<a href="package.json"><img alt="Node.js 20 or newer" src="https://img.shields.io/badge/node-%3E%3D20-339933?logo=nodedotjs&logoColor=white"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<img src="docs/assets/tibo-quota.gif" alt="Animated GIF of a cartoon Tibo in sunglasses while a quota bar fills from 5% to full reset" width="512">
|
|
15
|
+
<p><sub>Animated GIF output from <code>openai_image_edit</code> (<code>output_format: "gif"</code>).</sub></p>
|
|
16
|
+
|
|
17
|
+
<p>
|
|
18
|
+
<a href="README.md">English</a>
|
|
19
|
+
·
|
|
20
|
+
<a href="README_ZH.md">简体中文</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
`openai-image` is a stdio [Model Context Protocol](https://modelcontextprotocol.io) server. It calls the official OpenAI Images API, writes the files to an absolute `output_dir`, and returns paths plus metadata. Tool results never include base64.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npx -y @felixpy/openai-image
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
### 1. API key
|
|
34
|
+
|
|
35
|
+
The server reads `OPENAI_API_KEY` from the environment. It is required for a tool call and is never logged. A missing key does not stop the process from starting.
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
export OPENAI_API_KEY=sk-...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Variable | Default | Role |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `OPENAI_API_KEY` | required for tool calls | Sent by the OpenAI SDK. Never commit it. |
|
|
44
|
+
| `OPENAI_IMAGE_MODEL` | `gpt-image-2.6-flare` | Model used when a call omits `model`. |
|
|
45
|
+
| `OPENAI_BASE_URL` | OpenAI default | Optional. Read by the OpenAI SDK. |
|
|
46
|
+
|
|
47
|
+
### 2. Register the server
|
|
48
|
+
|
|
49
|
+
Every snippet starts the published package over stdio. Replace `sk-...` locally. Do not commit a real key.
|
|
50
|
+
|
|
51
|
+
#### Cursor
|
|
52
|
+
|
|
53
|
+
`~/.cursor/mcp.json` or `.cursor/mcp.json`:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"openai-image": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["-y", "@felixpy/openai-image"],
|
|
61
|
+
"env": {
|
|
62
|
+
"OPENAI_API_KEY": "sk-..."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Claude Desktop
|
|
70
|
+
|
|
71
|
+
macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
72
|
+
Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
73
|
+
Linux: `~/.config/Claude/claude_desktop_config.json`
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"openai-image": {
|
|
79
|
+
"command": "npx",
|
|
80
|
+
"args": ["-y", "@felixpy/openai-image"],
|
|
81
|
+
"env": {
|
|
82
|
+
"OPENAI_API_KEY": "sk-..."
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Claude Code
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
claude mcp add openai-image \
|
|
93
|
+
-e OPENAI_API_KEY=sk-... \
|
|
94
|
+
-- npx -y @felixpy/openai-image
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Codex
|
|
98
|
+
|
|
99
|
+
`~/.codex/config.toml`:
|
|
100
|
+
|
|
101
|
+
```toml
|
|
102
|
+
[mcp_servers.openai-image]
|
|
103
|
+
command = "npx"
|
|
104
|
+
args = ["-y", "@felixpy/openai-image"]
|
|
105
|
+
env = { OPENAI_API_KEY = "sk-..." }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Tools
|
|
109
|
+
|
|
110
|
+
| Tool | API | Required | Also accepts |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `openai_image_generate` | `images.generate` | `prompt`, `output_dir` | Shared image fields, plus `svg` |
|
|
113
|
+
| `openai_image_edit` | `images.edit` | `prompt`, `image_paths`, `output_dir` | Shared image fields, plus `mask_path`, `markup_path`, `input_fidelity` |
|
|
114
|
+
|
|
115
|
+
`openai_image_generate` creates still images, animated GIFs (`output_format=gif`), or SVG. `openai_image_edit` edits or composes up to 16 reference images, optionally guided by a mask or a markup overlay. Both save files under `output_dir` and return their paths.
|
|
116
|
+
|
|
117
|
+
A successful result is text JSON. URLs and `revised_prompt` are included only when the API sends them.
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"model": "gpt-image-2.6-flare",
|
|
122
|
+
"files": ["/absolute/path/image-2026-11-05T00-00-00-000Z-0.png"],
|
|
123
|
+
"usage": null,
|
|
124
|
+
"size": "1024x1024",
|
|
125
|
+
"quality": "auto",
|
|
126
|
+
"output_format": "png"
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
API failures, including `moderation_blocked`, come back as tool errors with `status`, `code`, `type`, `param`, `message`, `request_id`, and the raw `error` body. The client uses `maxRetries: 2` and a 180 second timeout.
|
|
131
|
+
|
|
132
|
+
## Parameters
|
|
133
|
+
|
|
134
|
+
Schemas are loose strings on purpose. The API validates values, so a new option usually needs no code change. Unknown fields can go through `extra`, which is shallow-merged last.
|
|
135
|
+
|
|
136
|
+
### Shared
|
|
137
|
+
|
|
138
|
+
| Parameter | Required | Range and notes |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `output_dir` | yes | Absolute directory. A relative path is rejected. The directory is created if needed. |
|
|
141
|
+
| `model` | no | Default is `OPENAI_IMAGE_MODEL` or `gpt-image-2.6-flare`. Examples: `gpt-image-2.6-flare` (fast), `gpt-image-2.6-sunburst` (detail and precise edits), `gpt-image-2.5-flare`, `gpt-image-2.5-sunburst`. |
|
|
142
|
+
| `size` | no | `auto`, `1024x1024`, `1536x1024`, `1024x1536`, or `WIDTHxHEIGHT`. Both sides must be multiples of 16, the aspect ratio must be from 1:3 to 3:1, and the maximum is `3840x2160`. For GIF this is the size of each frame. Keep frames small, for example `512x512`, so the animation fits the pixel budget. |
|
|
143
|
+
| `quality` | no | `auto` (default), `low`, `medium`, `high`, `xhigh`, `max`. |
|
|
144
|
+
| `complexity` | no | Planning effort, separate from quality: `low`, `medium`, `high`. gpt-image-2.6 defaults to `high`. |
|
|
145
|
+
| `n` | no | Integer from 1 to 10. |
|
|
146
|
+
| `background` | no | `auto` (default), `opaque`, `transparent`. Transparent requires `png`, `webp`, or `svg`. |
|
|
147
|
+
| `output_format` | no | `png` (default), `jpeg`, `webp`. Generate also supports `gif` (animated) and `svg`. |
|
|
148
|
+
| `output_compression` | no | Integer from 0 to 100. `jpeg` and `webp` only. |
|
|
149
|
+
| `frame_count` | no | GIF only. Integer, minimum 2, default 9. The SDK documents 2–16 and the image guide documents 2–64. Describe the motion and a return to the starting pose so the loop is smooth. |
|
|
150
|
+
| `frame_duration_ms` | no | GIF only. Integer from 20 to 2000, rounded to 10. Default 200. |
|
|
151
|
+
| `moderation` | no | `auto` (default), `low`. |
|
|
152
|
+
| `safety_identifier` | no | Optional stable hashed end-user id for OpenAI abuse monitoring. |
|
|
153
|
+
| `extra` | no | Object shallow-merged into the request last, for example `{"complexity":"high"}`. |
|
|
154
|
+
|
|
155
|
+
### `openai_image_generate`
|
|
156
|
+
|
|
157
|
+
| Parameter | Required | Range and notes |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `prompt` | yes | Text prompt. Maximum 32000 characters. |
|
|
160
|
+
| `svg` | no | SVG only, with `output_format=svg` and `n=1`. Object `{"model":"...","reasoning":{"effort":"high"}}`. Needs SVG model-selection access. Omit it otherwise. |
|
|
161
|
+
|
|
162
|
+
### `openai_image_edit`
|
|
163
|
+
|
|
164
|
+
| Parameter | Required | Range and notes |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `prompt` | yes | Edit instructions. |
|
|
167
|
+
| `image_paths` | yes | Local paths. Minimum 1. The API accepts up to 16 reference images. |
|
|
168
|
+
| `mask_path` | no | PNG mask, same size as the first image. Transparent pixels are the area to edit. Cannot be combined with `markup_path`. |
|
|
169
|
+
| `markup_path` | no | Transparent PNG overlay with marks, arrows, or notes, same size as the first image. Cannot be combined with `mask_path`. |
|
|
170
|
+
| `input_fidelity` | no | `high` or `low`. Applies to `gpt-image-1.5` and `1-mini` only. Omit it for gpt-image-2 and later. |
|
|
171
|
+
|
|
172
|
+
Edit accepts the shared fields, including `output_format`. GIF output from edit was verified on 2026-11-05.
|
|
173
|
+
|
|
174
|
+
## Notes
|
|
175
|
+
|
|
176
|
+
- **GIF.** Set `output_format` to `gif` on generate or edit. Use `frame_count` and `frame_duration_ms` to shape the loop. Edit GIF output was verified on 2026-11-05.
|
|
177
|
+
- **Absolute `output_dir`.** The path must be absolute. Relative paths are rejected before the API is called.
|
|
178
|
+
- **No base64 in tool results.** Bytes are written to disk. The tool returns file paths, the model, `usage`, and `revised_prompt` when the API provides one.
|
|
179
|
+
- **Pass-through.** `extra` is shallow-merged last, so new API fields work without a release.
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
Node.js 20 or newer.
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
npm ci
|
|
187
|
+
npm test
|
|
188
|
+
npm run build
|
|
189
|
+
npm start
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`npm test` builds `dist/` and completes an MCP `initialize` plus `tools/list` handshake. It does not call the OpenAI API and does not need `OPENAI_API_KEY`.
|
|
193
|
+
|
|
194
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for commit style and pull requests, and [docs/publishing.md](docs/publishing.md) for the one-time npm setup.
|
|
195
|
+
|
|
196
|
+
## Project
|
|
197
|
+
|
|
198
|
+
- [Contributing](CONTRIBUTING.md)
|
|
199
|
+
- [Security](SECURITY.md)
|
|
200
|
+
- [License](LICENSE) (MIT)
|
|
201
|
+
- [Changelog](CHANGELOG.md)
|
|
202
|
+
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
203
|
+
- [Publishing](docs/publishing.md)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Minimal MCP server (stdio) wrapping OpenAI images.generate and images.edit.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import OpenAI, { toFile } from 'openai';
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
const IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL || 'gpt-image-2.6-flare';
|
|
10
|
+
// Reported on MCP initialize. Read from package.json so release-please version bumps stay in sync.
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
12
|
+
// Lazy client: a missing key becomes a tool error, not a startup crash.
|
|
13
|
+
// The SDK reads OPENAI_API_KEY and OPENAI_BASE_URL from env itself; the key is never logged.
|
|
14
|
+
let client;
|
|
15
|
+
const openai = () => (client ??= new OpenAI({ maxRetries: 2, timeout: 180_000 }));
|
|
16
|
+
// Loose schemas on purpose: the API validates values, so new options need no code change.
|
|
17
|
+
const extra = z.record(z.string(), z.unknown()).optional()
|
|
18
|
+
.describe('Extra API params shallow-merged into the request last, e.g. {"complexity":"high"}');
|
|
19
|
+
const outputDir = z.string().refine(path.isAbsolute, 'must be an absolute path')
|
|
20
|
+
.describe('Absolute directory to save images in. Follow your workspace conventions: the current project folder if any, else your own tmp dir.');
|
|
21
|
+
const imageOptions = {
|
|
22
|
+
output_dir: outputDir,
|
|
23
|
+
model: z.string().optional().describe(`Image model (default ${IMAGE_MODEL}). e.g. gpt-image-2.6-flare (fast), gpt-image-2.6-sunburst (detail/precise edits), gpt-image-2.5-flare, gpt-image-2.5-sunburst`),
|
|
24
|
+
size: z.string().optional().describe('auto, 1024x1024, 1536x1024, 1024x1536, or WIDTHxHEIGHT (both multiples of 16, aspect 1:3 to 3:1, max 3840x2160). For GIF this is the size of each frame; keep it small (e.g. 512x512) to fit the pixel budget'),
|
|
25
|
+
quality: z.string().optional().describe('auto (default), low, medium, high, xhigh, max'),
|
|
26
|
+
complexity: z.string().optional().describe('Planning effort, separate from quality: low, medium, high (2.6 default high)'),
|
|
27
|
+
n: z.number().int().min(1).max(10).optional().describe('Number of images, 1-10'),
|
|
28
|
+
background: z.string().optional().describe('auto (default), opaque, transparent (transparent needs png, webp or svg)'),
|
|
29
|
+
output_format: z.string().optional().describe('png (default), jpeg, webp; generate also supports gif (animated) and svg'),
|
|
30
|
+
output_compression: z.number().int().min(0).max(100).optional().describe('0-100, jpeg/webp only'),
|
|
31
|
+
frame_count: z.number().int().min(2).optional().describe('GIF only: number of frames (default 9; SDK says 2-16, guide says 2-64). Describe the motion and a return to the starting pose in the prompt for a smooth loop'),
|
|
32
|
+
frame_duration_ms: z.number().int().min(20).optional().describe('GIF only: ms per frame, 20-2000, rounded to 10 (default 200)'),
|
|
33
|
+
moderation: z.string().optional().describe('auto (default), low'),
|
|
34
|
+
safety_identifier: z.string().optional().describe('Optional stable hashed end-user id for OpenAI abuse monitoring'),
|
|
35
|
+
extra,
|
|
36
|
+
};
|
|
37
|
+
// Defined fields first, then `extra` on top. Undefined values are dropped.
|
|
38
|
+
function merge(defaults, { extra, ...fields }) {
|
|
39
|
+
const defined = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
40
|
+
return { ...defaults, ...defined, ...extra };
|
|
41
|
+
}
|
|
42
|
+
const stamp = () => new Date().toISOString().replace(/[:.]/g, '-');
|
|
43
|
+
const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
44
|
+
function toolError(e) {
|
|
45
|
+
// APIError carries status/code/type/param; `error` holds the raw body (e.g. moderation_blocked details).
|
|
46
|
+
const err = e instanceof OpenAI.APIError
|
|
47
|
+
? { status: e.status, code: e.code, type: e.type, param: e.param, message: e.message, request_id: e.requestID, error: e.error }
|
|
48
|
+
: { message: e instanceof Error ? e.message : String(e) };
|
|
49
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(err, null, 2) }] };
|
|
50
|
+
}
|
|
51
|
+
const MIME = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp' };
|
|
52
|
+
const upload = (p) => toFile(fs.createReadStream(p), path.basename(p), { type: MIME[path.extname(p).slice(1).toLowerCase()] ?? 'application/octet-stream' });
|
|
53
|
+
// Write b64 images to disk; return only paths + metadata (no base64 into the agent's context).
|
|
54
|
+
function saveImages(res, dir, model, requestedFormat) {
|
|
55
|
+
const format = res.output_format ?? requestedFormat ?? 'png';
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
const prefix = stamp();
|
|
58
|
+
const files = [], urls = [], revised = [];
|
|
59
|
+
(res.data ?? []).forEach((d, i) => {
|
|
60
|
+
if (d.revised_prompt)
|
|
61
|
+
revised.push(d.revised_prompt);
|
|
62
|
+
if (d.url)
|
|
63
|
+
urls.push(d.url);
|
|
64
|
+
if (!d.b64_json)
|
|
65
|
+
return;
|
|
66
|
+
const file = path.join(dir, `image-${prefix}-${i}.${format}`);
|
|
67
|
+
fs.writeFileSync(file, Buffer.from(d.b64_json, 'base64'));
|
|
68
|
+
files.push(file);
|
|
69
|
+
});
|
|
70
|
+
return ok({
|
|
71
|
+
model, files, ...(urls.length && { urls }), usage: res.usage ?? null,
|
|
72
|
+
...(revised.length && { revised_prompt: revised.length === 1 ? revised[0] : revised }),
|
|
73
|
+
size: res.size, quality: res.quality, output_format: format,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const server = new McpServer({ name: 'openai-image', version: pkg.version });
|
|
77
|
+
server.registerTool('openai_image_generate', {
|
|
78
|
+
description: 'Generate images (OpenAI images.generate): still images, animated GIF (output_format=gif) or SVG. Saves files to output_dir and returns their paths.',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
prompt: z.string().describe('Text prompt (max 32000 chars)'),
|
|
81
|
+
...imageOptions,
|
|
82
|
+
svg: z.object({ model: z.string(), reasoning: z.record(z.string(), z.unknown()).optional() }).optional()
|
|
83
|
+
.describe('SVG only (output_format=svg, n=1): author model and reasoning, e.g. {"model":"...","reasoning":{"effort":"high"}}. Needs SVG model-selection access; omit otherwise'),
|
|
84
|
+
},
|
|
85
|
+
}, async ({ output_dir, ...args }) => {
|
|
86
|
+
try {
|
|
87
|
+
const params = { ...merge({ model: IMAGE_MODEL }, args), stream: false };
|
|
88
|
+
return saveImages(await openai().images.generate(params), output_dir, String(params.model), params.output_format);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
return toolError(e);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
server.registerTool('openai_image_edit', {
|
|
95
|
+
description: 'Edit images or compose from up to 16 reference images (OpenAI images.edit), optionally guided by a mask or markup overlay. Saves files to output_dir and returns their paths.',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
prompt: z.string().describe('Edit instructions'),
|
|
98
|
+
image_paths: z.array(z.string()).min(1).describe('Local paths of one or more input/reference images'),
|
|
99
|
+
mask_path: z.string().optional().describe('Optional PNG mask, same size as the first image (transparent = area to edit). Cannot combine with markup_path'),
|
|
100
|
+
markup_path: z.string().optional().describe('Optional transparent PNG overlay with marks/arrows/notes drawn on it, same size as the first image, to guide the edit. Cannot combine with mask_path'),
|
|
101
|
+
input_fidelity: z.string().optional().describe('high or low; gpt-image-1.5 / 1-mini only, omit for gpt-image-2 and later'),
|
|
102
|
+
...imageOptions,
|
|
103
|
+
},
|
|
104
|
+
}, async ({ output_dir, image_paths, mask_path, markup_path, ...rest }) => {
|
|
105
|
+
try {
|
|
106
|
+
const params = {
|
|
107
|
+
...merge({ model: IMAGE_MODEL }, rest),
|
|
108
|
+
image: await Promise.all(image_paths.map(upload)), // SDK type: Uploadable | Array<Uploadable>
|
|
109
|
+
...(mask_path && { mask: await upload(mask_path) }),
|
|
110
|
+
...(markup_path && { markup: await upload(markup_path) }),
|
|
111
|
+
stream: false,
|
|
112
|
+
};
|
|
113
|
+
return saveImages(await openai().images.edit(params), output_dir, String(params.model), params.output_format);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
return toolError(e);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@felixpy/openai-image",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for OpenAI image generation and editing, including GIF and SVG",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Felix Yang (felixpy)",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"openai-image": "dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/felixpy/openai-image.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/felixpy/openai-image/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/felixpy/openai-image#readme",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"openai",
|
|
34
|
+
"gpt-image",
|
|
35
|
+
"image-generation",
|
|
36
|
+
"image-editing",
|
|
37
|
+
"model-context-protocol"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"start": "node dist/index.js",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"pretest": "npm run build",
|
|
44
|
+
"test": "node --test test/smoke.test.js",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
49
|
+
"openai": "7.51.0",
|
|
50
|
+
"zod": "4.6.7"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "26.6.6",
|
|
54
|
+
"typescript": "7.0.2"
|
|
55
|
+
}
|
|
56
|
+
}
|