mcp-duojie-image 0.15.2 → 0.16.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
@@ -48,10 +48,9 @@ Optional parameters:
48
48
  | `aspect_ratio` | preset or custom `W:H` | `1:1` | Presets `1:1` \| `3:2` \| `2:3` \| `16:9` \| `9:16`, or any custom ratio such as `9:21`; custom ratios get neutral canvas-ratio prompt guidance (no art-direction wording) and an exact local crop |
49
49
  | `reference_images` | string[] | - | Local references used to create a new image |
50
50
  | `input_images` | object[] | - | Structured local image inputs with `path`, optional `role`, and optional `note` |
51
- | `use_case` | string | - | Prompt taxonomy slug, for example `product-mockup` or `background-extraction` |
52
51
  | `asset_type` | string | - | Where the asset will be used |
53
52
  | `constraints` | string | - | Must-keep or must-avoid constraints |
54
- | `format` | `png` \| `jpg` \| `jpeg` | `png` | Output format; content is encoded to match the extension |
53
+ | `format` | `png` \| `jpg` \| `jpeg` \| `webp` | `png` | Output format; content is encoded to match the extension |
55
54
  | `overwrite` | boolean | `false` | Overwrite existing file |
56
55
  | `negative_prompt` | string | - | Content to avoid |
57
56
  | `reference_max_side` | number | `512` / `256` | Default 512 for one generation reference and 256 for multiple |
@@ -131,10 +130,16 @@ Done response:
131
130
  "task_id": "uuid",
132
131
  "status": "done",
133
132
  "saved_path": "D:/images/output.png",
134
- "effective_prompt": "Primary request: ..."
133
+ "model": "gpt-image-2.5",
134
+ "aspect_ratio": "9:21",
135
+ "saved_dimensions": "864x2016"
135
136
  }
136
137
  ```
137
138
 
139
+ Polling responses carry only the status fields above plus
140
+ `poll_required`/`retry_after_ms`; process metadata such as `effective_prompt`
141
+ and `prompt_stats` is returned once, on the submission response.
142
+
138
143
  Failed response:
139
144
 
140
145
  ```json
