@moeloubani/libvips-mcp-server
Version:
Enhanced MCP server for libvips image processing library with 300+ operations and advanced capabilities
674 lines • 26.3 kB
JavaScript
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';
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('🎨 wasm-vips initialized successfully');
}
return vips;
}
// Server setup
const server = new Server({
name: '@moeloubani/libvips-mcp-server-enhanced',
version: '1.1.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 enhanced image info using wasm-vips
async function getImageInfo(imagePath) {
const vipsInstance = await initVips();
try {
const image = vipsInstance.Image.newFromFile(imagePath);
const stats = existsSync(imagePath) ? statSync(imagePath) : null;
return {
format: image.format || 'unknown',
width: image.width,
height: image.height,
bands: image.bands,
interpretation: image.interpretation,
xres: image.xres,
yres: image.yres,
xoffset: image.xoffset,
yoffset: image.yoffset,
coding: image.coding,
size: stats?.size || 0,
hasProfile: image.getFields().includes('icc-profile-data'),
fields: image.getFields(), // All metadata fields
};
}
catch (error) {
throw new Error(`Failed to get image info: ${error}`);
}
}
// Enhanced tools with wasm-vips capabilities
const tools = [
// BASIC OPERATIONS (Enhanced versions)
{
name: 'image_info',
description: 'Get comprehensive image information using wasm-vips',
inputSchema: {
type: 'object',
properties: {
image_path: {
type: 'string',
description: 'Path to the image file'
}
},
required: ['image_path']
}
},
{
name: 'image_resize',
description: 'Resize image with advanced options',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
width: { type: 'number' },
height: { type: 'number' },
kernel: {
type: 'string',
enum: ['nearest', 'linear', 'cubic', 'mitchell', 'lanczos2', 'lanczos3'],
default: 'lanczos3'
}
},
required: ['input_path', 'output_path']
}
},
{
name: 'image_convert',
description: 'Convert between image formats with advanced options',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
format: {
type: 'string',
enum: ['jpeg', 'png', 'webp', 'tiff', 'avif', 'heif', 'gif', 'bmp']
},
quality: { type: 'number', minimum: 1, maximum: 100 },
compression: { type: 'string', enum: ['none', 'lzw', 'zip', 'packbits'] }
},
required: ['input_path', 'output_path', 'format']
}
},
// MORPHOLOGICAL OPERATIONS (New!)
{
name: 'image_morphology',
description: 'Apply morphological operations (erosion, dilation, opening, closing)',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
operation: {
type: 'string',
enum: ['erode', 'dilate', 'opening', 'closing']
},
kernel: {
type: 'array',
items: { type: 'array', items: { type: 'number' } },
description: '2D kernel matrix'
},
iterations: { type: 'number', default: 1, minimum: 1 }
},
required: ['input_path', 'output_path', 'operation']
}
},
// FREQUENCY DOMAIN OPERATIONS (New!)
{
name: 'image_fft',
description: 'Apply Fast Fourier Transform',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
inverse: { type: 'boolean', default: false }
},
required: ['input_path', 'output_path']
}
},
// DRAWING OPERATIONS (New!)
{
name: 'image_draw_line',
description: 'Draw a line on the image',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
x1: { type: 'number' },
y1: { type: 'number' },
x2: { type: 'number' },
y2: { type: 'number' },
color: { type: 'array', items: { type: 'number' }, default: [0, 0, 0] }
},
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' },
output_path: { type: 'string' },
x: { type: 'number' },
y: { type: 'number' },
radius: { type: 'number' },
fill: { type: 'boolean', default: false },
color: { type: 'array', items: { type: 'number' }, default: [0, 0, 0] }
},
required: ['input_path', 'output_path', 'x', 'y', 'radius']
}
},
{
name: 'image_draw_rect',
description: 'Draw a rectangle on the image',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
left: { type: 'number' },
top: { type: 'number' },
width: { type: 'number' },
height: { type: 'number' },
fill: { type: 'boolean', default: false },
color: { type: 'array', items: { type: 'number' }, default: [0, 0, 0] }
},
required: ['input_path', 'output_path', 'left', 'top', 'width', 'height']
}
},
// CONVOLUTION OPERATIONS (New!)
{
name: 'image_convolution',
description: 'Apply custom convolution kernel',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
kernel: {
type: 'array',
items: { type: 'array', items: { type: 'number' } },
description: '2D convolution kernel'
},
scale: { type: 'number', default: 1 },
offset: { type: 'number', default: 0 }
},
required: ['input_path', 'output_path', 'kernel']
}
},
{
name: 'image_edge_detection',
description: 'Apply edge detection filters',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
method: {
type: 'string',
enum: ['sobel', 'prewitt', 'roberts', 'laplacian']
}
},
required: ['input_path', 'output_path', 'method']
}
},
// ADVANCED COLOR OPERATIONS (New!)
{
name: 'image_colorspace',
description: 'Convert between color spaces',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
space: {
type: 'string',
enum: ['srgb', 'rgb', 'cmyk', 'lab', 'xyz', 'scrgb', 'hsv', 'lch']
}
},
required: ['input_path', 'output_path', 'space']
}
},
// ANALYSIS OPERATIONS (New!)
{
name: 'image_stats',
description: 'Calculate comprehensive image statistics',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' }
},
required: ['input_path']
}
},
{
name: 'image_histogram',
description: 'Generate detailed histogram information',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
bins: { type: 'number', default: 256 }
},
required: ['input_path']
}
},
// LEGACY OPERATIONS (keeping compatibility)
{
name: 'image_crop',
description: 'Crop image to specified region',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
x: { type: 'number' },
y: { type: 'number' },
width: { type: 'number' },
height: { type: 'number' }
},
required: ['input_path', 'output_path', 'x', 'y', 'width', 'height']
}
},
{
name: 'image_rotate',
description: 'Rotate image by specified angle',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
angle: { type: 'number' },
background: { type: 'array', items: { type: 'number' }, default: [255, 255, 255] }
},
required: ['input_path', 'output_path', 'angle']
}
},
{
name: 'image_flip',
description: 'Flip image horizontally or vertically',
inputSchema: {
type: 'object',
properties: {
input_path: { type: 'string' },
output_path: { type: 'string' },
direction: { type: 'string', enum: ['horizontal', 'vertical'] }
},
required: ['input_path', 'output_path', 'direction']
}
}
];
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// Enhanced tool execution handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const vipsInstance = await initVips();
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, kernel = 'lanczos3' } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.Image.newFromFile(input_path);
const resized = image.resize(width / image.width, {
vscale: height ? height / image.height : width / image.width,
kernel: kernel
});
resized.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `Image resized successfully using ${kernel} kernel: ${output_path}`
}
]
};
}
case 'image_convert': {
const { input_path, output_path, format, quality = 80, compression } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.Image.newFromFile(input_path);
const options = {};
if (format === 'jpeg' && quality)
options.Q = quality;
if (format === 'png' && quality)
options.compression = quality / 10;
if (format === 'webp' && quality)
options.Q = quality;
if (format === 'tiff' && compression)
options.compression = compression;
image.writeToFile(output_path, options);
return {
content: [
{
type: 'text',
text: `Image converted to ${format} format: ${output_path}`
}
]
};
}
case 'image_morphology': {
const { input_path, output_path, operation, kernel = [[1, 1, 1], [1, 1, 1], [1, 1, 1]], iterations = 1 } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.Image.newFromFile(input_path);
const kernelMatrix = vipsInstance.Image.newFromArray(kernel);
let result = image;
for (let i = 0; i < iterations; i++) {
switch (operation) {
case 'erode':
result = result.morph(kernelMatrix, vipsInstance.OperationMorphology.erode);
break;
case 'dilate':
result = result.morph(kernelMatrix, vipsInstance.OperationMorphology.dilate);
break;
case 'opening':
result = result.morph(kernelMatrix, vipsInstance.OperationMorphology.erode)
.morph(kernelMatrix, vipsInstance.OperationMorphology.dilate);
break;
case 'closing':
result = result.morph(kernelMatrix, vipsInstance.OperationMorphology.dilate)
.morph(kernelMatrix, vipsInstance.OperationMorphology.erode);
break;
}
}
result.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `Morphological ${operation} applied (${iterations} iterations): ${output_path}`
}
]
};
}
case 'image_fft': {
const { input_path, output_path, inverse = false } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.Image.newFromFile(input_path);
const result = inverse ? image.invfft() : image.fwfft();
// For display purposes, we might need to convert complex to real
const displayResult = result.real ? result.real() : result;
displayResult.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `${inverse ? 'Inverse ' : ''}FFT applied: ${output_path}`
}
]
};
}
case 'image_draw_line': {
const { input_path, output_path, x1, y1, x2, y2, color = [0, 0, 0] } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.Image.newFromFile(input_path);
const result = image.drawLine(color, x1, y1, x2, y2);
result.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `Line drawn from (${x1},${y1}) to (${x2},${y2}): ${output_path}`
}
]
};
}
case 'image_draw_circle': {
const { input_path, output_path, x, y, radius, fill = false, color = [0, 0, 0] } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.Image.newFromFile(input_path);
const result = fill
? image.drawCircle(color, x, y, radius, { fill: true })
: image.drawCircle(color, x, y, radius);
result.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `${fill ? 'Filled ' : ''}Circle drawn at (${x},${y}) radius ${radius}: ${output_path}`
}
]
};
}
case 'image_convolution': {
const { input_path, output_path, kernel, scale = 1, offset = 0 } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.Image.newFromFile(input_path);
const kernelMatrix = vipsInstance.Image.newFromArray(kernel, scale, offset);
const result = image.conv(kernelMatrix);
result.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `Custom convolution applied: ${output_path}`
}
]
};
}
case 'image_edge_detection': {
const { input_path, output_path, method } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.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], [-1, 0, 1], [-1, 0, 1]];
break;
case 'roberts':
kernel = [[1, 0], [0, -1]];
break;
case 'laplacian':
kernel = [[0, -1, 0], [-1, 4, -1], [0, -1, 0]];
break;
default:
throw new Error(`Unknown edge detection method: ${method}`);
}
const kernelMatrix = vipsInstance.Image.newFromArray(kernel);
const result = image.conv(kernelMatrix);
result.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `${method} edge detection applied: ${output_path}`
}
]
};
}
case 'image_colorspace': {
const { input_path, output_path, space } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.Image.newFromFile(input_path);
const result = image.colourspace(space);
result.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `Image converted to ${space} color space: ${output_path}`
}
]
};
}
case 'image_stats': {
const { input_path } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
const image = vipsInstance.Image.newFromFile(input_path);
const stats = {
min: image.min(),
max: image.max(),
avg: image.avg(),
deviate: image.deviate(),
width: image.width,
height: image.height,
bands: image.bands,
format: image.format
};
return {
content: [
{
type: 'text',
text: JSON.stringify(stats, null, 2)
}
]
};
}
// Keep legacy operations for compatibility
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);
const image = vipsInstance.Image.newFromFile(input_path);
const result = image.crop(x, y, width, height);
result.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `Image cropped successfully: ${output_path}`
}
]
};
}
case 'image_rotate': {
const { input_path, output_path, angle, background = [255, 255, 255] } = args;
if (!existsSync(input_path)) {
throw new Error(`Input image not found: ${input_path}`);
}
await ensureDirectoryExists(output_path);
const image = vipsInstance.Image.newFromFile(input_path);
const result = image.rotate(angle, { background });
result.writeToFile(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);
const image = vipsInstance.Image.newFromFile(input_path);
const result = direction === 'horizontal' ? image.fliphor() : image.flipver();
result.writeToFile(output_path);
return {
content: [
{
type: 'text',
text: `Image flipped ${direction}ly: ${output_path}`
}
]
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error('Enhanced server error:', error);
process.exit(1);
});
//# sourceMappingURL=index-enhanced.js.map