Skip to main content
Glama
index.ts14.5 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import sharp from 'sharp'; import { promises as fs } from 'fs'; import path from 'path'; const server = new Server( { name: 'imagician', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'resize_image', description: 'Resize an image to specified dimensions', inputSchema: { type: 'object', properties: { inputPath: { type: 'string', description: 'Path to input image' }, outputPath: { type: 'string', description: 'Path to save resized image' }, width: { type: 'number', description: 'Target width in pixels' }, height: { type: 'number', description: 'Target height in pixels' }, fit: { type: 'string', enum: ['cover', 'contain', 'fill', 'inside', 'outside'], description: 'How the image should be resized to fit', default: 'cover' }, preserveAspectRatio: { type: 'boolean', description: 'Maintain original aspect ratio', default: true } }, required: ['inputPath', 'outputPath'] } }, { name: 'convert_format', description: 'Convert image between formats (jpeg, png, webp, avif)', inputSchema: { type: 'object', properties: { inputPath: { type: 'string', description: 'Path to input image' }, outputPath: { type: 'string', description: 'Path to save converted image' }, format: { type: 'string', enum: ['jpeg', 'jpg', 'png', 'webp', 'avif'], description: 'Target format' }, quality: { type: 'number', minimum: 1, maximum: 100, description: 'Quality for lossy formats (1-100)', default: 80 } }, required: ['inputPath', 'outputPath', 'format'] } }, { name: 'crop_image', description: 'Crop an image to specified region', inputSchema: { type: 'object', properties: { inputPath: { type: 'string', description: 'Path to input image' }, outputPath: { type: 'string', description: 'Path to save cropped image' }, left: { type: 'number', description: 'Left offset in pixels' }, top: { type: 'number', description: 'Top offset in pixels' }, width: { type: 'number', description: 'Width of crop area' }, height: { type: 'number', description: 'Height of crop area' } }, required: ['inputPath', 'outputPath', 'left', 'top', 'width', 'height'] } }, { name: 'compress_image', description: 'Compress an image with quality settings', inputSchema: { type: 'object', properties: { inputPath: { type: 'string', description: 'Path to input image' }, outputPath: { type: 'string', description: 'Path to save compressed image' }, quality: { type: 'number', minimum: 1, maximum: 100, description: 'Compression quality (1-100)', default: 80 }, progressive: { type: 'boolean', description: 'Use progressive encoding (for JPEG)', default: true } }, required: ['inputPath', 'outputPath'] } }, { name: 'rotate_image', description: 'Rotate an image by specified degrees', inputSchema: { type: 'object', properties: { inputPath: { type: 'string', description: 'Path to input image' }, outputPath: { type: 'string', description: 'Path to save rotated image' }, angle: { type: 'number', description: 'Rotation angle in degrees (positive = clockwise)' }, background: { type: 'string', description: 'Background color for exposed areas (hex or named color)', default: '#000000' } }, required: ['inputPath', 'outputPath', 'angle'] } }, { name: 'flip_image', description: 'Flip an image horizontally or vertically', inputSchema: { type: 'object', properties: { inputPath: { type: 'string', description: 'Path to input image' }, outputPath: { type: 'string', description: 'Path to save flipped image' }, direction: { type: 'string', enum: ['horizontal', 'vertical', 'both'], description: 'Flip direction' } }, required: ['inputPath', 'outputPath', 'direction'] } }, { name: 'get_image_info', description: 'Get metadata and information about an image', inputSchema: { type: 'object', properties: { inputPath: { type: 'string', description: 'Path to image file' } }, required: ['inputPath'] } }, { name: 'batch_resize', description: 'Generate multiple sizes from one image', inputSchema: { type: 'object', properties: { inputPath: { type: 'string', description: 'Path to input image' }, outputDir: { type: 'string', description: 'Directory to save resized images' }, sizes: { type: 'array', items: { type: 'object', properties: { width: { type: 'number', description: 'Width in pixels' }, height: { type: 'number', description: 'Height in pixels' }, suffix: { type: 'string', description: 'Suffix to add to filename' } }, required: ['width', 'suffix'] }, description: 'Array of sizes to generate' }, format: { type: 'string', enum: ['jpeg', 'jpg', 'png', 'webp', 'avif'], description: 'Output format for all sizes' } }, required: ['inputPath', 'outputDir', 'sizes'] } } ] })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params as { name: string; arguments: any }; try { switch (name) { case 'resize_image': { const { inputPath, outputPath, width, height, fit = 'cover', preserveAspectRatio = true } = args; await fs.mkdir(path.dirname(outputPath), { recursive: true }); let pipeline = sharp(inputPath); if (width || height) { pipeline = pipeline.resize({ width: width || undefined, height: height || undefined, fit: fit as any, withoutEnlargement: preserveAspectRatio }); } await pipeline.toFile(outputPath); return { content: [ { type: 'text', text: `Image resized successfully. Saved to: ${outputPath}` } ] }; } case 'convert_format': { const { inputPath, outputPath, format, quality = 80 } = args; await fs.mkdir(path.dirname(outputPath), { recursive: true }); let pipeline = sharp(inputPath); switch (format) { case 'jpeg': case 'jpg': pipeline = pipeline.jpeg({ quality, progressive: true }); break; case 'png': pipeline = pipeline.png({ quality }); break; case 'webp': pipeline = pipeline.webp({ quality }); break; case 'avif': pipeline = pipeline.avif({ quality }); break; } await pipeline.toFile(outputPath); return { content: [ { type: 'text', text: `Image converted to ${format} format. Saved to: ${outputPath}` } ] }; } case 'crop_image': { const { inputPath, outputPath, left, top, width, height } = args; await fs.mkdir(path.dirname(outputPath), { recursive: true }); await sharp(inputPath) .extract({ left, top, width, height }) .toFile(outputPath); return { content: [ { type: 'text', text: `Image cropped successfully. Saved to: ${outputPath}` } ] }; } case 'compress_image': { const { inputPath, outputPath, quality = 80, progressive = true } = args; await fs.mkdir(path.dirname(outputPath), { recursive: true }); const metadata = await sharp(inputPath).metadata(); let pipeline = sharp(inputPath); // Apply compression based on format if (metadata.format === 'jpeg') { pipeline = pipeline.jpeg({ quality, progressive }); } else if (metadata.format === 'png') { pipeline = pipeline.png({ quality }); } else if (metadata.format === 'webp') { pipeline = pipeline.webp({ quality }); } else if (metadata.format === 'avif') { pipeline = pipeline.avif({ quality }); } await pipeline.toFile(outputPath); const originalSize = (await fs.stat(inputPath)).size; const compressedSize = (await fs.stat(outputPath)).size; const savings = ((originalSize - compressedSize) / originalSize * 100).toFixed(1); return { content: [ { type: 'text', text: `Image compressed successfully. Saved to: ${outputPath}\nSize reduction: ${savings}% (${originalSize} → ${compressedSize} bytes)` } ] }; } case 'rotate_image': { const { inputPath, outputPath, angle, background = '#000000' } = args; await fs.mkdir(path.dirname(outputPath), { recursive: true }); await sharp(inputPath) .rotate(angle, { background }) .toFile(outputPath); return { content: [ { type: 'text', text: `Image rotated ${angle}° successfully. Saved to: ${outputPath}` } ] }; } case 'flip_image': { const { inputPath, outputPath, direction } = args; await fs.mkdir(path.dirname(outputPath), { recursive: true }); let pipeline = sharp(inputPath); if (direction === 'horizontal' || direction === 'both') { pipeline = pipeline.flop(); } if (direction === 'vertical' || direction === 'both') { pipeline = pipeline.flip(); } await pipeline.toFile(outputPath); return { content: [ { type: 'text', text: `Image flipped ${direction} successfully. Saved to: ${outputPath}` } ] }; } case 'get_image_info': { const { inputPath } = args; const metadata = await sharp(inputPath).metadata(); const stats = await fs.stat(inputPath); return { content: [ { type: 'text', text: JSON.stringify({ format: metadata.format, width: metadata.width, height: metadata.height, channels: metadata.channels, bitDepth: metadata.depth, colorSpace: metadata.space, density: metadata.density, hasAlpha: metadata.hasAlpha, fileSize: stats.size, lastModified: stats.mtime }, null, 2) } ] }; } case 'batch_resize': { const { inputPath, outputDir, sizes, format } = args; await fs.mkdir(outputDir, { recursive: true }); const basename = path.basename(inputPath, path.extname(inputPath)); const results = []; for (const size of sizes) { const outputName = `${basename}${size.suffix}${format ? `.${format}` : path.extname(inputPath)}`; const outputPath = path.join(outputDir, outputName); let pipeline = sharp(inputPath) .resize({ width: size.width, height: size.height || undefined, fit: 'cover', withoutEnlargement: true }); if (format) { switch (format) { case 'jpeg': case 'jpg': pipeline = pipeline.jpeg({ quality: 80, progressive: true }); break; case 'png': pipeline = pipeline.png(); break; case 'webp': pipeline = pipeline.webp({ quality: 80 }); break; case 'avif': pipeline = pipeline.avif({ quality: 80 }); break; } } await pipeline.toFile(outputPath); results.push(outputPath); } return { content: [ { type: 'text', text: `Batch resize complete. Generated ${results.length} images:\n${results.join('\n')}` } ] }; } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Image processing error: ${error instanceof Error ? error.message : String(error)}` ); } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Imagician MCP server running'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/flowy11/imagician'

If you have feedback or need assistance with the MCP directory API, please join our Discord server