@alonetech/gpt-image-mcp 0.1.0 → 0.3.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/README.md +1 -0
- package/package.json +1 -1
- package/src/cli.js +3 -5
- package/src/image-api.js +46 -11
- package/src/mcp-server.js +63 -4
package/README.md
CHANGED
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,15 +3,13 @@ import { basename } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { generateImage } from "./image-api.js";
|
|
5
5
|
|
|
6
|
-
const DEFAULT_OUTPUT = "output.png";
|
|
7
|
-
|
|
8
6
|
function usage() {
|
|
9
7
|
return `Usage:
|
|
10
|
-
gpt-image-mcp generate --base-url <url> --token <token> --prompt <text> [options]
|
|
8
|
+
gpt-image-mcp generate --base-url <url> --token <token> --prompt <text> --output <path> [options]
|
|
11
9
|
|
|
12
10
|
Options:
|
|
13
11
|
--image <path> Optional input image. Repeat for image-to-image mode.
|
|
14
|
-
--output <path>
|
|
12
|
+
--output <path> Required output image path.
|
|
15
13
|
--model <model> Image model. Default: gpt-image-2
|
|
16
14
|
--size <size> Image size. Default: auto
|
|
17
15
|
--quality <quality> auto, low, medium, or high. Default: auto
|
|
@@ -31,7 +29,6 @@ function readOption(args, index) {
|
|
|
31
29
|
function parseGenerateArgs(args) {
|
|
32
30
|
const options = {
|
|
33
31
|
images: [],
|
|
34
|
-
output: DEFAULT_OUTPUT,
|
|
35
32
|
output_format: "png",
|
|
36
33
|
};
|
|
37
34
|
|
|
@@ -134,6 +131,7 @@ async function runGenerate(args, dependencies) {
|
|
|
134
131
|
requireOption(options, "baseUrl", "--base-url");
|
|
135
132
|
requireOption(options, "token", "--token");
|
|
136
133
|
requireOption(options, "prompt", "--prompt");
|
|
134
|
+
requireOption(options, "output", "--output");
|
|
137
135
|
|
|
138
136
|
const input = {
|
|
139
137
|
prompt: options.prompt,
|
package/src/image-api.js
CHANGED
|
@@ -97,19 +97,50 @@ function commonOptions(input, outputFormat) {
|
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function resultFromResponseBody(body, outputFormat, model, mode) {
|
|
101
|
-
const
|
|
100
|
+
async function resultFromResponseBody(body, outputFormat, model, mode, fetchImpl) {
|
|
101
|
+
const firstResult = body?.data?.[0];
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Try b64_json first
|
|
104
|
+
const b64Data = firstResult?.b64_json;
|
|
105
|
+
if (b64Data) {
|
|
106
|
+
return {
|
|
107
|
+
data: b64Data,
|
|
108
|
+
mimeType: mimeTypeForOutputFormat(outputFormat),
|
|
109
|
+
model,
|
|
110
|
+
mode,
|
|
111
|
+
};
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
// Try url - fetch image from URL and convert to base64
|
|
115
|
+
const url = firstResult?.url;
|
|
116
|
+
if (url) {
|
|
117
|
+
// Use globalThis.fetch for fetching image URLs (not fetchImpl which may have API-specific headers)
|
|
118
|
+
const response = await globalThis.fetch(url);
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(`Failed to fetch image from URL: ${response.status} ${response.statusText}`);
|
|
121
|
+
}
|
|
122
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
123
|
+
const base64 = Buffer.from(arrayBuffer).toString('base64');
|
|
124
|
+
// Get correct mimeType from response Content-Type
|
|
125
|
+
const contentType = response.headers.get('content-type') || '';
|
|
126
|
+
const actualMimeType = contentType.startsWith('image/') ? contentType : mimeTypeForOutputFormat(outputFormat);
|
|
127
|
+
return {
|
|
128
|
+
data: base64,
|
|
129
|
+
mimeType: actualMimeType,
|
|
130
|
+
model,
|
|
131
|
+
mode,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Debug: include response info in error message
|
|
136
|
+
const responseSummary = {
|
|
137
|
+
bodyKeys: Object.keys(body || {}),
|
|
138
|
+
dataType: typeof body?.data,
|
|
139
|
+
dataLength: body?.data?.length,
|
|
140
|
+
firstResultKeys: firstResult ? Object.keys(firstResult) : null,
|
|
141
|
+
bodyPreview: JSON.stringify(body).substring(0, 500)
|
|
112
142
|
};
|
|
143
|
+
throw new Error(`Image API response did not include base64 image data or URL. Response summary: ${JSON.stringify(responseSummary)}`);
|
|
113
144
|
}
|
|
114
145
|
|
|
115
146
|
export async function generateImage(input, config = {}) {
|
|
@@ -125,6 +156,8 @@ export async function generateImage(input, config = {}) {
|
|
|
125
156
|
const outputFormat = input.output_format || input.outputFormat || DEFAULT_OUTPUT_FORMAT;
|
|
126
157
|
const options = commonOptions(input, outputFormat);
|
|
127
158
|
|
|
159
|
+
const signal = config.signal;
|
|
160
|
+
|
|
128
161
|
if (!Array.isArray(images) || images.length === 0) {
|
|
129
162
|
const { background, ...body } = options;
|
|
130
163
|
if (background) {
|
|
@@ -138,6 +171,7 @@ export async function generateImage(input, config = {}) {
|
|
|
138
171
|
"Content-Type": "application/json",
|
|
139
172
|
},
|
|
140
173
|
body: JSON.stringify(body),
|
|
174
|
+
signal,
|
|
141
175
|
});
|
|
142
176
|
|
|
143
177
|
if (!response.ok) {
|
|
@@ -145,7 +179,7 @@ export async function generateImage(input, config = {}) {
|
|
|
145
179
|
throw new Error(`Image API request failed (${response.status} ${response.statusText}): ${message}`);
|
|
146
180
|
}
|
|
147
181
|
|
|
148
|
-
return resultFromResponseBody(await response.json(), outputFormat, options.model, "text");
|
|
182
|
+
return resultFromResponseBody(await response.json(), outputFormat, options.model, "text", fetchImpl);
|
|
149
183
|
}
|
|
150
184
|
|
|
151
185
|
const form = new FormData();
|
|
@@ -169,6 +203,7 @@ export async function generateImage(input, config = {}) {
|
|
|
169
203
|
Authorization: `Bearer ${token}`,
|
|
170
204
|
},
|
|
171
205
|
body: form,
|
|
206
|
+
signal,
|
|
172
207
|
});
|
|
173
208
|
|
|
174
209
|
if (!response.ok) {
|
|
@@ -176,7 +211,7 @@ export async function generateImage(input, config = {}) {
|
|
|
176
211
|
throw new Error(`Image API request failed (${response.status} ${response.statusText}): ${message}`);
|
|
177
212
|
}
|
|
178
213
|
|
|
179
|
-
return resultFromResponseBody(await response.json(), outputFormat, options.model, "edit");
|
|
214
|
+
return resultFromResponseBody(await response.json(), outputFormat, options.model, "edit", fetchImpl);
|
|
180
215
|
}
|
|
181
216
|
|
|
182
217
|
export const generateEditedImage = generateImage;
|
package/src/mcp-server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appendFileSync } from "node:fs";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
2
3
|
|
|
3
4
|
import { generateImage } from "./image-api.js";
|
|
4
5
|
|
|
@@ -12,12 +13,16 @@ const TOOL_DEFINITION = {
|
|
|
12
13
|
"Generate one image in text-to-image mode from a prompt only, or in image-to-image mode from a prompt plus optional reference images.",
|
|
13
14
|
inputSchema: {
|
|
14
15
|
type: "object",
|
|
15
|
-
required: ["prompt"],
|
|
16
|
+
required: ["prompt", "output"],
|
|
16
17
|
properties: {
|
|
17
18
|
prompt: {
|
|
18
19
|
type: "string",
|
|
19
20
|
description: "Prompt describing the single output image to generate.",
|
|
20
21
|
},
|
|
22
|
+
output: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Required filesystem path where the generated image will be saved.",
|
|
25
|
+
},
|
|
21
26
|
images: {
|
|
22
27
|
type: "array",
|
|
23
28
|
minItems: 0,
|
|
@@ -111,6 +116,15 @@ export function createServerConfig({ argv = process.argv, env = process.env } =
|
|
|
111
116
|
config.debugLog = debugLog;
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
const timeoutStr = valueAfterFlag(argv, "--timeout") || env.GPT_IMAGE_TIMEOUT;
|
|
120
|
+
if (timeoutStr) {
|
|
121
|
+
const timeoutMs = Number.parseInt(timeoutStr, 10);
|
|
122
|
+
if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
123
|
+
throw new Error(`Invalid timeout value: ${timeoutStr}`);
|
|
124
|
+
}
|
|
125
|
+
config.timeoutMs = timeoutMs;
|
|
126
|
+
}
|
|
127
|
+
|
|
114
128
|
return config;
|
|
115
129
|
}
|
|
116
130
|
|
|
@@ -302,15 +316,36 @@ async function handleToolCall(message, dependencies) {
|
|
|
302
316
|
return failure(message.id, -32602, `Unknown tool: ${name || ""}`);
|
|
303
317
|
}
|
|
304
318
|
|
|
319
|
+
const requestId = message.id;
|
|
320
|
+
const pendingRequests = dependencies.pendingRequests;
|
|
321
|
+
const timeoutMs = dependencies.config?.timeoutMs ?? 300000; // default 5 minutes
|
|
322
|
+
const controller = new AbortController();
|
|
323
|
+
const timeoutTimer = setTimeout(() => {
|
|
324
|
+
controller.abort(new Error(`Tool call timed out after ${timeoutMs}ms`));
|
|
325
|
+
}, timeoutMs);
|
|
326
|
+
|
|
327
|
+
if (pendingRequests && requestId !== undefined && requestId !== null) {
|
|
328
|
+
pendingRequests.set(requestId, { controller, timer: timeoutTimer });
|
|
329
|
+
}
|
|
330
|
+
|
|
305
331
|
try {
|
|
332
|
+
const args = message.params?.arguments || {};
|
|
333
|
+
const outputPath = args.output;
|
|
334
|
+
if (typeof outputPath !== "string" || outputPath.trim() === "") {
|
|
335
|
+
throw new Error("output is required");
|
|
336
|
+
}
|
|
337
|
+
|
|
306
338
|
const image = await generateImage(
|
|
307
|
-
|
|
339
|
+
args,
|
|
308
340
|
{
|
|
309
341
|
...dependencies.config,
|
|
310
342
|
fetchImpl: dependencies.fetchImpl || globalThis.fetch,
|
|
343
|
+
signal: controller.signal,
|
|
311
344
|
},
|
|
312
345
|
);
|
|
313
346
|
|
|
347
|
+
await (dependencies.writeFile || writeFile)(outputPath, Buffer.from(image.data, "base64"));
|
|
348
|
+
|
|
314
349
|
return success(message.id, {
|
|
315
350
|
content: [
|
|
316
351
|
{
|
|
@@ -326,15 +361,23 @@ async function handleToolCall(message, dependencies) {
|
|
|
326
361
|
isError: false,
|
|
327
362
|
});
|
|
328
363
|
} catch (error) {
|
|
364
|
+
const isAborted = error instanceof Error && error.name === "AbortError";
|
|
329
365
|
return success(message.id, {
|
|
330
366
|
content: [
|
|
331
367
|
{
|
|
332
368
|
type: "text",
|
|
333
|
-
text:
|
|
369
|
+
text: isAborted
|
|
370
|
+
? `Image generation was cancelled or timed out: ${error.message}`
|
|
371
|
+
: error instanceof Error ? error.message : String(error),
|
|
334
372
|
},
|
|
335
373
|
],
|
|
336
374
|
isError: true,
|
|
337
375
|
});
|
|
376
|
+
} finally {
|
|
377
|
+
clearTimeout(timeoutTimer);
|
|
378
|
+
if (pendingRequests && requestId !== undefined && requestId !== null) {
|
|
379
|
+
pendingRequests.delete(requestId);
|
|
380
|
+
}
|
|
338
381
|
}
|
|
339
382
|
}
|
|
340
383
|
|
|
@@ -361,6 +404,19 @@ export async function handleJsonRpcMessage(message, dependencies = {}) {
|
|
|
361
404
|
});
|
|
362
405
|
case "notifications/initialized":
|
|
363
406
|
return undefined;
|
|
407
|
+
case "notifications/cancelled": {
|
|
408
|
+
const pendingRequests = dependencies.pendingRequests;
|
|
409
|
+
if (pendingRequests) {
|
|
410
|
+
const requestId = message.params?.requestId;
|
|
411
|
+
const pending = pendingRequests.get(requestId);
|
|
412
|
+
if (pending) {
|
|
413
|
+
clearTimeout(pending.timer);
|
|
414
|
+
pending.controller.abort(new Error("Request cancelled by client"));
|
|
415
|
+
pendingRequests.delete(requestId);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
364
420
|
case "ping":
|
|
365
421
|
return success(message.id, {});
|
|
366
422
|
case "tools/list":
|
|
@@ -386,8 +442,11 @@ export async function runStdioServer({
|
|
|
386
442
|
debug("server_started", {
|
|
387
443
|
hasBaseUrl: Boolean(config.baseUrl),
|
|
388
444
|
hasToken: Boolean(config.token),
|
|
445
|
+
timeoutMs: config.timeoutMs || 300000,
|
|
389
446
|
});
|
|
390
447
|
|
|
448
|
+
const pendingRequests = new Map();
|
|
449
|
+
|
|
391
450
|
try {
|
|
392
451
|
for await (const { message, transport } of readFramedMessages(input, debug)) {
|
|
393
452
|
debug("message_received", {
|
|
@@ -395,7 +454,7 @@ export async function runStdioServer({
|
|
|
395
454
|
method: message?.method,
|
|
396
455
|
transport,
|
|
397
456
|
});
|
|
398
|
-
const response = await handleJsonRpcMessage(message, { config });
|
|
457
|
+
const response = await handleJsonRpcMessage(message, { config, pendingRequests });
|
|
399
458
|
if (response) {
|
|
400
459
|
debug("message_sent", {
|
|
401
460
|
id: response.id,
|