christmas-mcp-image-describe
Version:
MCP server that analyzes images and provides structured metadata for website placement decisions using OpenAI GPT-4o Vision API
175 lines (153 loc) • 5.6 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import axios from 'axios';
import { fileTypeFromFile } from 'file-type';
import mime from 'mime';
/**
* Analyzes an image using OpenAI's GPT-4o Vision API and returns structured metadata
* for website placement decisions.
*
* @param {string} imagePath - Path to the local image file
* @param {string} context - Optional context like "about section" or "homepage hero"
* @param {string} apiKey - OpenAI API key
* @param {string} project - OpenAI project ID (optional)
* @returns {Promise<Object>} Structured image analysis result
*/
export async function analyzeImage(imagePath, context = '', apiKey, project = null) {
if (!apiKey) {
throw new Error('OpenAI API key is required');
}
try {
// Check if file exists
await fs.access(imagePath);
// Read the image file
const imageBuffer = await fs.readFile(imagePath);
// Detect MIME type
let mimeType;
try {
const fileType = await fileTypeFromFile(imagePath);
mimeType = fileType?.mime || mime.getType(imagePath) || 'image/jpeg';
} catch (error) {
mimeType = mime.getType(imagePath) || 'image/jpeg';
}
// Validate image type
if (!mimeType.startsWith('image/')) {
throw new Error(`Invalid image file type: ${mimeType}`);
}
// Convert to base64
const base64Image = imageBuffer.toString('base64');
const dataUrl = `data:${mimeType};base64,${base64Image}`;
// Prepare the prompt
const prompt = `Analyze this image and provide structured metadata for web development purposes.
${context ? `Context: This image is intended for use in a ${context}.` : ''}
Please provide:
1. A clear, concise description of what's in the image
2. The best HTML/CSS placement suggestion (e.g., "header.logo", "section.hero", "section.about.team", "footer.contact", "article.product")
3. Relevant semantic tags that describe the content
4. Appropriate alt text for accessibility
Return your response as a JSON object with exactly these fields:
- description: string (brief description of image content)
- placement: string (suggested HTML/CSS selector for placement)
- tags: array of strings (semantic tags)
- alt: string (accessibility alt text)
Focus on practical web development usage and accessibility.`;
// Prepare headers
const headers = {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
};
if (project) {
headers['OpenAI-Project'] = project;
}
// Make API request to OpenAI
const response = await axios.post('https://api.openai.com/v1/chat/completions', {
model: 'gpt-4o',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: prompt
},
{
type: 'image_url',
image_url: {
url: dataUrl,
detail: 'high'
}
}
]
}
],
max_tokens: 500,
temperature: 0.3
}, { headers });
// Parse the response
const content = response.data.choices[0].message.content;
// Try to extract JSON from the response
let result;
try {
// Look for JSON in the response
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
result = JSON.parse(jsonMatch[0]);
} else {
throw new Error('No JSON found in response');
}
} catch (parseError) {
// If parsing fails, create a structured response from the text
result = {
description: content.split('\n')[0] || 'Image analysis available',
placement: 'section.content',
tags: ['image', 'content'],
alt: content.split('\n')[0] || 'Analyzed image'
};
}
// Validate and clean the result
const cleanResult = {
description: result.description || 'Image content',
placement: result.placement || 'section.content',
tags: Array.isArray(result.tags) ? result.tags : ['image'],
alt: result.alt || result.description || 'Image'
};
return cleanResult;
} catch (error) {
if (error.response) {
// OpenAI API error
throw new Error(`OpenAI API error: ${error.response.status} - ${error.response.data?.error?.message || 'Unknown error'}`);
} else if (error.code === 'ENOENT') {
throw new Error(`Image file not found: ${imagePath}`);
} else {
throw new Error(`Failed to analyze image: ${error.message}`);
}
}
}
/**
* Batch analyze multiple images
*
* @param {Array<{path: string, context?: string}>} images - Array of image objects
* @param {string} apiKey - OpenAI API key
* @param {string} project - OpenAI project ID (optional)
* @returns {Promise<Array<Object>>} Array of analysis results
*/
export async function analyzeImages(images, apiKey, project = null) {
const results = [];
for (const image of images) {
try {
const result = await analyzeImage(image.path, image.context, apiKey, project);
results.push({
path: image.path,
success: true,
data: result
});
} catch (error) {
results.push({
path: image.path,
success: false,
error: error.message
});
}
}
return results;
}