Initial commit
This commit is contained in:
211
mcp-server/tools/media/uploadImageToAssets.js
Normal file
211
mcp-server/tools/media/uploadImageToAssets.js
Normal file
@@ -0,0 +1,211 @@
|
||||
import { z } from "zod";
|
||||
import axios from "axios";
|
||||
import sharp from "sharp";
|
||||
import { withAuth, getSessionCredentials } from "../../auth/index.js";
|
||||
import { handleToolError } from "../helpers/errorHandler.js";
|
||||
import { saveFileBuilder } from "../helpers/fileBuilder.js";
|
||||
import { withAuthParams } from "../helpers/authSchema.js";
|
||||
|
||||
/**
|
||||
* Upload an image to the website assets folder
|
||||
* Accepts base64, data URI, or URL
|
||||
* Optionally resizes/compresses the image
|
||||
*/
|
||||
export function registerUploadImageToAssetsTool(server) {
|
||||
server.tool(
|
||||
"upload_image_to_assets",
|
||||
"Upload an image to website assets (/images/). Accepts: base64, data URI, or URL. Optional resize (maxWidth/maxHeight) and compression (quality). Returns public URL.",
|
||||
withAuthParams({
|
||||
image: z.string().describe("Image data: base64 string, data URI, or URL to download from"),
|
||||
fileName: z.string().optional().describe("Custom filename (without extension). If not provided, auto-generated name will be used"),
|
||||
path: z.string().optional().default("/images/").describe("Path within assets folder (default: '/images/')"),
|
||||
|
||||
// Resize options
|
||||
maxWidth: z.number().optional().describe("Maximum width in pixels. Image will be resized proportionally if larger"),
|
||||
maxHeight: z.number().optional().describe("Maximum height in pixels. Image will be resized proportionally if larger"),
|
||||
quality: z.number().min(1).max(100).optional().default(85).describe("JPEG/WebP quality (1-100, default: 85)"),
|
||||
format: z.enum(["png", "jpg", "webp"]).optional().default("png").describe("Output format (default: png)"),
|
||||
}),
|
||||
{ readOnlyHint: false, destructiveHint: false },
|
||||
withAuth(async ({
|
||||
image,
|
||||
fileName,
|
||||
path: assetsPath = "/images/",
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
quality = 85,
|
||||
format = "png"
|
||||
}, extra) => {
|
||||
try {
|
||||
let imageBuffer;
|
||||
|
||||
// Step 1: Get image buffer from various sources
|
||||
if (image.startsWith('data:')) {
|
||||
// Data URI format: data:image/png;base64,xxxxx
|
||||
const match = image.match(/data:image\/[^;]+;base64,(.+)/);
|
||||
if (match) {
|
||||
imageBuffer = Buffer.from(match[1], 'base64');
|
||||
} else {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "Error: Invalid data URI format. Expected: data:image/xxx;base64,..."
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
} else if (image.startsWith('http://') || image.startsWith('https://')) {
|
||||
// URL - download the image
|
||||
try {
|
||||
const response = await axios.get(image, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 30000,
|
||||
maxContentLength: 50 * 1024 * 1024 // 50MB max
|
||||
});
|
||||
imageBuffer = Buffer.from(response.data, 'binary');
|
||||
} catch (downloadError) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Error downloading image from URL: ${downloadError.message}`
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Assume it's raw base64
|
||||
try {
|
||||
imageBuffer = Buffer.from(image, 'base64');
|
||||
} catch (base64Error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "Error: Could not parse image data. Provide base64, data URI, or URL"
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Validate we have a buffer
|
||||
if (!imageBuffer || imageBuffer.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "Error: No valid image data received"
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 2: Process image with sharp (resize/compress)
|
||||
let sharpInstance = sharp(imageBuffer);
|
||||
|
||||
// Get original metadata
|
||||
const metadata = await sharpInstance.metadata();
|
||||
const originalSize = imageBuffer.length;
|
||||
|
||||
// Resize if dimensions specified
|
||||
if (maxWidth || maxHeight) {
|
||||
sharpInstance = sharpInstance.resize({
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
fit: 'inside', // Maintain aspect ratio
|
||||
withoutEnlargement: true // Don't upscale
|
||||
});
|
||||
}
|
||||
|
||||
// Convert to target format with quality
|
||||
let outputBuffer;
|
||||
let mimeType;
|
||||
let extension;
|
||||
|
||||
switch (format) {
|
||||
case 'jpg':
|
||||
outputBuffer = await sharpInstance.jpeg({ quality }).toBuffer();
|
||||
mimeType = 'image/jpeg';
|
||||
extension = 'jpg';
|
||||
break;
|
||||
case 'webp':
|
||||
outputBuffer = await sharpInstance.webp({ quality }).toBuffer();
|
||||
mimeType = 'image/webp';
|
||||
extension = 'webp';
|
||||
break;
|
||||
case 'png':
|
||||
default:
|
||||
outputBuffer = await sharpInstance.png({
|
||||
compressionLevel: Math.floor((100 - quality) / 11) // 0-9 compression
|
||||
}).toBuffer();
|
||||
mimeType = 'image/png';
|
||||
extension = 'png';
|
||||
break;
|
||||
}
|
||||
|
||||
// Step 3: Upload to assets
|
||||
const base64Image = outputBuffer.toString('base64');
|
||||
|
||||
// Generate filename if not provided
|
||||
const finalFileName = fileName
|
||||
? fileName.replace(/\.(jpg|jpeg|png|webp|gif)$/i, '') + '.' + extension
|
||||
: `uploaded-${Date.now()}.${extension}`;
|
||||
|
||||
// Get credentials
|
||||
const credentials = await getSessionCredentials(extra.sessionId);
|
||||
|
||||
// Upload using saveFileBuilder
|
||||
const uploadResult = await saveFileBuilder({
|
||||
web_url: credentials.web_url,
|
||||
token: credentials.token,
|
||||
tokenHash: credentials.tokenHash,
|
||||
path: assetsPath,
|
||||
fileName: finalFileName,
|
||||
content: base64Image,
|
||||
rawDataSended: false
|
||||
});
|
||||
|
||||
if (uploadResult && uploadResult.success) {
|
||||
// Build the public URL for the uploaded image
|
||||
const imageUrl = `${credentials.web_url}/template/estandar/images/${finalFileName}`;
|
||||
|
||||
// Get final metadata
|
||||
const finalMetadata = await sharp(outputBuffer).metadata();
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
imageUrl: imageUrl,
|
||||
fileName: finalFileName,
|
||||
path: assetsPath,
|
||||
format: format,
|
||||
originalSize: originalSize,
|
||||
finalSize: outputBuffer.length,
|
||||
compressionRatio: ((1 - outputBuffer.length / originalSize) * 100).toFixed(1) + '%',
|
||||
dimensions: {
|
||||
original: { width: metadata.width, height: metadata.height },
|
||||
final: { width: finalMetadata.width, height: finalMetadata.height }
|
||||
},
|
||||
message: `Image uploaded successfully. Use this URL: ${imageUrl}`
|
||||
}, null, 2)
|
||||
}],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: uploadResult?.message || "Unknown error uploading to assets"
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return handleToolError(error, 'upload_image_to_assets', { fileName, path: assetsPath });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user