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 +115 -176
- package/dist/config.js +5 -1
- package/dist/index.js +63 -7
- package/dist/provider.js +40 -68
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,179 +1,118 @@
|
|
|
1
|
-
# mcp-duojie-image
|
|
2
|
-
|
|
3
|
-
MCP server for image generation via duojie.games
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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(
|
|
33
|
-
aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("
|
|
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(["
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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({
|
|
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 (
|
|
158
|
-
|
|
129
|
+
if (model !== GPT_IMAGE_MODEL) {
|
|
130
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", `Unsupported model: ${model}`);
|
|
159
131
|
}
|
|
160
|
-
return
|
|
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.
|
|
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
|
-
"
|
|
21
|
+
"gpt",
|
|
22
|
+
"openai",
|
|
22
23
|
"ai"
|
|
23
24
|
],
|
|
24
25
|
"author": "",
|