@vint.tri/report_gen_mcp 1.3.9 → 1.4.1

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.
@@ -0,0 +1,54 @@
1
+ # Publication Confirmation - Version 1.4.0
2
+
3
+ Version 1.4.0 of @vint.tri/report_gen_mcp has been successfully published to npm.
4
+
5
+ ## Published Features
6
+
7
+ This release includes significant improvements to image generation and editing capabilities:
8
+
9
+ 1. **Enhanced Image Generation Tool** (`generate-image`):
10
+ - Full integration with Python scripts for actual image generation
11
+ - Proper error handling and response parsing
12
+ - File path and URL generation for generated images
13
+ - Configurable timeouts for generation process
14
+
15
+ 2. **Enhanced Image Editing Tool** (`edit-image`):
16
+ - Full integration with Python scripts for actual image editing
17
+ - Proper error handling and response parsing
18
+ - File path and URL generation for edited images
19
+ - Configurable timeouts for editing process
20
+
21
+ 3. **Improved Python Integration**:
22
+ - Proper communication between Node.js MCP server and Python scripts via stdin/stdout
23
+ - Environment variable passing (CHUTES_API_TOKEN)
24
+ - File existence verification
25
+ - Better error handling for Python script execution
26
+
27
+ 4. **Version Consistency**:
28
+ - Updated package version from 1.3.9 to 1.4.0
29
+ - Updated MCP server version to match package version
30
+
31
+ ## Verification
32
+
33
+ - Package version: 1.4.0 ✓
34
+ - NPM registry confirmation: ✓
35
+ - Build verification: ✓
36
+ - Tool registration testing: ✓
37
+ - Image generation and editing functionality testing: ✓
38
+
39
+ ## Next Steps
40
+
41
+ Users can now install the updated package with:
42
+ ```bash
43
+ npm install @vint.tri/report_gen_mcp@1.4.0
44
+ ```
45
+
46
+ Or globally:
47
+ ```bash
48
+ npm install -g @vint.tri/report_gen_mcp@1.4.0
49
+ ```
50
+
51
+ To use the enhanced image features, users will need to:
52
+ 1. Install Python dependencies: `npm run install-python-deps`
53
+ 2. Set the CHUTES_API_TOKEN environment variable
54
+ 3. Set the REPORTS_DIR environment variable for storing generated reports
@@ -64,7 +64,8 @@
64
64
 
65
65
  1. **Путь к файлу**: Абсолютный путь к созданному отчету
66
66
  2. **Ссылка на файл**: Кликабельная file:// ссылка для открытия отчета в браузере
67
- 3. **Содержимое файла**: Полный текст HTML содержимого отчета
67
+ 3. **HTTP ссылка**: Если сервер запущен, ссылка вида http://localhost:3000/report/filename.html для доступа к отчету через веб-интерфейс
68
+ 4. **Содержимое файла**: Полный текст HTML содержимого отчета
68
69
 
69
70
  Обязательно покажите пользователю содержимое файла, чтобы он мог сразу ознакомиться с результатом без необходимости открывать файл отдельно.
70
71
 
@@ -76,6 +77,7 @@
76
77
 
77
78
  📁 Путь к файлу: /полный/путь/к/отчету.html
78
79
  🌐 Ссылка для открытия в браузере: file:///полный/путь/к/отчету.html
80
+ 🔗 HTTP ссылка: http://localhost:3000/report/отчет.html
79
81
 
80
82
  📄 Содержимое отчета:
81
83
  <!DOCTYPE html>
@@ -522,6 +524,7 @@
522
524
 
523
525
  📁 Путь к файлу: /path/to/report.html
524
526
  🌐 Ссылка для открытия в браузере: file:///path/to/report.html
527
+ 🔗 HTTP ссылка: http://localhost:3000/report/report.html
525
528
 
526
529
  Содержимое отчета:
527
530
  <!DOCTYPE html>
