gitlab-review-mcp
Version:
Node.js MCP server for GitLab code review operations with AI integration support
170 lines • 6.26 kB
JavaScript
import axios from 'axios';
export class ApiClient {
baseURL;
token;
timeout;
maxRetries;
constructor(baseURL, token, timeout = 30000, maxRetries = 3) {
this.baseURL = baseURL;
this.token = token;
this.timeout = timeout;
this.maxRetries = maxRetries;
}
async request(endpoint, options = {}) {
const { method = 'GET', data, headers = {}, timeout = this.timeout } = options;
const requestHeaders = {
'User-Agent': 'node-code-review-mcp/1.0.0',
'Accept': 'application/json',
...headers,
};
if (this.token) {
requestHeaders['Authorization'] = `Bearer ${this.token}`;
}
const config = {
method,
url: `${this.baseURL}/${endpoint}`,
headers: requestHeaders,
timeout,
data,
};
return this.executeWithRetry(async () => {
try {
const response = await axios(config);
return response;
}
catch (error) {
if (this.isAxiosError(error)) {
throw this.handleApiError(error);
}
throw error;
}
});
}
async executeWithRetry(operation) {
let lastError;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await operation();
}
catch (error) {
lastError = error;
if (attempt === this.maxRetries) {
break;
}
// Only retry on specific errors
if (this.shouldRetry(error)) {
const delay = this.calculateDelay(attempt);
console.warn(`Request failed (attempt ${attempt}/${this.maxRetries}), retrying in ${delay}ms...`);
await this.sleep(delay);
}
else {
break;
}
}
}
throw lastError;
}
shouldRetry(error) {
if (this.isAxiosError(error)) {
const status = error.response?.status;
// Retry on network errors, timeouts, and 5xx server errors
return !status || status >= 500 || status === 408 || status === 429;
}
// Retry on network/timeout errors
return error.code === 'ECONNRESET' ||
error.code === 'ENOTFOUND' ||
error.code === 'ECONNREFUSED' ||
error.code === 'ETIMEDOUT';
}
calculateDelay(attempt) {
// Exponential backoff with jitter
const baseDelay = 1000; // 1 second
const maxDelay = 10000; // 10 seconds
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
// Add jitter (±25%)
const jitter = delay * 0.25 * (Math.random() * 2 - 1);
return Math.round(delay + jitter);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
isAxiosError(error) {
return error.isAxiosError === true;
}
handleApiError(error) {
const status = error.response?.status;
const statusText = error.response?.statusText;
const data = error.response?.data;
let message = `API request failed: ${status} ${statusText}`;
if (data && typeof data === 'object') {
// GitHub/GitLab error format
if ('message' in data) {
message += ` - ${data.message}`;
}
if ('error_description' in data) {
message += ` - ${data.error_description}`;
}
}
const apiError = new Error(message);
apiError.status = status;
apiError.statusText = statusText;
apiError.data = data;
return apiError;
}
// Rate limiting support
async requestWithRateLimit(endpoint, options = {}) {
try {
const response = await this.request(endpoint, options);
// Check rate limit headers (GitHub format)
const remaining = response.headers['x-ratelimit-remaining'];
const reset = response.headers['x-ratelimit-reset'];
if (remaining && parseInt(remaining) < 10) {
console.warn(`API rate limit low: ${remaining} requests remaining`);
if (reset) {
const resetTime = new Date(parseInt(reset) * 1000);
console.warn(`Rate limit resets at: ${resetTime.toISOString()}`);
}
}
return response;
}
catch (error) {
// Handle rate limit exceeded
if (this.isAxiosError(error) && error.response?.status === 403) {
const resetHeader = error.response.headers['x-ratelimit-reset'];
if (resetHeader) {
const resetTime = parseInt(resetHeader) * 1000;
const waitTime = resetTime - Date.now();
if (waitTime > 0 && waitTime < 3600000) { // Wait max 1 hour
console.warn(`Rate limit exceeded. Waiting ${Math.ceil(waitTime / 1000)} seconds...`);
await this.sleep(waitTime + 1000); // Add 1 second buffer
return this.request(endpoint, options); // Retry once
}
}
}
throw error;
}
}
// Health check
async healthCheck() {
try {
// Try a simple request to test connectivity - use a valid GitLab API endpoint
// GitLab API v4 has a version endpoint that should always be available
await this.request('version', { timeout: 5000 });
return true;
}
catch (error) {
console.warn('API health check failed:', error instanceof Error ? error.message : String(error));
return false;
}
}
// Get API info
getApiInfo() {
return {
baseURL: this.baseURL,
hasToken: !!this.token,
timeout: this.timeout,
maxRetries: this.maxRetries,
};
}
}
//# sourceMappingURL=api-client.js.map