UNPKG

@bratcliffe909/mcp-server-segmind

Version:

Model Context Protocol server for Segmind API - Generate images and videos using AI models

234 lines 10.3 kB
import { config } from '../utils/config.js'; import { NetworkError, TimeoutError, AuthenticationError, RateLimitError, InsufficientCreditsError, mapToSafeError, } from '../utils/errors.js'; import { logger } from '../utils/logger.js'; export class SegmindApiClient { baseUrl; apiKey; defaultTimeout; retryConfig; constructor() { this.baseUrl = config.baseUrl; this.apiKey = config.apiKey || ''; this.defaultTimeout = config.limits.requestTimeout; this.retryConfig = { maxRetries: 3, baseDelay: 1000, maxDelay: 10000, retryableStatuses: [408, 429, 500, 502, 503, 504], }; } async request(endpoint, options = {}) { if (!this.apiKey || this.apiKey.length === 0) { throw new AuthenticationError('SEGMIND_API_KEY is not configured. Please set it in your environment or MCP configuration.'); } const { method = 'GET', body, headers = {}, timeout = this.defaultTimeout, retries = this.retryConfig.maxRetries, responseType = 'auto', } = options; const url = `${this.baseUrl}${endpoint}`; const requestHeaders = { 'x-api-key': this.apiKey, 'Content-Type': 'application/json', 'User-Agent': '@segmind/mcp-server/0.1.0', ...headers, }; const fetchOptions = { method, headers: requestHeaders, signal: AbortSignal.timeout(timeout), }; if (body) { fetchOptions.body = JSON.stringify(body); } logger.debug('API request', { endpoint, method, timeout, }); return this.executeWithRetry(url, fetchOptions, retries, responseType); } async executeWithRetry(url, options, retriesLeft, responseType = 'auto') { try { const response = await fetch(url, options); if (response.status === 429) { const retryAfter = response.headers.get('Retry-After'); const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : this.retryConfig.baseDelay; if (retriesLeft > 0) { logger.warn('Rate limited, retrying', { delay, retriesLeft }); await this.delay(delay); return this.executeWithRetry(url, options, retriesLeft - 1, responseType); } throw new RateLimitError(); } if (response.status === 401 || response.status === 403) { throw new AuthenticationError(); } if (response.status === 402) { throw new InsufficientCreditsError(); } if (response.status === 406) { const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { try { const errorData = await response.json(); if (errorData?.error && typeof errorData.error === 'string' && errorData.error.toLowerCase().includes('credits')) { throw new InsufficientCreditsError(errorData.error); } throw new Error(errorData?.error || `API request failed with status ${response.status}`); } catch (e) { if (e instanceof InsufficientCreditsError) throw e; throw new Error(`API request failed with status ${response.status}`); } } throw new Error(`API request failed with status ${response.status}`); } if (this.retryConfig.retryableStatuses.includes(response.status) && retriesLeft > 0) { const delay = this.calculateRetryDelay(this.retryConfig.maxRetries - retriesLeft); logger.warn('Request failed, retrying', { status: response.status, delay, retriesLeft, }); await this.delay(delay); return this.executeWithRetry(url, options, retriesLeft - 1, responseType); } const contentType = response.headers.get('content-type') || ''; const isImage = contentType.includes('image/'); const isVideo = contentType.includes('video/'); const isAudio = contentType.includes('audio/'); const isBinary = isImage || isVideo || isAudio; const isJson = contentType.includes('application/json'); let responseData; if (responseType === 'buffer' || (responseType === 'auto' && isBinary)) { if (!response.ok) { const errorText = await response.text(); throw new Error(`API request failed with status ${response.status}: ${errorText}`); } const buffer = await response.arrayBuffer(); const base64 = Buffer.from(buffer).toString('base64'); const mimeType = contentType.split(';')[0]; let dataKey; if (isImage) { dataKey = 'image'; } else if (isVideo) { dataKey = 'video'; } else if (isAudio) { dataKey = 'audio'; } else { dataKey = 'data'; } responseData = { data: { [dataKey]: base64, format: mimeType ? mimeType.split('/')[1] : 'unknown', size: buffer.byteLength, mimeType: mimeType, }, credits: { used: parseInt(response.headers.get('x-credits-consumed') || '0', 10), remaining: parseInt(response.headers.get('x-remaining-credits') || '0', 10), }, }; } else if (responseType === 'json' || (responseType === 'auto' && isJson)) { responseData = await response.json(); if (!response.ok || responseData.error) { logger.error('API error response', { status: response.status, statusText: response.statusText, url: response.url, error: responseData.error, data: responseData, }); let errorMessage = typeof responseData.error === 'string' ? responseData.error : responseData.error?.message || `API request failed with status ${response.status}`; if (response.status === 400) { logger.error('Bad Request details', { url: response.url, requestBody: options.body, responseError: responseData.error, responseData, }); errorMessage = `Bad Request: ${errorMessage}. Check that all parameters match the model's requirements.`; } throw new Error(errorMessage); } const remainingCredits = response.headers.get('x-remaining-credits'); if (remainingCredits && !responseData.credits?.remaining) { responseData.credits = { used: responseData.credits?.used || parseInt(response.headers.get('x-credits-consumed') || '0', 10), remaining: parseInt(remainingCredits, 10), }; } } else { throw new Error(`Unexpected response type: ${contentType}`); } logger.debug('API request successful', { responseType: isBinary ? (isImage ? 'image' : isVideo ? 'video' : 'audio') : 'json', credits: responseData.credits, }); return responseData; } catch (error) { if (error instanceof Error && error.name === 'AbortError') { throw new TimeoutError(); } if (error instanceof TypeError && error.message.includes('fetch')) { throw new NetworkError('Failed to connect to Segmind API'); } if (error instanceof Error && 'code' in error) { throw error; } throw mapToSafeError(error); } } calculateRetryDelay(attempt) { const delay = Math.min(this.retryConfig.baseDelay * Math.pow(2, attempt), this.retryConfig.maxDelay); const jitter = Math.random() * 0.1 * delay; return Math.floor(delay + jitter); } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async healthCheck() { try { const response = await this.request('/health', { timeout: 5000, retries: 1, }); return !response.error; } catch (error) { logger.error('Health check failed', { error }); return false; } } async getCredits() { const response = await this.request('/credits'); if (!response.data?.credits) { throw new Error('Invalid credits response'); } return response.data.credits; } async generateImage(model, params) { const modelEndpointMap = { 'sdxl': '/sdxl1.0-txt2img', 'sdxl-img2img': '/sdxl1.0-img2img', 'sd15-img2img': '/sd1.5-img2img', 'esrgan': '/esrgan', }; const endpoint = modelEndpointMap[model] || `/${model}`; return this.request(endpoint, { method: 'POST', body: params, responseType: 'auto', }); } } export const apiClient = new SegmindApiClient(); //# sourceMappingURL=client.js.map