Skip to main content
Glama

MCP Image Generator

by shinpr
geminiClient.ts•16 kB
/** * Gemini API client for image generation * Integrates with Google's Gemini AI API using the official SDK * Supports automatic URL Context processing and feature parameters */ import { GoogleGenAI } from '@google/genai' import type { Result } from '../types/result' import { Err, Ok } from '../types/result' import type { Config } from '../utils/config' import { GeminiAPIError, NetworkError } from '../utils/errors' /** * Simplified Gemini API response types */ interface ContentPart { inlineData?: { data: string mimeType: string } text?: string } interface GeminiResponse { candidates?: Array<{ content?: { parts?: ContentPart[] } finishReason?: string }> modelVersion?: string responseId?: string sdkHttpResponse?: unknown usageMetadata?: unknown } interface GeminiClientInstance { models: { generateContent(params: { model: string contents: unknown[] | string systemInstruction?: string generationConfig?: { [key: string]: unknown } config?: { imageConfig?: { aspectRatio?: string } responseModalities?: string[] } }): Promise<unknown> // Response is unknown, we'll validate with type guards } } /** * Safely analyze response structure for debugging (removes sensitive data) */ function analyzeResponseStructure(obj: unknown): Record<string, unknown> { if (!obj || typeof obj !== 'object') { return { type: typeof obj, value: obj } } const seen = new WeakSet() const sanitize = (value: unknown, depth = 0): unknown => { if (depth > 3) return '[max depth]' if (value === null || value === undefined) return value if (typeof value !== 'object') return typeof value === 'string' && value.length > 100 ? `[string length: ${value.length}]` : value if (seen.has(value)) return '[circular]' seen.add(value) if (Array.isArray(value)) { return value.slice(0, 3).map((v) => sanitize(v, depth + 1)) } const record = value as Record<string, unknown> const result: Record<string, unknown> = {} for (const [key, val] of Object.entries(record)) { // Skip sensitive keys if (/apikey|token|secret|password|credential/i.test(key)) { result[key] = '[REDACTED]' } else if (key === 'data' && typeof val === 'string' && val.length > 100) { // Likely base64 image data result[key] = `[base64 data, length: ${val.length}]` } else { result[key] = sanitize(val, depth + 1) } } return result } return sanitize(obj) as Record<string, unknown> } /** * Type guard for Gemini response validation */ function isGeminiResponse(obj: unknown): obj is GeminiResponse { if (!obj || typeof obj !== 'object') return false const response = obj as Record<string, unknown> // Check if it has response property (wrapped response) if ('response' in response && response['response'] && typeof response['response'] === 'object') { const innerResponse = response['response'] as Record<string, unknown> return 'candidates' in innerResponse && Array.isArray(innerResponse['candidates']) } // Check direct candidates property (direct response) return 'candidates' in response && Array.isArray(response['candidates']) } interface ErrorWithCode extends Error { code?: string } /** * Metadata for generated images */ export interface GeminiGenerationMetadata { model: string prompt: string mimeType: string timestamp: Date inputImageProvided: boolean // Additional metadata from flat structure responses modelVersion?: string responseId?: string } /** * Parameters for Gemini API image generation */ export interface GeminiApiParams { prompt: string inputImage?: string aspectRatio?: string } /** * Result of image generation */ export interface GeneratedImageResult { imageData: Buffer metadata: GeminiGenerationMetadata } /** * Gemini API client interface */ export interface GeminiClient { generateImage( params: GeminiApiParams ): Promise<Result<GeneratedImageResult, GeminiAPIError | NetworkError>> } /** * Implementation of Gemini API client */ class GeminiClientImpl implements GeminiClient { private readonly modelName = 'gemini-2.5-flash-image' constructor(private readonly genai: GeminiClientInstance) {} async generateImage( params: GeminiApiParams ): Promise<Result<GeneratedImageResult, GeminiAPIError | NetworkError>> { try { // Prepare the request content with proper structure for multimodal input const requestContent: unknown[] = [] // Structure the contents properly for image generation/editing if (params.inputImage) { // For image editing: provide image first, then text instructions requestContent.push({ parts: [ { inlineData: { data: params.inputImage, mimeType: 'image/jpeg', // TODO: Dynamic MIME type support }, }, { text: params.prompt, }, ], }) } else { // For text-to-image: provide only text prompt requestContent.push({ parts: [ { text: params.prompt, }, ], }) } // Construct config object for generateContent const config = params.aspectRatio ? { imageConfig: { aspectRatio: params.aspectRatio }, responseModalities: ['IMAGE'], } : { responseModalities: ['IMAGE'], } // Generate content using Gemini API (@google/genai v1.17.0+) const rawResponse = await this.genai.models.generateContent({ model: this.modelName, contents: requestContent, config, }) // Validate response structure with type guard if (!isGeminiResponse(rawResponse)) { const responseStructure = analyzeResponseStructure(rawResponse) // Check if it's an error response from Gemini const asRecord = rawResponse as Record<string, unknown> if (asRecord['error']) { const error = asRecord['error'] as Record<string, unknown> return Err( new GeminiAPIError(`Gemini API Error: ${error['message'] || 'Unknown error'}`, { code: error['code'], status: error['status'], details: error['details'] || responseStructure, stage: 'api_error', }) ) } return Err( new GeminiAPIError('Invalid response structure from Gemini API', { message: 'The API returned an unexpected response format', responseStructure: responseStructure, stage: 'response_validation', suggestion: 'Check if the API endpoint or model configuration is correct', }) ) } // Extract the actual response data (handle wrapped responses) const responseData = (rawResponse as Record<string, unknown>)['response'] ? ((rawResponse as Record<string, unknown>)['response'] as GeminiResponse) : (rawResponse as GeminiResponse) // Check for prompt feedback (safety blocking) const responseAsRecord = responseData as Record<string, unknown> if (responseAsRecord['promptFeedback']) { const promptFeedback = responseAsRecord['promptFeedback'] as Record<string, unknown> if (promptFeedback['blockReason'] === 'SAFETY') { return Err( new GeminiAPIError('Image generation blocked for safety reasons', { stage: 'prompt_analysis', blockReason: promptFeedback['blockReason'], suggestion: 'Rephrase your prompt to avoid potentially sensitive content', }) ) } if ( promptFeedback['blockReason'] === 'OTHER' || promptFeedback['blockReason'] === 'PROHIBITED_CONTENT' ) { return Err( new GeminiAPIError('Image generation blocked due to prohibited content', { stage: 'prompt_analysis', blockReason: promptFeedback['blockReason'], suggestion: 'Remove any prohibited content from your prompt and try again', }) ) } } // Check for candidates if (!responseData.candidates || responseData.candidates.length === 0) { return Err( new GeminiAPIError('No image generated: Content may have been filtered', { stage: 'generation', candidatesCount: 0, suggestion: 'Try rephrasing your prompt to avoid potentially sensitive content', }) ) } const candidate = responseData.candidates[0] if (!candidate || !candidate.content || !candidate.content.parts) { return Err( new GeminiAPIError('No valid content in response', { stage: 'candidate_extraction', suggestion: 'The API response was incomplete. Please try again', }) ) } const parts = candidate.content.parts // Handle finish reason specific errors before checking parts if (candidate.finishReason) { const finishReason = candidate.finishReason if (finishReason === 'IMAGE_SAFETY') { return Err( new GeminiAPIError('Image generation stopped for safety reasons', { finishReason, stage: 'generation_stopped', suggestion: 'Modify your prompt to avoid potentially sensitive content', safetyRatings: (candidate as Record<string, unknown>)['safetyRatings'] ? ( (candidate as Record<string, unknown>)['safetyRatings'] as Record< string, unknown >[] ) ?.map((rating: Record<string, unknown>) => { const category = (rating['category'] as string) .replace('HARM_CATEGORY_', '') .split('_') .map((word: string) => word.charAt(0) + word.slice(1).toLowerCase()) .join(' ') return `${category} (${rating['blocked'] ? 'BLOCKED' : 'ALLOWED'})` }) .join(', ') : undefined, }) ) } if (finishReason === 'MAX_TOKENS') { return Err( new GeminiAPIError('Maximum token limit reached during generation', { finishReason, stage: 'generation_stopped', suggestion: 'Try using a shorter or simpler prompt', }) ) } } if (parts.length === 0) { return Err( new GeminiAPIError('No content parts in response', { stage: 'content_extraction', suggestion: 'The generation was incomplete. Please try again', }) ) } // Check if we got an image or text (error message) const imagePart = parts.find((part) => part.inlineData?.data) const textPart = parts.find((part) => part.text) if (!imagePart?.inlineData) { // If there's text, it's likely an error message from Gemini const errorMessage = textPart?.text || 'Image generation failed' return Err( new GeminiAPIError('Image generation failed due to content filtering', { reason: errorMessage, stage: 'image_extraction', suggestion: 'The prompt was blocked by safety filters. Try rephrasing your prompt to avoid potentially sensitive content.', }) ) } // Convert base64 image data to Buffer const imageBuffer = Buffer.from(imagePart.inlineData.data, 'base64') const mimeType = imagePart.inlineData.mimeType || 'image/png' // Create metadata const metadata: GeminiGenerationMetadata = { model: this.modelName, prompt: params.prompt, mimeType, timestamp: new Date(), inputImageProvided: !!params.inputImage, ...(responseData.modelVersion && { modelVersion: responseData.modelVersion }), ...(responseData.responseId && { responseId: responseData.responseId }), } return Ok({ imageData: imageBuffer, metadata, }) } catch (error) { return this.handleError(error, params.prompt) } } private handleError( error: unknown, prompt: string ): Result<never, GeminiAPIError | NetworkError> { const errorMessage = error instanceof Error ? error.message : 'Unknown error' // Check if it's a network error if (this.isNetworkError(error)) { return Err( new NetworkError( `Network error during image generation: ${errorMessage}`, 'Check your internet connection and try again', error instanceof Error ? error : undefined ) ) } // Check if it's an API-specific error if (this.isAPIError(error)) { return Err( new GeminiAPIError( `Failed to generate image: ${errorMessage}`, this.getAPIErrorSuggestion(errorMessage), this.extractStatusCode(error) ) ) } // Generic API error return Err( new GeminiAPIError( `Failed to generate image with prompt "${prompt}": ${errorMessage}`, 'Check your API key, quota, and prompt validity. Try again with a different prompt' ) ) } private isNetworkError(error: unknown): boolean { if (error instanceof Error) { const networkErrorCodes = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'] return networkErrorCodes.some( (code) => error.message.includes(code) || (error as ErrorWithCode).code === code ) } return false } private isAPIError(error: unknown): boolean { if (error instanceof Error) { const apiErrorKeywords = ['quota', 'rate limit', 'unauthorized', 'forbidden', 'api key'] return apiErrorKeywords.some((keyword) => error.message.toLowerCase().includes(keyword)) } return false } private getAPIErrorSuggestion(errorMessage: string): string { const lowerMessage = errorMessage.toLowerCase() if (lowerMessage.includes('quota') || lowerMessage.includes('rate limit')) { return 'You have exceeded your API quota or rate limit. Wait before making more requests or upgrade your plan' } if (lowerMessage.includes('unauthorized') || lowerMessage.includes('api key')) { return 'Check that your GEMINI_API_KEY is valid and has the necessary permissions' } if (lowerMessage.includes('forbidden')) { return 'Your API key does not have permission for this operation' } return 'Check your API configuration and try again' } private extractStatusCode(error: unknown): number | undefined { if (error && typeof error === 'object' && 'status' in error) { return typeof error.status === 'number' ? error.status : undefined } return undefined } } /** * Creates a new Gemini API client * @param config Configuration containing API key and other settings * @returns Result containing the client or an error */ export function createGeminiClient(config: Config): Result<GeminiClient, GeminiAPIError> { try { const genai = new GoogleGenAI({ apiKey: config.geminiApiKey, }) as unknown as GeminiClientInstance return Ok(new GeminiClientImpl(genai)) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' return Err( new GeminiAPIError( `Failed to initialize Gemini client: ${errorMessage}`, 'Verify your GEMINI_API_KEY is valid and the @google/genai package is properly installed' ) ) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shinpr/mcp-image'

If you have feedback or need assistance with the MCP directory API, please join our Discord server