mcp-duojie-image 0.5.6 → 0.5.7

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
@@ -1,179 +1,118 @@
1
- # mcp-duojie-image
2
-
3
- MCP server for image generation via duojie.games API (Gemini Flash Image).
4
-
5
- ## Features
6
-
7
- - **Text-to-image generation** — generate images from prompts with configurable size and aspect ratio
8
- - **Reference image support** — provide local image files as visual references (auto-compressed to 1024px long-side)
9
- - **Async mode** — for long-running generations (especially with reference images), use `async_mode: true` to get a `task_id` immediately and poll for completion; prevents MCP client timeout
10
- - **Sync mode** — for simple text-to-image generation without reference images, the default sync mode returns the result directly
11
- - **File saving** — automatic output directory creation, conflict handling (`overwrite` flag, auto-incrementing filename suffix)
12
- - **Configurable output** — size (512px/1K/2K/4K), templated aspect ratio, format
13
-
14
- ## Installation
15
-
16
- ```bash
17
- npx mcp-duojie-image
18
- ```
19
-
20
- Or with a specific version:
21
-
22
- ```bash
23
- npx mcp-duojie-image@0.1.3
24
- ```
25
-
26
- ## Configuration
27
-
28
- Set the `DUOJIE_API_KEY` environment variable:
29
-
30
- ```bash
31
- export DUOJIE_API_KEY=sk-your-key-here
32
- ```
33
-
34
- Or configure in your MCP client (e.g., Claude Desktop, Factory):
35
-
36
- ```json
37
- {
38
- "mcpServers": {
39
- "duojie-image": {
40
- "command": "node",
41
- "args": ["/path/to/mcp-duojie-image/dist/index.js"],
42
- "env": {
43
- "DUOJIE_API_KEY": "sk-your-key-here"
44
- }
45
- }
46
- }
47
- }
48
- ```
49
-
50
- Optional environment variables:
51
-
52
- | Variable | Default | Description |
53
- |----------|---------|-------------|
54
- | `DUOJIE_API_KEY` | (required) | API key |
55
- | `DUOJIE_BASE_URL` | `https://api.duojie.ai/v1` | API endpoint |
56
- | `DUOJIE_MODEL` | `gemini-2.0-flash-exp-image-generation` | Model to use |
57
-
58
- ## Tools
59
-
60
- ### `generate_image`
61
-
62
- Generate an image and save it to disk.
63
-
64
- **Required parameters:**
65
- - `prompt` (string) — Image description
66
- - `output_dir` (string) — Output directory (absolute path)
67
- - `output_filename` (string) — Output filename (no extension needed)
68
-
69
- **Optional parameters:**
70
- - `reference_images` (string[]) — Local image paths for reference. Images are auto-compressed to max 1024px long-side before sending to API.
71
- - `size` ("512px" | "1K" | "2K" | "4K") — Output image size. Default: `1K`
72
- - `aspect_ratio` (`"1:1"` | `"3:2"` | `"2:3"`) — Templated aspect ratio. Default: `"1:1"` for GPT image models
73
- - `format` (string) — Output format. Default: `"png"`
74
- - `model` (string) — Model override
75
- - `overwrite` (boolean) — Overwrite existing file. Default: `false` (auto-increments filename)
76
- - `negative_prompt` (string) — What to avoid in the image
77
- - `async_mode` (boolean) — When `true`, returns `task_id` immediately and generates in background. **Recommended when using `reference_images`** to avoid MCP client timeout. Default: `false`
78
-
79
- **Response (sync mode):**
80
- ```json
81
- { "success": true, "saved_path": "/absolute/path/to/image.png", "model": "gemini-..." }
82
- ```
83
-
84
- **Response (async mode):**
85
- ```json
86
- { "task_id": "uuid", "status": "pending" }
87
- ```
88
-
89
- ### `check_image_task`
90
-
91
- Check the status of an async image generation task.
92
-
93
- **Required parameters:**
94
- - `task_id` (string) — Task ID returned by `generate_image` with `async_mode: true`
95
-
96
- **Polling workflow:**
97
- 1. Call `generate_image` with `async_mode: true` → get `task_id`
98
- 2. Call `check_image_task` with `task_id` every few seconds
99
- 3. When `status` is `"done"`, retrieve `saved_path` from the response
100
- 4. If `status` is `"failed"`, check `error` and `message` fields
101
-
102
- **Response:**
103
- ```json
104
- { "task_id": "uuid", "status": "done", "saved_path": "/path/to/image.png", "model": "gemini-..." }
105
- ```
106
-
107
- ## Usage Examples
108
-
109
- ### Simple text-to-image (sync)
110
-
111
- ```
112
- generate_image(
113
- prompt="A sunset over the ocean, photorealistic",
114
- output_dir="/tmp/images",
115
- output_filename="sunset"
1
+ # mcp-duojie-image
2
+
3
+ MCP server for image generation via duojie.games GPT image API.
4
+
5
+ ## Features
6
+
7
+ - Text-to-image generation with `gpt-image-2`
8
+ - Reference image editing via local image paths
9
+ - Async submission with task status lookup
10
+ - File saving with overwrite/conflict handling
11
+ - Verified size templates for `1K`, `2K`, and `4K`
12
+
13
+ ## Configuration
14
+
15
+ Required:
16
+
17
+ | Variable | Default | Description |
18
+ |---|---|---|
19
+ | `DUOJIE_API_KEY` | required | duojie.games API key |
20
+ | `DUOJIE_BASE_URL` | `https://api.duojie.games` | API base URL |
21
+ | `DUOJIE_IMAGE_MODEL` | `gpt-image-2` | Optional; only `gpt-image-2` is supported |
22
+
23
+ ## Tool: `generate_image`
24
+
25
+ Generates an image asynchronously and saves it to disk.
26
+
27
+ Required parameters:
28
+
29
+ | Parameter | Type | Description |
30
+ |---|---|---|
31
+ | `prompt` | string | Image prompt |
32
+ | `output_dir` | string | Absolute output directory |
33
+ | `output_filename` | string | Filename without extension |
34
+ | `model` | `gpt-image-2` | Only supported model |
35
+
36
+ Optional parameters:
37
+
38
+ | Parameter | Type | Default | Description |
39
+ |---|---|---:|---|
40
+ | `size` | `1K` \| `2K` \| `4K` | `1K` | Output size tier |
41
+ | `aspect_ratio` | `1:1` \| `3:2` \| `2:3` \| `16:9` \| `9:16` | `1:1` | Verified ratio template |
42
+ | `reference_images` | string[] | - | Local reference image paths |
43
+ | `format` | string | `png` | Output format |
44
+ | `overwrite` | boolean | `false` | Overwrite existing file |
45
+ | `negative_prompt` | string | - | Content to avoid |
46
+ | `silent` | boolean | `false` | Disable desktop notification |
47
+
48
+ Response:
49
+
50
+ ```json
51
+ {
52
+ "success": true,
53
+ "status": "submitted",
54
+ "task_id": "uuid",
55
+ "message": "已提交异步生成;如需诊断失败原因,可调用 check_image_task"
56
+ }
57
+ ```
58
+
59
+ ## Tool: `check_image_task`
60
+
61
+ Checks async task status and failure details.
62
+
63
+ | Parameter | Type | Description |
64
+ |---|---|---|
65
+ | `task_id` | string | Task id returned by `generate_image` |
66
+
67
+ Done response:
68
+
69
+ ```json
70
+ {
71
+ "success": true,
72
+ "task_id": "uuid",
73
+ "status": "done",
74
+ "saved_path": "D:/images/output.png",
75
+ "model": "gpt-image-2"
76
+ }
77
+ ```
78
+
79
+ Failed response:
80
+
81
+ ```json
82
+ {
83
+ "success": false,
84
+ "task_id": "uuid",
85
+ "status": "failed",
86
+ "error": "E_PROVIDER_BAD_REQUEST",
87
+ "message": "Upstream error details"
88
+ }
89
+ ```
90
+
91
+ ## Verified GPT size templates
92
+
93
+ These templates were tested directly against `/v1/images/generations`; all returned the requested dimensions.
94
+
95
+ | `size` | `1:1` | `3:2` | `2:3` | `16:9` | `9:16` |
96
+ |---|---:|---:|---:|---:|---:|
97
+ | `1K` | `1024x1024` | `1536x1024` | `1024x1536` | `1792x1024` | `1024x1792` |
98
+ | `2K` | `2048x2048` | `2304x1536` | `1536x2304` | `2560x1440` | `1440x2560` |
99
+ | `4K` | `2880x2880` | `3456x2304` | `2304x3456` | `3840x2160` | `2160x3840` |
100
+
101
+ `512px` is intentionally unsupported because direct API testing returned `Invalid size ... below the current minimum pixel budget`.
102
+
103
+ ## Example
104
+
105
+ ```text
106
+ generate_image(
107
+ prompt="A futuristic city at sunset",
108
+ model="gpt-image-2",
109
+ size="2K",
110
+ aspect_ratio="16:9",
111
+ output_dir="D:/ai-images",
112
+ output_filename="city_sunset"
116
113
  )
117
114
  ```
