Files
agenticSystem/mcp-server/tools/media/uploadImageToAssets.js
2026-04-01 23:16:45 +01:00

212 lines
9.3 KiB
JavaScript

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 });
}
})
);
}