@alonetech/gpt-image-mcp 0.1.0 → 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/README.md CHANGED
@@ -90,6 +90,7 @@ generate_image
90
90
  Required arguments:
91
91
 
92
92
  - `prompt`: prompt describing the output image
93
+ - `output`: filesystem path where the generated image will be saved
93
94
 
94
95
  Optional arguments:
95
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alonetech/gpt-image-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP stdio server and CLI for text-to-image and image-to-image generation.",
5
5
  "type": "module",
6
6
  "bin": {
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> Output image path. Default: ${DEFAULT_OUTPUT}
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
@@ -125,6 +125,8 @@ export async function generateImage(input, config = {}) {
125
125
  const outputFormat = input.output_format || input.outputFormat || DEFAULT_OUTPUT_FORMAT;
126
126
  const options = commonOptions(input, outputFormat);
127
127
 
128
+ const signal = config.signal;
129
+
128
130
  if (!Array.isArray(images) || images.length === 0) {
129
131
  const { background, ...body } = options;
130
132
  if (background) {
@@ -138,6 +140,7 @@ export async function generateImage(input, config = {}) {
138
140
  "Content-Type": "application/json",
139
141
  },
140
142
  body: JSON.stringify(body),
143
+ signal,
141
144
  });
142
145
 
143
146
  if (!response.ok) {
@@ -169,6 +172,7 @@ export async function generateImage(input, config = {}) {
169
172
  Authorization: `Bearer ${token}`,
170
173
  },
171
174
  body: form,
175
+ signal,
172
176
  });
173
177
 
174
178
  if (!response.ok) {
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
- message.params?.arguments || {},
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: error instanceof Error ? error.message : String(error),
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,