UNPKG

@aiondadotcom/mcp-openai-image

Version:

MCP server for OpenAI image generation with STDIO transport

293 lines (292 loc) 12.6 kB
import OpenAI from 'openai'; import { SUPPORTED_SIZES, SUPPORTED_QUALITIES, SUPPORTED_FORMATS, SUPPORTED_BACKGROUNDS } from './types.js'; export class ImageGenerator { openai = null; configManager; fileManager; constructor(configManager, fileManager) { this.configManager = configManager; this.fileManager = fileManager; } async initializeOpenAI() { const apiKey = await this.configManager.getApiKey(); const organization = await this.configManager.getOrganization(); if (!apiKey) { throw new Error('OpenAI API key not configured. Please run configure-server first.'); } this.openai = new OpenAI({ apiKey: apiKey, organization: organization }); } async generateImage(params) { try { await this.initializeOpenAI(); if (!this.openai) { throw new Error('OpenAI client not initialized'); } // Validate parameters const validatedParams = this.validateGenerateParams(params); // Call OpenAI API const response = await this.callOpenAI(validatedParams.prompt, { size: validatedParams.size, quality: validatedParams.quality, format: validatedParams.format, background: validatedParams.background, compression: validatedParams.compression }); // Process response const imageCall = response.output.find((output) => output.type === 'image_generation_call'); if (!imageCall || !imageCall.result) { throw new Error('No image generated in response'); } // Create metadata const metadata = { prompt: validatedParams.prompt, revisedPrompt: imageCall.revised_prompt || validatedParams.prompt, size: validatedParams.size, quality: validatedParams.quality, format: validatedParams.format, model: await this.configManager.getModel(), timestamp: new Date().toISOString(), responseId: response.id, imageId: imageCall.id }; // Save image to desktop const filePath = await this.fileManager.saveImageToDesktop(imageCall.result, validatedParams.format, metadata); // Update last used await this.configManager.updateLastUsed(); return { success: true, filePath: filePath, fileName: filePath.split('/').pop(), responseId: response.id, imageId: imageCall.id, revisedPrompt: imageCall.revised_prompt, metadata: metadata }; } catch (error) { let errorMessage = 'Unknown error occurred'; let errorCode = 'GENERATION_FAILED'; let suggestions = ['Check your API key', 'Verify your prompt', 'Try again later']; if (error instanceof Error) { errorMessage = error.message; // Provide specific error handling based on error message if (error.message.includes('API key')) { errorCode = 'INVALID_API_KEY'; suggestions = ['Configure your OpenAI API key using configure-server', 'Verify your API key is valid']; } else if (error.message.includes('billing')) { errorCode = 'BILLING_ISSUE'; suggestions = ['Check your OpenAI billing status', 'Add payment method to your OpenAI account']; } else if (error.message.includes('quota')) { errorCode = 'QUOTA_EXCEEDED'; suggestions = ['Check your API usage limits', 'Upgrade your OpenAI plan if needed']; } else if (error.message.includes('model')) { errorCode = 'MODEL_ERROR'; suggestions = ['Try using a different model', 'Check if the model is available']; } else if (error.message.includes('prompt')) { errorCode = 'PROMPT_ERROR'; suggestions = ['Review your prompt for inappropriate content', 'Try a different prompt']; } } return { success: false, error: { code: errorCode, message: errorMessage, details: error, suggestions: suggestions } }; } } async editImage(params) { try { await this.initializeOpenAI(); if (!this.openai) { throw new Error('OpenAI client not initialized'); } // For now, treat editing as a new image generation with the edit prompt // In a full implementation, you'd need to handle the original image const response = await this.openai.images.generate({ model: 'dall-e-3', prompt: params.editPrompt, size: '1024x1024', quality: 'standard', n: 1, response_format: 'b64_json' }); const imageData = response.data?.[0]; if (!imageData) { throw new Error('No image data received from OpenAI'); } const responseId = `edit_${Date.now()}`; const imageId = `img_${Date.now()}`; // Create metadata const metadata = { prompt: params.editPrompt, revisedPrompt: imageData.revised_prompt || params.editPrompt, size: '1024x1024', quality: 'standard', format: 'png', model: await this.configManager.getModel(), timestamp: new Date().toISOString(), responseId: responseId, imageId: imageId }; // Save edited image to desktop const filePath = await this.fileManager.saveImageToDesktop(imageData.b64_json || '', metadata.format, metadata); // Update last used await this.configManager.updateLastUsed(); return { success: true, filePath: filePath, fileName: filePath.split('/').pop(), responseId: responseId, imageId: imageId, revisedPrompt: imageData.revised_prompt }; } catch (error) { return { success: false, error: { code: 'EDIT_FAILED', message: error instanceof Error ? error.message : 'Unknown error', details: error, suggestions: ['Check your response ID or image ID', 'Verify your edit prompt', 'Try again later'] } }; } } async streamImage(params) { try { await this.initializeOpenAI(); if (!this.openai) { throw new Error('OpenAI client not initialized'); } const validatedParams = this.validateStreamParams(params); // For now, simulate streaming by generating a single image // In a full implementation, you'd need OpenAI's streaming API const response = await this.openai.images.generate({ model: 'dall-e-3', prompt: validatedParams.prompt, size: (validatedParams.size || '1024x1024'), quality: 'standard', n: 1, response_format: 'b64_json' }); const imageData = response.data?.[0]; if (!imageData) { throw new Error('No image data received from OpenAI'); } const responseId = `stream_${Date.now()}`; // Create metadata const metadata = { prompt: validatedParams.prompt, revisedPrompt: imageData.revised_prompt || validatedParams.prompt, size: (validatedParams.size || '1024x1024'), quality: 'standard', format: 'png', model: await this.configManager.getModel(), timestamp: new Date().toISOString(), responseId: responseId, imageId: `img_${Date.now()}` }; // Save final image const finalImagePath = await this.fileManager.saveImageToDesktop(imageData.b64_json || '', metadata.format, metadata); // Update last used await this.configManager.updateLastUsed(); return { success: true, finalImagePath: finalImagePath, partialImagePaths: [], responseId: responseId, revisedPrompt: imageData.revised_prompt }; } catch (error) { return { success: false, error: { code: 'STREAM_FAILED', message: error instanceof Error ? error.message : 'Unknown error', details: error, suggestions: ['Check your API key', 'Verify your prompt', 'Try again later'] } }; } } async callOpenAI(prompt, options) { if (!this.openai) { throw new Error('OpenAI client not initialized'); } // Use the standard OpenAI image generation API const response = await this.openai.images.generate({ model: 'dall-e-3', prompt: prompt, size: (options.size || '1024x1024'), quality: (options.quality || 'standard'), n: 1, response_format: 'b64_json' }); // Transform the response to match expected format const imageData = response.data?.[0]; if (!imageData) { throw new Error('No image data received from OpenAI'); } return { id: `img_${Date.now()}`, output: [{ type: 'image_generation_call', id: `call_${Date.now()}`, result: imageData.b64_json || '', revised_prompt: imageData.revised_prompt || prompt }] }; } validateGenerateParams(params) { const validated = { ...params }; // Set defaults validated.size = validated.size || '1024x1024'; validated.quality = validated.quality || 'standard'; validated.format = validated.format || 'png'; validated.background = validated.background || 'auto'; // Validate values if (!SUPPORTED_SIZES.includes(validated.size)) { throw new Error(`Unsupported size: ${validated.size}. Supported sizes: ${SUPPORTED_SIZES.join(', ')}`); } if (!SUPPORTED_QUALITIES.includes(validated.quality)) { throw new Error(`Unsupported quality: ${validated.quality}. Supported qualities: ${SUPPORTED_QUALITIES.join(', ')}`); } if (!SUPPORTED_FORMATS.includes(validated.format)) { throw new Error(`Unsupported format: ${validated.format}. Supported formats: ${SUPPORTED_FORMATS.join(', ')}`); } if (!SUPPORTED_BACKGROUNDS.includes(validated.background)) { throw new Error(`Unsupported background: ${validated.background}. Supported backgrounds: ${SUPPORTED_BACKGROUNDS.join(', ')}`); } if (validated.compression !== undefined && (validated.compression < 0 || validated.compression > 100)) { throw new Error('Compression must be between 0 and 100'); } return validated; } validateStreamParams(params) { const validated = { ...params }; // Set defaults validated.partialImages = validated.partialImages || 2; validated.size = validated.size || '1024x1024'; // Validate values if (validated.partialImages < 1 || validated.partialImages > 3) { throw new Error('Partial images must be between 1 and 3'); } if (!SUPPORTED_SIZES.includes(validated.size)) { throw new Error(`Unsupported size: ${validated.size}. Supported sizes: ${SUPPORTED_SIZES.join(', ')}`); } return validated; } }