mcp-duojie-image 0.5.0 → 0.5.2
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 +169 -169
- package/dist/index.js +12 -12
- package/dist/provider.js +88 -9
- package/package.json +45 -45
package/README.md
CHANGED
|
@@ -1,169 +1,169 @@
|
|
|
1
|
-
# mcp-duojie-image
|
|
2
|
-
|
|
3
|
-
MCP server for image generation via duojie.games API (Gemini Flash Image).
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Text-to-image generation** — generate images from prompts with configurable size and aspect ratio
|
|
8
|
-
- **Reference image support** — provide local image files as visual references (auto-compressed to 1024px long-side)
|
|
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
|
-
- **Sync mode** — for simple text-to-image generation without reference images, the default sync mode returns the result directly
|
|
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
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npx mcp-duojie-image
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Or with a specific version:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npx mcp-duojie-image@0.1.3
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Configuration
|
|
27
|
-
|
|
28
|
-
Set the `DUOJIE_API_KEY` environment variable:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
export DUOJIE_API_KEY=sk-your-key-here
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Or configure in your MCP client (e.g., Claude Desktop, Factory):
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
{
|
|
38
|
-
"mcpServers": {
|
|
39
|
-
"duojie-image": {
|
|
40
|
-
"command": "node",
|
|
41
|
-
"args": ["/path/to/mcp-duojie-image/dist/index.js"],
|
|
42
|
-
"env": {
|
|
43
|
-
"DUOJIE_API_KEY": "sk-your-key-here"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Optional environment variables:
|
|
51
|
-
|
|
52
|
-
| Variable | Default | Description |
|
|
53
|
-
|----------|---------|-------------|
|
|
54
|
-
| `DUOJIE_API_KEY` | (required) | API key |
|
|
55
|
-
| `DUOJIE_BASE_URL` | `https://api.duojie.ai/v1` | API endpoint |
|
|
56
|
-
| `DUOJIE_MODEL` | `gemini-2.0-flash-exp-image-generation` | Model to use |
|
|
57
|
-
|
|
58
|
-
## Tools
|
|
59
|
-
|
|
60
|
-
### `generate_image`
|
|
61
|
-
|
|
62
|
-
Generate an image and save it to disk.
|
|
63
|
-
|
|
64
|
-
**Required parameters:**
|
|
65
|
-
- `prompt` (string) — Image description
|
|
66
|
-
- `output_dir` (string) — Output directory (absolute path)
|
|
67
|
-
- `output_filename` (string) — Output filename (no extension needed)
|
|
68
|
-
|
|
69
|
-
**Optional parameters:**
|
|
70
|
-
- `reference_images` (string[]) — Local image paths for reference. Images are auto-compressed to max 1024px long-side before sending to API.
|
|
71
|
-
- `size` ("512px" | "1K" | "2K" | "4K") — Output image size. Default: `1K`
|
|
72
|
-
- `aspect_ratio` (string) — e.g., `"16:9"`, `"1:1"`
|
|
73
|
-
- `format` (string) — Output format. Default: `"png"`
|
|
74
|
-
- `model` (string) — Model override
|
|
75
|
-
- `overwrite` (boolean) — Overwrite existing file. Default: `false` (auto-increments filename)
|
|
76
|
-
- `negative_prompt` (string) — What to avoid in the image
|
|
77
|
-
- `async_mode` (boolean) — When `true`, returns `task_id` immediately and generates in background. **Recommended when using `reference_images`** to avoid MCP client timeout. Default: `false`
|
|
78
|
-
|
|
79
|
-
**Response (sync mode):**
|
|
80
|
-
```json
|
|
81
|
-
{ "success": true, "saved_path": "/absolute/path/to/image.png", "model": "gemini-..." }
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**Response (async mode):**
|
|
85
|
-
```json
|
|
86
|
-
{ "task_id": "uuid", "status": "pending" }
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### `check_image_task`
|
|
90
|
-
|
|
91
|
-
Check the status of an async image generation task.
|
|
92
|
-
|
|
93
|
-
**Required parameters:**
|
|
94
|
-
- `task_id` (string) — Task ID returned by `generate_image` with `async_mode: true`
|
|
95
|
-
|
|
96
|
-
**Polling workflow:**
|
|
97
|
-
1. Call `generate_image` with `async_mode: true` → get `task_id`
|
|
98
|
-
2. Call `check_image_task` with `task_id` every few seconds
|
|
99
|
-
3. When `status` is `"done"`, retrieve `saved_path` from the response
|
|
100
|
-
4. If `status` is `"failed"`, check `error` and `message` fields
|
|
101
|
-
|
|
102
|
-
**Response:**
|
|
103
|
-
```json
|
|
104
|
-
{ "task_id": "uuid", "status": "done", "saved_path": "/path/to/image.png", "model": "gemini-..." }
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## Usage Examples
|
|
108
|
-
|
|
109
|
-
### Simple text-to-image (sync)
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
generate_image(
|
|
113
|
-
prompt="A sunset over the ocean, photorealistic",
|
|
114
|
-
output_dir="/tmp/images",
|
|
115
|
-
output_filename="sunset"
|
|
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 API (Gemini Flash Image).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Text-to-image generation** — generate images from prompts with configurable size and aspect ratio
|
|
8
|
+
- **Reference image support** — provide local image files as visual references (auto-compressed to 1024px long-side)
|
|
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
|
+
- **Sync mode** — for simple text-to-image generation without reference images, the default sync mode returns the result directly
|
|
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
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx mcp-duojie-image
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or with a specific version:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx mcp-duojie-image@0.1.3
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
Set the `DUOJIE_API_KEY` environment variable:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export DUOJIE_API_KEY=sk-your-key-here
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or configure in your MCP client (e.g., Claude Desktop, Factory):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"duojie-image": {
|
|
40
|
+
"command": "node",
|
|
41
|
+
"args": ["/path/to/mcp-duojie-image/dist/index.js"],
|
|
42
|
+
"env": {
|
|
43
|
+
"DUOJIE_API_KEY": "sk-your-key-here"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Optional environment variables:
|
|
51
|
+
|
|
52
|
+
| Variable | Default | Description |
|
|
53
|
+
|----------|---------|-------------|
|
|
54
|
+
| `DUOJIE_API_KEY` | (required) | API key |
|
|
55
|
+
| `DUOJIE_BASE_URL` | `https://api.duojie.ai/v1` | API endpoint |
|
|
56
|
+
| `DUOJIE_MODEL` | `gemini-2.0-flash-exp-image-generation` | Model to use |
|
|
57
|
+
|
|
58
|
+
## Tools
|
|
59
|
+
|
|
60
|
+
### `generate_image`
|
|
61
|
+
|
|
62
|
+
Generate an image and save it to disk.
|
|
63
|
+
|
|
64
|
+
**Required parameters:**
|
|
65
|
+
- `prompt` (string) — Image description
|
|
66
|
+
- `output_dir` (string) — Output directory (absolute path)
|
|
67
|
+
- `output_filename` (string) — Output filename (no extension needed)
|
|
68
|
+
|
|
69
|
+
**Optional parameters:**
|
|
70
|
+
- `reference_images` (string[]) — Local image paths for reference. Images are auto-compressed to max 1024px long-side before sending to API.
|
|
71
|
+
- `size` ("512px" | "1K" | "2K" | "4K") — Output image size. Default: `1K`
|
|
72
|
+
- `aspect_ratio` (string) — e.g., `"16:9"`, `"1:1"`
|
|
73
|
+
- `format` (string) — Output format. Default: `"png"`
|
|
74
|
+
- `model` (string) — Model override
|
|
75
|
+
- `overwrite` (boolean) — Overwrite existing file. Default: `false` (auto-increments filename)
|
|
76
|
+
- `negative_prompt` (string) — What to avoid in the image
|
|
77
|
+
- `async_mode` (boolean) — When `true`, returns `task_id` immediately and generates in background. **Recommended when using `reference_images`** to avoid MCP client timeout. Default: `false`
|
|
78
|
+
|
|
79
|
+
**Response (sync mode):**
|
|
80
|
+
```json
|
|
81
|
+
{ "success": true, "saved_path": "/absolute/path/to/image.png", "model": "gemini-..." }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Response (async mode):**
|
|
85
|
+
```json
|
|
86
|
+
{ "task_id": "uuid", "status": "pending" }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `check_image_task`
|
|
90
|
+
|
|
91
|
+
Check the status of an async image generation task.
|
|
92
|
+
|
|
93
|
+
**Required parameters:**
|
|
94
|
+
- `task_id` (string) — Task ID returned by `generate_image` with `async_mode: true`
|
|
95
|
+
|
|
96
|
+
**Polling workflow:**
|
|
97
|
+
1. Call `generate_image` with `async_mode: true` → get `task_id`
|
|
98
|
+
2. Call `check_image_task` with `task_id` every few seconds
|
|
99
|
+
3. When `status` is `"done"`, retrieve `saved_path` from the response
|
|
100
|
+
4. If `status` is `"failed"`, check `error` and `message` fields
|
|
101
|
+
|
|
102
|
+
**Response:**
|
|
103
|
+
```json
|
|
104
|
+
{ "task_id": "uuid", "status": "done", "saved_path": "/path/to/image.png", "model": "gemini-..." }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Usage Examples
|
|
108
|
+
|
|
109
|
+
### Simple text-to-image (sync)
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
generate_image(
|
|
113
|
+
prompt="A sunset over the ocean, photorealistic",
|
|
114
|
+
output_dir="/tmp/images",
|
|
115
|
+
output_filename="sunset"
|
|
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
|
package/dist/index.js
CHANGED
|
@@ -23,19 +23,19 @@ const server = new McpServer({
|
|
|
23
23
|
version: "0.1.0",
|
|
24
24
|
});
|
|
25
25
|
server.registerTool("generate_image", {
|
|
26
|
-
description: "Generate
|
|
26
|
+
description: "Generate image async. Ask user to choose model before calling.",
|
|
27
27
|
inputSchema: z.object({
|
|
28
|
-
prompt: z.string().min(1).describe("
|
|
29
|
-
output_dir: z.string().describe("
|
|
30
|
-
output_filename: z.string().describe("
|
|
31
|
-
reference_images: z.array(z.string()).optional().describe("
|
|
32
|
-
size: z.enum(["512px", "1K", "2K", "4K"]).optional().describe("
|
|
28
|
+
prompt: z.string().min(1).describe("Prompt"),
|
|
29
|
+
output_dir: z.string().describe("Absolute output dir"),
|
|
30
|
+
output_filename: z.string().describe("Filename without ext"),
|
|
31
|
+
reference_images: z.array(z.string()).optional().describe("Reference image paths"),
|
|
32
|
+
size: z.enum(["512px", "1K", "2K", "4K"]).optional().describe("Default 1K"),
|
|
33
33
|
aspect_ratio: z.string().optional().describe("e.g. 16:9"),
|
|
34
|
-
format: z.string().optional().describe("
|
|
35
|
-
model: z.
|
|
36
|
-
overwrite: z.boolean().optional().describe("
|
|
37
|
-
negative_prompt: z.string().optional().describe("
|
|
38
|
-
silent: z.boolean().optional().describe("
|
|
34
|
+
format: z.string().optional().describe("Default png"),
|
|
35
|
+
model: z.enum(["gemini-3.1-flash-image", "gpt-image-2"]).describe("Required"),
|
|
36
|
+
overwrite: z.boolean().optional().describe("Default false"),
|
|
37
|
+
negative_prompt: z.string().optional().describe("Avoid"),
|
|
38
|
+
silent: z.boolean().optional().describe("No notification"),
|
|
39
39
|
}),
|
|
40
40
|
}, async (args) => {
|
|
41
41
|
let config;
|
|
@@ -93,7 +93,7 @@ server.registerTool("generate_image", {
|
|
|
93
93
|
type: "text",
|
|
94
94
|
text: JSON.stringify({
|
|
95
95
|
status: "pending",
|
|
96
|
-
message:
|
|
96
|
+
message: `已开始:${args.output_filename}`,
|
|
97
97
|
}),
|
|
98
98
|
}],
|
|
99
99
|
};
|
package/dist/provider.js
CHANGED
|
@@ -43,7 +43,50 @@ function mapSize(size) {
|
|
|
43
43
|
return "512";
|
|
44
44
|
return size; // "1K", "2K", "4K" are used as-is
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
function isOpenAIImageModel(model) {
|
|
47
|
+
return model === "gpt-image-2";
|
|
48
|
+
}
|
|
49
|
+
function mapOpenAISize(size, aspectRatio) {
|
|
50
|
+
const squareBySize = {
|
|
51
|
+
"512px": "512x512",
|
|
52
|
+
"1K": "1024x1024",
|
|
53
|
+
"2K": "2048x2048",
|
|
54
|
+
"4K": "4096x4096",
|
|
55
|
+
};
|
|
56
|
+
if (aspectRatio === "16:9" || aspectRatio === "3:2" || aspectRatio === "4:3") {
|
|
57
|
+
return size === "2K" ? "2048x1536" : size === "4K" ? "4096x3072" : "1536x1024";
|
|
58
|
+
}
|
|
59
|
+
if (aspectRatio === "9:16" || aspectRatio === "2:3" || aspectRatio === "3:4") {
|
|
60
|
+
return size === "2K" ? "1536x2048" : size === "4K" ? "3072x4096" : "1024x1536";
|
|
61
|
+
}
|
|
62
|
+
return squareBySize[size] ?? "1024x1024";
|
|
63
|
+
}
|
|
64
|
+
async function imageFromOpenAIResponse(res) {
|
|
65
|
+
const data = (await res.json());
|
|
66
|
+
const image = data.data?.[0];
|
|
67
|
+
if (image?.b64_json) {
|
|
68
|
+
return Buffer.from(image.b64_json, "base64");
|
|
69
|
+
}
|
|
70
|
+
if (image?.url) {
|
|
71
|
+
const imgRes = await fetch(image.url);
|
|
72
|
+
if (!imgRes.ok) {
|
|
73
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", `Image URL fetch failed: ${imgRes.status}`);
|
|
74
|
+
}
|
|
75
|
+
return Buffer.from(await imgRes.arrayBuffer());
|
|
76
|
+
}
|
|
77
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", "No image in response");
|
|
78
|
+
}
|
|
79
|
+
async function handleProviderError(res) {
|
|
80
|
+
if (res.status === 401 || res.status === 403) {
|
|
81
|
+
throw new AppError("E_PROVIDER_AUTH", `Auth failed (HTTP ${res.status})`);
|
|
82
|
+
}
|
|
83
|
+
if (res.status === 429) {
|
|
84
|
+
throw new AppError("E_PROVIDER_RATE_LIMIT", "Rate limit exceeded");
|
|
85
|
+
}
|
|
86
|
+
const text = await res.text().catch(() => "");
|
|
87
|
+
throw new AppError("E_PROVIDER_BAD_REQUEST", `Upstream error ${res.status}: ${text.slice(0, 200)}`);
|
|
88
|
+
}
|
|
89
|
+
async function generateGeminiImage(config, opts) {
|
|
47
90
|
const parts = await buildParts(opts);
|
|
48
91
|
const model = opts.model ?? config.model;
|
|
49
92
|
const imageConfig = {
|
|
@@ -68,15 +111,8 @@ export async function generateImage(config, opts) {
|
|
|
68
111
|
},
|
|
69
112
|
body,
|
|
70
113
|
}, config.timeoutMs);
|
|
71
|
-
if (res.status === 401 || res.status === 403) {
|
|
72
|
-
throw new AppError("E_PROVIDER_AUTH", `Auth failed (HTTP ${res.status})`);
|
|
73
|
-
}
|
|
74
|
-
if (res.status === 429) {
|
|
75
|
-
throw new AppError("E_PROVIDER_RATE_LIMIT", "Rate limit exceeded");
|
|
76
|
-
}
|
|
77
114
|
if (!res.ok) {
|
|
78
|
-
|
|
79
|
-
throw new AppError("E_PROVIDER_BAD_REQUEST", `Upstream error ${res.status}: ${text.slice(0, 200)}`);
|
|
115
|
+
await handleProviderError(res);
|
|
80
116
|
}
|
|
81
117
|
const data = (await res.json());
|
|
82
118
|
const imagePart = data.candidates?.[0]?.content?.parts?.find((p) => p.inlineData);
|
|
@@ -85,3 +121,46 @@ export async function generateImage(config, opts) {
|
|
|
85
121
|
}
|
|
86
122
|
return Buffer.from(imagePart.inlineData.data, "base64");
|
|
87
123
|
}
|
|
124
|
+
async function generateOpenAIImage(config, opts) {
|
|
125
|
+
const model = opts.model ?? config.model;
|
|
126
|
+
const prompt = opts.negativePrompt ? `${opts.prompt}\n\nAvoid: ${opts.negativePrompt}` : opts.prompt;
|
|
127
|
+
const size = mapOpenAISize(opts.size ?? "1K", opts.aspectRatio);
|
|
128
|
+
const hasReferenceImages = Boolean(opts.referenceImages?.length);
|
|
129
|
+
const url = `${config.baseUrl}${hasReferenceImages ? "/v1/images/edits" : "/v1/images/generations"}`;
|
|
130
|
+
let body;
|
|
131
|
+
let headers = {
|
|
132
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
133
|
+
};
|
|
134
|
+
if (hasReferenceImages) {
|
|
135
|
+
const form = new FormData();
|
|
136
|
+
form.set("model", model);
|
|
137
|
+
form.set("prompt", prompt);
|
|
138
|
+
form.set("size", size);
|
|
139
|
+
for (const [index, imgPath] of opts.referenceImages.entries()) {
|
|
140
|
+
const data = await resizeIfNeeded(imgPath);
|
|
141
|
+
const bytes = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
142
|
+
form.append("image", new Blob([bytes], { type: "image/png" }), `reference-${index}.png`);
|
|
143
|
+
}
|
|
144
|
+
body = form;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
headers = { ...headers, "Content-Type": "application/json" };
|
|
148
|
+
body = JSON.stringify({ model, prompt, size });
|
|
149
|
+
}
|
|
150
|
+
const res = await fetchWithRetry(url, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers,
|
|
153
|
+
body,
|
|
154
|
+
}, config.timeoutMs);
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
await handleProviderError(res);
|
|
157
|
+
}
|
|
158
|
+
return imageFromOpenAIResponse(res);
|
|
159
|
+
}
|
|
160
|
+
export async function generateImage(config, opts) {
|
|
161
|
+
const model = opts.model ?? config.model;
|
|
162
|
+
if (isOpenAIImageModel(model)) {
|
|
163
|
+
return generateOpenAIImage(config, opts);
|
|
164
|
+
}
|
|
165
|
+
return generateGeminiImage(config, opts);
|
|
166
|
+
}
|
package/package.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mcp-duojie-image",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "MCP server for image generation via duojie.games",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"mcp-duojie-image": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"dev": "tsx src/index.ts",
|
|
12
|
-
"build": "tsc",
|
|
13
|
-
"start": "node dist/index.js",
|
|
14
|
-
"test": "vitest run",
|
|
15
|
-
"lint": "tsc --noEmit",
|
|
16
|
-
"prepublishOnly": "npm run build"
|
|
17
|
-
},
|
|
18
|
-
"keywords": [
|
|
19
|
-
"mcp",
|
|
20
|
-
"image-generation",
|
|
21
|
-
"gemini",
|
|
22
|
-
"ai"
|
|
23
|
-
],
|
|
24
|
-
"author": "",
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
28
|
-
"jimp": "^1.6.0",
|
|
29
|
-
"node-notifier": "^10.0.1",
|
|
30
|
-
"zod": "^3.23.8"
|
|
31
|
-
},
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"@types/node": "^22.0.0",
|
|
34
|
-
"@types/node-notifier": "^8.0.5",
|
|
35
|
-
"tsx": "^4.19.0",
|
|
36
|
-
"typescript": "^5.5.0",
|
|
37
|
-
"vitest": "^2.0.0"
|
|
38
|
-
},
|
|
39
|
-
"engines": {
|
|
40
|
-
"node": ">=18.0.0"
|
|
41
|
-
},
|
|
42
|
-
"files": [
|
|
43
|
-
"dist"
|
|
44
|
-
]
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-duojie-image",
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "MCP server for image generation via duojie.games",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-duojie-image": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"lint": "tsc --noEmit",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"image-generation",
|
|
21
|
+
"gemini",
|
|
22
|
+
"ai"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
28
|
+
"jimp": "^1.6.0",
|
|
29
|
+
"node-notifier": "^10.0.1",
|
|
30
|
+
"zod": "^3.23.8"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
|
+
"@types/node-notifier": "^8.0.5",
|
|
35
|
+
"tsx": "^4.19.0",
|
|
36
|
+
"typescript": "^5.5.0",
|
|
37
|
+
"vitest": "^2.0.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
]
|
|
45
|
+
}
|