Skip to main content
Glama

image-reader MCP Server

by Rupeebw
index.ts18.3 kB
#!/usr/bin/env node /** * MCP server for image processing and analysis. * This server provides tools and resources for working with images: * - List images in a directory as resources * - Read image metadata and thumbnails * - Analyze images (dimensions, format, size) * - Process images (resize, convert) */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import * as fs from 'fs'; import * as path from 'path'; import * as fsExtra from 'fs-extra'; import sharp from 'sharp'; // Default directory to scan for images if not provided const DEFAULT_IMAGE_DIR = process.env.IMAGE_DIR || process.cwd(); // Supported image formats const SUPPORTED_FORMATS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tiff', '.svg', '.bmp']; // Cache for image metadata to avoid repeated processing const imageMetadataCache = new Map<string, ImageMetadata>(); // Type for image metadata interface ImageMetadata { path: string; filename: string; format: string; width?: number; height?: number; size: number; created: Date; modified: Date; } /** * Get all image files in a directory */ async function getImageFiles(directory: string): Promise<string[]> { try { // Use fs.promises.readdir instead of fsExtra.readdir const files = await fs.promises.readdir(directory); return files .filter(file => { const ext = path.extname(file).toLowerCase(); return SUPPORTED_FORMATS.includes(ext); }) .map(file => path.join(directory, file)); } catch (error) { console.error(`Error reading directory ${directory}:`, error); return []; } } /** * Get metadata for an image file */ async function getImageMetadata(imagePath: string): Promise<ImageMetadata> { // Check cache first if (imageMetadataCache.has(imagePath)) { return imageMetadataCache.get(imagePath)!; } try { // Use fs.promises.stat instead of fsExtra.stat const stats = await fs.promises.stat(imagePath); const metadata = await sharp(imagePath).metadata(); const imageMetadata: ImageMetadata = { path: imagePath, filename: path.basename(imagePath), format: metadata.format || path.extname(imagePath).replace('.', ''), width: metadata.width, height: metadata.height, size: stats.size, created: stats.birthtime, modified: stats.mtime }; // Cache the metadata imageMetadataCache.set(imagePath, imageMetadata); return imageMetadata; } catch (error) { console.error(`Error getting metadata for ${imagePath}:`, error); throw new McpError( ErrorCode.InternalError, `Failed to process image: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Generate a base64 thumbnail of an image */ async function generateThumbnail(imagePath: string, maxWidth = 300): Promise<string> { try { const buffer = await sharp(imagePath) .resize({ width: maxWidth, withoutEnlargement: true }) .toBuffer(); return `data:image/${path.extname(imagePath).replace('.', '')};base64,${buffer.toString('base64')}`; } catch (error) { console.error(`Error generating thumbnail for ${imagePath}:`, error); throw new McpError( ErrorCode.InternalError, `Failed to generate thumbnail: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Create an MCP server with capabilities for image processing */ const server = new Server( { name: "image-reader", version: "0.1.0", }, { capabilities: { resources: {}, tools: {}, }, } ); /** * Handler for listing available images as resources */ server.setRequestHandler(ListResourcesRequestSchema, async () => { const imageDir = process.env.IMAGE_DIR || DEFAULT_IMAGE_DIR; const imageFiles = await getImageFiles(imageDir); return { resources: await Promise.all(imageFiles.map(async (imagePath) => { try { const metadata = await getImageMetadata(imagePath); return { uri: `image://${encodeURIComponent(imagePath)}`, mimeType: `image/${metadata.format}`, name: metadata.filename, description: `Image: ${metadata.filename} (${metadata.width}x${metadata.height}, ${(metadata.size / 1024).toFixed(2)} KB)` }; } catch (error) { console.error(`Error processing ${imagePath}:`, error); return { uri: `image://${encodeURIComponent(imagePath)}`, mimeType: "image/unknown", name: path.basename(imagePath), description: `Image: ${path.basename(imagePath)} (metadata unavailable)` }; } })) }; }); /** * Handler for reading image content and metadata */ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; if (!uri.startsWith('image://')) { throw new McpError( ErrorCode.InvalidRequest, `Invalid URI scheme: ${uri}` ); } const imagePath = decodeURIComponent(uri.replace('image://', '')); try { if (!fs.existsSync(imagePath)) { throw new McpError( ErrorCode.InvalidRequest, `Image not found: ${imagePath}` ); } const metadata = await getImageMetadata(imagePath); const thumbnail = await generateThumbnail(imagePath); // Format metadata as JSON const metadataJson = JSON.stringify({ filename: metadata.filename, format: metadata.format, dimensions: `${metadata.width}x${metadata.height}`, size: `${(metadata.size / 1024).toFixed(2)} KB`, created: metadata.created.toISOString(), modified: metadata.modified.toISOString(), path: metadata.path }, null, 2); return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: metadataJson }, { uri: `${request.params.uri}#thumbnail`, mimeType: `image/${metadata.format}`, text: thumbnail } ] }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Error processing image: ${error instanceof Error ? error.message : String(error)}` ); } }); /** * Handler that lists available tools for image processing */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "analyze_image", description: "Get detailed metadata about an image", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path to the image file" } }, required: ["path"] } }, { name: "resize_image", description: "Resize an image and save to a new file", inputSchema: { type: "object", properties: { input: { type: "string", description: "Path to the input image file" }, output: { type: "string", description: "Path to save the resized image" }, width: { type: "number", description: "Target width in pixels" }, height: { type: "number", description: "Target height in pixels (optional)" }, fit: { type: "string", description: "Fit method: cover, contain, fill, inside, outside", enum: ["cover", "contain", "fill", "inside", "outside"] } }, required: ["input", "output", "width"] } }, { name: "convert_format", description: "Convert an image to a different format", inputSchema: { type: "object", properties: { input: { type: "string", description: "Path to the input image file" }, output: { type: "string", description: "Path to save the converted image" }, format: { type: "string", description: "Target format: jpeg, png, webp, avif, tiff, etc.", enum: ["jpeg", "png", "webp", "avif", "tiff", "gif"] }, quality: { type: "number", description: "Quality level (1-100, for formats that support it)" } }, required: ["input", "output", "format"] } }, { name: "scan_directory", description: "Scan a directory for images and return metadata", inputSchema: { type: "object", properties: { directory: { type: "string", description: "Directory path to scan for images" }, recursive: { type: "boolean", description: "Whether to scan subdirectories recursively" } }, required: ["directory"] } } ] }; }); /** * Handler for image processing tools */ server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "analyze_image": { const { path: imagePath } = request.params.arguments as { path: string }; if (!imagePath) { throw new McpError(ErrorCode.InvalidParams, "Image path is required"); } if (!fs.existsSync(imagePath)) { throw new McpError(ErrorCode.InvalidRequest, `Image not found: ${imagePath}`); } try { const metadata = await getImageMetadata(imagePath); const thumbnail = await generateThumbnail(imagePath); return { content: [ { type: "text", text: JSON.stringify({ filename: metadata.filename, format: metadata.format, dimensions: `${metadata.width}x${metadata.height}`, size: { bytes: metadata.size, kilobytes: (metadata.size / 1024).toFixed(2), megabytes: (metadata.size / (1024 * 1024)).toFixed(2) }, created: metadata.created.toISOString(), modified: metadata.modified.toISOString(), path: metadata.path, thumbnail }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error analyzing image: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } case "resize_image": { const { input, output, width, height, fit = "cover" } = request.params.arguments as { input: string, output: string, width: number, height?: number, fit?: string }; if (!input || !output || !width) { throw new McpError(ErrorCode.InvalidParams, "Input path, output path, and width are required"); } if (!fs.existsSync(input)) { throw new McpError(ErrorCode.InvalidRequest, `Input image not found: ${input}`); } try { // Create output directory if it doesn't exist await fsExtra.ensureDir(path.dirname(output)); // Resize the image await sharp(input) .resize({ width, height, fit: fit as keyof sharp.FitEnum }) .toFile(output); // Get metadata of the resized image const metadata = await getImageMetadata(output); return { content: [ { type: "text", text: `Image resized successfully:\n` + `- Original: ${input}\n` + `- Resized: ${output}\n` + `- New dimensions: ${metadata.width}x${metadata.height}\n` + `- Size: ${(metadata.size / 1024).toFixed(2)} KB` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error resizing image: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } case "convert_format": { const { input, output, format, quality = 80 } = request.params.arguments as { input: string, output: string, format: string, quality?: number }; if (!input || !output || !format) { throw new McpError(ErrorCode.InvalidParams, "Input path, output path, and format are required"); } if (!fs.existsSync(input)) { throw new McpError(ErrorCode.InvalidRequest, `Input image not found: ${input}`); } try { // Create output directory if it doesn't exist await fsExtra.ensureDir(path.dirname(output)); // Convert the image let processor = sharp(input); // Apply format-specific options switch (format.toLowerCase()) { case 'jpeg': case 'jpg': processor = processor.jpeg({ quality }); break; case 'png': processor = processor.png({ quality: Math.floor(quality / 100 * 9) }); break; case 'webp': processor = processor.webp({ quality }); break; case 'avif': processor = processor.avif({ quality }); break; case 'tiff': processor = processor.tiff({ quality }); break; case 'gif': processor = processor.gif(); break; default: throw new McpError(ErrorCode.InvalidParams, `Unsupported format: ${format}`); } await processor.toFile(output); // Get metadata of the converted image const metadata = await getImageMetadata(output); return { content: [ { type: "text", text: `Image converted successfully:\n` + `- Original: ${input}\n` + `- Converted: ${output}\n` + `- Format: ${metadata.format}\n` + `- Dimensions: ${metadata.width}x${metadata.height}\n` + `- Size: ${(metadata.size / 1024).toFixed(2)} KB` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error converting image: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } case "scan_directory": { const { directory, recursive = false } = request.params.arguments as { directory: string, recursive?: boolean }; if (!directory) { throw new McpError(ErrorCode.InvalidParams, "Directory path is required"); } if (!fs.existsSync(directory)) { throw new McpError(ErrorCode.InvalidRequest, `Directory not found: ${directory}`); } try { // Function to scan directory recursively async function scanDir(dir: string, results: ImageMetadata[] = []): Promise<ImageMetadata[]> { // Use fs instead of fsExtra for readdir const entries = await fs.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && recursive) { await scanDir(fullPath, results); } else if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); if (SUPPORTED_FORMATS.includes(ext)) { try { const metadata = await getImageMetadata(fullPath); results.push(metadata); } catch (error) { console.error(`Error processing ${fullPath}:`, error); } } } } return results; } const images = await scanDir(directory); return { content: [ { type: "text", text: `Found ${images.length} images in ${directory}${recursive ? ' (including subdirectories)' : ''}:\n\n` + JSON.stringify(images.map(img => ({ filename: img.filename, path: img.path, format: img.format, dimensions: `${img.width}x${img.height}`, size: `${(img.size / 1024).toFixed(2)} KB` })), null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error scanning directory: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } }); /** * Start the server using stdio transport */ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Image Reader MCP server running on stdio'); // Error handling process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); }); process.on('unhandledRejection', (reason) => { console.error('Unhandled rejection:', reason); }); process.on('SIGINT', async () => { await server.close(); process.exit(0); }); } main().catch((error) => { console.error("Server error:", error); process.exit(1); });

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/Rupeebw/mcp-image-reader'

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