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 +8 -4
- package/dist/index.js +23 -28
- package/dist/models.js +2 -2
- package/dist/prompt.js +0 -22
- package/dist/task-response.js +7 -23
- package/package.json +1 -1
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
|
-
"
|
|
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 {
|
|
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 {
|
|
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.
|
|
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("
|
|
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("
|
|
157
|
-
})).optional().describe("Structured references;
|
|
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
|
|
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
|
|
165
|
-
size: z.enum(IMAGE_SIZES).optional().describe("
|
|
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.
|
|
168
|
-
|
|
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;
|
|
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(`
|
|
177
|
-
preserve_reference_detail: z.boolean().optional().describe("Upload
|
|
178
|
-
silent: z.boolean().optional().describe("
|
|
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("
|
|
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
|
|
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
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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(`
|
|
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
|
|
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),
|
package/dist/task-response.js
CHANGED
|
@@ -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.
|
|
60
|
-
response.
|
|
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;
|