openrouter-image-mcp
Version:
MCP server for image analysis using OpenRouter's vision models
159 lines (158 loc) • 5.92 kB
JavaScript
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);
}
}