@@ -0,0 +1,88 @@
1
+ # Version 1.4.0 Release Notes
2
+
3
+ ## New Features and Enhancements
4
+
5
+ ### Major Version Update
6
+ - Updated package version from 1.3.9 to 1.4.0
7
+ - Updated MCP server version to match package version
8
+
9
+ ### Image Generation and Editing Improvements
10
+ Based on the IMAGE_GENERATION_FIX.md documentation, this version includes significant improvements to image handling:
11
+
12
+ 1. **Fixed Image Generation Tool (`generate-image`)**
13
+ - Replaced placeholder implementation with actual integration with `src/python/mcp_img_gen.py`
14
+ - Added proper child_process execution to call Python script
15
+ - Implemented proper error handling and response parsing
16
+ - Added file path and URL generation for generated images
17
+ - Set appropriate timeouts (60 seconds for generation)
18
+
19
+ 2. **Fixed Image Editing Tool (`edit-image`)**
20
+ - Replaced placeholder implementation with actual integration with `src/python/mcp_image_edit.py`
21
+ - Added proper child_process execution to call Python script
22
+ - Implemented proper error handling and response parsing
23
+ - Added file path and URL generation for edited images
24
+ - Set appropriate timeouts (120 seconds for editing)
25
+
26
+ 3. **Key Improvements**
27
+ - Both tools now properly communicate with Python scripts via stdin/stdout
28
+ - Added proper environment variable passing (CHUTES_API_TOKEN)
29
+ - Implemented file existence verification
30
+ - Added proper error handling for Python script execution
31
+ - Return actual file paths and URLs instead of placeholder text
32
+ - Maintain backward compatibility with existing API
33
+
34
+ ## Technical Details
35
+
36
+ ### Communication Flow
37
+ 1. MCP client calls `generate-image` or `edit-image` tool
38
+ 2. Node.js MCP server receives the call
39
+ 3. Server spawns Python process with appropriate script
40
+ 4. Server sends tool parameters as JSON to Python script stdin
41
+ 5. Python script processes the request and generates image
42
+ 6. Python script returns result as JSON to stdout
43
+ 7. Node.js server parses response and returns proper content to client
44
+
45
+ ### Required Setup
46
+ 1. Python dependencies must be installed:
47
+ ```bash
48
+ npm run install-python-deps
49
+ ```
50
+ 2. CHUTES_API_TOKEN environment variable must be set for actual image generation
51
+
52
+ ## Files Updated
53
+ - `package.json` - Version bump from 1.3.9 to 1.4.0
54
+ - `src/index.ts` - Updated MCP server version from 1.3.8 to 1.4.0
55
+ - `dist/index.js` - Regenerated with updated version
56
+ - `VERSION_1.4.0_RELEASE_NOTES.md` - This file
57
+
58
+ ## Verification
59
+ The improvements have been tested and verified to:
60
+ - ✅ Properly integrate with Python image generation scripts
61
+ - ✅ Return actual file paths and URLs instead of placeholder text
62
+ - ✅ Handle errors gracefully
63
+ - ✅ Maintain compatibility with existing API
64
+ - ✅ Support both image generation and editing functionalities
65
+
66
+ ## Installation
67
+ To upgrade to version 1.4.0, run:
68
+ ```bash
69
+ npm install @vint.tri/report_gen_mcp@1.4.0
70
+ ```
71
+
72
+ Or update your package.json dependency:
73
+ ```json
74
+ {
75
+ "dependencies": {
76
+ "@vint.tri/report_gen_mcp": "^1.4.0"
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Required Environment Variables
82
+ - `REPORTS_DIR` - Directory for storing generated reports and locating Python scripts
83
+ - `CHUTES_API_TOKEN` - Required for actual image generation/editing (optional for testing)
84
+
85
+ ## Python Dependencies
86
+ If using image generation features, ensure Python dependencies are installed:
87
+ ```bash
88
+ npm run install-python-deps
package/dist/index.js CHANGED
@@ -38,6 +38,10 @@ if (!isStdioMode) {
38
38
  const app = express();
39
39
  const port = 3000;
40
40
  app.use(express.json());
41
+ // Serve static files from the reports directory at /report/ endpoint
42
+ if (reportsDir) {
43
+ app.use('/report', express.static(reportsDir));
44
+ }
41
45
  app.post('/generate-report', async (req, res) => {
42
46
  // For HTTP API mode, use the REPORTS_DIR environment variable
43
47
  // This endpoint only runs in non-stdio mode where reportsDir is guaranteed to be defined
@@ -103,7 +107,7 @@ if (process.argv.length === 2) {
103
107
  // No command specified, run in stdio mode using MCP SDK
104
108
  const mcpServer = new McpServer({
105
109
  name: "report_gen_mcp",
106
- version: "1.3.8",
110
+ version: "1.4.1",
107
111
  }, {
108
112
  // Disable health check to prevent automatic calls
109
113
  capabilities: {
@@ -263,8 +267,10 @@ if (process.argv.length === 2) {
263
267
  fileUrl: fileUrl,
264
268
  fileContent: fileContent
265
269
  };
270
+ // Generate HTTP link if server is running
271
+ const httpLink = reportsDir ? `http://localhost:${port}/report/${path.basename(outputPath)}` : null;
266
272
  // Return comprehensive information for neural network to use
267
- // Neural network should present to user: file path, web link, and file content
273
+ // Neural network should present to user: file path, web link, HTTP link, and file content
268
274
  return {
269
275
  content: [
270
276
  {
@@ -274,8 +280,7 @@ if (process.argv.length === 2) {
274
280
  FOR THE NEURAL NETWORK: Please present the following information to the user:
275
281
  1. Path to file: ${outputPath}
276
282
  2. Web browser link: ${fileUrl}
277
- 3. File content:
278
- ${fileContent}`
283
+ ${httpLink ? `3. HTTP link: ${httpLink}\n4. File content:\n${fileContent}` : `3. File content:\n${fileContent}`}`
279
284
  }
280
285
  ]
281
286
  };
@@ -386,6 +391,8 @@ ${fileContent}`
386
391
  await fs.writeFile(filePath, fileContent, 'utf8');
387
392
  // Generate proper file URL
388
393
  const fileUrl = pathToFileURL(filePath).href;
394
+ // Generate HTTP link if server is running
395
+ const httpLink = reportsDir ? `http://localhost:${port}/report/${path.basename(filePath)}` : null;
389
396
  return {
390
397
  content: [
391
398
  {
@@ -395,7 +402,7 @@ ${fileContent}`
395
402
  FOR THE NEURAL NETWORK: Please present the following information to the user:
396
403
  1. Path to file: ${filePath}
397
404
  2. Web browser link: ${fileUrl}
398
- 3. Operation performed: ${operation}`
405
+ ${httpLink ? `3. HTTP link: ${httpLink}\n4. Operation performed: ${operation}` : `3. Operation performed: ${operation}`}`
399
406
  }
400
407
  ]
401
408
  };
@@ -448,9 +455,8 @@ FOR THE NEURAL NETWORK: Please present the following information to the user:
448
455
  }]
449
456
  };
450
457
  }
451
- // Import child_process for calling Python scripts
452
- const { spawn } = await import('child_process');
453
- const { promises: fsPromises } = await import('fs');
458
+ // Import our TypeScript implementation
459
+ const { ImageGenerator } = await import('./mcp/imageGenerationServer.js');
454
460
  const path = await import('path');
455
461
  const os = await import('os');
456
462
  // Determine the output directory:
@@ -461,7 +467,7 @@ FOR THE NEURAL NETWORK: Please present the following information to the user:
461
467
  outputDir = process.env.REPORTS_DIR;
462
468
  // Ensure the reports directory exists
463
469
  try {
464
- await fsPromises.access(outputDir).catch(() => fsPromises.mkdir(outputDir, { recursive: true }));
470
+ await fs.access(outputDir).catch(() => fs.mkdir(outputDir, { recursive: true }));
465
471
  }
466
472
  catch (error) {
467
473
  throw new Error(`Cannot create or access the reports directory: ${outputDir}`);
@@ -471,141 +477,44 @@ FOR THE NEURAL NETWORK: Please present the following information to the user:
471
477
  outputDir = os.tmpdir();
472
478
  }
473
479
  // Generate a unique filename if not provided
474
- const fileName = outputFile || `generated-image-${Date.now()}.png`;
480
+ const fileName = outputFile || `generated-image-${Date.now()}.jpeg`;
475
481
  const fullPath = path.resolve(outputDir, fileName);
476
- // Prepare arguments for the Python script
477
- // Use REPORTS_DIR environment variable to locate Python scripts
478
- const reportsDir = process.env.REPORTS_DIR || process.cwd();
479
- const pythonScriptPath = path.resolve(reportsDir, 'src', 'python', 'mcp_img_gen.py');
480
- const pythonArgs = [
481
- '-c',
482
- `import sys; sys.path.insert(0, '${path.dirname(pythonScriptPath)}'); ` +
483
- `import mcp_img_gen; ` +
484
- `import asyncio; ` +
485
- `asyncio.run(mcp_img_gen.main())`
486
- ];
487
- // Prepare the tool call arguments as JSON
488
- const toolCallArgs = {
489
- name: "generate_image_to_file",
490
- arguments: {
491
- prompt: prompt,
492
- directory: outputDir,
493
- filename: fileName,
482
+ // Create image generator instance
483
+ const imageGen = new ImageGenerator();
484
+ try {
485
+ // Generate image using our TypeScript implementation
486
+ const imageDataUri = await imageGen.generateImage(prompt, {
494
487
  width: width,
495
488
  height: height,
496
489
  guidance_scale: guidanceScale,
497
490
  negative_prompt: negativePrompt,
498
491
  num_inference_steps: numInferenceSteps,
499
- seed: seed
500
- }
501
- };
502
- // Execute the Python script
503
- return new Promise((resolve, reject) => {
504
- const pythonProcess = spawn('python3', pythonArgs, {
505
- env: {
506
- ...process.env,
507
- CHUTES_API_TOKEN: process.env.CHUTES_API_TOKEN,
508
- },
509
- stdio: ['pipe', 'pipe', 'pipe']
510
- });
511
- let stdoutData = '';
512
- let stderrData = '';
513
- pythonProcess.stdout.on('data', (data) => {
514
- stdoutData += data.toString();
515
- });
516
- pythonProcess.stderr.on('data', (data) => {
517
- stderrData += data.toString();
518
- });
519
- pythonProcess.on('close', async (code) => {
520
- if (code !== 0) {
521
- reject(new Error(`Python script exited with code ${code}. Error: ${stderrData}`));
522
- return;
523
- }
524
- try {
525
- // Parse the response from the Python script
526
- const responseLines = stdoutData.trim().split('\n');
527
- const jsonResponse = responseLines.find(line => line.startsWith('{') && line.endsWith('}'));
528
- if (jsonResponse) {
529
- const response = JSON.parse(jsonResponse);
530
- if (response.result && response.result.content) {
531
- // Look for success message in the response
532
- const successContent = response.result.content.find((item) => item.type === "text" && item.text.includes("успешно сгенерировано"));
533
- if (successContent) {
534
- // Check if file was created
535
- try {
536
- await fsPromises.access(fullPath);
537
- // Generate proper file URL
538
- const { pathToFileURL } = await import('url');
539
- const fileUrl = pathToFileURL(fullPath).href;
540
- resolve({
541
- content: [{
542
- type: "text",
543
- text: `Image successfully generated!\n\nFile saved to: ${fullPath}\nWeb link: ${fileUrl}`
544
- }]
545
- });
546
- }
547
- catch (fileError) {
548
- resolve({
549
- content: [{
550
- type: "text",
551
- text: `Image generation completed according to Python script, but file was not found at expected location: ${fullPath}`
552
- }]
553
- });
554
- }
555
- }
556
- else {
557
- // Look for error message
558
- const errorContent = response.result.content.find((item) => item.type === "text" && item.text.includes("Ошибка"));
559
- if (errorContent) {
560
- reject(new Error(errorContent.text));
561
- }
562
- else {
563
- resolve({
564
- content: [{
565
- type: "text",
566
- text: `Image generation completed. Response: ${JSON.stringify(response.result.content, null, 2)}`
567
- }]
568
- });
569
- }
570
- }
571
- }
572
- else {
573
- resolve({
574
- content: [{
575
- type: "text",
576
- text: `Image generation tool executed. Response: ${JSON.stringify(response, null, 2)}`
577
- }]
578
- });
579
- }
580
- }
581
- else {
582
- // If we can't parse JSON, return the raw output
583
- resolve({
584
- content: [{
585
- type: "text",
586
- text: `Image generation completed.\n\nOutput:\n${stdoutData}`
587
- }]
588
- });
589
- }
590
- }
591
- catch (parseError) {
592
- resolve({
593
- content: [{
594
- type: "text",
595
- text: `Image generation completed.\n\nRaw output:\n${stdoutData}\n\nError parsing response: ${parseError}`
596
- }]
597
- });
598
- }
492
+ seed: seed !== 0 ? seed : null,
493
+ model: model
599
494
  });
600
- // Send the tool call to the Python script
601
- pythonProcess.stdin.write(JSON.stringify(toolCallArgs) + '\n');
602
- pythonProcess.stdin.end();
603
- // Set a timeout to prevent hanging
604
- setTimeout(() => {
605
- pythonProcess.kill();
606
- reject(new Error('Image generation timed out after 60 seconds'));
607
- }, 60000);
608
- });
495
+ // Extract base64 data
496
+ let base64Data = imageDataUri;
497
+ if (imageDataUri.startsWith("data:image/jpeg;base64,")) {
498
+ base64Data = imageDataUri.substring("data:image/jpeg;base64,".length);
499
+ }
500
+ // Save image to file
501
+ const imageBuffer = Buffer.from(base64Data, 'base64');
502
+ await fs.writeFile(fullPath, imageBuffer);
503
+ // Generate proper file URL
504
+ const { pathToFileURL } = await import('url');
505
+ const fileUrl = pathToFileURL(fullPath).href;
506
+ // Generate HTTP link if server is running
507
+ const httpLink = reportsDir ? `http://localhost:${port}/report/${path.basename(fullPath)}` : null;
508
+ return {
509
+ content: [{
510
+ type: "text",
511
+ text: `Image successfully generated!\n\nFile saved to: ${fullPath}\nWeb link: ${fileUrl}${httpLink ? `\nHTTP link: ${httpLink}` : ''}`
512
+ }]
513
+ };
514
+ }
515
+ catch (error) {
516
+ throw new Error(`Image generation failed: ${error.message}`);
517
+ }
609
518
  });
610
519
  // Register image editing tool
611
520
  mcpServer.registerTool("edit-image", {
@@ -657,143 +566,60 @@ FOR THE NEURAL NETWORK: Please present the following information to the user:
657
566
  }]
658
567
  };
659
568
  }
660
- // Import child_process for calling Python scripts
661
- const { spawn } = await import('child_process');
662
- const { promises: fsPromises } = await import('fs');
569
+ // Import our TypeScript implementation
570
+ const { ImageEditor } = await import('./mcp/imageEditingServer.js');
663
571
  const path = await import('path');
664
- // Prepare arguments for the Python script
665
- // Use REPORTS_DIR environment variable to locate Python scripts
666
- const reportsDir = process.env.REPORTS_DIR || process.cwd();
667
- const pythonScriptPath = path.resolve(reportsDir, 'src', 'python', 'mcp_image_edit.py');
668
- const pythonArgs = [
669
- '-c',
670
- `import sys; sys.path.insert(0, '${path.dirname(pythonScriptPath)}'); ` +
671
- `import mcp_image_edit; ` +
672
- `import asyncio; ` +
673
- `asyncio.run(mcp_image_edit.main())`
674
- ];
675
- // Prepare the tool call arguments as JSON
676
- const toolCallArgs = {
677
- name: "edit_image_file",
678
- arguments: {
679
- prompt: prompt,
680
- image_path: imagePath,
681
- output_path: output_path,
572
+ // Create image editor instance
573
+ const imageEditor = new ImageEditor();
574
+ try {
575
+ // Обрабатываем путь к исходному файлу с учетом базового каталога
576
+ const baseSaveDirectory = process.env.IMG_SAVE_BASE_DIR || "";
577
+ const fullImagePath = baseSaveDirectory && !path.isAbsolute(imagePath)
578
+ ? path.normalize(path.join(baseSaveDirectory, imagePath))
579
+ : path.normalize(imagePath);
580
+ // Проверяем существование входного файла
581
+ if (!await fs.pathExists(fullImagePath)) {
582
+ throw new Error(`Файл изображения не найден: ${fullImagePath}`);
583
+ }
584
+ // Загружаем изображение и конвертируем в base64
585
+ const imageBuffer = await fs.readFile(fullImagePath);
586
+ const imageB64 = imageBuffer.toString('base64');
587
+ // Редактируем изображение через base64 метод
588
+ const editedImageB64 = await imageEditor.editImage(prompt, imageB64, {
682
589
  width: width,
683
590
  height: height,
684
591
  true_cfg_scale: cfgScale,
685
592
  negative_prompt: negativePrompt,
686
593
  num_inference_steps: numInferenceSteps,
687
- seed: seed
688
- }
689
- };
690
- // Execute the Python script
691
- return new Promise((resolve, reject) => {
692
- const pythonProcess = spawn('python3', pythonArgs, {
693
- env: {
694
- ...process.env,
695
- CHUTES_API_TOKEN: process.env.CHUTES_API_TOKEN,
696
- },
697
- stdio: ['pipe', 'pipe', 'pipe']
698
- });
699
- let stdoutData = '';
700
- let stderrData = '';
701
- pythonProcess.stdout.on('data', (data) => {
702
- stdoutData += data.toString();
594
+ seed: seed || null
703
595
  });
704
- pythonProcess.stderr.on('data', (data) => {
705
- stderrData += data.toString();
706
- });
707
- pythonProcess.on('close', async (code) => {
708
- if (code !== 0) {
709
- reject(new Error(`Python script exited with code ${code}. Error: ${stderrData}`));
710
- return;
711
- }
712
- try {
713
- // Parse the response from the Python script
714
- const responseLines = stdoutData.trim().split('\n');
715
- const jsonResponse = responseLines.find(line => line.startsWith('{') && line.endsWith('}'));
716
- if (jsonResponse) {
717
- const response = JSON.parse(jsonResponse);
718
- if (response.result && response.result.content) {
719
- // Look for success message in the response
720
- const successContent = response.result.content.find((item) => item.type === "text" && item.text.includes("успешно отредактировано"));
721
- if (successContent) {
722
- // Check if output file was created
723
- try {
724
- await fsPromises.access(output_path);
725
- // Generate proper file URL
726
- const { pathToFileURL } = await import('url');
727
- const fileUrl = pathToFileURL(path.resolve(output_path)).href;
728
- resolve({
729
- content: [{
730
- type: "text",
731
- text: `Image successfully edited!\n\nOutput file: ${output_path}\nWeb link: ${fileUrl}`
732
- }]
733
- });
734
- }
735
- catch (fileError) {
736
- resolve({
737
- content: [{
738
- type: "text",
739
- text: `Image editing completed according to Python script, but output file was not found at expected location: ${output_path}`
740
- }]
741
- });
742
- }
743
- }
744
- else {
745
- // Look for error message
746
- const errorContent = response.result.content.find((item) => item.type === "text" && item.text.includes("Ошибка"));
747
- if (errorContent) {
748
- reject(new Error(errorContent.text));
749
- }
750
- else {
751
- resolve({
752
- content: [{
753
- type: "text",
754
- text: `Image editing completed. Response: ${JSON.stringify(response.result.content, null, 2)}`
755
- }]
756
- });
757
- }
758
- }
759
- }
760
- else {
761
- resolve({
762
- content: [{
763
- type: "text",
764
- text: `Image editing tool executed. Response: ${JSON.stringify(response, null, 2)}`
765
- }]
766
- });
767
- }
768
- }
769
- else {
770
- // If we can't parse JSON, return the raw output
771
- resolve({
772
- content: [{
773
- type: "text",
774
- text: `Image editing completed.\n\nOutput:\n${stdoutData}`
775
- }]
776
- });
777
- }
778
- }
779
- catch (parseError) {
780
- resolve({
781
- content: [{
782
- type: "text",
783
- text: `Image editing completed.\n\nRaw output:\n${stdoutData}\n\nError parsing response: ${parseError}`
784
- }]
785
- });
786
- }
787
- });
788
- // Send the tool call to the Python script
789
- pythonProcess.stdin.write(JSON.stringify(toolCallArgs) + '\n');
790
- pythonProcess.stdin.end();
791
- // Set a timeout to prevent hanging
792
- setTimeout(() => {
793
- pythonProcess.kill();
794
- reject(new Error('Image editing timed out after 120 seconds'));
795
- }, 120000);
796
- });
596
+ // Декодируем и сохраняем результат
597
+ const editedImageBuffer = Buffer.from(editedImageB64, 'base64');
598
+ // Обрабатываем путь к выходному файлу с учетом базового каталога
599
+ const fullOutputPath = baseSaveDirectory && !path.isAbsolute(output_path)
600
+ ? path.normalize(path.join(baseSaveDirectory, output_path))
601
+ : path.normalize(output_path);
602
+ // Создаем папку для выходного файла если нужно
603
+ const outputDir = path.dirname(fullOutputPath);
604
+ if (outputDir && !await fs.pathExists(outputDir)) {
605
+ await fs.ensureDir(outputDir);
606
+ }
607
+ await fs.writeFile(fullOutputPath, editedImageBuffer);
608
+ // Создаем file URI для выходного файла
609
+ const { pathToFileURL } = await import('url');
610
+ const fileUrl = pathToFileURL(path.resolve(fullOutputPath)).href;
611
+ // Generate HTTP link if server is running
612
+ const httpLink = reportsDir ? `http://localhost:${port}/report/${path.basename(fullOutputPath)}` : null;
613
+ return {
614
+ content: [{
615
+ type: "text",
616
+ text: `Image successfully edited!\n\nOutput file: ${fullOutputPath}\nWeb link: ${fileUrl}${httpLink ? `\nHTTP link: ${httpLink}` : ''}`
617
+ }]
618
+ };
619
+ }
620
+ catch (error) {
621
+ throw new Error(`Image editing failed: ${error.message}`);
622
+ }
797
623
  });
798
624
  async function main() {
799
625
  const transport = new StdioServerTransport();
@@ -0,0 +1,294 @@
1
+ /**
2
+ * MCP Server для редактирования изображений с использованием Chutes AI API
3
+ * Реализация на TypeScript без использования Python
4
+ */
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import axios from 'axios';
8
+ import fs from 'fs-extra';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { z } from 'zod';
12
+ // Получаем __dirname в ES модуле
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ class ImageEditor {
16
+ apiToken;
17
+ defaultSettings;
18
+ constructor() {
19
+ this.apiToken = process.env.CHUTES_API_TOKEN || "";
20
+ // Настройки по умолчанию
21
+ this.defaultSettings = {
22
+ width: parseInt(process.env.EDIT_WIDTH || "1024"),
23
+ height: parseInt(process.env.EDIT_HEIGHT || "1024"),
24
+ true_cfg_scale: parseFloat(process.env.EDIT_CFG_SCALE || "4"),
25
+ negative_prompt: process.env.EDIT_NEGATIVE_PROMPT || "",
26
+ num_inference_steps: parseInt(process.env.EDIT_INFERENCE_STEPS || "50"),
27
+ seed: null
28
+ };
29
+ }
30
+ async editImage(prompt, imageB64, additionalParams = {}) {
31
+ if (!this.apiToken) {
32
+ throw new Error("API токен не настроен. Установите переменную окружения CHUTES_API_TOKEN");
33
+ }
34
+ if (!prompt.trim()) {
35
+ throw new Error("Пустой промпт недопустим");
36
+ }
37
+ if (!imageB64.trim()) {
38
+ throw new Error("Изображение для редактирования не предоставлено");
39
+ }
40
+ // Объединяем настройки по умолчанию с переданными параметрами
41
+ const settings = {
42
+ ...this.defaultSettings,
43
+ ...additionalParams
44
+ };
45
+ const headers = {
46
+ "Authorization": `Bearer ${this.apiToken}`,
47
+ "Content-Type": "application/json",
48
+ };
49
+ const body = {
50
+ seed: settings.seed,
51
+ width: settings.width,
52
+ height: settings.height,
53
+ prompt: prompt.trim(),
54
+ image_b64: imageB64,
55
+ true_cfg_scale: settings.true_cfg_scale,
56
+ negative_prompt: settings.negative_prompt,
57
+ num_inference_steps: settings.num_inference_steps
58
+ };
59
+ try {
60
+ const response = await axios.post("https://chutes-qwen-image-edit.chutes.ai/generate", body, {
61
+ headers,
62
+ responseType: 'arraybuffer',
63
+ timeout: 120000 // Редактирование может занимать больше времени
64
+ });
65
+ if (response.status !== 200) {
66
+ throw new Error(`API Error ${response.status}: ${response.statusText}`);
67
+ }
68
+ // Проверяем content-type ответа
69
+ const contentType = response.headers['content-type'] || '';
70
+ if (contentType.includes('application/json')) {
71
+ // Если JSON ответ
72
+ const result = response.data;
73
+ // Извлекаем отредактированное изображение из ответа
74
+ if (result.image) {
75
+ return result.image;
76
+ }
77
+ else if (result.data) {
78
+ return result.data;
79
+ }
80
+ else {
81
+ throw new Error(`Неожиданный формат ответа API: ${JSON.stringify(result)}`);
82
+ }
83
+ }
84
+ else if (contentType.includes('image/')) {
85
+ // Если изображение напрямую
86
+ const imageData = response.data;
87
+ if (!imageData || imageData.length < 2) {
88
+ throw new Error("Пустые или некорректные данные изображения");
89
+ }
90
+ // Конвертируем в base64
91
+ return imageData.toString('base64');
92
+ }
93
+ else {
94
+ throw new Error(`Неожиданный content-type: ${contentType}`);
95
+ }
96
+ }
97
+ catch (error) {
98
+ if (axios.isAxiosError(error)) {
99
+ if (error.code === 'ECONNABORTED') {
100
+ throw new Error("Таймаут при редактировании изображения");
101
+ }
102
+ if (error.response) {
103
+ throw new Error(`API Error ${error.response.status}: ${error.response.data}`);
104
+ }
105
+ }
106
+ throw new Error(`Ошибка сетевого соединения: ${error.message}`);
107
+ }
108
+ }
109
+ }
110
+ // Создаем экземпляр сервера
111
+ const app = new McpServer({
112
+ name: "image-editor-ts",
113
+ version: "1.0.0",
114
+ }, {
115
+ capabilities: {
116
+ tools: {}
117
+ }
118
+ });
119
+ const imageEditor = new ImageEditor();
120
+ // Регистрируем инструменты
121
+ app.registerTool("edit_image", {
122
+ description: "Редактирует изображение на основе текстового описания желаемых изменений. ВАЖНО: промпт и негативный промпт должны быть СТРОГО на английском языке!",
123
+ inputSchema: {
124
+ prompt: z.string().describe("Текстовое описание желаемых изменений к изображению НА АНГЛИЙСКОМ ЯЗЫКЕ"),
125
+ image_b64: z.string().describe("Исходное изображение в формате base64 (без data URI префикса)"),
126
+ width: z.number().optional().describe("Ширина результата (512-2048)"),
127
+ height: z.number().optional().describe("Высота результата (512-2048)"),
128
+ true_cfg_scale: z.number().optional().describe("Сила следования промпту (1.0-10.0)"),
129
+ negative_prompt: z.string().optional().describe("Негативный промпт (что НЕ должно быть в результате)"),
130
+ num_inference_steps: z.number().optional().describe("Количество шагов обработки (10-100)"),
131
+ seed: z.number().optional().describe("Seed для воспроизводимости (оставьте пустым для случайного)")
132
+ },
133
+ }, async (args) => {
134
+ try {
135
+ // Извлекаем параметры
136
+ const prompt = args.prompt || "";
137
+ const imageB64 = args.image_b64 || "";
138
+ if (!prompt) {
139
+ throw new Error("Параметр 'prompt' обязателен");
140
+ }
141
+ if (!imageB64) {
142
+ throw new Error("Параметр 'image_b64' обязателен");
143
+ }
144
+ // Дополнительные параметры (необязательные)
145
+ const editParams = {};
146
+ if (args.width !== undefined)
147
+ editParams.width = args.width;
148
+ if (args.height !== undefined)
149
+ editParams.height = args.height;
150
+ if (args.true_cfg_scale !== undefined)
151
+ editParams.true_cfg_scale = args.true_cfg_scale;
152
+ if (args.negative_prompt !== undefined)
153
+ editParams.negative_prompt = args.negative_prompt;
154
+ if (args.num_inference_steps !== undefined)
155
+ editParams.num_inference_steps = args.num_inference_steps;
156
+ if (args.seed !== undefined)
157
+ editParams.seed = args.seed;
158
+ // Редактируем изображение
159
+ const editedImageB64 = await imageEditor.editImage(prompt, imageB64, editParams);
160
+ // Возвращаем результат
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: `✅ Изображение успешно отредактировано по промпту: '${prompt}'`
166
+ },
167
+ {
168
+ type: "image",
169
+ data: editedImageB64,
170
+ mimeType: "image/jpeg"
171
+ }
172
+ ]
173
+ };
174
+ }
175
+ catch (error) {
176
+ // Возвращаем ошибку как текстовый контент
177
+ return {
178
+ content: [
179
+ {
180
+ type: "text",
181
+ text: `❌ Ошибка при редактировании изображения: ${error.message}`
182
+ }
183
+ ]
184
+ };
185
+ }
186
+ });
187
+ app.registerTool("edit_image_file", {
188
+ description: "Редактирует изображение из файла на основе текстового описания желаемых изменений. ВАЖНО: промпт и негативный промпт должны быть СТРОГО на английском языке!",
189
+ inputSchema: {
190
+ prompt: z.string().describe("Текстовое описание желаемых изменений к изображению НА АНГЛИЙСКОМ ЯЗЫКЕ"),
191
+ image_path: z.string().describe("Путь к исходному файлу изображения"),
192
+ output_path: z.string().describe("Путь для сохранения отредактированного изображения"),
193
+ width: z.number().optional().describe("Ширина результата (512-2048)"),
194
+ height: z.number().optional().describe("Высота результата (512-2048)"),
195
+ true_cfg_scale: z.number().optional().describe("Сила следования промпту (1.0-10.0)"),
196
+ negative_prompt: z.string().optional().describe("Негативный промпт (что НЕ должно быть в результате)"),
197
+ num_inference_steps: z.number().optional().describe("Количество шагов обработки (10-100)"),
198
+ seed: z.number().optional().describe("Seed для воспроизводимости (оставьте пустым для случайного)")
199
+ },
200
+ }, async (args) => {
201
+ try {
202
+ // Извлекаем параметры
203
+ const prompt = args.prompt || "";
204
+ const imagePath = args.image_path || "";
205
+ const outputPath = args.output_path || "";
206
+ if (!prompt) {
207
+ throw new Error("Параметр 'prompt' обязателен");
208
+ }
209
+ if (!imagePath) {
210
+ throw new Error("Параметр 'image_path' обязателен");
211
+ }
212
+ if (!outputPath) {
213
+ throw new Error("Параметр 'output_path' обязателен");
214
+ }
215
+ // Обрабатываем путь к исходному файлу с учетом базового каталога
216
+ const baseSaveDirectory = process.env.IMG_SAVE_BASE_DIR || "";
217
+ const fullImagePath = baseSaveDirectory && !path.isAbsolute(imagePath)
218
+ ? path.normalize(path.join(baseSaveDirectory, imagePath))
219
+ : path.normalize(imagePath);
220
+ // Проверяем существование входного файла
221
+ if (!await fs.pathExists(fullImagePath)) {
222
+ throw new Error(`Файл изображения не найден: ${fullImagePath}`);
223
+ }
224
+ // Загружаем изображение и конвертируем в base64
225
+ const imageBuffer = await fs.readFile(fullImagePath);
226
+ const imageB64 = imageBuffer.toString('base64');
227
+ // Дополнительные параметры (необязательные)
228
+ const editParams = {};
229
+ if (args.width !== undefined)
230
+ editParams.width = args.width;
231
+ if (args.height !== undefined)
232
+ editParams.height = args.height;
233
+ if (args.true_cfg_scale !== undefined)
234
+ editParams.true_cfg_scale = args.true_cfg_scale;
235
+ if (args.negative_prompt !== undefined)
236
+ editParams.negative_prompt = args.negative_prompt;
237
+ if (args.num_inference_steps !== undefined)
238
+ editParams.num_inference_steps = args.num_inference_steps;
239
+ if (args.seed !== undefined)
240
+ editParams.seed = args.seed;
241
+ // Редактируем изображение через base64 метод
242
+ const editedImageB64 = await imageEditor.editImage(prompt, imageB64, editParams);
243
+ // Декодируем и сохраняем результат
244
+ const editedImageBuffer = Buffer.from(editedImageB64, 'base64');
245
+ // Обрабатываем путь к выходному файлу с учетом базового каталога
246
+ const fullOutputPath = baseSaveDirectory && !path.isAbsolute(outputPath)
247
+ ? path.normalize(path.join(baseSaveDirectory, outputPath))
248
+ : path.normalize(outputPath);
249
+ // Создаем папку для выходного файла если нужно
250
+ const outputDir = path.dirname(fullOutputPath);
251
+ if (outputDir && !await fs.pathExists(outputDir)) {
252
+ await fs.ensureDir(outputDir);
253
+ }
254
+ await fs.writeFile(fullOutputPath, editedImageBuffer);
255
+ // Создаем file URI для выходного файла
256
+ const fileUrl = new URL(`file://${path.resolve(fullOutputPath)}`).href;
257
+ // Возвращаем результат
258
+ return {
259
+ content: [
260
+ {
261
+ type: "text",
262
+ text: `✅ Изображение успешно отредактировано: '${fullImagePath}' -> '${fullOutputPath}' (URI: ${fileUrl})`
263
+ }
264
+ ]
265
+ };
266
+ }
267
+ catch (error) {
268
+ // Возвращаем ошибку как текстовый контент
269
+ return {
270
+ content: [
271
+ {
272
+ type: "text",
273
+ text: `❌ Ошибка при редактировании файла изображения: ${error.message}`
274
+ }
275
+ ]
276
+ };
277
+ }
278
+ });
279
+ async function main() {
280
+ try {
281
+ const transport = new StdioServerTransport();
282
+ await app.connect(transport);
283
+ console.log("MCP Image Editing Server (TypeScript) is running...");
284
+ }
285
+ catch (error) {
286
+ console.error("Server error:", error);
287
+ process.exit(1);
288
+ }
289
+ }
290
+ // Запускаем сервер если этот файл запущен напрямую
291
+ if (import.meta.url === `file://${process.argv[1]}`) {
292
+ main();
293
+ }
294
+ export { main, ImageEditor };
@@ -0,0 +1,270 @@
1
+ /**
2
+ * MCP Server для генерации изображений с использованием Chutes AI API
3
+ * Реализация на TypeScript без использования Python
4
+ */
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import axios from 'axios';
8
+ import fs from 'fs-extra';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { z } from 'zod';
12
+ // Получаем __dirname в ES модуле
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ class ImageGenerator {
16
+ apiToken;
17
+ defaultSettings;
18
+ constructor() {
19
+ this.apiToken = process.env.CHUTES_API_TOKEN || "";
20
+ // Настройки по умолчанию
21
+ this.defaultSettings = {
22
+ model: process.env.SD_MODEL || "JuggernautXL",
23
+ width: this.validateRange(parseInt(process.env.SD_WIDTH || "1024"), 128, 2048),
24
+ height: this.validateRange(parseInt(process.env.SD_HEIGHT || "1024"), 128, 2048),
25
+ guidance_scale: this.validateRange(parseFloat(process.env.SD_GUIDANCE_SCALE || "7.5"), 1.0, 20.0),
26
+ negative_prompt: process.env.SD_NEGATIVE_PROMPT || "",
27
+ num_inference_steps: this.validateRange(parseInt(process.env.SD_NUM_INFERENCE_STEPS || "25"), 1, 50),
28
+ seed: Math.max(0, parseInt(process.env.SD_SEED || "0")) || null
29
+ };
30
+ }
31
+ validateRange(value, minVal, maxVal) {
32
+ return Math.max(minVal, Math.min(value, maxVal));
33
+ }
34
+ async generateImage(prompt, additionalParams = {}) {
35
+ if (!this.apiToken) {
36
+ throw new Error("API токен не настроен. Установите переменную окружения CHUTES_API_TOKEN");
37
+ }
38
+ if (!prompt.trim()) {
39
+ throw new Error("Пустой промпт недопустим");
40
+ }
41
+ // Объединяем настройки по умолчанию с переданными параметрами
42
+ const settings = {
43
+ ...this.defaultSettings,
44
+ ...additionalParams
45
+ };
46
+ const headers = {
47
+ "Authorization": `Bearer ${this.apiToken}`,
48
+ "Content-Type": "application/json",
49
+ };
50
+ const body = {
51
+ model: settings.model,
52
+ prompt: prompt.trim(),
53
+ width: settings.width,
54
+ height: settings.height,
55
+ guidance_scale: settings.guidance_scale,
56
+ negative_prompt: settings.negative_prompt,
57
+ num_inference_steps: settings.num_inference_steps,
58
+ seed: settings.seed !== 0 ? settings.seed : null
59
+ };
60
+ try {
61
+ const response = await axios.post("https://image.chutes.ai/generate", body, {
62
+ headers,
63
+ responseType: 'arraybuffer',
64
+ timeout: 60000
65
+ });
66
+ if (response.status !== 200) {
67
+ throw new Error(`API Error ${response.status}: ${response.statusText}`);
68
+ }
69
+ const imageData = response.data;
70
+ if (!imageData || imageData.length < 2) {
71
+ throw new Error("Пустые или некорректные данные изображения");
72
+ }
73
+ // Проверяем JPEG signature
74
+ if (imageData[0] !== 0xFF || imageData[1] !== 0xD8) {
75
+ console.warn("Предупреждение: Данные могут не быть JPEG");
76
+ }
77
+ const base64Image = imageData.toString('base64');
78
+ return `data:image/jpeg;base64,${base64Image}`;
79
+ }
80
+ catch (error) {
81
+ if (axios.isAxiosError(error)) {
82
+ if (error.code === 'ECONNABORTED') {
83
+ throw new Error("Таймаут при генерации изображения");
84
+ }
85
+ if (error.response) {
86
+ throw new Error(`API Error ${error.response.status}: ${error.response.data}`);
87
+ }
88
+ }
89
+ throw new Error(`Ошибка сетевого соединения: ${error.message}`);
90
+ }
91
+ }
92
+ }
93
+ // Создаем экземпляр сервера
94
+ const app = new McpServer({
95
+ name: "image-generator-ts",
96
+ version: "1.0.0",
97
+ }, {
98
+ capabilities: {
99
+ tools: {}
100
+ }
101
+ });
102
+ const imageGen = new ImageGenerator();
103
+ // Регистрируем инструменты
104
+ app.registerTool("generate_image", {
105
+ description: "Генерирует изображения. ВАЖНО: промпт и негативный промпт должны быть СТРОГО на английском языке!",
106
+ inputSchema: {
107
+ prompt: z.string().describe("Текстовый промпт для генерации изображения НА АНГЛИЙСКОМ ЯЗЫКЕ"),
108
+ width: z.number().optional().describe("Ширина изображения (128-2048)"),
109
+ height: z.number().optional().describe("Высота изображения (128-2048)"),
110
+ guidance_scale: z.number().optional().describe("Сила следования промпту (1.0-20.0)"),
111
+ negative_prompt: z.string().optional().describe("Негативный промпт (что НЕ включать в изображение)"),
112
+ num_inference_steps: z.number().optional().describe("Количество шагов генерации (1-50)"),
113
+ seed: z.number().optional().describe("Seed для воспроизводимости (0 = случайный)")
114
+ },
115
+ }, async (args) => {
116
+ try {
117
+ // Извлекаем параметры
118
+ const prompt = args.prompt || "";
119
+ if (!prompt) {
120
+ throw new Error("Параметр 'prompt' обязателен");
121
+ }
122
+ // Дополнительные параметры (необязательные)
123
+ const generationParams = {};
124
+ if (args.width !== undefined)
125
+ generationParams.width = args.width;
126
+ if (args.height !== undefined)
127
+ generationParams.height = args.height;
128
+ if (args.guidance_scale !== undefined)
129
+ generationParams.guidance_scale = args.guidance_scale;
130
+ if (args.negative_prompt !== undefined)
131
+ generationParams.negative_prompt = args.negative_prompt;
132
+ if (args.num_inference_steps !== undefined)
133
+ generationParams.num_inference_steps = args.num_inference_steps;
134
+ if (args.seed !== undefined)
135
+ generationParams.seed = args.seed;
136
+ // Генерируем изображение
137
+ const imageDataUri = await imageGen.generateImage(prompt, generationParams);
138
+ // Извлекаем только base64 данные без префикса
139
+ let base64Data = imageDataUri;
140
+ if (imageDataUri.startsWith("data:image/jpeg;base64,")) {
141
+ base64Data = imageDataUri.substring("data:image/jpeg;base64,".length);
142
+ }
143
+ // Возвращаем результат
144
+ return {
145
+ content: [
146
+ {
147
+ type: "text",
148
+ text: `✅ Изображение успешно сгенерировано по промпту: '${prompt}'`
149
+ },
150
+ {
151
+ type: "image",
152
+ data: base64Data,
153
+ mimeType: "image/jpeg"
154
+ }
155
+ ]
156
+ };
157
+ }
158
+ catch (error) {
159
+ // Возвращаем ошибку как текстовый контент
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: `❌ Ошибка при генерации изображения: ${error.message}`
165
+ }
166
+ ]
167
+ };
168
+ }
169
+ });
170
+ app.registerTool("generate_image_to_file", {
171
+ description: "Генерирует изображение и сохраняет его в указанный файл. ВАЖНО: промпт и негативный промпт должны быть СТРОГО на английском языке!",
172
+ inputSchema: {
173
+ prompt: z.string().describe("Текстовый промпт для генерации изображения НА АНГЛИЙСКОМ ЯЗЫКЕ"),
174
+ directory: z.string().describe("Каталог для сохранения изображения"),
175
+ filename: z.string().describe("Имя файла для сохранения (с расширением)"),
176
+ width: z.number().optional().describe("Ширина изображения (128-2048)"),
177
+ height: z.number().optional().describe("Высота изображения (128-2048)"),
178
+ guidance_scale: z.number().optional().describe("Сила следования промпту (1.0-20.0)"),
179
+ negative_prompt: z.string().optional().describe("Негативный промпт (что НЕ включать в изображение)"),
180
+ num_inference_steps: z.number().optional().describe("Количество шагов генерации (1-50)"),
181
+ seed: z.number().optional().describe("Seed для воспроизводимости (0 = случайный)")
182
+ },
183
+ }, async (args) => {
184
+ try {
185
+ // Извлекаем параметры
186
+ const prompt = args.prompt || "";
187
+ const directory = args.directory || "";
188
+ const filename = args.filename || "";
189
+ if (!prompt) {
190
+ throw new Error("Параметр 'prompt' обязателен");
191
+ }
192
+ if (!directory) {
193
+ throw new Error("Параметр 'directory' обязателен");
194
+ }
195
+ if (!filename) {
196
+ throw new Error("Параметр 'filename' обязателен");
197
+ }
198
+ // Дополнительные параметры (необязательные)
199
+ const generationParams = {};
200
+ if (args.width !== undefined)
201
+ generationParams.width = args.width;
202
+ if (args.height !== undefined)
203
+ generationParams.height = args.height;
204
+ if (args.guidance_scale !== undefined)
205
+ generationParams.guidance_scale = args.guidance_scale;
206
+ if (args.negative_prompt !== undefined)
207
+ generationParams.negative_prompt = args.negative_prompt;
208
+ if (args.num_inference_steps !== undefined)
209
+ generationParams.num_inference_steps = args.num_inference_steps;
210
+ if (args.seed !== undefined)
211
+ generationParams.seed = args.seed;
212
+ // Генерируем изображение
213
+ const imageDataUri = await imageGen.generateImage(prompt, generationParams);
214
+ // Извлекаем только base64 данные без префикса
215
+ let base64Data = imageDataUri;
216
+ if (imageDataUri.startsWith("data:image/jpeg;base64,")) {
217
+ base64Data = imageDataUri.substring("data:image/jpeg;base64,".length);
218
+ }
219
+ // Проверяем базовый каталог из переменной окружения
220
+ const baseSaveDirectory = process.env.IMG_SAVE_BASE_DIR || "";
221
+ const fullDirectory = baseSaveDirectory
222
+ ? path.normalize(path.join(baseSaveDirectory, directory))
223
+ : path.normalize(directory);
224
+ // Создаем каталог если не существует
225
+ await fs.ensureDir(fullDirectory);
226
+ // Формируем полный путь к файлу
227
+ const outputPath = path.join(fullDirectory, filename);
228
+ // Сохраняем изображение
229
+ const imageBuffer = Buffer.from(base64Data, 'base64');
230
+ await fs.writeFile(outputPath, imageBuffer);
231
+ // Создаем file URI
232
+ const fileUrl = new URL(`file://${path.resolve(outputPath)}`).href;
233
+ // Возвращаем результат
234
+ return {
235
+ content: [
236
+ {
237
+ type: "text",
238
+ text: `✅ Изображение успешно сгенерировано и сохранено в '${outputPath}' (URI: ${fileUrl})`
239
+ }
240
+ ]
241
+ };
242
+ }
243
+ catch (error) {
244
+ // Возвращаем ошибку как текстовый контент
245
+ return {
246
+ content: [
247
+ {
248
+ type: "text",
249
+ text: `❌ Ошибка при генерации изображения в файл: ${error.message}`
250
+ }
251
+ ]
252
+ };
253
+ }
254
+ });
255
+ async function main() {
256
+ try {
257
+ const transport = new StdioServerTransport();
258
+ await app.connect(transport);
259
+ console.log("MCP Image Generation Server (TypeScript) is running...");
260
+ }
261
+ catch (error) {
262
+ console.error("Server error:", error);
263
+ process.exit(1);
264
+ }
265
+ }
266
+ // Запускаем сервер если этот файл запущен напрямую
267
+ if (import.meta.url === `file://${process.argv[1]}`) {
268
+ main();
269
+ }
270
+ export { main, ImageGenerator };
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vint.tri/report_gen_mcp",
3
- "version": "1.3.9",
3
+ "version": "1.4.1",
4
4
  "description": "CLI tool for generating HTML reports with embedded charts and images",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@modelcontextprotocol/sdk": "^1.0.3",
19
+ "axios": "^1.7.9",
19
20
  "chart.js": "^4.4.4",
20
21
  "commander": "^12.1.0",
21
22
  "ejs": "^3.1.10",