UNPKG

@spaik/mcp-server-roi

Version:

MCP server for AI ROI prediction and tracking with Monte Carlo simulations

191 lines 6.85 kB
import { circuitBreakerManager } from '../utils/circuit-breaker.js'; import { rateLimiters } from '../utils/rate-limiter.js'; import { structuredLogger } from '../utils/structured-logger.js'; /** * Protected API client with circuit breaker and rate limiting */ export class ProtectedApiClient { serviceName; baseUrl; defaultHeaders; circuitBreakerOptions; constructor(config) { this.serviceName = config.serviceName; this.baseUrl = config.baseUrl; this.defaultHeaders = { 'Content-Type': 'application/json', ...config.defaultHeaders }; if (config.apiKey) { this.defaultHeaders['Authorization'] = `Bearer ${config.apiKey}`; } this.circuitBreakerOptions = { failureThreshold: 3, resetTimeout: 60000, // 1 minute timeout: 30000, // 30 seconds ...config.circuitBreakerOptions, fallback: async () => { throw new Error(`${this.serviceName} service is currently unavailable`); } }; } /** * Make a protected API request */ async request(endpoint, options = {}) { const logger = structuredLogger.createApi(this.serviceName, endpoint, options.correlationId); const breaker = circuitBreakerManager.getBreaker(this.serviceName, this.circuitBreakerOptions); return breaker.execute(async () => { // Apply rate limiting // Use appropriate rate limiter based on service const limiter = this.serviceName.toLowerCase().includes('perplexity') ? rateLimiters.perplexity : rateLimiters.fmp; return limiter.executeWithRateLimit(async () => { const url = `${this.baseUrl}${endpoint}`; const method = options.method || 'GET'; logger.debug(`Making ${method} request`, { url }); const response = await fetch(url, { method, headers: { ...this.defaultHeaders, ...options.headers }, body: options.body ? JSON.stringify(options.body) : undefined }); if (!response.ok) { const error = new Error(`API request failed: ${response.status} ${response.statusText}`); error.status = response.status; // Don't trip circuit for client errors if (response.status >= 400 && response.status < 500) { error.skipCircuitBreaker = true; } throw error; } const data = await response.json(); logger.debug('Request successful', { status: response.status, dataSize: JSON.stringify(data).length }); return data; }, { priority: 'normal', timeout: this.circuitBreakerOptions.timeout }); }, options.correlationId); } /** * Check if the service is available */ isAvailable() { const breaker = circuitBreakerManager.getBreaker(this.serviceName); return breaker.isAvailable(); } /** * Get circuit breaker statistics */ getStats() { const breaker = circuitBreakerManager.getBreaker(this.serviceName); return breaker.getStats(); } } /** * Create protected API clients for all external services */ export const protectedClients = { perplexity: new ProtectedApiClient({ serviceName: 'Perplexity', baseUrl: 'https://api.perplexity.ai', apiKey: process.env.PERPLEXITY_API_KEY, circuitBreakerOptions: { failureThreshold: 3, resetTimeout: 120000, // 2 minutes errorFilter: (error) => { // Don't trip for rate limits, handle them differently return !error.status || error.status !== 429; } } }), financialModelingPrep: new ProtectedApiClient({ serviceName: 'FinancialModelingPrep', baseUrl: 'https://financialmodelingprep.com/api/v3', circuitBreakerOptions: { failureThreshold: 5, resetTimeout: 300000, // 5 minutes } }), dutchGovAPI: new ProtectedApiClient({ serviceName: 'DutchGovAPI', baseUrl: 'https://opendata.cbs.nl/api/v3', circuitBreakerOptions: { failureThreshold: 2, resetTimeout: 600000, // 10 minutes fallback: async () => { // Return cached or default data return { data: [], message: 'Using cached Dutch government data', cached: true }; } } }) }; /** * Enhanced Sonar client with circuit breaker */ export class ProtectedSonarClient extends ProtectedApiClient { constructor(apiKey) { super({ serviceName: 'Perplexity-Sonar', baseUrl: 'https://api.perplexity.ai', apiKey, defaultHeaders: { 'Content-Type': 'application/json', }, circuitBreakerOptions: { failureThreshold: 3, resetTimeout: 60000, timeout: 15000, // 15 seconds for Sonar errorFilter: (error) => { const status = error.status; // Don't trip for client errors or rate limits return !status || (status >= 500); }, fallback: async () => { return { content: 'Service temporarily unavailable. Using default benchmarks.', citations: [], fallback: true }; } } }); } async chat(messages, options) { return this.request('/chat/completions', { method: 'POST', body: { model: options?.model || 'sonar', messages, ...options }, correlationId: options?.correlationId }); } } /** * Health check endpoint data */ export function getApiHealth() { return { services: { perplexity: protectedClients.perplexity.getStats(), financialModelingPrep: protectedClients.financialModelingPrep.getStats(), dutchGovAPI: protectedClients.dutchGovAPI.getStats() }, overallHealth: circuitBreakerManager.isHealthy(), timestamp: new Date().toISOString() }; } //# sourceMappingURL=protected-api-client.js.map