@moeloubani/libvips-mcp-server
Version:
Enhanced MCP server for libvips image processing library with 300+ operations and advanced capabilities
1,259 lines • 73 kB
JavaScript
#!/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],