modelscope-image-mcp 0.0.0-stage → 1.0.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 +91 -3
- package/package.json +31 -6
- package/server.js +399 -0
package/README.md
CHANGED
|
@@ -1,3 +1,91 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# modelscope-image-mcp
|
|
2
|
+
|
|
3
|
+
魔搭(ModelScope)文生图 / 图生图 MCP 服务。零依赖,Node.js >= 18。
|
|
4
|
+
|
|
5
|
+
免费调用 Qwen-Image、Z-Image、FLUX.1 等模型,生成的图片自动保存到本地。
|
|
6
|
+
|
|
7
|
+
## 工具
|
|
8
|
+
|
|
9
|
+
| 工具 | 说明 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `text_to_image` | 按文字描述生成图片 |
|
|
12
|
+
| `image_to_image` | 基于输入图片 + 文字指令做图片编辑(换背景、改风格、增删元素) |
|
|
13
|
+
| `list_models` | 列出已实测可用的模型 |
|
|
14
|
+
|
|
15
|
+
## 已验证可用的模型
|
|
16
|
+
|
|
17
|
+
文生图:
|
|
18
|
+
|
|
19
|
+
- `Qwen/Qwen-Image`
|
|
20
|
+
- `Tongyi-MAI/Z-Image-Turbo`
|
|
21
|
+
- `Tongyi-MAI/Z-Image`
|
|
22
|
+
- `black-forest-labs/FLUX.1-Krea-dev`
|
|
23
|
+
|
|
24
|
+
图生图 / 图片编辑(**必须**提供 `image` 参数):
|
|
25
|
+
|
|
26
|
+
- `Qwen/Qwen-Image-Edit`
|
|
27
|
+
- `Qwen/Qwen-Image-Edit-2509`
|
|
28
|
+
- `black-forest-labs/FLUX.1-Kontext-dev`
|
|
29
|
+
|
|
30
|
+
## 环境变量
|
|
31
|
+
|
|
32
|
+
| 变量 | 必填 | 默认值 | 说明 |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `MODELSCOPE_API_KEY` | 是 | — | 在 https://modelscope.cn 个人中心获取 |
|
|
35
|
+
| `MODELSCOPE_API_BASE` | 否 | `https://api-inference.modelscope.cn/v1` | 接口地址 |
|
|
36
|
+
| `MODELSCOPE_OUTPUT_DIR` | 否 | `~/modelscope-images` | 图片保存目录 |
|
|
37
|
+
| `MODELSCOPE_TIMEOUT_MS` | 否 | `300000` | 任务超时毫秒数 |
|
|
38
|
+
|
|
39
|
+
## 安装(免注册,本地文件方式)
|
|
40
|
+
|
|
41
|
+
不需要 npm 账号,把整个目录拷到任意位置即可。MCP 配置二选一:
|
|
42
|
+
|
|
43
|
+
**方式一:直接跑源码**(推荐,最简单)
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
"modelscope-image": {
|
|
47
|
+
"command": "node",
|
|
48
|
+
"args": ["C:/path/to/modelscope-image-mcp/server.js"],
|
|
49
|
+
"env": { "MODELSCOPE_API_KEY": "ms-你的Key" }
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> Windows 上如果 `node` 不在 PATH 里,把 `command` 换成 node.exe 的绝对路径,
|
|
54
|
+
> 例如 `C:/nvm4w/nodejs/node.exe`。
|
|
55
|
+
|
|
56
|
+
**方式二:打包成 tgz 再跑**(整个包压缩成一个文件,便于拷贝)
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
cd C:\path\to\modelscope-image-mcp
|
|
60
|
+
npm pack
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
会生成 `modelscope-image-mcp-1.0.0.tgz`,然后:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
"modelscope-image": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "C:/path/to/modelscope-image-mcp-1.0.0.tgz"],
|
|
69
|
+
"env": { "MODELSCOPE_API_KEY": "ms-你的Key" }
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 使用示例
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
用 text_to_image 生成一张“赛博朋克风格的广州塔夜景”,尺寸 1024x1024
|
|
77
|
+
|
|
78
|
+
用 image_to_image 把这张图 <图片URL> 的背景换成雪山
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 注意事项
|
|
82
|
+
|
|
83
|
+
- 接口是**异步任务制**:提交后拿 `task_id` 轮询,通常几十秒出图。
|
|
84
|
+
- 轮询最长等 `MODELSCOPE_TIMEOUT_MS`(默认 5 分钟)。传**本地大图**做图生图时会慢很多,
|
|
85
|
+
因为要转 base64 上传,建议优先用公网图片 URL。
|
|
86
|
+
- 图片默认保存在 `~/modelscope-images`,文件名形如 `ms-2026-10-15T06-30-00-000Z-1234.png`。
|
|
87
|
+
- 本服务不打印任何 API Key,Key 只从环境变量读取。
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "modelscope-image-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "modelscope-image-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ModelScope (魔搭) 文生图 / 图生图 MCP 服务:免费调用 Qwen-Image、Z-Image、FLUX.1 等模型",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"modelscope-image-mcp": "server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"server.js"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"modelscope",
|
|
20
|
+
"text-to-image",
|
|
21
|
+
"image-to-image",
|
|
22
|
+
"image-editing",
|
|
23
|
+
"qwen-image",
|
|
24
|
+
"z-image",
|
|
25
|
+
"flux"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"start": "node server.js"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ModelScope(魔搭)图片生成 MCP 服务
|
|
4
|
+
*
|
|
5
|
+
* 提供三个工具:
|
|
6
|
+
* - text_to_image 文生图
|
|
7
|
+
* - image_to_image 图生图 / 图片编辑
|
|
8
|
+
* - list_models 列出本服务已实测可用的模型
|
|
9
|
+
*
|
|
10
|
+
* 配置(环境变量):
|
|
11
|
+
* MODELSCOPE_API_KEY (必填)魔搭 API Key,在 https://modelscope.cn 个人中心获取
|
|
12
|
+
* MODELSCOPE_API_BASE 接口地址,默认 https://api-inference.modelscope.cn/v1
|
|
13
|
+
* MODELSCOPE_OUTPUT_DIR 图片保存目录,默认 ~/modelscope-images
|
|
14
|
+
* MODELSCOPE_TIMEOUT_MS 任务超时毫秒数,默认 300000
|
|
15
|
+
*
|
|
16
|
+
* 说明:零依赖,需要 Node.js >= 18(内置 fetch)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import readline from "node:readline";
|
|
23
|
+
|
|
24
|
+
const API_BASE = (
|
|
25
|
+
process.env.MODELSCOPE_API_BASE || "https://api-inference.modelscope.cn/v1"
|
|
26
|
+
).replace(/\/+$/, "");
|
|
27
|
+
const API_KEY = process.env.MODELSCOPE_API_KEY || "";
|
|
28
|
+
const OUTPUT_DIR =
|
|
29
|
+
process.env.MODELSCOPE_OUTPUT_DIR || path.join(os.homedir(), "modelscope-images");
|
|
30
|
+
const TASK_TIMEOUT_MS = Number(process.env.MODELSCOPE_TIMEOUT_MS || 300000);
|
|
31
|
+
const POLL_INTERVAL_MS = 3000;
|
|
32
|
+
|
|
33
|
+
const TEXT_TO_IMAGE_MODELS = [
|
|
34
|
+
{ id: "Qwen/Qwen-Image", name: "Qwen-Image", desc: "通义千问图像生成,中英文文字渲染强,综合最稳" },
|
|
35
|
+
{ id: "Tongyi-MAI/Z-Image-Turbo", name: "Z-Image-Turbo", desc: "轻量快速,出图速度快" },
|
|
36
|
+
{ id: "Tongyi-MAI/Z-Image", name: "Z-Image", desc: "Z-Image 标准版,画质更好" },
|
|
37
|
+
{ id: "black-forest-labs/FLUX.1-Krea-dev", name: "FLUX.1-Krea-dev", desc: "FLUX 系列,质感细腻" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const IMAGE_TO_IMAGE_MODELS = [
|
|
41
|
+
{ id: "Qwen/Qwen-Image-Edit", name: "Qwen-Image-Edit", desc: "指令式图片编辑(必须提供 image)" },
|
|
42
|
+
{ id: "Qwen/Qwen-Image-Edit-2509", name: "Qwen-Image-Edit-2509", desc: "图片编辑增强版(必须提供 image)" },
|
|
43
|
+
{ id: "black-forest-labs/FLUX.1-Kontext-dev", name: "FLUX.1-Kontext-dev", desc: "FLUX 图片编辑(必须提供 image)" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const DEFAULT_TEXT_TO_IMAGE_MODEL = "Qwen/Qwen-Image";
|
|
47
|
+
const DEFAULT_IMAGE_TO_IMAGE_MODEL = "Qwen/Qwen-Image-Edit";
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------- 基础工具
|
|
50
|
+
|
|
51
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
52
|
+
|
|
53
|
+
function log(...args) {
|
|
54
|
+
// 只能往 stderr 写,stdout 被 JSON-RPC 占用
|
|
55
|
+
process.stderr.write(args.join(" ") + "\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function timestamp() {
|
|
59
|
+
return new Date().toISOString().replace(/[:.]/g, "-").replace("Z", "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------- ModelScope 调用
|
|
63
|
+
|
|
64
|
+
async function submitTask(payload) {
|
|
65
|
+
if (!API_KEY) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"未配置 MODELSCOPE_API_KEY 环境变量:请在 https://modelscope.cn 个人中心获取 API Key,并在 MCP 配置的 env 中设置"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const res = await fetch(`${API_BASE}/images/generations`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
// 关键:不带这个头会拿不到 task_id
|
|
77
|
+
"X-ModelScope-Async-Mode": "true",
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify(payload),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const text = await res.text();
|
|
83
|
+
let data;
|
|
84
|
+
try {
|
|
85
|
+
data = JSON.parse(text);
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error(`提交任务失败:HTTP ${res.status} ${text.slice(0, 300)}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!res.ok || !data.task_id) {
|
|
91
|
+
throw new Error(`提交任务失败:HTTP ${res.status} ${JSON.stringify(data).slice(0, 500)}`);
|
|
92
|
+
}
|
|
93
|
+
return data.task_id;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function waitTask(taskId, timeoutMs = TASK_TIMEOUT_MS) {
|
|
97
|
+
const deadline = Date.now() + timeoutMs;
|
|
98
|
+
|
|
99
|
+
while (Date.now() < deadline) {
|
|
100
|
+
const res = await fetch(`${API_BASE}/tasks/${taskId}`, {
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
103
|
+
// 关键:轮询不带这个头拿不到 output_images
|
|
104
|
+
"X-ModelScope-Task-Type": "image_generation",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const text = await res.text();
|
|
109
|
+
let data;
|
|
110
|
+
try {
|
|
111
|
+
data = JSON.parse(text);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`查询任务失败:HTTP ${res.status} ${text.slice(0, 300)}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const status = data.task_status;
|
|
117
|
+
if (status === "SUCCEED") return data;
|
|
118
|
+
if (status === "FAILED") {
|
|
119
|
+
throw new Error(`任务失败:${JSON.stringify(data.errors || data).slice(0, 500)}`);
|
|
120
|
+
}
|
|
121
|
+
// PENDING / RUNNING 继续等
|
|
122
|
+
await sleep(POLL_INTERVAL_MS);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw new Error(`任务超时(${Math.round(timeoutMs / 1000)}s),可调大 MODELSCOPE_TIMEOUT_MS`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function downloadImage(url, dir) {
|
|
129
|
+
const res = await fetch(url);
|
|
130
|
+
if (!res.ok) throw new Error(`下载图片失败:HTTP ${res.status} ${url}`);
|
|
131
|
+
|
|
132
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
133
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
let ext = ".png";
|
|
136
|
+
try {
|
|
137
|
+
const extname = path.extname(new URL(url).pathname);
|
|
138
|
+
if (extname) ext = extname;
|
|
139
|
+
} catch {
|
|
140
|
+
/* 忽略,用默认后缀 */
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const file = path.join(dir, `ms-${timestamp()}-${Math.floor(Math.random() * 1e4)}${ext}`);
|
|
144
|
+
await fs.promises.writeFile(file, buf);
|
|
145
|
+
return file;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 把入参转成接口能接受的图片引用:http(s) 直接透传,本地文件转 data URI */
|
|
149
|
+
async function toImageRef(input) {
|
|
150
|
+
if (/^https?:\/\//i.test(input)) return input;
|
|
151
|
+
|
|
152
|
+
const abs = path.resolve(input);
|
|
153
|
+
const buf = await fs.promises.readFile(abs);
|
|
154
|
+
const raw = path.extname(abs).slice(1).toLowerCase();
|
|
155
|
+
const mime =
|
|
156
|
+
raw === "jpg" || raw === "jpeg"
|
|
157
|
+
? "image/jpeg"
|
|
158
|
+
: raw === "webp"
|
|
159
|
+
? "image/webp"
|
|
160
|
+
: `image/${raw || "png"}`;
|
|
161
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ------------------------------------------------------------------ 生成流程
|
|
165
|
+
|
|
166
|
+
async function runGeneration({ model, prompt, negativePrompt, size, seed, image, outputDir }) {
|
|
167
|
+
const payload = { model, prompt };
|
|
168
|
+
if (negativePrompt) payload.negative_prompt = negativePrompt;
|
|
169
|
+
if (size) payload.size = size;
|
|
170
|
+
if (seed !== undefined && seed !== null) payload.seed = seed;
|
|
171
|
+
if (image) payload.image_url = await toImageRef(image);
|
|
172
|
+
|
|
173
|
+
const taskId = await submitTask(payload);
|
|
174
|
+
const result = await waitTask(taskId);
|
|
175
|
+
|
|
176
|
+
const raw = result.output_images ?? result.output_image ?? [];
|
|
177
|
+
const urls = (Array.isArray(raw) ? raw : [raw]).filter(Boolean);
|
|
178
|
+
|
|
179
|
+
const dir = outputDir || OUTPUT_DIR;
|
|
180
|
+
const files = [];
|
|
181
|
+
for (const url of urls) {
|
|
182
|
+
files.push(await downloadImage(url, dir));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { taskId, files, urls };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function renderResult({ taskId, files, urls }) {
|
|
189
|
+
if (!files.length) {
|
|
190
|
+
return `任务 ${taskId} 已完成,但未返回图片。原始响应:${JSON.stringify(urls)}`;
|
|
191
|
+
}
|
|
192
|
+
const lines = [`已生成 ${files.length} 张图片:`, ""];
|
|
193
|
+
files.forEach((f, i) => {
|
|
194
|
+
lines.push(`${i + 1}. 本地文件:${f}`);
|
|
195
|
+
if (urls[i]) lines.push(` 原始链接:${urls[i]}`);
|
|
196
|
+
});
|
|
197
|
+
lines.push("", `任务 ID:${taskId}`);
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderModelList() {
|
|
202
|
+
const fmt = (list) =>
|
|
203
|
+
list.map((m) => ` - ${m.id}\n ${m.name}:${m.desc}`).join("\n");
|
|
204
|
+
return [
|
|
205
|
+
"文生图模型:",
|
|
206
|
+
fmt(TEXT_TO_IMAGE_MODELS),
|
|
207
|
+
"",
|
|
208
|
+
"图生图 / 图片编辑模型(必须提供 image 参数):",
|
|
209
|
+
fmt(IMAGE_TO_IMAGE_MODELS),
|
|
210
|
+
"",
|
|
211
|
+
`默认:文生图 = ${DEFAULT_TEXT_TO_IMAGE_MODEL},图生图 = ${DEFAULT_IMAGE_TO_IMAGE_MODEL}`,
|
|
212
|
+
].join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// -------------------------------------------------------------------- 工具层
|
|
216
|
+
|
|
217
|
+
async function callTool(name, args) {
|
|
218
|
+
switch (name) {
|
|
219
|
+
case "list_models":
|
|
220
|
+
return renderModelList();
|
|
221
|
+
|
|
222
|
+
case "text_to_image": {
|
|
223
|
+
const prompt = args.prompt;
|
|
224
|
+
if (!prompt) throw new Error("缺少必填参数 prompt");
|
|
225
|
+
const result = await runGeneration({
|
|
226
|
+
model: args.model || DEFAULT_TEXT_TO_IMAGE_MODEL,
|
|
227
|
+
prompt,
|
|
228
|
+
negativePrompt: args.negative_prompt,
|
|
229
|
+
size: args.size,
|
|
230
|
+
seed: args.seed,
|
|
231
|
+
outputDir: args.output_dir,
|
|
232
|
+
});
|
|
233
|
+
return renderResult(result);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case "image_to_image": {
|
|
237
|
+
const image = args.image;
|
|
238
|
+
const prompt = args.prompt;
|
|
239
|
+
if (!image) throw new Error("缺少必填参数 image(公网 URL 或本地文件绝对路径)");
|
|
240
|
+
if (!prompt) throw new Error("缺少必填参数 prompt");
|
|
241
|
+
const result = await runGeneration({
|
|
242
|
+
model: args.model || DEFAULT_IMAGE_TO_IMAGE_MODEL,
|
|
243
|
+
prompt,
|
|
244
|
+
negativePrompt: args.negative_prompt,
|
|
245
|
+
size: args.size,
|
|
246
|
+
seed: args.seed,
|
|
247
|
+
image,
|
|
248
|
+
outputDir: args.output_dir,
|
|
249
|
+
});
|
|
250
|
+
return renderResult(result);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
default:
|
|
254
|
+
throw new Error(`未知工具:${name}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const TOOLS = [
|
|
259
|
+
{
|
|
260
|
+
name: "text_to_image",
|
|
261
|
+
description:
|
|
262
|
+
"用魔搭(ModelScope)的文生图模型,按文字描述生成图片,自动保存到本地并返回文件路径。",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {
|
|
266
|
+
prompt: { type: "string", description: "图片的文字描述,中英文均可" },
|
|
267
|
+
model: {
|
|
268
|
+
type: "string",
|
|
269
|
+
description: `模型 ID,默认 ${DEFAULT_TEXT_TO_IMAGE_MODEL}。可选:${TEXT_TO_IMAGE_MODELS.map((m) => m.id).join("、")}`,
|
|
270
|
+
},
|
|
271
|
+
negative_prompt: { type: "string", description: "反向提示词,描述不希望出现的内容" },
|
|
272
|
+
size: { type: "string", description: "图片尺寸,如 1024x1024、1280x720(部分模型支持)" },
|
|
273
|
+
seed: { type: "integer", description: "随机种子,便于复现同一张图" },
|
|
274
|
+
output_dir: { type: "string", description: "图片保存目录,默认 ~/modelscope-images" },
|
|
275
|
+
},
|
|
276
|
+
required: ["prompt"],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "image_to_image",
|
|
281
|
+
description:
|
|
282
|
+
"用魔搭(ModelScope)的图片编辑模型,基于输入图片 + 文字指令生成新图片(换背景、改风格、增删元素等)。必须提供 image。",
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: "object",
|
|
285
|
+
properties: {
|
|
286
|
+
image: {
|
|
287
|
+
type: "string",
|
|
288
|
+
description:
|
|
289
|
+
"输入图片:推荐公网 URL(快);本地文件请给绝对路径(会转 base64 上传,较慢,可能需调大超时)",
|
|
290
|
+
},
|
|
291
|
+
prompt: { type: "string", description: "编辑指令,例如“把背景换成夜晚的城市街景”" },
|
|
292
|
+
model: {
|
|
293
|
+
type: "string",
|
|
294
|
+
description: `模型 ID,默认 ${DEFAULT_IMAGE_TO_IMAGE_MODEL}。可选:${IMAGE_TO_IMAGE_MODELS.map((m) => m.id).join("、")}`,
|
|
295
|
+
},
|
|
296
|
+
negative_prompt: { type: "string", description: "反向提示词" },
|
|
297
|
+
size: { type: "string", description: "输出图片尺寸,如 1024x1024" },
|
|
298
|
+
seed: { type: "integer", description: "随机种子" },
|
|
299
|
+
output_dir: { type: "string", description: "图片保存目录" },
|
|
300
|
+
},
|
|
301
|
+
required: ["image", "prompt"],
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "list_models",
|
|
306
|
+
description: "列出本服务已实测可用的文生图 / 图生图模型清单。",
|
|
307
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
// ------------------------------------------------------------ MCP JSON-RPC
|
|
312
|
+
|
|
313
|
+
function send(msg) {
|
|
314
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function reply(id, result) {
|
|
318
|
+
send({ jsonrpc: "2.0", id, result });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function replyError(id, code, message) {
|
|
322
|
+
send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function handleMessage(msg) {
|
|
326
|
+
const { id, method, params } = msg;
|
|
327
|
+
|
|
328
|
+
switch (method) {
|
|
329
|
+
case "initialize":
|
|
330
|
+
reply(id, {
|
|
331
|
+
protocolVersion: params?.protocolVersion || "2024-11-05",
|
|
332
|
+
capabilities: { tools: {} },
|
|
333
|
+
serverInfo: { name: "modelscope-image-mcp", version: "1.0.0" },
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
|
|
337
|
+
case "ping":
|
|
338
|
+
reply(id, {});
|
|
339
|
+
return;
|
|
340
|
+
|
|
341
|
+
case "tools/list":
|
|
342
|
+
reply(id, { tools: TOOLS });
|
|
343
|
+
return;
|
|
344
|
+
|
|
345
|
+
case "tools/call": {
|
|
346
|
+
const name = params?.name;
|
|
347
|
+
const args = params?.arguments || {};
|
|
348
|
+
try {
|
|
349
|
+
const text = await callTool(name, args);
|
|
350
|
+
reply(id, { content: [{ type: "text", text }] });
|
|
351
|
+
} catch (err) {
|
|
352
|
+
reply(id, {
|
|
353
|
+
content: [{ type: "text", text: `错误: ${err?.message || String(err)}` }],
|
|
354
|
+
isError: true,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
default:
|
|
361
|
+
replyError(id, -32601, `不支持的方法: ${method}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let pending = 0;
|
|
366
|
+
let stdinEnded = false;
|
|
367
|
+
|
|
368
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
369
|
+
|
|
370
|
+
rl.on("line", (line) => {
|
|
371
|
+
const trimmed = line.trim();
|
|
372
|
+
if (!trimmed) return;
|
|
373
|
+
|
|
374
|
+
let msg;
|
|
375
|
+
try {
|
|
376
|
+
msg = JSON.parse(trimmed);
|
|
377
|
+
} catch {
|
|
378
|
+
log("无法解析的输入:", trimmed.slice(0, 200));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 通知(无 id)不需要回复
|
|
383
|
+
if (msg.id === undefined || msg.id === null) return;
|
|
384
|
+
|
|
385
|
+
pending++;
|
|
386
|
+
handleMessage(msg)
|
|
387
|
+
.catch((err) => replyError(msg.id, -32603, err?.message || String(err)))
|
|
388
|
+
.finally(() => {
|
|
389
|
+
pending--;
|
|
390
|
+
if (stdinEnded && pending === 0) process.exit(0);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
rl.on("close", () => {
|
|
395
|
+
stdinEnded = true;
|
|
396
|
+
if (pending === 0) process.exit(0);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
log("modelscope-image-mcp 已启动" + (API_KEY ? "" : "(警告:未配置 MODELSCOPE_API_KEY)"));
|