mcp-duojie-image 0.5.4 → 0.5.6

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
@@ -9,7 +9,7 @@ MCP server for image generation via duojie.games API (Gemini Flash Image).
9
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
10
  - **Sync mode** — for simple text-to-image generation without reference images, the default sync mode returns the result directly
11
11
  - **File saving** — automatic output directory creation, conflict handling (`overwrite` flag, auto-incrementing filename suffix)
12
- - **Configurable output** — size (512px/1K/2K/4K), aspect ratio, format
12
+ - **Configurable output** — size (512px/1K/2K/4K), templated aspect ratio, format
13
13
 
14
14
  ## Installation
15
15
 
@@ -69,7 +69,7 @@ Generate an image and save it to disk.
69
69
  **Optional parameters:**
70
70
  - `reference_images` (string[]) — Local image paths for reference. Images are auto-compressed to max 1024px long-side before sending to API.
71
71
  - `size` ("512px" | "1K" | "2K" | "4K") — Output image size. Default: `1K`
72
- - `aspect_ratio` (string) — e.g., `"16:9"`, `"1:1"`
72
+ - `aspect_ratio` (`"1:1"` | `"3:2"` | `"2:3"`) — Templated aspect ratio. Default: `"1:1"` for GPT image models
73
73
  - `format` (string) — Output format. Default: `"png"`
74
74
  - `model` (string) — Model override
75
75
  - `overwrite` (boolean) — Overwrite existing file. Default: `false` (auto-increments filename)
@@ -106,17 +106,27 @@ Check the status of an async image generation task.
106
106
 
107
107
  ## Usage Examples
108
108
 
109
- ### Simple text-to-image (sync)
109
+ ### Simple text-to-image (sync)
110
110
 
111
111
  ```
112
112
  generate_image(
113
113
  prompt="A sunset over the ocean, photorealistic",
114
114
  output_dir="/tmp/images",
115
115
  output_filename="sunset"
116
- )
117
- ```
118
-
119
- ### With reference image (use async_mode to avoid timeout)
116
+ )
117
+ ```
118
+
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)
120
130
 
121
131
  ```
122
132
  generate_image(
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ 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 { generateImage } from "./provider.js";
7
+ import { ASPECT_RATIOS, generateImage } from "./provider.js";
8
8
  import { saveImage } from "./saver.js";
9
9
  import { AppError } from "./errors.js";
10
10
  const require = createRequire(import.meta.url);
@@ -30,7 +30,7 @@ server.registerTool("generate_image", {
30
30
  output_filename: z.string().describe("Filename without ext"),
31
31
  reference_images: z.array(z.string()).optional().describe("Reference image paths"),
32
32
  size: z.enum(["512px", "1K", "2K", "4K"]).optional().describe("Default 1K"),
33
- aspect_ratio: z.string().optional().describe("e.g. 16:9"),
33
+ aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("Aspect ratio template: 1:1, 3:2, or 2:3"),
34
34
  format: z.string().optional().describe("Default png"),
35
35
  model: z.enum(["gemini-3.1-flash-image", "gpt-image-2"]).describe("Required; ask once per conversation if unknown"),
36
36
  overwrite: z.boolean().optional().describe("Default false"),
package/dist/provider.js CHANGED
@@ -1,5 +1,6 @@
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
4
  async function fetchWithRetry(url, init, timeoutMs, retries = 0) {
4
5
  let lastErr;
5
6
  for (let attempt = 0; attempt <= retries; attempt++) {
@@ -41,50 +42,19 @@ async function buildParts(opts) {
41
42
  function mapSize(size) {
42
43
  if (size === "512px")
43
44
  return "512";
44
- return size; // "1K", "2K", "4K" are used as-is
45
+ return size;
45
46
  }
46
47
  function isOpenAIImageModel(model) {
47
48
  return model === "gpt-image-2";
48
49
  }
49
- function parseAspectRatio(aspectRatio) {
50
- const match = aspectRatio?.match(/^(\d+):(\d+)$/);
51
- if (!match)
52
- return [1, 1];
53
- return [Number(match[1]), Number(match[2])];
54
- }
55
- function mapOpenAI4KSize(aspectRatio) {
56
- const [ratioWidth, ratioHeight] = parseAspectRatio(aspectRatio);
57
- const maxPixels = 3840 * 2160;
58
- const maxEdge = 3840;
59
- let width = Math.floor(Math.sqrt((maxPixels * ratioWidth) / ratioHeight));
60
- let height = Math.floor((width * ratioHeight) / ratioWidth);
61
- if (width > maxEdge) {
62
- width = maxEdge;
63
- height = Math.floor((width * ratioHeight) / ratioWidth);
64
- }
65
- if (height > maxEdge) {
66
- height = maxEdge;
67
- width = Math.floor((height * ratioWidth) / ratioHeight);
68
- }
69
- width = Math.floor(width / 8) * 8;
70
- height = Math.floor(height / 8) * 8;
71
- return `${width}x${height}`;
72
- }
73
- function mapOpenAISize(size, aspectRatio) {
74
- const squareBySize = {
75
- "512px": "512x512",
76
- "1K": "1024x1024",
77
- "2K": "2048x2048",
50
+ function mapOpenAISize(aspectRatio) {
51
+ const ratio = aspectRatio ?? "1:1";
52
+ const sizes = {
53
+ "1:1": "1024x1024",
54
+ "3:2": "1536x1024",
55
+ "2:3": "1024x1536",
78
56
  };
79
- if (size === "4K")
80
- return mapOpenAI4KSize(aspectRatio);
81
- if (aspectRatio === "16:9" || aspectRatio === "3:2" || aspectRatio === "4:3") {
82
- return size === "2K" ? "2048x1536" : "1536x1024";
83
- }
84
- if (aspectRatio === "9:16" || aspectRatio === "2:3" || aspectRatio === "3:4") {
85
- return size === "2K" ? "1536x2048" : "1024x1536";
86
- }
87
- return squareBySize[size] ?? "1024x1024";
57
+ return sizes[ratio];
88
58
  }
89
59
  async function imageFromOpenAIResponse(res) {
90
60
  const data = (await res.json());
@@ -149,7 +119,7 @@ async function generateGeminiImage(config, opts) {
149
119
  async function generateOpenAIImage(config, opts) {
150
120
  const model = opts.model ?? config.model;
151
121
  const prompt = opts.negativePrompt ? `${opts.prompt}\n\nAvoid: ${opts.negativePrompt}` : opts.prompt;
152
- const size = mapOpenAISize(opts.size ?? "1K", opts.aspectRatio);
122
+ const size = mapOpenAISize(opts.aspectRatio);
153
123
  const hasReferenceImages = Boolean(opts.referenceImages?.length);
154
124
  const url = `${config.baseUrl}${hasReferenceImages ? "/v1/images/edits" : "/v1/images/generations"}`;
155
125
  let body;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-duojie-image",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "MCP server for image generation via duojie.games",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",