UNPKG

openrouter-image-mcp

Version:

MCP server for image analysis using OpenRouter's vision models

159 lines (158 loc) 5.92 kB
import axios from 'axios'; import { readFile } from 'fs/promises'; import { Logger } from './logger.js'; export class ImageProcessor { static instance; logger; constructor() { this.logger = Logger.getInstance(); } static getInstance() { if (!ImageProcessor.instance) { ImageProcessor.instance = new ImageProcessor(); } return ImageProcessor.instance; } async processImage(input) { try { switch (input.type) { case 'base64': return this.processBase64Image(input.data, input.mimeType); case 'file': return this.processFileImage(input.data); case 'url': return this.processUrlImage(input.data); default: throw new Error(`Unsupported image input type: ${input.type}`); } } catch (error) { this.logger.error('Failed to process image', error); throw error; } } async processBase64Image(data, mimeType) { try { // Remove data URL prefix if present const base64Data = data.replace(/^data:image\/[a-z]+;base64,/, ''); // Validate base64 data if (!base64Data || base64Data.length === 0) { throw new Error('Empty or invalid base64 data provided'); } // Check if base64 data is reasonable size (warn if >10MB) const estimatedSize = Math.ceil(base64Data.length * 0.75); // Base64 is ~33% larger if (estimatedSize > 10 * 1024 * 1024) { this.logger.warn(`Large base64 image detected: ${estimatedSize} bytes. Processing may take time.`); } // Create buffer and validate let buffer; try { buffer = Buffer.from(base64Data, 'base64'); } catch (error) { throw new Error('Invalid base64 encoding provided'); } // Validate buffer size if (buffer.length === 0) { throw new Error('Base64 data resulted in empty buffer'); } // Detect MIME type if not provided const detectedMimeType = mimeType || await this.detectMimeType(buffer); this.logger.debug(`Processed base64 image, size: ${buffer.length}, type: ${detectedMimeType}`); return { data: base64Data, mimeType: detectedMimeType, size: buffer.length, }; } catch (error) { this.logger.error('Failed to process base64 image', error); throw new Error(`Base64 image processing failed: ${error.message}`); } } async processFileImage(filePath) { try { const buffer = await readFile(filePath); const mimeType = await this.detectMimeType(buffer); const base64Data = buffer.toString('base64'); this.logger.debug(`Processed file image: ${filePath}, size: ${buffer.length}, type: ${mimeType}`); return { data: base64Data, mimeType, size: buffer.length, }; } catch (error) { throw new Error(`Failed to read file ${filePath}: ${error.message}`); } } async processUrlImage(url) { try { this.logger.debug(`Fetching image from URL: ${url}`); const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 30000, headers: { 'User-Agent': 'OpenRouter-Image-MCP/1.0', }, }); const buffer = Buffer.from(response.data); const mimeType = this.detectMimeTypeFromHeaders(response.headers) || await this.detectMimeType(buffer); const base64Data = buffer.toString('base64'); this.logger.debug(`Processed URL image: ${url}, size: ${buffer.length}, type: ${mimeType}`); return { data: base64Data, mimeType, size: buffer.length, }; } catch (error) { throw new Error(`Failed to fetch image from URL ${url}: ${error.message}`); } } async detectMimeType(buffer) { // Use signature-based detection (no native dependencies needed) return this.detectFromSignature(buffer); } detectFromSignature(buffer) { if (buffer.length < 4) return 'application/octet-stream'; const signature = buffer.subarray(0, 4).toString('hex'); // JPEG signature: FF D8 FF if (signature.startsWith('ffd8ff')) { return 'image/jpeg'; } // PNG signature: 89 50 4E 47 if (signature === '89504e47') { return 'image/png'; } // GIF signature: 47 49 46 38 if (signature.startsWith('47494638')) { return 'image/gif'; } // WebP signature: 52 49 46 46 ... 57 45 42 50 if (signature.startsWith('52494646') && buffer.length > 12) { const webpSignature = buffer.subarray(8, 12).toString('ascii'); if (webpSignature === 'WEBP') { return 'image/webp'; } } return 'application/octet-stream'; } detectMimeTypeFromHeaders(headers) { const contentType = headers['content-type']; if (contentType && typeof contentType === 'string') { return contentType.split(';')[0].trim(); } return null; } isValidImageType(mimeType) { const validTypes = [ 'image/jpeg', 'image/png', 'image/webp', 'image/gif', ]; return validTypes.includes(mimeType); } }