@@ -264,7 +269,6 @@ edges (icons clean; extreme fur edges may show slight color fringe — add
264
269
  ```text
265
270
  generate_image(
266
271
  prompt="A futuristic city at sunset",
267
- use_case="stylized-concept",
268
272
  asset_type="website hero",
269
273
  constraints="no text, no watermark",
270
274
  size="2K",
package/dist/index.js CHANGED
@@ -4,18 +4,18 @@ 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, CUSTOM_ASPECT_RATIO_PATTERN, DEFAULT_GENERATION_REFERENCE_MAX_SIDE, DEFAULT_MULTI_REFERENCE_MAX_SIDE, IMAGE_SIZES, editImage, generateImage, mapOpenAISize, } from "./provider.js";
7
+ import { CUSTOM_ASPECT_RATIO_PATTERN, DEFAULT_GENERATION_REFERENCE_MAX_SIDE, DEFAULT_MULTI_REFERENCE_MAX_SIDE, IMAGE_SIZES, editImage, generateImage, mapOpenAISize, } from "./provider.js";
8
8
  import { saveImage } from "./saver.js";
9
9
  import { AppError } from "./errors.js";
10
10
  import { DEFAULT_IMAGE_MODEL, IMAGE_MODEL_IDS, MODEL_GUIDANCE } from "./models.js";
11
11
  import { createTask, getTask, updateTask, waitForAllTasks } from "./task-queue.js";
12
12
  import { setServerVersion, taskStatusResponse, taskSubmissionResponse, waitAllTasksResponse, } from "./task-response.js";
13
- import { PROMPT_USE_CASES, REFERENCE_IMAGE_ROLES, preparePrompt, } from "./prompt.js";
13
+ import { REFERENCE_IMAGE_ROLES, preparePrompt, } from "./prompt.js";
14
14
  import { OUTPUT_FORMATS, fitImageToSize, readDimensions, } from "./image-processing.js";
15
15
  const require = createRequire(import.meta.url);
16
16
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
17
  const notifier = require("node-notifier");
18
- export const SERVER_VERSION = "0.15.1";
18
+ export const SERVER_VERSION = "0.16.0";
19
19
  function notify(title, message) {
20
20
  try {
21
21
  notifier.notify({ title, message, sound: false });
@@ -59,7 +59,6 @@ async function submitImageTask(args, operation) {
59
59
  prompt: args.prompt,
60
60
  editTarget: args.edit_target,
61
61
  negativePrompt: args.negative_prompt,
62
- useCase: args.use_case,
63
62
  assetType: args.asset_type,
64
63
  constraints: args.constraints,
65
64
  inputImages: args.input_images,
@@ -151,56 +150,52 @@ async function submitImageTask(args, operation) {
151
150
  return toolText(taskSubmissionResponse(getTask(task.id) ?? task, args.output_filename));
152
151
  }
153
152
  const structuredReferences = z.array(z.object({
154
- path: z.string().min(1).describe("Local reference image path"),
153
+ path: z.string().min(1).describe("Image path"),
155
154
  role: z.enum(REFERENCE_IMAGE_ROLES).optional().describe("How the reference guides the result"),
156
- note: z.string().optional().describe("Short role-specific instruction"),
157
- })).optional().describe("Structured references; do not combine with reference_images");
155
+ note: z.string().optional().describe("Role-specific instruction"),
156
+ })).optional().describe("Structured references; don't combine with reference_images");
158
157
  const commonFields = {
159
- prompt: z.string().min(1).describe("What the image should show; be specific about subject, style, and composition"),
158
+ prompt: z.string().min(1).describe("What to render: subject, style, composition"),
160
159
  output_dir: z.string().describe("Absolute output directory"),
161
160
  output_filename: z.string().describe("Filename without extension"),
162
- use_case: z.enum(PROMPT_USE_CASES).optional().describe("Optional request category, e.g. product-mockup or ui-mockup; shapes prompt formatting"),
163
161
  asset_type: z.string().optional().describe("Where the image will be used"),
164
- constraints: z.string().optional().describe("Must-keep or must-avoid constraints"),
165
- size: z.enum(IMAGE_SIZES).optional().describe("Pixel tier. Default 1K"),
162
+ constraints: z.string().optional().describe("Must-keep or must-avoid rules"),
163
+ size: z.enum(IMAGE_SIZES).optional().describe("Default 1K"),
166
164
  model: z.enum(IMAGE_MODEL_IDS).optional().describe(MODEL_GUIDANCE),
167
- aspect_ratio: z.union([
168
- z.enum(ASPECT_RATIOS),
169
- z.string().regex(new RegExp(CUSTOM_ASPECT_RATIO_PATTERN)),
170
- ]).optional()
171
- .describe("Preset 1:1 | 3:2 | 2:3 | 16:9 | 9:16, or any custom W:H such as 9:21. Default 1:1; the saved file always matches the requested ratio exactly"),
165
+ aspect_ratio: z.string().regex(new RegExp(CUSTOM_ASPECT_RATIO_PATTERN), "aspect_ratio must be W:H with 1-4 digit positive parts, e.g. 1:1, 16:9, 9:21").optional()
166
+ .describe("W:H, e.g. 1:1, 16:9, 9:21. Default 1:1; output file matches exactly"),
172
167
  format: z.enum(OUTPUT_FORMATS).optional().describe("Default png"),
173
- overwrite: z.boolean().optional().describe("Default false; an existing file with the same name fails the task"),
168
+ overwrite: z.boolean().optional().describe("Default false; same-name file fails"),
174
169
  negative_prompt: z.string().optional().describe("Content to avoid"),
175
170
  reference_max_side: z.number().int().min(256).max(3840).optional()
176
- .describe(`Longest side for uploaded inputs. generate defaults: ${DEFAULT_GENERATION_REFERENCE_MAX_SIDE} for one reference, ${DEFAULT_MULTI_REFERENCE_MAX_SIDE} for multiple; edit default 1024`),
177
- preserve_reference_detail: z.boolean().optional().describe("Upload inputs without resizing; less stable"),
178
- silent: z.boolean().optional().describe("Disable the desktop notification"),
171
+ .describe(`Resize cap for reference uploads; default ${DEFAULT_GENERATION_REFERENCE_MAX_SIDE} (single) / ${DEFAULT_MULTI_REFERENCE_MAX_SIDE} (multi), edit 1024`),
172
+ preserve_reference_detail: z.boolean().optional().describe("Upload references unresized (less stable)"),
173
+ silent: z.boolean().optional().describe("Skip desktop notification"),
179
174
  };
180
175
  const referenceFields = {
181
176
  reference_images: z.array(z.string()).optional()
182
- .describe("Optional local reference image paths used as guidance for a new image; never edited in place"),
177
+ .describe("Local image paths guiding the result; never edited in place"),
183
178
  input_images: structuredReferences,
184
179
  };
185
180
  server.registerTool("generate_image", {
186
- description: "Create a new image from a text prompt, optionally guided by reference images for style, identity, composition, or material. Use this by default for new visuals; for a small change to an existing image, use edit_image instead; if intent is unclear, ask the user first. Usage: returns a task_id immediately — poll check_image_task until status is done (saved_path holds the output file) or failed; after submitting several tasks, call wait_all_tasks once to wait for all of them instead of polling one by one.",
181
+ description: "Create a new image from a text prompt, optionally guided by reference images (style, identity, composition, material). To modify an existing image use edit_image. Async: returns task_id — poll with check_image_task / wait_all_tasks.",
187
182
  inputSchema: z.object({
188
183
  ...commonFields,
189
184
  ...referenceFields,
190
185
  }),
191
186
  }, async (args) => submitImageTask(args, "generate"));
192
187
  server.registerTool("edit_image", {
193
- description: "Apply a small, explicit change to one existing image (e.g. remove an object, change a color) while preserving everything else. Requires exactly one edit_target. For new images or restyled variants, use generate_image instead. Usage: returns a task_id immediately — poll check_image_task until status is done (saved_path holds the output file) or failed; after submitting several tasks, call wait_all_tasks once to wait for all of them instead of polling one by one.",
188
+ description: "Apply a small explicit change (e.g. remove an object, recolor) to one existing image, preserving everything else; requires exactly one edit_target. For new images or restyled variants use generate_image. Async: returns task_id — poll with check_image_task / wait_all_tasks.",
194
189
  inputSchema: z.object({
195
190
  ...commonFields,
196
- edit_target: z.string().min(1).describe("The single existing image to modify"),
191
+ edit_target: z.string().min(1).describe("Existing image to modify"),
197
192
  ...referenceFields,
198
193
  }),
199
194
  }, async (args) => submitImageTask(args, "edit"));
200
195
  server.registerTool("check_image_task", {
201
- description: "Poll an async generate_image or edit_image task. Keep polling while poll_required=true; status done returns saved_path, status failed returns error details.",
196
+ description: "Poll an image task until terminal: done → saved_path; failed → error.",
202
197
  inputSchema: z.object({
203
- task_id: z.string().min(1).describe("Task id returned by generate_image or edit_image"),
198
+ task_id: z.string().min(1).describe("Task id from generate_image/edit_image"),
204
199
  }),
205
200
  }, async (args) => {
206
201
  const task = getTask(args.task_id);
@@ -213,10 +208,10 @@ server.registerTool("check_image_task", {
213
208
  const DEFAULT_WAIT_TIMEOUT_SECONDS = 300;
214
209
  const MAX_WAIT_TIMEOUT_SECONDS = 600;
215
210
  server.registerTool("wait_all_tasks", {
216
- description: "Wait for every in-flight generate_image/edit_image task to finish, then return all results in one response — each done task carries saved_path, each failed task its error. Use this after submitting a batch instead of polling check_image_task per task. Returns immediately when nothing is running; on timeout returns completed results plus still-pending task ids — just call again to keep waiting.",
211
+ description: "Wait for all in-flight image tasks and return all results at once. Immediate when none are running; on timeout returns partial results — call again.",
217
212
  inputSchema: z.object({
218
213
  timeout_seconds: z.number().int().min(1).max(MAX_WAIT_TIMEOUT_SECONDS).optional()
219
- .describe(`Max seconds to block, default ${DEFAULT_WAIT_TIMEOUT_SECONDS}, upper bound ${MAX_WAIT_TIMEOUT_SECONDS}. On timeout partial results are returned; call again to continue waiting`),
214
+ .describe(`Block at most this long, default ${DEFAULT_WAIT_TIMEOUT_SECONDS}, max ${MAX_WAIT_TIMEOUT_SECONDS}`),
220
215
  }),
221
216
  },
222
217
  // 长阻塞期间客户端取消 (extra.signal) 时以当前快照返回, 后台任务不受影响
package/dist/models.js CHANGED
@@ -23,5 +23,5 @@ export const DEFAULT_IMAGE_MODEL = "gpt-image-2.5";
23
23
  export function isImageModelId(value) {
24
24
  return value in IMAGE_MODELS;
25
25
  }
26
- /** 供 model 参数 describe 使用的单句版模型选择说明 */
27
- export const MODEL_GUIDANCE = `Default ${DEFAULT_IMAGE_MODEL} (balanced). Alternatives: gpt-image-2.5-flare (fastest, for drafts and previews), gpt-image-2.5-sunburst (best quality, slowest). If the user has not specified a model, ask which one to use before calling.`;
26
+ /** 供 model 参数 describe 使用的精简版模型选择说明;"未指定时先询问"的规则由 AGENTS.md 承担 */
27
+ export const MODEL_GUIDANCE = `Default gpt-image-2.5 (balanced); flare = fastest, drafts; sunburst = best quality, slowest`;
package/dist/prompt.js CHANGED
@@ -6,27 +6,6 @@ export const REFERENCE_IMAGE_ROLES = [
6
6
  "composition_reference",
7
7
  "insert_source",
8
8
  ];
9
- export const PROMPT_USE_CASES = [
10
- "photorealistic-natural",
11
- "product-mockup",
12
- "ui-mockup",
13
- "infographic-diagram",
14
- "scientific-educational",
15
- "ads-marketing",
16
- "productivity-visual",
17
- "logo-brand",
18
- "illustration-story",
19
- "stylized-concept",
20
- "historical-scene",
21
- "text-localization",
22
- "identity-preserve",
23
- "precise-object-edit",
24
- "lighting-weather",
25
- "background-extraction",
26
- "style-transfer",
27
- "compositing",
28
- "sketch-to-render",
29
- ];
30
9
  function compactLines(lines) {
31
10
  return lines.filter((line) => Boolean(line?.trim())).join("\n");
32
11
  }
@@ -112,7 +91,6 @@ export function preparePrompt(opts) {
112
91
  ? `Avoid: ${opts.negativePrompt.trim()}`
113
92
  : undefined;
114
93
  const prompt = compactLines([
115
- opts.useCase ? `Use case: ${opts.useCase}` : undefined,
116
94
  opts.assetType ? `Asset type: ${opts.assetType}` : undefined,
117
95
  operationPrompt(opts.operation, Boolean(references?.length)),
118
96
  imageLines(imageInputs),
@@ -23,6 +23,8 @@ export function taskSubmissionResponse(task, outputFilename) {
23
23
  ...(serverVersion ? { version: serverVersion } : {}),
24
24
  };
25
25
  }
26
+ // 提交响应带全量元数据 (一次性); 轮询是高频路径, taskStatusResponse 只保留
27
+ // 终态高信号字段, effective_prompt/prompt_stats 等过程元数据不在轮询中重复注入。
26
28
  export function taskStatusResponse(task) {
27
29
  const response = {
28
30
  task_id: task.id,
@@ -32,32 +34,14 @@ export function taskStatusResponse(task) {
32
34
  if (serverVersion) {
33
35
  response.version = serverVersion;
34
36
  }
35
- if (task.effectivePrompt) {
36
- response.effective_prompt = task.effectivePrompt;
37
- }
38
- if (task.promptStats) {
39
- response.prompt_stats = task.promptStats;
40
- }
41
- if (task.resolvedIntent) {
42
- response.resolved_intent = task.resolvedIntent;
43
- }
44
- if (task.model) {
45
- response.model = task.model;
46
- }
47
- if (task.requestedSize) {
48
- response.size = task.requestedSize;
49
- }
50
- if (task.aspectRatio) {
51
- response.aspect_ratio = task.aspectRatio;
52
- }
53
- if (task.targetDimensions) {
54
- response.target_dimensions = task.targetDimensions;
55
- }
56
37
  if (task.status === "done") {
57
38
  response.success = true;
58
39
  response.saved_path = task.savedPath;
59
- if (task.upstreamDimensions) {
60
- response.upstream_dimensions = task.upstreamDimensions;
40
+ if (task.model) {
41
+ response.model = task.model;
42
+ }
43
+ if (task.aspectRatio) {
44
+ response.aspect_ratio = task.aspectRatio;
61
45
  }
62
46
  if (task.savedDimensions) {
63
47
  response.saved_dimensions = task.savedDimensions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-duojie-image",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "MCP server for image generation via duojie.games",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",