@bratcliffe909/mcp-server-segmind
Version:
Model Context Protocol server for Segmind API - Generate images and videos using AI models
234 lines • 10.3 kB
JavaScript
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