mcp-duojie-image 0.5.5 → 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 +118 -169
- package/dist/config.js +5 -1
- package/dist/index.js +63 -7
- package/dist/provider.js +41 -117
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,169 +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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
### With reference image (use async_mode to avoid timeout)
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
generate_image(
|
|
123
|
-
prompt="Same style as reference, but with mountains instead",
|
|
124
|
-
reference_images=["/path/to/reference.png"],
|
|
125
|
-
output_dir="/tmp/images",
|
|
126
|
-
output_filename="mountain_scene",
|
|
127
|
-
async_mode=true
|
|
128
|
-
)
|
|
129
|
-
→ { "task_id": "abc-123", "status": "pending" }
|
|
130
|
-
|
|
131
|
-
check_image_task(task_id="abc-123")
|
|
132
|
-
→ { "status": "running" } # poll again
|
|
133
|
-
|
|
134
|
-
check_image_task(task_id="abc-123")
|
|
135
|
-
→ { "status": "done", "saved_path": "/tmp/images/mountain_scene.png" }
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Development
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
# Install dependencies
|
|
142
|
-
npm install
|
|
143
|
-
|
|
144
|
-
# Build
|
|
145
|
-
npm run build
|
|
146
|
-
|
|
147
|
-
# Run tests
|
|
148
|
-
npm test
|
|
149
|
-
|
|
150
|
-
# Type check / lint
|
|
151
|
-
npm run lint
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Architecture
|
|
155
|
-
|
|
156
|
-
```
|
|
157
|
-
src/
|
|
158
|
-
index.ts — MCP server, tool registration (generate_image, check_image_task)
|
|
159
|
-
provider.ts — HTTP client for image generation API
|
|
160
|
-
resizer.ts — Reference image compression (jimp, max 1024px long-side)
|
|
161
|
-
task-queue.ts — In-memory async task state machine (pending→running→done/failed, 30min TTL)
|
|
162
|
-
config.ts — Environment variable loading
|
|
163
|
-
saver.ts — File saving with conflict handling
|
|
164
|
-
errors.ts — Error codes (E_CONFIG_MISSING_KEY, E_PROVIDER_AUTH, etc.)
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## License
|
|
168
|
-
|
|
169
|
-
MIT
|
|
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"
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
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 { 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.
|
|
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,5 +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", "16:9", "9:16"];
|
|
4
|
+
export const IMAGE_SIZES = ["1K", "2K", "4K"];
|
|
3
5
|
async function fetchWithRetry(url, init, timeoutMs, retries = 0) {
|
|
4
6
|
let lastErr;
|
|
5
7
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
@@ -24,81 +26,34 @@ async function fetchWithRetry(url, init, timeoutMs, retries = 0) {
|
|
|
24
26
|
}
|
|
25
27
|
throw new AppError("E_PROVIDER_BAD_REQUEST", `Network error: ${lastErr}`);
|
|
26
28
|
}
|
|
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
|
-
const match = normalized?.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
|
|
52
|
-
if (!match)
|
|
53
|
-
return undefined;
|
|
54
|
-
const width = Number(match[1]);
|
|
55
|
-
const height = Number(match[2]);
|
|
56
|
-
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
return `${match[1]}:${match[2]}`;
|
|
60
|
-
}
|
|
61
|
-
function parseAspectRatio(aspectRatio) {
|
|
62
|
-
const normalized = normalizeAspectRatio(aspectRatio);
|
|
63
|
-
const match = normalized?.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
|
|
64
|
-
if (!match)
|
|
65
|
-
return [1, 1];
|
|
66
|
-
return [Number(match[1]), Number(match[2])];
|
|
67
|
-
}
|
|
68
|
-
function mapOpenAI4KSize(aspectRatio) {
|
|
69
|
-
const [ratioWidth, ratioHeight] = parseAspectRatio(aspectRatio);
|
|
70
|
-
const maxPixels = 3840 * 2160;
|
|
71
|
-
const maxEdge = 3840;
|
|
72
|
-
let width = Math.floor(Math.sqrt((maxPixels * ratioWidth) / ratioHeight));
|
|
73
|
-
let height = Math.floor((width * ratioHeight) / ratioWidth);
|
|
74
|
-
if (width > maxEdge) {
|
|
75
|
-
width = maxEdge;
|
|
76
|
-
height = Math.floor((width * ratioHeight) / ratioWidth);
|
|
77
|
-
}
|
|
78
|
-
if (height > maxEdge) {
|
|
79
|
-
height = maxEdge;
|
|
80
|
-
width = Math.floor((height * ratioWidth) / ratioHeight);
|
|
81
|
-
}
|
|
82
|
-
width = Math.floor(width / 8) * 8;
|
|
83
|
-
height = Math.floor(height / 8) * 8;
|
|
84
|
-
return `${width}x${height}`;
|
|
85
|
-
}
|
|
29
|
+
const GPT_IMAGE_MODEL = "gpt-image-2";
|
|
30
|
+
const SIZE_TEMPLATES = {
|
|
31
|
+
"1K": {
|
|
32
|
+
"1:1": "1024x1024",
|
|
33
|
+
"3:2": "1536x1024",
|
|
34
|
+
"2:3": "1024x1536",
|
|
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
|
+
};
|
|
86
53
|
function mapOpenAISize(size, aspectRatio) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"2K": "2048x2048",
|
|
91
|
-
};
|
|
92
|
-
if (size === "4K")
|
|
93
|
-
return mapOpenAI4KSize(aspectRatio);
|
|
94
|
-
const [ratioWidth, ratioHeight] = parseAspectRatio(aspectRatio);
|
|
95
|
-
if (ratioWidth > ratioHeight) {
|
|
96
|
-
return size === "2K" ? "2048x1536" : "1536x1024";
|
|
97
|
-
}
|
|
98
|
-
if (ratioHeight > ratioWidth) {
|
|
99
|
-
return size === "2K" ? "1536x2048" : "1024x1536";
|
|
100
|
-
}
|
|
101
|
-
return squareBySize[size] ?? "1024x1024";
|
|
54
|
+
const imageSize = size ?? "1K";
|
|
55
|
+
const ratio = aspectRatio ?? "1:1";
|
|
56
|
+
return SIZE_TEMPLATES[imageSize][ratio];
|
|
102
57
|
}
|
|
103
58
|
async function imageFromOpenAIResponse(res) {
|
|
104
59
|
const data = (await res.json());
|
|
@@ -125,46 +80,10 @@ async function handleProviderError(res) {
|
|
|
125
80
|
const text = await res.text().catch(() => "");
|
|
126
81
|
throw new AppError("E_PROVIDER_BAD_REQUEST", `Upstream error ${res.status}: ${text.slice(0, 200)}`);
|
|
127
82
|
}
|
|
128
|
-
async function generateGeminiImage(config, opts) {
|
|
129
|
-
const parts = await buildParts(opts);
|
|
130
|
-
const model = opts.model ?? config.model;
|
|
131
|
-
const imageConfig = {
|
|
132
|
-
imageSize: mapSize(opts.size ?? "1K"),
|
|
133
|
-
};
|
|
134
|
-
if (opts.aspectRatio) {
|
|
135
|
-
imageConfig.aspectRatio = opts.aspectRatio;
|
|
136
|
-
}
|
|
137
|
-
const body = JSON.stringify({
|
|
138
|
-
contents: [{ role: "user", parts }],
|
|
139
|
-
generationConfig: {
|
|
140
|
-
responseModalities: ["IMAGE"],
|
|
141
|
-
imageConfig,
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
const url = `${config.baseUrl}/v1beta/models/${model}:generateContent`;
|
|
145
|
-
const res = await fetchWithRetry(url, {
|
|
146
|
-
method: "POST",
|
|
147
|
-
headers: {
|
|
148
|
-
"x-goog-api-key": config.apiKey,
|
|
149
|
-
"Content-Type": "application/json",
|
|
150
|
-
},
|
|
151
|
-
body,
|
|
152
|
-
}, config.timeoutMs);
|
|
153
|
-
if (!res.ok) {
|
|
154
|
-
await handleProviderError(res);
|
|
155
|
-
}
|
|
156
|
-
const data = (await res.json());
|
|
157
|
-
const imagePart = data.candidates?.[0]?.content?.parts?.find((p) => p.inlineData);
|
|
158
|
-
if (!imagePart?.inlineData?.data) {
|
|
159
|
-
throw new AppError("E_PROVIDER_BAD_REQUEST", "No image in response");
|
|
160
|
-
}
|
|
161
|
-
return Buffer.from(imagePart.inlineData.data, "base64");
|
|
162
|
-
}
|
|
163
83
|
async function generateOpenAIImage(config, opts) {
|
|
164
84
|
const model = opts.model ?? config.model;
|
|
165
85
|
const prompt = opts.negativePrompt ? `${opts.prompt}\n\nAvoid: ${opts.negativePrompt}` : opts.prompt;
|
|
166
|
-
const
|
|
167
|
-
const size = mapOpenAISize(opts.size ?? "1K", aspectRatio);
|
|
86
|
+
const size = mapOpenAISize(opts.size, opts.aspectRatio);
|
|
168
87
|
const hasReferenceImages = Boolean(opts.referenceImages?.length);
|
|
169
88
|
const url = `${config.baseUrl}${hasReferenceImages ? "/v1/images/edits" : "/v1/images/generations"}`;
|
|
170
89
|
let body;
|
|
@@ -176,8 +95,8 @@ async function generateOpenAIImage(config, opts) {
|
|
|
176
95
|
form.set("model", model);
|
|
177
96
|
form.set("prompt", prompt);
|
|
178
97
|
form.set("size", size);
|
|
179
|
-
if (aspectRatio) {
|
|
180
|
-
form.set("aspect_ratio", aspectRatio);
|
|
98
|
+
if (opts.aspectRatio) {
|
|
99
|
+
form.set("aspect_ratio", opts.aspectRatio);
|
|
181
100
|
}
|
|
182
101
|
for (const [index, imgPath] of opts.referenceImages.entries()) {
|
|
183
102
|
const data = await resizeIfNeeded(imgPath);
|
|
@@ -188,7 +107,12 @@ async function generateOpenAIImage(config, opts) {
|
|
|
188
107
|
}
|
|
189
108
|
else {
|
|
190
109
|
headers = { ...headers, "Content-Type": "application/json" };
|
|
191
|
-
body = JSON.stringify({
|
|
110
|
+
body = JSON.stringify({
|
|
111
|
+
model,
|
|
112
|
+
prompt,
|
|
113
|
+
size,
|
|
114
|
+
...(opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {}),
|
|
115
|
+
});
|
|
192
116
|
}
|
|
193
117
|
const res = await fetchWithRetry(url, {
|
|
194
118
|
method: "POST",
|
|
@@ -202,8 +126,8 @@ async function generateOpenAIImage(config, opts) {
|
|
|
202
126
|
}
|
|
203
127
|
export async function generateImage(config, opts) {
|
|
204
128
|
const model = opts.model ?? config.model;
|
|
205
|
-
if (
|
|
206
|
-
|
|
129
|
+
if (model !== GPT_IMAGE_MODEL) {
|
|
130
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", `Unsupported model: ${model}`);
|
|
207
131
|
}
|
|
208
|
-
return
|
|
132
|
+
return generateOpenAIImage(config, opts);
|
|
209
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": "",
|