@aiondadotcom/mcp-openai-image
Version:
MCP server for OpenAI image generation with STDIO transport
293 lines (292 loc) • 12.6 kB
JavaScript
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;
}
}