modelscope-image-mcp 1.0.2 → 1.0.3
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/package.json +3 -2
- package/server.js +118 -17
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelscope-image-mcp",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "ModelScope (魔搭) 文生图 / 图生图 MCP 服务:免费调用 Qwen-Image、Z-Image、FLUX.1 等模型",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "ModelScope (魔搭) + SenseNova (商汤日日新) 文生图 / 图生图 MCP 服务:免费调用 Qwen-Image、Z-Image、FLUX.1、SenseNova U1.5 等模型",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
7
7
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"mcp",
|
|
18
18
|
"model-context-protocol",
|
|
19
19
|
"modelscope",
|
|
20
|
+
"sensenova",
|
|
20
21
|
"text-to-image",
|
|
21
22
|
"image-to-image",
|
|
22
23
|
"image-editing",
|
package/server.js
CHANGED
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
* MODELSCOPE_OUTPUT_DIR 图片保存目录,默认 ~/modelscope-images
|
|
14
14
|
* MODELSCOPE_TIMEOUT_MS 任务超时毫秒数,默认 300000
|
|
15
15
|
*
|
|
16
|
+
* SENSENOVA_API_KEY (可选)商汤日日新 API Key,配置后才能调用 sensenova-* 模型
|
|
17
|
+
* SENSENOVA_API_BASE 商汤接口地址,默认 https://token.sensenova.cn/v1
|
|
18
|
+
*
|
|
16
19
|
* 说明:零依赖,需要 Node.js >= 18(内置 fetch)。
|
|
20
|
+
* 魔搭走异步任务(提交 + 轮询);商汤走同步接口(直接返回 base64),调用方无感。
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import fs from "node:fs";
|
|
@@ -30,11 +34,21 @@ const OUTPUT_DIR =
|
|
|
30
34
|
const TASK_TIMEOUT_MS = Number(process.env.MODELSCOPE_TIMEOUT_MS || 300000);
|
|
31
35
|
const POLL_INTERVAL_MS = 3000;
|
|
32
36
|
|
|
37
|
+
const SENSENOVA_API_BASE = (
|
|
38
|
+
process.env.SENSENOVA_API_BASE || "https://token.sensenova.cn/v1"
|
|
39
|
+
).replace(/\/+$/, "");
|
|
40
|
+
const SENSENOVA_API_KEY = process.env.SENSENOVA_API_KEY || "";
|
|
41
|
+
|
|
33
42
|
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
|
|
43
|
+
{ id: "Qwen/Qwen-Image", name: "Qwen-Image", desc: "魔搭|通义千问图像生成,中英文文字渲染强,综合最稳" },
|
|
44
|
+
{ id: "Tongyi-MAI/Z-Image-Turbo", name: "Z-Image-Turbo", desc: "魔搭|轻量快速,出图速度快" },
|
|
45
|
+
{ id: "Tongyi-MAI/Z-Image", name: "Z-Image", desc: "魔搭|Z-Image 标准版,画质更好" },
|
|
46
|
+
{ id: "black-forest-labs/FLUX.1-Krea-dev", name: "FLUX.1-Krea-dev", desc: "魔搭|FLUX 系列,质感细腻,中文 prompt 遵循差,建议英文" },
|
|
47
|
+
{
|
|
48
|
+
id: "sensenova-u1.5-lite",
|
|
49
|
+
name: "SenseNova U1.5 Lite",
|
|
50
|
+
desc: "商汤|中文 prompt 直出效果好,默认无水印;不支持 negative_prompt,需配置 SENSENOVA_API_KEY",
|
|
51
|
+
},
|
|
38
52
|
];
|
|
39
53
|
|
|
40
54
|
const IMAGE_TO_IMAGE_MODELS = [
|
|
@@ -135,12 +149,20 @@ function modelSlug(model) {
|
|
|
135
149
|
.replace(/^-+|-+$/g, "");
|
|
136
150
|
}
|
|
137
151
|
|
|
152
|
+
/** 把图片字节写到输出目录,返回绝对路径。tag 是模型标识,会拼进文件名便于事后辨认 */
|
|
153
|
+
async function saveImageBuffer(buf, ext, dir, tag = "") {
|
|
154
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
155
|
+
const prefix = tag ? `ms-${tag}` : "ms";
|
|
156
|
+
const file = path.join(dir, `${prefix}-${timestamp()}-${Math.floor(Math.random() * 1e4)}${ext}`);
|
|
157
|
+
await fs.promises.writeFile(file, buf);
|
|
158
|
+
return file;
|
|
159
|
+
}
|
|
160
|
+
|
|
138
161
|
async function downloadImage(url, dir, tag = "") {
|
|
139
162
|
const res = await fetch(url);
|
|
140
163
|
if (!res.ok) throw new Error(`下载图片失败:HTTP ${res.status} ${url}`);
|
|
141
164
|
|
|
142
165
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
143
|
-
await fs.promises.mkdir(dir, { recursive: true });
|
|
144
166
|
|
|
145
167
|
let ext = ".png";
|
|
146
168
|
try {
|
|
@@ -150,10 +172,78 @@ async function downloadImage(url, dir, tag = "") {
|
|
|
150
172
|
/* 忽略,用默认后缀 */
|
|
151
173
|
}
|
|
152
174
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
return saveImageBuffer(buf, ext, dir, tag);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --------------------------------------------------------------- 商汤调用
|
|
179
|
+
|
|
180
|
+
/** sensenova-* 走商汤同步接口,其余走魔搭异步任务 */
|
|
181
|
+
function isSenseNova(model) {
|
|
182
|
+
return /^sensenova-/i.test(String(model));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 商汤 size 约束:32 的倍数、512–4096、宽高比不超过 3:1 */
|
|
186
|
+
function checkSenseNovaSize(size) {
|
|
187
|
+
if (!size) return undefined;
|
|
188
|
+
if (String(size).trim() === "auto") return "auto";
|
|
189
|
+
|
|
190
|
+
const m = /^(\d+)x(\d+)$/.exec(String(size).trim());
|
|
191
|
+
if (!m) throw new Error(`商汤 size 格式应为 WIDTHxHEIGHT(如 1024x1024)或 auto,当前:${size}`);
|
|
192
|
+
|
|
193
|
+
const w = Number(m[1]);
|
|
194
|
+
const h = Number(m[2]);
|
|
195
|
+
const bad = [];
|
|
196
|
+
if (w % 32 || h % 32) bad.push("宽高都必须是 32 的倍数(如 1280x720 需改成 1280x736)");
|
|
197
|
+
if (w < 512 || w > 4096 || h < 512 || h > 4096) bad.push("宽高需在 512–4096 之间");
|
|
198
|
+
if (Math.max(w, h) / Math.min(w, h) > 3) bad.push("宽高比不能超过 3:1");
|
|
199
|
+
if (bad.length) throw new Error(`商汤 size 不合法(${size}):${bad.join(";")}`);
|
|
200
|
+
|
|
201
|
+
return `${w}x${h}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function runSenseNova({ model, prompt, size, dir, tag }) {
|
|
205
|
+
if (!SENSENOVA_API_KEY) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"未配置 SENSENOVA_API_KEY 环境变量:请在商汤日日新控制台获取 API Key,并在 MCP 配置的 env 中设置"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// watermark: false 关掉右下角的「日日新 sensenova」水印
|
|
212
|
+
const payload = { model, prompt, n: 1, watermark: false };
|
|
213
|
+
const checkedSize = checkSenseNovaSize(size);
|
|
214
|
+
if (checkedSize) payload.size = checkedSize;
|
|
215
|
+
|
|
216
|
+
const res = await fetch(`${SENSENOVA_API_BASE}/images/generations`, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: {
|
|
219
|
+
Authorization: `Bearer ${SENSENOVA_API_KEY}`,
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify(payload),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const text = await res.text();
|
|
226
|
+
let data;
|
|
227
|
+
try {
|
|
228
|
+
data = JSON.parse(text);
|
|
229
|
+
} catch {
|
|
230
|
+
throw new Error(`商汤生成失败:HTTP ${res.status} ${text.slice(0, 300)}`);
|
|
231
|
+
}
|
|
232
|
+
if (!res.ok) {
|
|
233
|
+
throw new Error(`商汤生成失败:HTTP ${res.status} ${JSON.stringify(data).slice(0, 500)}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const items = Array.isArray(data.data) ? data.data : [];
|
|
237
|
+
const files = [];
|
|
238
|
+
for (const item of items) {
|
|
239
|
+
if (item?.b64_json) {
|
|
240
|
+
files.push(await saveImageBuffer(Buffer.from(item.b64_json, "base64"), ".png", dir, tag));
|
|
241
|
+
} else if (item?.url) {
|
|
242
|
+
files.push(await downloadImage(item.url, dir, tag));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { files, urls: [], model };
|
|
157
247
|
}
|
|
158
248
|
|
|
159
249
|
/** 把入参转成接口能接受的图片引用:http(s) 直接透传,本地文件转 data URI */
|
|
@@ -175,6 +265,15 @@ async function toImageRef(input) {
|
|
|
175
265
|
// ------------------------------------------------------------------ 生成流程
|
|
176
266
|
|
|
177
267
|
async function runGeneration({ model, prompt, negativePrompt, size, seed, image, outputDir }) {
|
|
268
|
+
const dir = outputDir || OUTPUT_DIR;
|
|
269
|
+
const tag = modelSlug(model);
|
|
270
|
+
|
|
271
|
+
if (isSenseNova(model)) {
|
|
272
|
+
if (image) throw new Error("商汤模型暂不支持图生图,请改用魔搭的图片编辑模型");
|
|
273
|
+
if (negativePrompt) log("提示:商汤接口不支持 negative_prompt,已忽略");
|
|
274
|
+
return runSenseNova({ model, prompt, size, dir, tag });
|
|
275
|
+
}
|
|
276
|
+
|
|
178
277
|
const payload = { model, prompt };
|
|
179
278
|
if (negativePrompt) payload.negative_prompt = negativePrompt;
|
|
180
279
|
if (size) payload.size = size;
|
|
@@ -187,8 +286,6 @@ async function runGeneration({ model, prompt, negativePrompt, size, seed, image,
|
|
|
187
286
|
const raw = result.output_images ?? result.output_image ?? [];
|
|
188
287
|
const urls = (Array.isArray(raw) ? raw : [raw]).filter(Boolean);
|
|
189
288
|
|
|
190
|
-
const dir = outputDir || OUTPUT_DIR;
|
|
191
|
-
const tag = modelSlug(model);
|
|
192
289
|
const files = [];
|
|
193
290
|
for (const url of urls) {
|
|
194
291
|
files.push(await downloadImage(url, dir, tag));
|
|
@@ -199,14 +296,14 @@ async function runGeneration({ model, prompt, negativePrompt, size, seed, image,
|
|
|
199
296
|
|
|
200
297
|
function renderResult({ taskId, files, urls, model }) {
|
|
201
298
|
if (!files.length) {
|
|
202
|
-
return `模型:${model}\n任务 ${taskId} 已完成,但未返回图片。原始响应:${JSON.stringify(urls)}`;
|
|
299
|
+
return `模型:${model}\n任务 ${taskId ?? "-"} 已完成,但未返回图片。原始响应:${JSON.stringify(urls)}`;
|
|
203
300
|
}
|
|
204
301
|
const lines = [`模型:${model}`, "", `已生成 ${files.length} 张图片:`, ""];
|
|
205
302
|
files.forEach((f, i) => {
|
|
206
303
|
lines.push(`${i + 1}. 本地文件:${f}`);
|
|
207
304
|
if (urls[i]) lines.push(` 原始链接:${urls[i]}`);
|
|
208
305
|
});
|
|
209
|
-
lines.push("", `任务 ID:${taskId}`);
|
|
306
|
+
if (taskId) lines.push("", `任务 ID:${taskId}`);
|
|
210
307
|
return lines.join("\n");
|
|
211
308
|
}
|
|
212
309
|
|
|
@@ -271,7 +368,7 @@ const TOOLS = [
|
|
|
271
368
|
{
|
|
272
369
|
name: "text_to_image",
|
|
273
370
|
description:
|
|
274
|
-
"
|
|
371
|
+
"文生图:用魔搭(ModelScope)或商汤日日新(SenseNova)的模型,按文字描述生成图片,自动保存到本地并返回文件路径。sensenova-* 模型走商汤同步接口(默认无水印)。",
|
|
275
372
|
inputSchema: {
|
|
276
373
|
type: "object",
|
|
277
374
|
properties: {
|
|
@@ -280,8 +377,12 @@ const TOOLS = [
|
|
|
280
377
|
type: "string",
|
|
281
378
|
description: `模型 ID,默认 ${DEFAULT_TEXT_TO_IMAGE_MODEL}。可选:${TEXT_TO_IMAGE_MODELS.map((m) => m.id).join("、")}`,
|
|
282
379
|
},
|
|
283
|
-
negative_prompt: { type: "string", description: "
|
|
284
|
-
size: {
|
|
380
|
+
negative_prompt: { type: "string", description: "反向提示词,描述不希望出现的内容(商汤模型不支持,会被忽略)" },
|
|
381
|
+
size: {
|
|
382
|
+
type: "string",
|
|
383
|
+
description:
|
|
384
|
+
"图片尺寸,如 1024x1024、1280x720(部分模型支持)。商汤模型需为 32 的倍数且宽高比不超过 3:1,如 1280x736",
|
|
385
|
+
},
|
|
285
386
|
seed: { type: "integer", description: "随机种子,便于复现同一张图" },
|
|
286
387
|
output_dir: { type: "string", description: "图片保存目录,默认 ~/modelscope-images" },
|
|
287
388
|
},
|
|
@@ -342,7 +443,7 @@ async function handleMessage(msg) {
|
|
|
342
443
|
reply(id, {
|
|
343
444
|
protocolVersion: params?.protocolVersion || "2024-11-05",
|
|
344
445
|
capabilities: { tools: {} },
|
|
345
|
-
serverInfo: { name: "modelscope-image-mcp", version: "1.0.
|
|
446
|
+
serverInfo: { name: "modelscope-image-mcp", version: "1.0.3" },
|
|
346
447
|
});
|
|
347
448
|
return;
|
|
348
449
|
|