118
115
 
119
- ### GPT image aspect ratio templates
120
-
121
- `gpt-image-2` does not accept free-form aspect ratios through MCP. Use one of these templates; the server converts it to a supported `size` value before calling the image API. For GPT image models, the `size` preset is ignored and the template controls the upstream size.
122
-
123
- | `aspect_ratio` | `size` sent upstream |
124
- |----------------|----------------------|
125
- | `1:1` | `1024x1024` |
126
- | `3:2` | `1536x1024` |
127
- | `2:3` | `1024x1536` |
128
-
129
- ### With reference image (use async_mode to avoid timeout)
130
-
131
- ```
132
- generate_image(
133
- prompt="Same style as reference, but with mountains instead",
134
- reference_images=["/path/to/reference.png"],
135
- output_dir="/tmp/images",
136
- output_filename="mountain_scene",
137
- async_mode=true
138
- )
139
- → { "task_id": "abc-123", "status": "pending" }
140
-
141
- check_image_task(task_id="abc-123")
142
- → { "status": "running" } # poll again
143
-
144
- check_image_task(task_id="abc-123")
145
- → { "status": "done", "saved_path": "/tmp/images/mountain_scene.png" }
146
- ```
147
-
148
- ## Development
149
-
150
- ```bash
151
- # Install dependencies
152
- npm install
153
-
154
- # Build
155
- npm run build
156
-
157
- # Run tests
158
- npm test
159
-
160
- # Type check / lint
161
- npm run lint
162
- ```
163
-
164
- ## Architecture
165
-
166
- ```
167
- src/
168
- index.ts — MCP server, tool registration (generate_image, check_image_task)
169
- provider.ts — HTTP client for image generation API
170
- resizer.ts — Reference image compression (jimp, max 1024px long-side)
171
- task-queue.ts — In-memory async task state machine (pending→running→done/failed, 30min TTL)
172
- config.ts — Environment variable loading
173
- saver.ts — File saving with conflict handling
174
- errors.ts — Error codes (E_CONFIG_MISSING_KEY, E_PROVIDER_AUTH, etc.)
175
- ```
176
-
177
- ## License
178
-
179
- MIT
116
+ ```text
117
+ check_image_task(task_id="uuid")
118
+ ```
package/dist/config.js CHANGED
@@ -9,10 +9,14 @@ export function loadConfig(overrides) {
9
9
  if (!apiKey) {
10
10
  throw new AppError("E_CONFIG_MISSING_KEY", "DUOJIE_API_KEY is not set");
11
11
  }
12
+ const model = overrides?.model ?? process.env.DUOJIE_IMAGE_MODEL ?? "gpt-image-2";
13
+ if (model !== "gpt-image-2") {
14
+ throw new AppError("E_INPUT_INVALID", "Only gpt-image-2 is supported");
15
+ }
12
16
  return {
13
17
  apiKey,
14
18
  baseUrl: overrides?.baseUrl ?? process.env.DUOJIE_BASE_URL ?? "https://api.duojie.games",
15
- model: overrides?.model ?? process.env.DUOJIE_IMAGE_MODEL ?? "gemini-3.1-flash-image",
19
+ model,
16
20
  timeoutMs: 300_000,
17
21
  };
18
22
  }
