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