Skip to main content
Glama

MCP Image Generator

by shinpr
geminiTextClient.ts•9.75 kB
/** * Gemini Text Client for text generation using Gemini 2.0 Flash * Pure API client for interacting with Google AI Studio * Handles text generation without any prompt optimization logic */ 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' /** * Options for text generation */ export interface GenerationConfig { temperature?: number maxTokens?: number timeout?: number systemInstruction?: string inputImage?: string // Optional base64-encoded image for multimodal context } /** * Interface for Gemini Text Client - pure API client */ export interface GeminiTextClient { /** * Generate text using Gemini 2.0 Flash API * @param prompt The prompt to send to the API * @param config Optional configuration for generation * @returns Result containing generated text or error */ generateText( prompt: string, config?: GenerationConfig ): Promise<Result<string, GeminiAPIError | NetworkError>> /** * Validate connection to Gemini 2.0 Flash API * @returns Result indicating if connection is successful */ validateConnection(): Promise<Result<boolean, GeminiAPIError | NetworkError>> } /** * Default configuration for text generation */ const DEFAULT_GENERATION_CONFIG = { temperature: 0.7, maxTokens: 8192, timeout: 15000, } as const /** * Interface for Gemini AI client instance (@google/genai v1.17.0+) */ interface GeminiAIInstance { models: { generateContent(params: { model: string contents: string | Array<{ role?: string; parts: Array<{ text?: string }> }> systemInstruction?: string generationConfig?: { temperature?: number maxOutputTokens?: number } }): Promise<{ text: string response?: { text?: () => string candidates?: Array<{ content: { parts: Array<{ text: string }> } }> } }> } } /** * Implementation of Gemini Text Client - pure API client */ class GeminiTextClientImpl implements GeminiTextClient { private readonly modelName = 'gemini-2.0-flash' private readonly genai: GeminiAIInstance constructor(config: Config) { this.genai = new GoogleGenAI({ apiKey: config.geminiApiKey, }) as unknown as GeminiAIInstance } async generateText( prompt: string, config: GenerationConfig = {} ): Promise<Result<string, GeminiAPIError | NetworkError>> { // Merge with default configuration const mergedConfig = { ...DEFAULT_GENERATION_CONFIG, ...config, } // Validate input const validationResult = this.validatePromptInput(prompt) if (!validationResult.success) { return validationResult } try { // Call Gemini API const generatedText = await this.callGeminiAPI(prompt, mergedConfig) return Ok(generatedText) } catch (error) { return this.handleError(error, 'text generation') } } /** * Call Gemini 2.0-flash API to generate text */ private async callGeminiAPI(prompt: string, config: GenerationConfig): Promise<string> { try { // Generate content with timeout const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error('API call timeout')), config.timeout || 15000) }) // Build contents based on whether input image is provided (multimodal support) let contents: | string | Array<{ role?: string parts: Array<{ text?: string; inlineData?: { data: string; mimeType: string } }> }> if (config.inputImage) { // Multimodal request: combine image and text contents = [ { parts: [ { inlineData: { data: config.inputImage, mimeType: 'image/jpeg', // Assuming JPEG for simplicity; can be enhanced later }, }, { text: prompt, }, ], }, ] } else { // Text-only request contents = prompt } // Use the updated API structure for @google/genai v1.17.0+ const apiCall = this.genai.models.generateContent({ model: this.modelName, contents, ...(config.systemInstruction && { systemInstruction: config.systemInstruction }), generationConfig: { temperature: config.temperature || 0.7, maxOutputTokens: config.maxTokens || 8192, }, }) const response = await Promise.race([apiCall, timeoutPromise]) // Extract text from response - handling both possible response structures let responseText: string if (typeof response.text === 'string') { responseText = response.text } else if (response.response?.text && typeof response.response.text === 'function') { responseText = response.response.text() } else if (response.response?.candidates?.[0]?.content?.parts?.[0]?.text) { responseText = response.response.candidates[0].content.parts[0].text } else { throw new Error('Unable to extract text from API response') } if (!responseText || responseText.trim().length === 0) { throw new Error('Empty response from Gemini API') } return responseText.trim() } catch (error) { // Re-throw with context for proper error handling throw new Error( `Gemini API call failed: ${error instanceof Error ? error.message : 'Unknown error'}` ) } } async validateConnection(): Promise<Result<boolean, GeminiAPIError | NetworkError>> { try { // Validate by checking if the models object exists if (!this.genai.models) { return Err( new GeminiAPIError( 'Failed to access Gemini models', 'Check your GEMINI_API_KEY configuration' ) ) } // API key validation happens during actual API calls return Ok(true) } catch (error) { return this.handleError(error, 'connection validation') } } private handleError( error: unknown, context: string ): Result<never, GeminiAPIError | NetworkError> { const errorMessage = error instanceof Error ? error.message : 'Unknown error' // Check for network errors if (this.isNetworkError(error)) { return Err( new NetworkError( `Network error during ${context}: ${errorMessage}`, 'Check your internet connection and try again' ) ) } // Check for API errors if (this.isAPIError(error)) { return Err( new GeminiAPIError( `Failed during ${context}: ${errorMessage}`, this.getAPIErrorSuggestion(errorMessage) ) ) } // Generic error return Err( new GeminiAPIError( `Failed during ${context}: ${errorMessage}`, 'Check your API configuration and try again' ) ) } 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 { code?: string }).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' } /** * Validate prompt input before processing */ private validatePromptInput(prompt: string): Result<true, GeminiAPIError> { if (!prompt || prompt.trim().length === 0) { return Err( new GeminiAPIError( 'Empty prompt provided', 'Please provide a non-empty prompt for generation' ) ) } if (prompt.length > 100000) { return Err( new GeminiAPIError( 'Prompt too long', 'Please provide a shorter prompt (under 100,000 characters)' ) ) } return Ok(true) } } /** * Creates a new Gemini Text Client for prompt generation * @param config Configuration containing API key and settings * @returns Result containing the client or an error */ export function createGeminiTextClient(config: Config): Result<GeminiTextClient, GeminiAPIError> { try { return Ok(new GeminiTextClientImpl(config)) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' return Err( new GeminiAPIError( `Failed to initialize Gemini Text 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