package/dist/index.js CHANGED
@@ -4,9 +4,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { createRequire } from "node:module";
6
6
  import { loadConfig } from "./config.js";
7
- import { ASPECT_RATIOS, generateImage } from "./provider.js";
7
+ import { ASPECT_RATIOS, IMAGE_SIZES, generateImage } from "./provider.js";
8
8
  import { saveImage } from "./saver.js";
9
9
  import { AppError } from "./errors.js";
10
+ import { createTask, getTask, updateTask } from "./task-queue.js";
10
11
  const require = createRequire(import.meta.url);
11
12
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
13
  const notifier = require("node-notifier");
@@ -18,6 +19,14 @@ function notify(title, message) {
18
19
  // 系统通知失败时静默忽略
19
20
  }
20
21
  }
22
+ function toolText(data) {
23
+ return {
24
+ content: [{
25
+ type: "text",
26
+ text: JSON.stringify(data),
27
+ }],
28
+ };
29
+ }
21
30
  const server = new McpServer({
22
31
  name: "duojie-image",
23
32
  version: "0.1.0",
@@ -29,10 +38,10 @@ server.registerTool("generate_image", {
29
38
  output_dir: z.string().describe("Absolute output dir"),
30
39
  output_filename: z.string().describe("Filename without ext"),
31
40
  reference_images: z.array(z.string()).optional().describe("Reference image paths"),
32
- size: z.enum(["512px", "1K", "2K", "4K"]).optional().describe("Default 1K"),
33
- aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("Aspect ratio template: 1:1, 3:2, or 2:3"),
41
+ size: z.enum(IMAGE_SIZES).optional().describe("Default 1K. Supported: 1K, 2K, 4K"),
42
+ aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("Supported: 1:1, 3:2, 2:3, 16:9, 9:16"),
34
43
  format: z.string().optional().describe("Default png"),
35
- model: z.enum(["gemini-3.1-flash-image", "gpt-image-2"]).describe("Required; ask once per conversation if unknown"),
44
+ model: z.enum(["gpt-image-2"]).describe("Required; only gpt-image-2 is supported"),
36
45
  overwrite: z.boolean().optional().describe("Default false"),
37
46
  negative_prompt: z.string().optional().describe("Avoid"),
38
47
  silent: z.boolean().optional().describe("No notification"),
@@ -47,9 +56,11 @@ server.registerTool("generate_image", {
47
56
  return { content: [{ type: "text", text: JSON.stringify({ success: false, error: code, message: String(err instanceof Error ? err.message : err) }) }], isError: true };
48
57
  }
49
58
  const silent = args.silent ?? false;
59
+ const task = createTask();
50
60
  // Start background generation (don't await)
51
61
  (async () => {
52
62
  try {
63
+ updateTask(task.id, { status: "running", startedAt: Date.now() });
53
64
  const imageBuffer = await generateImage(config, {
54
65
  prompt: args.prompt,
55
66
  referenceImages: args.reference_images,
@@ -64,9 +75,15 @@ server.registerTool("generate_image", {
64
75
  format: args.format ?? "png",
65
76
  overwrite: args.overwrite ?? false,
66
77
  });
78
+ updateTask(task.id, {
79
+ status: "done",
80
+ completedAt: Date.now(),
81
+ savedPath,
82
+ model: args.model,
83
+ });
67
84
  await server.sendLoggingMessage({
68
85
  level: "info",
69
- data: `图片生成完成:${savedPath}`,
86
+ data: `图片生成完成 [${task.id}]:${savedPath}`,
70
87
  });
71
88
  if (!silent)
72
89
  notify("图片生成完成", savedPath);
@@ -74,9 +91,15 @@ server.registerTool("generate_image", {
74
91
  catch (err) {
75
92
  const code = err instanceof AppError ? err.code : "E_INTERNAL";
76
93
  const msg = err instanceof Error ? err.message : String(err);
94
+ updateTask(task.id, {
95
+ status: "failed",
96
+ completedAt: Date.now(),
97
+ errorCode: code,
98
+ error: msg,
99
+ });
77
100
  await server.sendLoggingMessage({
78
101
  level: "error",
79
- data: `图片生成失败 [${code}]: ${msg}`,
102
+ data: `图片生成失败 [${task.id}] [${code}]: ${msg}`,
80
103
  }).catch(() => { });
81
104
  if (!silent) {
82
105
  if (code === "E_PROVIDER_TIMEOUT") {
@@ -94,10 +117,43 @@ server.registerTool("generate_image", {
94
117
  text: JSON.stringify({
95
118
  success: true,
96
119
  status: "submitted",
97
- message: `已提交异步生成:${args.output_filename};无需等待或检查结果`,
120
+ task_id: task.id,
121
+ message: `已提交异步生成:${args.output_filename};如需诊断失败原因,可调用 check_image_task`,
98
122
  }),
99
123
  }],
100
124
  };
101
125
  });
126
+ server.registerTool("check_image_task", {
127
+ description: "Check an async image generation task status and failure details.",
128
+ inputSchema: z.object({
129
+ task_id: z.string().min(1).describe("Task id returned by generate_image"),
130
+ }),
131
+ }, async (args) => {
132
+ const task = getTask(args.task_id);
133
+ if (!task) {
134
+ return {
135
+ ...toolText({
136
+ success: false,
137
+ error: "E_TASK_NOT_FOUND",
138
+ message: `Task ${args.task_id} not found. It may have expired or never existed.`,
139
+ }),
140
+ isError: true,
141
+ };
142
+ }
143
+ const response = {
144
+ success: task.status !== "failed",
145
+ task_id: task.id,
146
+ status: task.status,
147
+ };
148
+ if (task.status === "done") {
149
+ response.saved_path = task.savedPath;
150
+ response.model = task.model;
151
+ }
152
+ if (task.status === "failed") {
153
+ response.error = task.errorCode;
154
+ response.message = task.error;
155
+ }
156
+ return toolText(response);
157
+ });
102
158
  const transport = new StdioServerTransport();
103
159
  await server.connect(transport);
package/dist/provider.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AppError } from "./errors.js";
2
2
  import { resizeIfNeeded } from "./resizer.js";
3
- export const ASPECT_RATIOS = ["1:1", "3:2", "2:3"];
3
+ export const ASPECT_RATIOS = ["1:1", "3:2", "2:3", "16:9", "9:16"];
4
+ export const IMAGE_SIZES = ["1K", "2K", "4K"];
4
5
  async function fetchWithRetry(url, init, timeoutMs, retries = 0) {
5
6
  let lastErr;
6
7
  for (let attempt = 0; attempt <= retries; attempt++) {
@@ -25,36 +26,34 @@ async function fetchWithRetry(url, init, timeoutMs, retries = 0) {
25
26
  }
26
27
  throw new AppError("E_PROVIDER_BAD_REQUEST", `Network error: ${lastErr}`);
27
28
  }
28
- async function buildParts(opts) {
29
- const { prompt, referenceImages, negativePrompt } = opts;
30
- const fullPrompt = negativePrompt ? `${prompt}\n\nAvoid: ${negativePrompt}` : prompt;
31
- const parts = [];
32
- if (referenceImages && referenceImages.length > 0) {
33
- const imageParts = await Promise.all(referenceImages.map(async (imgPath) => {
34
- const data = await resizeIfNeeded(imgPath);
35
- return { inlineData: { mimeType: "image/png", data: data.toString("base64") } };
36
- }));
37
- parts.push(...imageParts);
38
- }
39
- parts.push({ text: fullPrompt });
40
- return parts;
41
- }
42
- function mapSize(size) {
43
- if (size === "512px")
44
- return "512";
45
- return size;
46
- }
47
- function isOpenAIImageModel(model) {
48
- return model === "gpt-image-2";
49
- }
50
- function mapOpenAISize(aspectRatio) {
51
- const ratio = aspectRatio ?? "1:1";
52
- const sizes = {
29
+ const GPT_IMAGE_MODEL = "gpt-image-2";
30
+ const SIZE_TEMPLATES = {
31
+ "1K": {
53
32
  "1:1": "1024x1024",
54
33
  "3:2": "1536x1024",
55
34
  "2:3": "1024x1536",
56
- };
57
- return sizes[ratio];
35
+ "16:9": "1792x1024",
36
+ "9:16": "1024x1792",
37
+ },
38
+ "2K": {
39
+ "1:1": "2048x2048",
40
+ "3:2": "2304x1536",
41
+ "2:3": "1536x2304",
42
+ "16:9": "2560x1440",
43
+ "9:16": "1440x2560",
44
+ },
45
+ "4K": {
46
+ "1:1": "2880x2880",
47
+ "3:2": "3456x2304",
48
+ "2:3": "2304x3456",
49
+ "16:9": "3840x2160",
50
+ "9:16": "2160x3840",
51
+ },
52
+ };
53
+ function mapOpenAISize(size, aspectRatio) {
54
+ const imageSize = size ?? "1K";
55
+ const ratio = aspectRatio ?? "1:1";
56
+ return SIZE_TEMPLATES[imageSize][ratio];
58
57
  }
59
58
  async function imageFromOpenAIResponse(res) {
60
59
  const data = (await res.json());
@@ -81,45 +80,10 @@ async function handleProviderError(res) {
81
80
  const text = await res.text().catch(() => "");
82
81
  throw new AppError("E_PROVIDER_BAD_REQUEST", `Upstream error ${res.status}: ${text.slice(0, 200)}`);
83
82
  }
84
- async function generateGeminiImage(config, opts) {
85
- const parts = await buildParts(opts);
86
- const model = opts.model ?? config.model;
87
- const imageConfig = {
88
- imageSize: mapSize(opts.size ?? "1K"),
89
- };
90
- if (opts.aspectRatio) {
91
- imageConfig.aspectRatio = opts.aspectRatio;
92
- }
93
- const body = JSON.stringify({
94
- contents: [{ role: "user", parts }],
95
- generationConfig: {
96
- responseModalities: ["IMAGE"],
97
- imageConfig,
98
- },
99
- });
100
- const url = `${config.baseUrl}/v1beta/models/${model}:generateContent`;
101
- const res = await fetchWithRetry(url, {
102
- method: "POST",
103
- headers: {
104
- "x-goog-api-key": config.apiKey,
105
- "Content-Type": "application/json",
106
- },
107
- body,
108
- }, config.timeoutMs);
109
- if (!res.ok) {
110
- await handleProviderError(res);
111
- }
112
- const data = (await res.json());
113
- const imagePart = data.candidates?.[0]?.content?.parts?.find((p) => p.inlineData);
114
- if (!imagePart?.inlineData?.data) {
115
- throw new AppError("E_PROVIDER_BAD_REQUEST", "No image in response");
116
- }
117
- return Buffer.from(imagePart.inlineData.data, "base64");
118
- }
119
83
  async function generateOpenAIImage(config, opts) {
120
84
  const model = opts.model ?? config.model;
121
85
  const prompt = opts.negativePrompt ? `${opts.prompt}\n\nAvoid: ${opts.negativePrompt}` : opts.prompt;
122
- const size = mapOpenAISize(opts.aspectRatio);
86
+ const size = mapOpenAISize(opts.size, opts.aspectRatio);
123
87
  const hasReferenceImages = Boolean(opts.referenceImages?.length);
124
88
  const url = `${config.baseUrl}${hasReferenceImages ? "/v1/images/edits" : "/v1/images/generations"}`;
125
89
  let body;
@@ -131,6 +95,9 @@ async function generateOpenAIImage(config, opts) {
131
95
  form.set("model", model);
132
96
  form.set("prompt", prompt);
133
97
  form.set("size", size);
98
+ if (opts.aspectRatio) {
99
+ form.set("aspect_ratio", opts.aspectRatio);
100
+ }
134
101
  for (const [index, imgPath] of opts.referenceImages.entries()) {
135
102
  const data = await resizeIfNeeded(imgPath);
136
103
  const bytes = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
@@ -140,7 +107,12 @@ async function generateOpenAIImage(config, opts) {
140
107
  }
141
108
  else {
142
109
  headers = { ...headers, "Content-Type": "application/json" };
143
- body = JSON.stringify({ model, prompt, size });
110
+ body = JSON.stringify({
111
+ model,
112
+ prompt,
113
+ size,
114
+ ...(opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {}),
115
+ });
144
116
  }
145
117
  const res = await fetchWithRetry(url, {
146
118
  method: "POST",
@@ -154,8 +126,8 @@ async function generateOpenAIImage(config, opts) {
154
126
  }
155
127
  export async function generateImage(config, opts) {
156
128
  const model = opts.model ?? config.model;
157
- if (isOpenAIImageModel(model)) {
158
- return generateOpenAIImage(config, opts);
129
+ if (model !== GPT_IMAGE_MODEL) {
130
+ throw new AppError("E_PROVIDER_BAD_REQUEST", `Unsupported model: ${model}`);
159
131
  }
160
- return generateGeminiImage(config, opts);
132
+ return generateOpenAIImage(config, opts);
161
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-duojie-image",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "MCP server for image generation via duojie.games",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,8 @@
18
18
  "keywords": [
19
19
  "mcp",
20
20
  "image-generation",
21
- "gemini",
21
+ "gpt",
22
+ "openai",
22
23
  "ai"
23
24
  ],
24
25
  "author": "",