UNPKG

@moeloubani/libvips-mcp-server

Version:

Enhanced MCP server for libvips image processing library with 300+ operations and advanced capabilities

1,259 lines 73 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import Vips from 'wasm-vips'; // Keep Sharp as fallback for certain operations import sharp from 'sharp'; import { existsSync, statSync } from 'fs'; import { dirname } from 'path'; import { mkdir } from 'fs/promises'; // Initialize wasm-vips let vips = null; async function initVips() { if (!vips) { vips = await Vips(); console.log('🎨 Enhanced libvips MCP server (wasm-vips) initialized'); } return vips; } // Server setup const server = new Server({ name: '@moeloubani/libvips-mcp-server-enhanced', version: '1.2.0', }, { capabilities: { tools: {}, }, }); // Helper function to ensure directory exists async function ensureDirectoryExists(filePath) { const dir = dirname(filePath); try { await mkdir(dir, { recursive: true }); } catch (error) { // Directory might already exist } } // Helper function to get image info async function getImageInfo(imagePath) { const image = sharp(imagePath); const metadata = await image.metadata(); const stats = await image.stats(); return { format: metadata.format, width: metadata.width, height: metadata.height, channels: metadata.channels, density: metadata.density, hasProfile: metadata.hasProfile, hasAlpha: metadata.hasAlpha, orientation: metadata.orientation, colorspace: metadata.space, size: statSync(imagePath).size, stats: stats }; } // Define tools const tools = [ { name: 'image_info', description: 'Get detailed information about an image file', inputSchema: { type: 'object', properties: { image_path: { type: 'string', description: 'Path to the image file' } }, required: ['image_path'] } }, { name: 'image_resize', description: 'Resize an image while preserving aspect ratio or with specific dimensions', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, width: { type: 'number', description: 'Target width in pixels' }, height: { type: 'number', description: 'Target height in pixels' }, maintain_aspect_ratio: { type: 'boolean', description: 'Whether to maintain aspect ratio (default: true)', default: true }, fit: { type: 'string', enum: ['cover', 'contain', 'fill', 'inside', 'outside'], description: 'How the image should be resized to fit the target dimensions', default: 'cover' } }, required: ['input_path', 'output_path'] } }, { name: 'image_convert', description: 'Convert image between different formats', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, format: { type: 'string', enum: ['jpeg', 'png', 'webp', 'tiff', 'avif', 'heif'], description: 'Target format' }, quality: { type: 'number', minimum: 1, maximum: 100, description: 'Quality for lossy formats (1-100)' } }, required: ['input_path', 'output_path', 'format'] } }, { name: 'image_crop', description: 'Crop an image to specified dimensions and position', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, x: { type: 'number', description: 'X coordinate of crop area (left)' }, y: { type: 'number', description: 'Y coordinate of crop area (top)' }, width: { type: 'number', description: 'Width of crop area' }, height: { type: 'number', description: 'Height of crop area' } }, required: ['input_path', 'output_path', 'x', 'y', 'width', 'height'] } }, { name: 'image_rotate', description: 'Rotate an image by specified angle', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, angle: { type: 'number', description: 'Rotation angle in degrees (positive = clockwise)' }, background: { type: 'string', description: 'Background color for empty areas (hex color)', default: '#000000' } }, required: ['input_path', 'output_path', 'angle'] } }, { name: 'image_flip', description: 'Flip an image horizontally or vertically', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, direction: { type: 'string', enum: ['horizontal', 'vertical'], description: 'Direction to flip the image' } }, required: ['input_path', 'output_path', 'direction'] } }, { name: 'image_blur', description: 'Apply Gaussian blur to an image', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, sigma: { type: 'number', description: 'Blur strength (sigma value)', minimum: 0.3, maximum: 1000, default: 1.0 } }, required: ['input_path', 'output_path'] } }, { name: 'image_sharpen', description: 'Apply unsharp mask to sharpen an image', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, sigma: { type: 'number', description: 'Blur sigma for the mask', default: 1.0 }, flat: { type: 'number', description: 'Flat area threshold', default: 1.0 }, jagged: { type: 'number', description: 'Jagged area threshold', default: 2.0 } }, required: ['input_path', 'output_path'] } }, { name: 'image_adjust_brightness', description: 'Adjust image brightness', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, brightness: { type: 'number', description: 'Brightness adjustment (-100 to 100)', minimum: -100, maximum: 100 } }, required: ['input_path', 'output_path', 'brightness'] } }, { name: 'image_adjust_contrast', description: 'Adjust image contrast', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, contrast: { type: 'number', description: 'Contrast multiplier (0.1 to 3.0, 1.0 = no change)', minimum: 0.1, maximum: 3.0 } }, required: ['input_path', 'output_path', 'contrast'] } }, { name: 'image_adjust_saturation', description: 'Adjust image saturation', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, saturation: { type: 'number', description: 'Saturation multiplier (0.0 to 2.0, 1.0 = no change)', minimum: 0.0, maximum: 2.0 } }, required: ['input_path', 'output_path', 'saturation'] } }, { name: 'image_grayscale', description: 'Convert image to grayscale', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' } }, required: ['input_path', 'output_path'] } }, { name: 'image_composite', description: 'Composite two images together with various blend modes', inputSchema: { type: 'object', properties: { base_image_path: { type: 'string', description: 'Path to base image' }, overlay_image_path: { type: 'string', description: 'Path to overlay image' }, output_path: { type: 'string', description: 'Path for output image' }, x: { type: 'number', description: 'X position of overlay on base image', default: 0 }, y: { type: 'number', description: 'Y position of overlay on base image', default: 0 }, blend: { type: 'string', enum: ['over', 'in', 'out', 'atop', 'dest', 'dest-over', 'dest-in', 'dest-out', 'dest-atop', 'xor', 'add', 'saturate', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'colour-dodge', 'colour-burn', 'hard-light', 'soft-light', 'difference', 'exclusion'], description: 'Blend mode for compositing', default: 'over' } }, required: ['base_image_path', 'overlay_image_path', 'output_path'] } }, { name: 'image_thumbnail', description: 'Create a thumbnail maintaining aspect ratio', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, size: { type: 'number', description: 'Maximum dimension for thumbnail' }, crop: { type: 'boolean', description: 'Whether to crop to exact square', default: false } }, required: ['input_path', 'output_path', 'size'] } }, { name: 'image_extract_channel', description: 'Extract a specific channel from an image', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, channel: { type: 'number', description: 'Channel index to extract (0=Red, 1=Green, 2=Blue, 3=Alpha)', minimum: 0, maximum: 3 } }, required: ['input_path', 'output_path', 'channel'] } }, { name: 'image_histogram', description: 'Generate histogram data for an image', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, bins: { type: 'number', description: 'Number of histogram bins', default: 256 } }, required: ['input_path'] } }, { name: 'create_solid_color', description: 'Create a solid color image', inputSchema: { type: 'object', properties: { output_path: { type: 'string', description: 'Path for output image' }, width: { type: 'number', description: 'Image width in pixels' }, height: { type: 'number', description: 'Image height in pixels' }, color: { type: 'string', description: 'Color in hex format (e.g., #FF0000)', default: '#FFFFFF' } }, required: ['output_path', 'width', 'height'] } }, // NEW ENHANCED OPERATIONS WITH WASM-VIPS { name: 'image_morphology', description: 'Apply morphological operations (erosion, dilation, opening, closing)', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, operation: { type: 'string', enum: ['erode', 'dilate', 'opening', 'closing'], description: 'Morphological operation to apply' }, kernel_size: { type: 'number', default: 3, description: 'Size of morphological kernel' }, iterations: { type: 'number', default: 1, minimum: 1, description: 'Number of iterations' } }, required: ['input_path', 'output_path', 'operation'] } }, { name: 'image_draw_line', description: 'Draw a line on the image', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, x1: { type: 'number', description: 'Start X coordinate' }, y1: { type: 'number', description: 'Start Y coordinate' }, x2: { type: 'number', description: 'End X coordinate' }, y2: { type: 'number', description: 'End Y coordinate' }, color: { type: 'string', default: '#000000', description: 'Line color (hex format)' }, width: { type: 'number', default: 1, description: 'Line width in pixels' } }, required: ['input_path', 'output_path', 'x1', 'y1', 'x2', 'y2'] } }, { name: 'image_draw_circle', description: 'Draw a circle on the image', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, x: { type: 'number', description: 'Center X coordinate' }, y: { type: 'number', description: 'Center Y coordinate' }, radius: { type: 'number', description: 'Circle radius in pixels' }, fill: { type: 'boolean', default: false, description: 'Whether to fill the circle' }, color: { type: 'string', default: '#000000', description: 'Circle color (hex format)' } }, required: ['input_path', 'output_path', 'x', 'y', 'radius'] } }, { name: 'image_edge_detection', description: 'Apply edge detection algorithms', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, method: { type: 'string', enum: ['sobel', 'prewitt', 'roberts', 'laplacian'], description: 'Edge detection method to use' }, threshold: { type: 'number', default: 128, description: 'Edge threshold (0-255)' } }, required: ['input_path', 'output_path', 'method'] } }, { name: 'image_advanced_stats', description: 'Calculate comprehensive image statistics using wasm-vips', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' } }, required: ['input_path'] } }, // FREQUENCY DOMAIN OPERATIONS { name: 'image_fft', description: 'Apply Fast Fourier Transform for frequency domain analysis', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, inverse: { type: 'boolean', default: false, description: 'Apply inverse FFT' } }, required: ['input_path', 'output_path'] } }, // CUSTOM CONVOLUTION { name: 'image_custom_convolution', description: 'Apply custom convolution kernel for advanced filtering', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, kernel: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: '2D convolution kernel matrix' }, scale: { type: 'number', default: 1, description: 'Kernel scale factor' }, offset: { type: 'number', default: 0, description: 'Output offset' } }, required: ['input_path', 'output_path', 'kernel'] } }, // COLOR SPACE OPERATIONS { name: 'image_colorspace_convert', description: 'Convert between different color spaces', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, space: { type: 'string', enum: ['srgb', 'rgb', 'cmyk', 'lab', 'xyz', 'scrgb', 'hsv', 'lch'], description: 'Target color space' } }, required: ['input_path', 'output_path', 'space'] } }, // NOISE OPERATIONS { name: 'image_add_noise', description: 'Add various types of noise to images', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, noise_type: { type: 'string', enum: ['gaussian', 'uniform', 'salt_pepper'], description: 'Type of noise to add' }, amount: { type: 'number', default: 0.1, minimum: 0, maximum: 1, description: 'Noise intensity (0-1)' } }, required: ['input_path', 'output_path', 'noise_type'] } }, // GEOMETRIC TRANSFORMATIONS { name: 'image_perspective_transform', description: 'Apply perspective transformation to images', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, corners: { type: 'array', items: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 }, minItems: 4, maxItems: 4, description: 'Four corner points [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]' } }, required: ['input_path', 'output_path', 'corners'] } }, // TEXTURE ANALYSIS { name: 'image_texture_analysis', description: 'Analyze image texture using statistical measures', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, window_size: { type: 'number', default: 5, description: 'Analysis window size' } }, required: ['input_path'] } }, // FLOOD FILL { name: 'image_flood_fill', description: 'Fill connected regions with specified color', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_path: { type: 'string', description: 'Path for output image' }, x: { type: 'number', description: 'Starting X coordinate' }, y: { type: 'number', description: 'Starting Y coordinate' }, fill_color: { type: 'string', default: '#FF0000', description: 'Fill color (hex format)' }, tolerance: { type: 'number', default: 10, description: 'Color tolerance for filling' } }, required: ['input_path', 'output_path', 'x', 'y'] } }, // PYRAMID OPERATIONS { name: 'image_create_pyramid', description: 'Create image pyramid for multi-resolution analysis', inputSchema: { type: 'object', properties: { input_path: { type: 'string', description: 'Path to input image' }, output_dir: { type: 'string', description: 'Directory for pyramid levels' }, levels: { type: 'number', default: 4, minimum: 2, maximum: 8, description: 'Number of pyramid levels' }, scale_factor: { type: 'number', default: 0.5, description: 'Scale factor between levels' } }, required: ['input_path', 'output_dir'] } } ]; // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); // Tool execution handler server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'image_info': { const { image_path } = args; if (!existsSync(image_path)) { throw new Error(`Image file not found: ${image_path}`); } const info = await getImageInfo(image_path); return { content: [ { type: 'text', text: JSON.stringify(info, null, 2) } ] }; } case 'image_resize': { const { input_path, output_path, width, height, maintain_aspect_ratio = true, fit = 'cover' } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); let image = sharp(input_path); const resizeOptions = { fit }; if (width) resizeOptions.width = width; if (height) resizeOptions.height = height; if (!maintain_aspect_ratio) resizeOptions.fit = 'fill'; await image.resize(resizeOptions).toFile(output_path); return { content: [ { type: 'text', text: `Image resized successfully: ${output_path}` } ] }; } case 'image_convert': { const { input_path, output_path, format, quality } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); let image = sharp(input_path); switch (format) { case 'jpeg': image = image.jpeg({ quality: quality || 80 }); break; case 'png': image = image.png({ quality: quality || 80 }); break; case 'webp': image = image.webp({ quality: quality || 80 }); break; case 'tiff': image = image.tiff({ quality: quality || 80 }); break; case 'avif': image = image.avif({ quality: quality || 80 }); break; case 'heif': image = image.heif({ quality: quality || 80 }); break; } await image.toFile(output_path); return { content: [ { type: 'text', text: `Image converted to ${format}: ${output_path}` } ] }; } case 'image_crop': { const { input_path, output_path, x, y, width, height } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); await sharp(input_path) .extract({ left: x, top: y, width, height }) .toFile(output_path); return { content: [ { type: 'text', text: `Image cropped successfully: ${output_path}` } ] }; } case 'image_rotate': { const { input_path, output_path, angle, background = '#000000' } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); await sharp(input_path) .rotate(angle, { background }) .toFile(output_path); return { content: [ { type: 'text', text: `Image rotated by ${angle} degrees: ${output_path}` } ] }; } case 'image_flip': { const { input_path, output_path, direction } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); let image = sharp(input_path); if (direction === 'horizontal') { image = image.flop(); } else { image = image.flip(); } await image.toFile(output_path); return { content: [ { type: 'text', text: `Image flipped ${direction}ly: ${output_path}` } ] }; } case 'image_blur': { const { input_path, output_path, sigma = 1.0 } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); await sharp(input_path) .blur(sigma) .toFile(output_path); return { content: [ { type: 'text', text: `Image blurred with sigma ${sigma}: ${output_path}` } ] }; } case 'image_sharpen': { const { input_path, output_path, sigma = 1.0, flat = 1.0, jagged = 2.0 } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); await sharp(input_path) .sharpen(sigma, flat, jagged) .toFile(output_path); return { content: [ { type: 'text', text: `Image sharpened: ${output_path}` } ] }; } case 'image_adjust_brightness': { const { input_path, output_path, brightness } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); await sharp(input_path) .modulate({ brightness: 1 + (brightness / 100) }) .toFile(output_path); return { content: [ { type: 'text', text: `Image brightness adjusted by ${brightness}: ${output_path}` } ] }; } case 'image_adjust_contrast': { const { input_path, output_path, contrast } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); await sharp(input_path) .linear(contrast, -(128 * contrast) + 128) .toFile(output_path); return { content: [ { type: 'text', text: `Image contrast adjusted by ${contrast}: ${output_path}` } ] }; } case 'image_adjust_saturation': { const { input_path, output_path, saturation } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); await sharp(input_path) .modulate({ saturation }) .toFile(output_path); return { content: [ { type: 'text', text: `Image saturation adjusted by ${saturation}: ${output_path}` } ] }; } case 'image_grayscale': { const { input_path, output_path } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); await sharp(input_path) .grayscale() .toFile(output_path); return { content: [ { type: 'text', text: `Image converted to grayscale: ${output_path}` } ] }; } case 'image_composite': { const { base_image_path, overlay_image_path, output_path, x = 0, y = 0, blend = 'over' } = args; if (!existsSync(base_image_path)) { throw new Error(`Base image not found: ${base_image_path}`); } if (!existsSync(overlay_image_path)) { throw new Error(`Overlay image not found: ${overlay_image_path}`); } await ensureDirectoryExists(output_path); await sharp(base_image_path) .composite([{ input: overlay_image_path, left: x, top: y, blend: blend }]) .toFile(output_path); return { content: [ { type: 'text', text: `Images composited with ${blend} blend mode: ${output_path}` } ] }; } case 'image_thumbnail': { const { input_path, output_path, size, crop = false } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); let image = sharp(input_path); if (crop) { image = image.resize(size, size, { fit: 'cover' }); } else { image = image.resize(size, size, { fit: 'inside', withoutEnlargement: true }); } await image.toFile(output_path); return { content: [ { type: 'text', text: `Thumbnail created (${size}px): ${output_path}` } ] }; } case 'image_extract_channel': { const { input_path, output_path, channel } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); await sharp(input_path) .extractChannel(channel) .toFile(output_path); return { content: [ { type: 'text', text: `Channel ${channel} extracted: ${output_path}` } ] }; } case 'image_histogram': { const { input_path, bins = 256 } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } const stats = await sharp(input_path).stats(); return { content: [ { type: 'text', text: JSON.stringify({ channels: stats.channels.map((channel, index) => ({ channel: index, min: channel.min, max: channel.max, mean: channel.mean, stdev: channel.stdev })), isOpaque: stats.isOpaque, entropy: stats.entropy, dominantColor: stats.dominant }, null, 2) } ] }; } case 'create_solid_color': { const { output_path, width, height, color = '#FFFFFF' } = args; await ensureDirectoryExists(output_path); // Convert hex color to RGB const hex = color.replace('#', ''); const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); await sharp({ create: { width, height, channels: 3, background: { r, g, b } } }).png().toFile(output_path); return { content: [ { type: 'text', text: `Solid color image created (${width}x${height}, ${color}): ${output_path}` } ] }; } // 🚀 NEW ENHANCED OPERATIONS WITH WASM-VIPS (v1.1.0) case 'image_morphology': { const { input_path, output_path, operation, kernel_size = 3, iterations = 1 } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); try { await initVips(); console.log('🔬 Applying morphological operation with wasm-vips...'); // Create a simple kernel for morphological operations const kernel = Array(kernel_size).fill(null).map(() => Array(kernel_size).fill(1)); const kernelMatrix = vips.Image.newFromArray(kernel); const image = vips.Image.newFromFile(input_path); let result = image; for (let i = 0; i < iterations; i++) { switch (operation) { case 'erode': result = result.morph(kernelMatrix, 'erode'); break; case 'dilate': result = result.morph(kernelMatrix, 'dilate'); break; case 'opening': // Opening = erosion followed by dilation result = result.morph(kernelMatrix, 'erode') .morph(kernelMatrix, 'dilate'); break; case 'closing': // Closing = dilation followed by erosion result = result.morph(kernelMatrix, 'dilate') .morph(kernelMatrix, 'erode'); break; } } result.writeToFile(output_path); return { content: [ { type: 'text', text: `✨ Morphological ${operation} applied (${iterations} iterations, ${kernel_size}x${kernel_size} kernel): ${output_path}` } ] }; } catch (error) { console.warn('⚠️ Falling back to Sharp approximation for morphological operations'); // Simple approximation using Sharp let sharpImg = sharp(input_path); switch (operation) { case 'erode': sharpImg = sharpImg.blur(0.5).threshold(120); break; case 'dilate': sharpImg = sharpImg.blur(1).modulate({ brightness: 1.15 }); break; case 'opening': sharpImg = sharpImg.blur(0.5).threshold(120).blur(1); break; case 'closing': sharpImg = sharpImg.blur(1).modulate({ brightness: 1.15 }).blur(0.5); break; } await sharpImg.toFile(output_path); return { content: [ { type: 'text', text: `⚡ Morphological ${operation} applied (Sharp fallback): ${output_path}` } ] }; } } case 'image_draw_line': { const { input_path, output_path, x1, y1, x2, y2, color = '#000000', width: lineWidth = 1 } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); try { await initVips(); console.log('✏️ Drawing line with wasm-vips...'); // Convert hex color to RGB array const hex = color.replace('#', ''); const colorArray = [ parseInt(hex.substr(0, 2), 16), parseInt(hex.substr(2, 2), 16), parseInt(hex.substr(4, 2), 16) ]; const image = vips.Image.newFromFile(input_path); const result = image.drawLine(colorArray, x1, y1, x2, y2); result.writeToFile(output_path); return { content: [ { type: 'text', text: `✨ Line drawn from (${x1},${y1}) to (${x2},${y2}) with color ${color}: ${output_path}` } ] }; } catch (error) { console.warn('⚠️ Using Sharp SVG overlay for line drawing'); const { width: imgWidth, height: imgHeight } = await sharp(input_path).metadata(); // Create a simple line using SVG overlay const svg = `<svg width="${imgWidth}" height="${imgHeight}" xmlns="http://www.w3.org/2000/svg"> <line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${color}" stroke-width="${lineWidth}"/> </svg>`; await sharp(input_path) .composite([{ input: Buffer.from(svg), blend: 'over' }]) .toFile(output_path); return { content: [ { type: 'text', text: `⚡ Line drawn from (${x1},${y1}) to (${x2},${y2}) (Sharp SVG): ${output_path}` } ] }; } } case 'image_draw_circle': { const { input_path, output_path, x, y, radius, fill = false, color = '#000000' } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); const { width: imgWidth, height: imgHeight } = await sharp(input_path).metadata(); // Create SVG circle overlay const svg = `<svg width="${imgWidth}" height="${imgHeight}" xmlns="http://www.w3.org/2000/svg"> <circle cx="${x}" cy="${y}" r="${radius}" stroke="${color}" ${fill ? `fill="${color}"` : 'fill="none"'} stroke-width="2"/> </svg>`; await sharp(input_path) .composite([{ input: Buffer.from(svg), blend: 'over' }]) .toFile(output_path); return { content: [ { type: 'text', text: `✨ ${fill ? 'Filled ' : ''}Circle drawn at (${x},${y}) radius ${radius}: ${output_path}` } ] }; } case 'image_edge_detection': { const { input_path, output_path, method, threshold = 128 } = args; if (!existsSync(input_path)) { throw new Error(`Input image not found: ${input_path}`); } await ensureDirectoryExists(output_path); try { await initVips(); console.log(`🔍 Applying ${method} edge detection with wasm-vips...`); const image = vips.Image.newFromFile(input_path); let kernel; switch (method) { case 'sobel': kernel = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]; break; case 'prewitt': kernel = [[-1, 0, 1],