mcp-duojie-image 0.1.2 → 0.3.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 +128 -21
- package/dist/config.js +1 -1
- package/dist/index.js +66 -36
- package/dist/provider.js +15 -18
- package/dist/resizer.js +17 -0
- package/dist/task-queue.js +37 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# mcp-duojie-image
|
|
2
2
|
|
|
3
|
-
MCP server for image generation via duojie.games API (Gemini
|
|
3
|
+
MCP server for image generation via duojie.games API (Gemini Flash Image).
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- Text-to-image generation
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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
|
|
11
13
|
|
|
12
14
|
## Installation
|
|
13
15
|
|
|
@@ -15,22 +17,28 @@ MCP server for image generation via duojie.games API (Gemini 3.1 Flash Image).
|
|
|
15
17
|
npx mcp-duojie-image
|
|
16
18
|
```
|
|
17
19
|
|
|
20
|
+
Or with a specific version:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx mcp-duojie-image@0.1.3
|
|
24
|
+
```
|
|
25
|
+
|
|
18
26
|
## Configuration
|
|
19
27
|
|
|
20
|
-
Set environment variable:
|
|
28
|
+
Set the `DUOJIE_API_KEY` environment variable:
|
|
21
29
|
|
|
22
30
|
```bash
|
|
23
31
|
export DUOJIE_API_KEY=sk-your-key-here
|
|
24
32
|
```
|
|
25
33
|
|
|
26
|
-
Or configure in MCP client (e.g., Claude Desktop):
|
|
34
|
+
Or configure in your MCP client (e.g., Claude Desktop, Factory):
|
|
27
35
|
|
|
28
36
|
```json
|
|
29
37
|
{
|
|
30
38
|
"mcpServers": {
|
|
31
39
|
"duojie-image": {
|
|
32
|
-
"command": "
|
|
33
|
-
"args": ["
|
|
40
|
+
"command": "node",
|
|
41
|
+
"args": ["/path/to/mcp-duojie-image/dist/index.js"],
|
|
34
42
|
"env": {
|
|
35
43
|
"DUOJIE_API_KEY": "sk-your-key-here"
|
|
36
44
|
}
|
|
@@ -39,23 +47,122 @@ Or configure in MCP client (e.g., Claude Desktop):
|
|
|
39
47
|
}
|
|
40
48
|
```
|
|
41
49
|
|
|
42
|
-
|
|
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 |
|
|
43
57
|
|
|
44
|
-
|
|
58
|
+
## Tools
|
|
59
|
+
|
|
60
|
+
### `generate_image`
|
|
61
|
+
|
|
62
|
+
Generate an image and save it to disk.
|
|
45
63
|
|
|
46
64
|
**Required parameters:**
|
|
47
|
-
- `prompt
|
|
48
|
-
- `output_dir
|
|
49
|
-
- `output_filename
|
|
65
|
+
- `prompt` (string) — Image description
|
|
66
|
+
- `output_dir` (string) — Output directory (absolute path)
|
|
67
|
+
- `output_filename` (string) — Output filename (no extension needed)
|
|
50
68
|
|
|
51
69
|
**Optional parameters:**
|
|
52
|
-
- `reference_images
|
|
53
|
-
- `size
|
|
54
|
-
- `aspect_ratio
|
|
55
|
-
- `format
|
|
56
|
-
- `model
|
|
57
|
-
- `overwrite
|
|
58
|
-
- `negative_prompt
|
|
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
|
+
```
|
|
59
166
|
|
|
60
167
|
## License
|
|
61
168
|
|
package/dist/config.js
CHANGED
|
@@ -13,6 +13,6 @@ export function loadConfig(overrides) {
|
|
|
13
13
|
apiKey,
|
|
14
14
|
baseUrl: overrides?.baseUrl ?? process.env.DUOJIE_BASE_URL ?? "https://api.duojie.games",
|
|
15
15
|
model: overrides?.model ?? process.env.DUOJIE_IMAGE_MODEL ?? "gemini-3.1-flash-image",
|
|
16
|
-
timeoutMs:
|
|
16
|
+
timeoutMs: 300_000,
|
|
17
17
|
};
|
|
18
18
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,27 +2,40 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { loadConfig } from "./config.js";
|
|
6
7
|
import { generateImage } from "./provider.js";
|
|
7
8
|
import { saveImage } from "./saver.js";
|
|
8
9
|
import { AppError } from "./errors.js";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
const notifier = require("node-notifier");
|
|
13
|
+
function notify(title, message) {
|
|
14
|
+
try {
|
|
15
|
+
notifier.notify({ title, message, sound: false });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// 系统通知失败时静默忽略
|
|
19
|
+
}
|
|
20
|
+
}
|
|
9
21
|
const server = new McpServer({
|
|
10
22
|
name: "duojie-image",
|
|
11
23
|
version: "0.1.0",
|
|
12
24
|
});
|
|
13
25
|
server.registerTool("generate_image", {
|
|
14
|
-
description: "Generate an image and save to disk.",
|
|
26
|
+
description: "Generate an image and save to disk. Returns immediately; generation runs in background (60~120s) with system notification on completion.",
|
|
15
27
|
inputSchema: z.object({
|
|
16
28
|
prompt: z.string().min(1).describe("Image prompt"),
|
|
17
29
|
output_dir: z.string().describe("Output directory (absolute path)"),
|
|
18
30
|
output_filename: z.string().describe("Output filename (no extension needed)"),
|
|
19
31
|
reference_images: z.array(z.string()).optional().describe("Local image paths for reference"),
|
|
20
|
-
size: z.enum(["512px", "1K", "2K", "4K"]).optional().describe("Image size"),
|
|
32
|
+
size: z.enum(["512px", "1K", "2K", "4K"]).optional().describe("Image size, default 1K. Use 512px for thumbnails, 2K/4K only if explicitly requested."),
|
|
21
33
|
aspect_ratio: z.string().optional().describe("e.g. 16:9"),
|
|
22
34
|
format: z.string().optional().describe("Output format, default png"),
|
|
23
35
|
model: z.string().optional().describe("Model override"),
|
|
24
36
|
overwrite: z.boolean().optional().describe("Overwrite existing file, default false"),
|
|
25
37
|
negative_prompt: z.string().optional().describe("What to avoid"),
|
|
38
|
+
silent: z.boolean().optional().describe("Suppress system notification on completion. Default false."),
|
|
26
39
|
}),
|
|
27
40
|
}, async (args) => {
|
|
28
41
|
let config;
|
|
@@ -33,40 +46,57 @@ server.registerTool("generate_image", {
|
|
|
33
46
|
const code = err instanceof AppError ? err.code : "E_CONFIG_MISSING_KEY";
|
|
34
47
|
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: code, message: String(err instanceof Error ? err.message : err) }) }], isError: true };
|
|
35
48
|
}
|
|
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
|
-
|
|
49
|
+
const silent = args.silent ?? false;
|
|
50
|
+
// Start background generation (don't await)
|
|
51
|
+
(async () => {
|
|
52
|
+
try {
|
|
53
|
+
const imageBuffer = await generateImage(config, {
|
|
54
|
+
prompt: args.prompt,
|
|
55
|
+
referenceImages: args.reference_images,
|
|
56
|
+
model: args.model,
|
|
57
|
+
negativePrompt: args.negative_prompt,
|
|
58
|
+
size: args.size ?? "1K",
|
|
59
|
+
aspectRatio: args.aspect_ratio,
|
|
60
|
+
});
|
|
61
|
+
const savedPath = saveImage(imageBuffer, {
|
|
62
|
+
outputDir: args.output_dir,
|
|
63
|
+
outputFilename: args.output_filename,
|
|
64
|
+
format: args.format ?? "png",
|
|
65
|
+
overwrite: args.overwrite ?? false,
|
|
66
|
+
});
|
|
67
|
+
await server.sendLoggingMessage({
|
|
68
|
+
level: "info",
|
|
69
|
+
data: `图片生成完成:${savedPath}`,
|
|
70
|
+
});
|
|
71
|
+
if (!silent)
|
|
72
|
+
notify("图片生成完成", savedPath);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const code = err instanceof AppError ? err.code : "E_INTERNAL";
|
|
76
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
+
await server.sendLoggingMessage({
|
|
78
|
+
level: "error",
|
|
79
|
+
data: `图片生成失败 [${code}]: ${msg}`,
|
|
80
|
+
}).catch(() => { });
|
|
81
|
+
if (!silent) {
|
|
82
|
+
if (code === "E_PROVIDER_TIMEOUT") {
|
|
83
|
+
notify("图片生成超时", `${args.output_filename} 生成超过 300 秒,请重试`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
notify("图片生成失败", `[${code}] ${msg}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
return {
|
|
92
|
+
content: [{
|
|
93
|
+
type: "text",
|
|
94
|
+
text: JSON.stringify({
|
|
95
|
+
status: "pending",
|
|
96
|
+
message: `开始异步生成 ${args.output_filename},通常需要等待 60~120s。请耐心等待。任务完成后会有通知。`,
|
|
97
|
+
}),
|
|
98
|
+
}],
|
|
99
|
+
};
|
|
70
100
|
});
|
|
71
101
|
const transport = new StdioServerTransport();
|
|
72
102
|
await server.connect(transport);
|
package/dist/provider.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { AppError } from "./errors.js";
|
|
3
|
-
|
|
2
|
+
import { resizeIfNeeded } from "./resizer.js";
|
|
3
|
+
async function fetchWithRetry(url, init, timeoutMs, retries = 0) {
|
|
4
4
|
let lastErr;
|
|
5
5
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
6
6
|
const controller = new AbortController();
|
|
@@ -24,33 +24,30 @@ async function fetchWithRetry(url, init, timeoutMs, retries = 2) {
|
|
|
24
24
|
}
|
|
25
25
|
throw new AppError("E_PROVIDER_BAD_REQUEST", `Network error: ${lastErr}`);
|
|
26
26
|
}
|
|
27
|
-
function buildMessages(opts) {
|
|
27
|
+
async function buildMessages(opts) {
|
|
28
28
|
const { prompt, referenceImages, negativePrompt } = opts;
|
|
29
29
|
const fullPrompt = negativePrompt ? `${prompt}\n\nAvoid: ${negativePrompt}` : prompt;
|
|
30
30
|
if (!referenceImages || referenceImages.length === 0) {
|
|
31
31
|
return [{ role: "user", content: fullPrompt }];
|
|
32
32
|
}
|
|
33
|
-
const parts = referenceImages.map((imgPath) => {
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
data = readFileSync(imgPath);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
throw new AppError("E_REFERENCE_NOT_FOUND", `Cannot read reference image: ${imgPath}`);
|
|
40
|
-
}
|
|
33
|
+
const parts = await Promise.all(referenceImages.map(async (imgPath) => {
|
|
34
|
+
const data = await resizeIfNeeded(imgPath);
|
|
41
35
|
const b64 = data.toString("base64");
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
: "image/png";
|
|
45
|
-
return { type: "image_url", image_url: { url: `data:${mime};base64,${b64}` } };
|
|
46
|
-
});
|
|
36
|
+
return { type: "image_url", image_url: { url: `data:image/png;base64,${b64}` } };
|
|
37
|
+
}));
|
|
47
38
|
parts.push({ type: "text", text: fullPrompt });
|
|
48
39
|
return [{ role: "user", content: parts }];
|
|
49
40
|
}
|
|
50
41
|
export async function generateImage(config, opts) {
|
|
51
|
-
const messages = buildMessages(opts);
|
|
42
|
+
const messages = await buildMessages(opts);
|
|
52
43
|
const model = opts.model ?? config.model;
|
|
53
|
-
const
|
|
44
|
+
const imageConfig = {
|
|
45
|
+
image_size: opts.size ?? "1K",
|
|
46
|
+
};
|
|
47
|
+
if (opts.aspectRatio) {
|
|
48
|
+
imageConfig.aspect_ratio = opts.aspectRatio;
|
|
49
|
+
}
|
|
50
|
+
const body = JSON.stringify({ model, messages, image_config: imageConfig });
|
|
54
51
|
const url = `${config.baseUrl}/v1/chat/completions`;
|
|
55
52
|
const res = await fetchWithRetry(url, {
|
|
56
53
|
method: "POST",
|
package/dist/resizer.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Jimp, JimpMime } from "jimp";
|
|
2
|
+
import { AppError } from "./errors.js";
|
|
3
|
+
const MAX_SIDE = 1024;
|
|
4
|
+
export async function resizeIfNeeded(imgPath) {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
let image;
|
|
7
|
+
try {
|
|
8
|
+
image = await Jimp.read(imgPath);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new AppError("E_REFERENCE_NOT_FOUND", `Cannot read reference image: ${imgPath}`);
|
|
12
|
+
}
|
|
13
|
+
if (image.width > MAX_SIDE || image.height > MAX_SIDE) {
|
|
14
|
+
image.scaleToFit({ w: MAX_SIDE, h: MAX_SIDE });
|
|
15
|
+
}
|
|
16
|
+
return image.getBuffer(JimpMime.png);
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const tasks = new Map();
|
|
3
|
+
// 清理超过 30 分钟的已完成/失败任务
|
|
4
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
|
5
|
+
const TASK_TTL_MS = 30 * 60 * 1000;
|
|
6
|
+
// Cleanup function exposed for testing
|
|
7
|
+
export function cleanupExpiredTasks() {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
for (const [id, task] of tasks) {
|
|
10
|
+
if ((task.status === "done" || task.status === "failed") &&
|
|
11
|
+
task.completedAt &&
|
|
12
|
+
now - task.completedAt > TASK_TTL_MS) {
|
|
13
|
+
tasks.delete(id);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
setInterval(() => {
|
|
18
|
+
cleanupExpiredTasks();
|
|
19
|
+
}, CLEANUP_INTERVAL_MS).unref();
|
|
20
|
+
export function createTask() {
|
|
21
|
+
const task = {
|
|
22
|
+
id: randomUUID(),
|
|
23
|
+
status: "pending",
|
|
24
|
+
createdAt: Date.now(),
|
|
25
|
+
};
|
|
26
|
+
tasks.set(task.id, task);
|
|
27
|
+
return task;
|
|
28
|
+
}
|
|
29
|
+
export function getTask(id) {
|
|
30
|
+
return tasks.get(id);
|
|
31
|
+
}
|
|
32
|
+
export function updateTask(id, updates) {
|
|
33
|
+
const task = tasks.get(id);
|
|
34
|
+
if (task) {
|
|
35
|
+
Object.assign(task, updates);
|
|
36
|
+
}
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-duojie-image",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server for image generation via duojie.games",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"mcp-duojie-image": "
|
|
8
|
+
"mcp-duojie-image": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "tsx src/index.ts",
|
|
@@ -25,10 +25,13 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
28
|
+
"jimp": "^1.6.0",
|
|
29
|
+
"node-notifier": "^10.0.1",
|
|
28
30
|
"zod": "^3.23.8"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@types/node": "^22.0.0",
|
|
34
|
+
"@types/node-notifier": "^8.0.5",
|
|
32
35
|
"tsx": "^4.19.0",
|
|
33
36
|
"typescript": "^5.5.0",
|
|
34
37
|
"vitest": "^2.0.0"
|