@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.
- package/PUBLICATION_CONFIRMATION_V1.4.0.md +54 -0
- package/UPDATED_NEURAL_NETWORK_INSTRUCTIONS.md +4 -1
- package/VERSION_1.4.0_RELEASE_NOTES.md +88 -0
- package/dist/index.js +93 -267
- package/dist/mcp/imageEditingServer.js +294 -0
- package/dist/mcp/imageGenerationServer.js +270 -0
- package/lizard_wedding.png +0 -0
- package/lizard_wedding_bitter.jpg +0 -0
- package/moose_image.png +0 -0
- package/package.json +2 -1
|
@@ -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.
|
|
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.
|
|
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
|
|
452
|
-
const {
|
|
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
|
|
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()}.
|
|
480
|
+
const fileName = outputFile || `generated-image-${Date.now()}.jpeg`;
|
|
475
481
|
const fullPath = path.resolve(outputDir, fileName);
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
661
|
-
const {
|
|
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
|
-
//
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
package/moose_image.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vint.tri/report_gen_mcp",
|
|
3
|
-
"version": "1.
|
|
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",
|