siliconflow-image-mcp 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/dist/index.js ADDED
@@ -0,0 +1,605 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+
5
+ // src/index.ts
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+
9
+ // src/services/siliconflow.ts
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ var SiliconFlowService = class {
13
+ static {
14
+ __name(this, "SiliconFlowService");
15
+ }
16
+ apiKey;
17
+ baseUrl;
18
+ constructor(apiKey) {
19
+ if (!apiKey || apiKey.trim() === "") {
20
+ throw new Error("SiliconFlow API key is required");
21
+ }
22
+ this.apiKey = apiKey;
23
+ this.baseUrl = "https://api.siliconflow.cn/v1";
24
+ }
25
+ /**
26
+ * Make API call to SiliconFlow
27
+ */
28
+ async makeApiCall(endpoint, body) {
29
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/json",
33
+ "Authorization": `Bearer ${this.apiKey}`
34
+ },
35
+ body: JSON.stringify(body)
36
+ });
37
+ if (!response.ok) {
38
+ const errorText = await response.text();
39
+ throw new Error(`API error (${response.status}): ${errorText}`);
40
+ }
41
+ return response.json();
42
+ }
43
+ /**
44
+ * Make GET API call to SiliconFlow
45
+ */
46
+ async makeGetCall(endpoint, params) {
47
+ const url = new URL(`${this.baseUrl}${endpoint}`);
48
+ if (params) {
49
+ Object.entries(params).forEach(([key, value]) => {
50
+ if (value !== void 0 && value !== null) {
51
+ url.searchParams.append(key, String(value));
52
+ }
53
+ });
54
+ }
55
+ const response = await fetch(url.toString(), {
56
+ method: "GET",
57
+ headers: {
58
+ "Authorization": `Bearer ${this.apiKey}`
59
+ }
60
+ });
61
+ if (!response.ok) {
62
+ const errorText = await response.text();
63
+ throw new Error(`API error (${response.status}): ${errorText}`);
64
+ }
65
+ return response.json();
66
+ }
67
+ /**
68
+ * Generate images using SiliconFlow's image generation models
69
+ */
70
+ async generateImage(prompt, model = "black-forest-labs/FLUX.1-dev", aspectRatio, imageSize, count = 1, negativePrompt, seed) {
71
+ try {
72
+ const requestBody = {
73
+ model,
74
+ prompt,
75
+ batch_size: Math.min(count, 4)
76
+ // SiliconFlow max is 4
77
+ };
78
+ if (aspectRatio || imageSize) {
79
+ requestBody.image_size = this.mapAspectRatioToSize(aspectRatio, imageSize, model);
80
+ }
81
+ if (negativePrompt) {
82
+ requestBody.negative_prompt = negativePrompt;
83
+ }
84
+ if (seed !== void 0) {
85
+ requestBody.seed = seed;
86
+ }
87
+ const result = await this.makeApiCall("/images/generations", requestBody);
88
+ if (!result.images || result.images.length === 0) {
89
+ throw new Error("No images were generated");
90
+ }
91
+ const images = [];
92
+ for (const img of result.images) {
93
+ const imageUrl = img.url;
94
+ const imageResponse = await fetch(imageUrl);
95
+ if (!imageResponse.ok) {
96
+ throw new Error(`Failed to download image: ${imageResponse.status}`);
97
+ }
98
+ const imageBuffer = await imageResponse.arrayBuffer();
99
+ const base64Data = Buffer.from(imageBuffer).toString("base64");
100
+ const mimeType = imageUrl.includes(".png") ? "image/png" : imageUrl.includes(".jpg") || imageUrl.includes(".jpeg") ? "image/jpeg" : "image/png";
101
+ images.push({
102
+ data: base64Data,
103
+ mimeType
104
+ });
105
+ }
106
+ return images;
107
+ } catch (error) {
108
+ if (error instanceof Error) {
109
+ throw new Error(`Image generation failed: ${error.message}`);
110
+ }
111
+ throw new Error("Image generation failed with unknown error");
112
+ }
113
+ }
114
+ /**
115
+ * Edit an existing image using SiliconFlow
116
+ */
117
+ async editImage(image, prompt, model = "Qwen/Qwen-Image-Edit-2509") {
118
+ try {
119
+ let imageContent;
120
+ if (image.startsWith("data:image/")) {
121
+ imageContent = image;
122
+ } else if (image.startsWith("http://") || image.startsWith("https://")) {
123
+ imageContent = image;
124
+ } else if (fs.existsSync(image) && fs.statSync(image).isFile()) {
125
+ const imageBuffer2 = fs.readFileSync(image);
126
+ const base64Data2 = imageBuffer2.toString("base64");
127
+ const ext = path.extname(image).toLowerCase();
128
+ const mimeType2 = ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : ext === ".webp" ? "image/webp" : "image/png";
129
+ imageContent = `data:${mimeType2};base64,${base64Data2}`;
130
+ } else {
131
+ imageContent = `data:image/png;base64,${image}`;
132
+ }
133
+ const requestBody = {
134
+ model,
135
+ prompt,
136
+ image: imageContent
137
+ };
138
+ const result = await this.makeApiCall("/images/generations", requestBody);
139
+ if (!result.images || result.images.length === 0) {
140
+ throw new Error("No edited image was returned");
141
+ }
142
+ const img = result.images[0];
143
+ const imageUrl = img.url;
144
+ const imageResponse = await fetch(imageUrl);
145
+ if (!imageResponse.ok) {
146
+ throw new Error(`Failed to download edited image: ${imageResponse.status}`);
147
+ }
148
+ const imageBuffer = await imageResponse.arrayBuffer();
149
+ const base64Data = Buffer.from(imageBuffer).toString("base64");
150
+ const mimeType = imageUrl.includes(".png") ? "image/png" : imageUrl.includes(".jpg") || imageUrl.includes(".jpeg") ? "image/jpeg" : "image/png";
151
+ return {
152
+ data: base64Data,
153
+ mimeType
154
+ };
155
+ } catch (error) {
156
+ if (error instanceof Error) {
157
+ throw new Error(`Image editing failed: ${error.message}`);
158
+ }
159
+ throw new Error("Image editing failed with unknown error");
160
+ }
161
+ }
162
+ /**
163
+ * List all available image generation models
164
+ */
165
+ async listImageModels() {
166
+ try {
167
+ const result = await this.makeGetCall("/models", { type: "image" });
168
+ if (!result.data || result.data.length === 0) {
169
+ return [];
170
+ }
171
+ const imageModels = result.data.filter((model) => {
172
+ const modelId = model.id.toLowerCase();
173
+ return modelId.includes("flux") || modelId.includes("sd") || modelId.includes("stable-diffusion") || modelId.includes("qwen-image") || modelId.includes("kolors") || modelId.includes("dall") || modelId.includes("painting");
174
+ }).map((model) => ({
175
+ id: model.id,
176
+ name: model.id,
177
+ description: `Image generation model: ${model.id}`,
178
+ output_modalities: ["image"]
179
+ }));
180
+ return imageModels;
181
+ } catch (error) {
182
+ if (error instanceof Error) {
183
+ throw new Error(`Failed to list models: ${error.message}`);
184
+ }
185
+ throw new Error("Failed to list models with unknown error");
186
+ }
187
+ }
188
+ /**
189
+ * Test the API key validity
190
+ */
191
+ async testConnection() {
192
+ try {
193
+ await this.makeGetCall("/models");
194
+ return true;
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+ /**
200
+ * Map aspect ratio and image size to SiliconFlow's image_size format
201
+ */
202
+ mapAspectRatioToSize(aspectRatio, imageSize, model) {
203
+ if (model && model.includes("qwen")) {
204
+ const qwenSizes = {
205
+ "1:1": "1328x1328",
206
+ "16:9": "1664x928",
207
+ "9:16": "928x1664",
208
+ "4:3": "1472x1140",
209
+ "3:4": "1140x1472",
210
+ "3:2": "1584x1056",
211
+ "2:3": "1056x1584"
212
+ };
213
+ if (aspectRatio && qwenSizes[aspectRatio]) {
214
+ return qwenSizes[aspectRatio];
215
+ }
216
+ }
217
+ const kolorsSizes = {
218
+ "1:1": "1024x1024",
219
+ "3:4": "960x1280",
220
+ "4:3": "1280x960",
221
+ "1:2": "720x1440",
222
+ "9:16": "720x1280",
223
+ "16:9": "1280x720"
224
+ };
225
+ if (aspectRatio && kolorsSizes[aspectRatio]) {
226
+ return kolorsSizes[aspectRatio];
227
+ }
228
+ if (imageSize === "4K") return "2048x2048";
229
+ if (imageSize === "2K") return "1536x1536";
230
+ return "1024x1024";
231
+ }
232
+ };
233
+
234
+ // src/types/index.ts
235
+ import { z } from "zod";
236
+ var GenerateImageInputSchema = z.object({
237
+ prompt: z.string().min(1, "Prompt is required").max(2e3, "Prompt must be 2000 characters or less").describe("Detailed description of the image to generate"),
238
+ model: z.string().optional().describe("Model to use for generation (defaults to black-forest-labs/FLUX.1-dev)"),
239
+ aspectRatio: z.enum(["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]).optional().describe("Aspect ratio for generated images"),
240
+ imageSize: z.enum(["1K", "2K", "4K"]).optional().describe("Image size (higher resolution)"),
241
+ count: z.number().int().min(1).max(4).optional().default(1).describe("Number of images to generate (1-4)"),
242
+ negativePrompt: z.string().optional().describe("Negative prompt - what to avoid in the image"),
243
+ seed: z.number().int().min(0).max(9999999999).optional().describe("Seed for reproducible results")
244
+ });
245
+ var EditImageInputSchema = z.object({
246
+ image: z.string().min(1, "Image data is required").describe("Base64 encoded image data, image URL, or local file path to edit"),
247
+ prompt: z.string().min(1, "Edit prompt is required").max(2e3, "Edit prompt must be 2000 characters or less").describe("Instructions for editing the image"),
248
+ model: z.string().optional().describe("Model to use for editing (defaults to Qwen/Qwen-Image-Edit-2509)")
249
+ });
250
+ var ListModelsInputSchema = z.object({});
251
+
252
+ // src/utils/file.ts
253
+ import { promises as fs2 } from "fs";
254
+ import path2 from "path";
255
+ import os from "os";
256
+ function getImageBaseDir() {
257
+ const customDir = process.env.SILICONFLOW_IMAGE_DIR;
258
+ if (customDir) {
259
+ return customDir;
260
+ }
261
+ return path2.join(os.tmpdir(), "siliconflow-images");
262
+ }
263
+ __name(getImageBaseDir, "getImageBaseDir");
264
+ async function saveImageToFile(base64Data, prefix, mimeType) {
265
+ const tempDir = getImageBaseDir();
266
+ await fs2.mkdir(tempDir, { recursive: true });
267
+ const extension = mimeType === "image/jpeg" ? "jpg" : "png";
268
+ const timestamp = Date.now();
269
+ const filename = `${prefix}_${timestamp}.${extension}`;
270
+ const filepath = path2.join(tempDir, filename);
271
+ const buffer = Buffer.from(base64Data, "base64");
272
+ await fs2.writeFile(filepath, buffer);
273
+ return filepath;
274
+ }
275
+ __name(saveImageToFile, "saveImageToFile");
276
+ function getTempDir() {
277
+ return getImageBaseDir();
278
+ }
279
+ __name(getTempDir, "getTempDir");
280
+
281
+ // src/tools/generate.ts
282
+ function createGenerateImageTool(service) {
283
+ return {
284
+ name: "generate_image",
285
+ description: "Generate images using SiliconFlow's AI models. Supports various aspect ratios, image sizes, negative prompts, and seeds for reproducible results. Images are saved to temporary files and paths are returned.",
286
+ inputSchema: GenerateImageInputSchema,
287
+ handler: /* @__PURE__ */ __name(async (input) => {
288
+ const parsed = GenerateImageInputSchema.safeParse(input);
289
+ if (!parsed.success) {
290
+ return {
291
+ content: [
292
+ {
293
+ type: "text",
294
+ text: `Invalid input: ${parsed.error.errors.map((e) => e.message).join(", ")}`
295
+ }
296
+ ],
297
+ isError: true
298
+ };
299
+ }
300
+ const { prompt, model, aspectRatio, imageSize, count, negativePrompt, seed } = parsed.data;
301
+ try {
302
+ const images = await service.generateImage(
303
+ prompt,
304
+ model || "black-forest-labs/FLUX.1-dev",
305
+ aspectRatio,
306
+ imageSize,
307
+ count,
308
+ negativePrompt,
309
+ seed
310
+ );
311
+ if (images.length === 0) {
312
+ return {
313
+ content: [
314
+ {
315
+ type: "text",
316
+ text: "No images were generated. Please try a different prompt."
317
+ }
318
+ ],
319
+ isError: true
320
+ };
321
+ }
322
+ const savedFiles = [];
323
+ for (let i = 0; i < images.length; i++) {
324
+ const img = images[i];
325
+ const filepath = await saveImageToFile(img.data, `generated_${i + 1}`, img.mimeType);
326
+ savedFiles.push(filepath);
327
+ }
328
+ const tempDir = getTempDir();
329
+ const response = {
330
+ content: [
331
+ {
332
+ type: "text",
333
+ text: `Successfully generated ${images.length} image${images.length > 1 ? "s" : ""} for prompt: "${prompt}"
334
+ Saved to:
335
+ ${savedFiles.map((f) => `- ${f}`).join("\n")}
336
+
337
+ Temporary directory: ${tempDir}
338
+ Note: These are temporary files. Use the file paths to access the images.`
339
+ }
340
+ ]
341
+ };
342
+ return response;
343
+ } catch (error) {
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: `Image generation failed: ${error instanceof Error ? error.message : "Unknown error"}`
349
+ }
350
+ ],
351
+ isError: true
352
+ };
353
+ }
354
+ }, "handler")
355
+ };
356
+ }
357
+ __name(createGenerateImageTool, "createGenerateImageTool");
358
+
359
+ // src/tools/edit.ts
360
+ function createEditImageTool(service) {
361
+ return {
362
+ name: "edit_image",
363
+ description: "Edit existing images using SiliconFlow's AI models. Accepts base64 encoded image data, image URLs, or local file paths. Provide instructions for modifications. Uses Qwen/Qwen-Image-Edit-2509 by default. Images are saved to temporary files and paths are returned.",
364
+ inputSchema: EditImageInputSchema,
365
+ handler: /* @__PURE__ */ __name(async (input) => {
366
+ const parsed = EditImageInputSchema.safeParse(input);
367
+ if (!parsed.success) {
368
+ return {
369
+ content: [
370
+ {
371
+ type: "text",
372
+ text: `Invalid input: ${parsed.error.errors.map((e) => e.message).join(", ")}`
373
+ }
374
+ ],
375
+ isError: true
376
+ };
377
+ }
378
+ const { image, prompt, model } = parsed.data;
379
+ try {
380
+ const editedImage = await service.editImage(
381
+ image,
382
+ prompt,
383
+ model || "Qwen/Qwen-Image-Edit-2509"
384
+ );
385
+ const filepath = await saveImageToFile(editedImage.data, "edited", editedImage.mimeType);
386
+ const tempDir = getTempDir();
387
+ const response = {
388
+ content: [
389
+ {
390
+ type: "text",
391
+ text: `Successfully edited image with prompt: "${prompt}"
392
+ Saved to: ${filepath}
393
+
394
+ Temporary directory: ${tempDir}
395
+ Note: This is a temporary file. Use the file path to access the image.`
396
+ }
397
+ ]
398
+ };
399
+ return response;
400
+ } catch (error) {
401
+ return {
402
+ content: [
403
+ {
404
+ type: "text",
405
+ text: `Image editing failed: ${error instanceof Error ? error.message : "Unknown error"}`
406
+ }
407
+ ],
408
+ isError: true
409
+ };
410
+ }
411
+ }, "handler")
412
+ };
413
+ }
414
+ __name(createEditImageTool, "createEditImageTool");
415
+
416
+ // src/tools/list-models.ts
417
+ function createListModelsTool(service) {
418
+ return {
419
+ name: "list_image_models",
420
+ description: "List all available image generation models from SiliconFlow. Shows model IDs and capabilities.",
421
+ inputSchema: ListModelsInputSchema,
422
+ handler: /* @__PURE__ */ __name(async (input) => {
423
+ const parsed = ListModelsInputSchema.safeParse(input);
424
+ if (!parsed.success) {
425
+ return {
426
+ content: [
427
+ {
428
+ type: "text",
429
+ text: `Invalid input: ${parsed.error.errors.map((e) => e.message).join(", ")}`
430
+ }
431
+ ],
432
+ isError: true
433
+ };
434
+ }
435
+ try {
436
+ const models = await service.listImageModels();
437
+ if (models.length === 0) {
438
+ return {
439
+ content: [
440
+ {
441
+ type: "text",
442
+ text: "No image generation models found. This might be a temporary issue with the API."
443
+ }
444
+ ],
445
+ isError: true
446
+ };
447
+ }
448
+ const modelList = models.map((model, index) => {
449
+ const parts = [
450
+ `${index + 1}. **${model.name}** (\`${model.id}\`)`,
451
+ model.description ? ` - ${model.description}` : null,
452
+ model.output_modalities ? ` - Capabilities: ${model.output_modalities.join(", ")}` : null
453
+ ].filter(Boolean);
454
+ return parts.join("\n");
455
+ }).join("\n\n");
456
+ const usageTips = `
457
+
458
+ ---
459
+
460
+ **Usage:** Use the \`generate_image\` tool with the model ID to generate images.
461
+
462
+ **SiliconFlow Tips:**
463
+ - Recommended models: \`black-forest-labs/FLUX.1-dev\` (generation), \`Qwen/Qwen-Image-Edit-2509\` (editing)
464
+ - Supports advanced options: negative prompts, seeds, CFG values
465
+ - Image sizes: Use aspect ratios like "1:1", "16:9", "9:16"
466
+ - Optimized for China users with fast local network access`;
467
+ return {
468
+ content: [
469
+ {
470
+ type: "text",
471
+ text: `## Available Image Generation Models (SiliconFlow)
472
+
473
+ Found ${models.length} model${models.length > 1 ? "s" : ""} that support image generation:
474
+
475
+ ${modelList}${usageTips}`
476
+ }
477
+ ]
478
+ };
479
+ } catch (error) {
480
+ return {
481
+ content: [
482
+ {
483
+ type: "text",
484
+ text: `Failed to list models: ${error instanceof Error ? error.message : "Unknown error"}`
485
+ }
486
+ ],
487
+ isError: true
488
+ };
489
+ }
490
+ }, "handler")
491
+ };
492
+ }
493
+ __name(createListModelsTool, "createListModelsTool");
494
+
495
+ // src/index.ts
496
+ if (process.argv.includes("--build")) {
497
+ console.log('\u2705 Project structure is ready. Use "npm start" to run the server.');
498
+ console.log("\u{1F4DD} Note: This project uses tsx for runtime execution.");
499
+ process.exit(0);
500
+ }
501
+ var SILICONFLOW_API_KEY = process.env.SILICONFLOW_API_KEY;
502
+ var MOCK_MODE = process.env.SILICONFLOW_MOCK === "true";
503
+ if (!SILICONFLOW_API_KEY && !MOCK_MODE) {
504
+ console.error("\u274C Error: SILICONFLOW_API_KEY environment variable is required");
505
+ console.error("\nTo use this MCP server:");
506
+ console.error("1. Get your API key from https://siliconflow.cn");
507
+ console.error("2. Set it as an environment variable:");
508
+ console.error(" export SILICONFLOW_API_KEY=your-api-key-here");
509
+ console.error("\nFor Claude Desktop, add to your config.json:");
510
+ console.error(`{
511
+ "mcpServers": {
512
+ "siliconflow-image-mcp": {
513
+ "command": "npx",
514
+ "args": ["-y", "siliconflow-image-mcp"],
515
+ "env": {
516
+ "SILICONFLOW_API_KEY": "your-api-key-here"
517
+ }
518
+ }
519
+ }
520
+ }`);
521
+ console.error("\nOptional configurations:");
522
+ console.error(" export SILICONFLOW_MOCK=true # Use mock mode for testing");
523
+ console.error(" export SILICONFLOW_IMAGE_DIR=/path # Custom directory for saved images");
524
+ process.exit(1);
525
+ }
526
+ async function main() {
527
+ console.error("\u{1F680} Starting SiliconFlow Image MCP Server...");
528
+ try {
529
+ let siliconFlowService;
530
+ if (MOCK_MODE) {
531
+ console.error("\u26A0\uFE0F Running in MOCK mode - API calls will be simulated");
532
+ siliconFlowService = {
533
+ generateImage: /* @__PURE__ */ __name(async () => [{ data: "mock-base64-data", mimeType: "image/png" }], "generateImage"),
534
+ editImage: /* @__PURE__ */ __name(async () => ({ data: "mock-base64-data", mimeType: "image/png" }), "editImage"),
535
+ listImageModels: /* @__PURE__ */ __name(async () => [
536
+ { id: "black-forest-labs/FLUX.1-dev", name: "FLUX.1-dev", description: "Mock model", output_modalities: ["image"] }
537
+ ], "listImageModels"),
538
+ testConnection: /* @__PURE__ */ __name(async () => true, "testConnection")
539
+ };
540
+ } else {
541
+ siliconFlowService = new SiliconFlowService(SILICONFLOW_API_KEY);
542
+ const isConnected = await siliconFlowService.testConnection();
543
+ if (!isConnected) {
544
+ console.error("\u274C Failed to connect to SiliconFlow. Please check your API key.");
545
+ process.exit(1);
546
+ }
547
+ console.error("\u2705 Connected to SiliconFlow successfully");
548
+ }
549
+ const server = new McpServer({
550
+ name: "siliconflow-image-mcp",
551
+ version: "1.0.0",
552
+ capabilities: {
553
+ tools: {
554
+ listChanged: true
555
+ }
556
+ }
557
+ });
558
+ const generateTool = createGenerateImageTool(siliconFlowService);
559
+ const editTool = createEditImageTool(siliconFlowService);
560
+ const listModelsTool = createListModelsTool(siliconFlowService);
561
+ server.registerTool(
562
+ generateTool.name,
563
+ {
564
+ description: generateTool.description,
565
+ inputSchema: generateTool.inputSchema
566
+ },
567
+ generateTool.handler
568
+ );
569
+ server.registerTool(
570
+ editTool.name,
571
+ {
572
+ description: editTool.description,
573
+ inputSchema: editTool.inputSchema
574
+ },
575
+ editTool.handler
576
+ );
577
+ server.registerTool(
578
+ listModelsTool.name,
579
+ {
580
+ description: listModelsTool.description,
581
+ inputSchema: listModelsTool.inputSchema
582
+ },
583
+ listModelsTool.handler
584
+ );
585
+ const transport = new StdioServerTransport();
586
+ await server.connect(transport);
587
+ console.error("\u2705 SiliconFlow Image MCP Server is running");
588
+ console.error("\u{1F4CB} Available tools: generate_image, edit_image, list_image_models");
589
+ console.error("\u{1F4DD} Waiting for requests...");
590
+ process.on("SIGINT", async () => {
591
+ console.error("\n\u{1F6D1} Shutting down gracefully...");
592
+ await server.close();
593
+ process.exit(0);
594
+ });
595
+ } catch (error) {
596
+ console.error("\u274C Failed to start server:", error instanceof Error ? error.message : "Unknown error");
597
+ process.exit(1);
598
+ }
599
+ }
600
+ __name(main, "main");
601
+ main().catch((error) => {
602
+ console.error("Fatal error:", error);
603
+ process.exit(1);
604
+ });
605
+ //# sourceMappingURL=index.js.map