Skip to main content
Glama
cameronsjo

MCP Server Template

by cameronsjo
http-client.ts7.01 kB
/** * HTTP client with rate limiting, retry, and observability * * Provides a resilient HTTP client for external API calls with: * - Rate limiting integration * - Automatic retries with exponential backoff * - Request/response logging * - User agent rotation (optional) * - Timeout handling */ import { createLogger } from './logger.js'; import { getRateLimiter } from './rate-limiter.js'; import { withRetry, RetryableErrors, type RetryOptions } from './retry.js'; import { ExternalApiError, TimeoutError } from './errors.js'; import { getConfig } from '../config/index.js'; const logger = createLogger('http-client'); /** * HTTP request options */ export interface HttpRequestOptions { /** HTTP method (default: GET) */ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; /** Request headers */ headers?: Record<string, string>; /** Request body (will be JSON stringified if object) */ body?: unknown; /** Request timeout in ms (default: from config) */ timeoutMs?: number; /** Rate limiter source key */ rateLimitSource?: string; /** Retry options */ retry?: RetryOptions; /** Skip rate limiting */ skipRateLimit?: boolean; /** Skip retry logic */ skipRetry?: boolean; } /** * HTTP response wrapper */ export interface HttpResponse<T = unknown> { status: number; statusText: string; headers: Record<string, string>; data: T; durationMs: number; } /** * Common user agents for rotation */ const USER_AGENTS = [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', ]; /** * Get a random user agent */ function getRandomUserAgent(): string { return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)] ?? USER_AGENTS[0]!; } /** * HTTP error with status code */ export class HttpError extends Error { readonly status: number; readonly statusText: string; readonly responseBody?: string; constructor(status: number, statusText: string, responseBody?: string) { super(`HTTP ${status}: ${statusText}`); this.name = 'HttpError'; this.status = status; this.statusText = statusText; this.responseBody = responseBody; } } /** * Make an HTTP request with rate limiting and retry */ export async function httpRequest<T = unknown>( url: string, options: HttpRequestOptions = {} ): Promise<HttpResponse<T>> { const config = getConfig(); const startTime = Date.now(); const { method = 'GET', headers = {}, body, timeoutMs = config.requestTimeoutMs, rateLimitSource, retry = {}, skipRateLimit = false, skipRetry = false, } = options; // Apply rate limiting if (!skipRateLimit && rateLimitSource) { const rateLimiter = getRateLimiter(); await rateLimiter.waitForSlot(rateLimitSource); } // Build request headers const requestHeaders: Record<string, string> = { 'User-Agent': getRandomUserAgent(), Accept: 'application/json', ...headers, }; if (body && !requestHeaders['Content-Type']) { requestHeaders['Content-Type'] = 'application/json'; } // Build request body const requestBody = body && typeof body === 'object' ? JSON.stringify(body) : (body as string | undefined); // Create abort controller for timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); const executeRequest = async (): Promise<HttpResponse<T>> => { try { logger.debug('HTTP request', { method, url }); const response = await fetch(url, { method, headers: requestHeaders, body: requestBody, signal: controller.signal, }); clearTimeout(timeoutId); // Extract response headers const responseHeaders: Record<string, string> = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); // Handle error responses if (!response.ok) { const responseBody = await response.text(); // Record failure for rate limiting if (rateLimitSource) { const rateLimiter = getRateLimiter(); if (response.status === 429 || response.status >= 500) { rateLimiter.recordFailure(rateLimitSource); } } throw new HttpError(response.status, response.statusText, responseBody); } // Parse response const contentType = response.headers.get('content-type') ?? ''; let data: T; if (contentType.includes('application/json')) { data = (await response.json()) as T; } else { data = (await response.text()) as T; } // Record success for rate limiting if (rateLimitSource) { const rateLimiter = getRateLimiter(); rateLimiter.recordSuccess(rateLimitSource); } const durationMs = Date.now() - startTime; logger.debug('HTTP response', { method, url, status: response.status, durationMs }); return { status: response.status, statusText: response.statusText, headers: responseHeaders, data, durationMs, }; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new TimeoutError(`HTTP ${method} ${url}`, timeoutMs); } if (error instanceof HttpError) { throw error; } throw new ExternalApiError( new URL(url).hostname, error instanceof Error ? error.message : String(error), error instanceof Error ? error : undefined ); } }; // Execute with or without retry if (skipRetry) { return executeRequest(); } return withRetry(executeRequest, { maxAttempts: 3, isRetryable: RetryableErrors.isTransientError, ...retry, }); } /** * Convenience methods */ export const http = { get: <T = unknown>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'body'>) => httpRequest<T>(url, { ...options, method: 'GET' }), post: <T = unknown>(url: string, body?: unknown, options?: Omit<HttpRequestOptions, 'method'>) => httpRequest<T>(url, { ...options, method: 'POST', body }), put: <T = unknown>(url: string, body?: unknown, options?: Omit<HttpRequestOptions, 'method'>) => httpRequest<T>(url, { ...options, method: 'PUT', body }), patch: <T = unknown>(url: string, body?: unknown, options?: Omit<HttpRequestOptions, 'method'>) => httpRequest<T>(url, { ...options, method: 'PATCH', body }), delete: <T = unknown>(url: string, options?: Omit<HttpRequestOptions, 'method' | 'body'>) => httpRequest<T>(url, { ...options, method: 'DELETE' }), };

Latest Blog Posts

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/cameronsjo/mcp-server-template'

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