Skip to main content
Glama
klappe-pm

Real Estate MCP Server

by klappe-pm
http-client.ts7.32 kB
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import NodeCache from 'node-cache'; import { appConfig } from '../config/index.js'; import { APIError, TimeoutError, RateLimitError, logError } from '../utils/errors.js'; /** * Rate limiter using token bucket algorithm */ class RateLimiter { private tokens: number; private lastRefill: number; constructor( private maxTokens: number, private refillRate: number // tokens per minute ) { this.tokens = maxTokens; this.lastRefill = Date.now(); } async waitForToken(): Promise<void> { this.refillTokens(); if (this.tokens <= 0) { const waitTime = (60 * 1000) / this.refillRate; // ms to wait for next token await new Promise(resolve => setTimeout(resolve, waitTime)); return this.waitForToken(); } this.tokens--; } private refillTokens(): void { const now = Date.now(); const elapsed = now - this.lastRefill; const tokensToAdd = Math.floor((elapsed / (60 * 1000)) * this.refillRate); if (tokensToAdd > 0) { this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd); this.lastRefill = now; } } } /** * HTTP client service with error handling, caching, and rate limiting */ export class HttpClientService { private axiosInstance: AxiosInstance; private cache: NodeCache; private rateLimiters: Map<string, RateLimiter> = new Map(); constructor() { this.cache = new NodeCache({ stdTTL: appConfig.cacheTtl }); this.axiosInstance = axios.create({ timeout: appConfig.apiTimeout, headers: { 'Content-Type': 'application/json', 'User-Agent': 'Real-Estate-MCP/1.0.0', }, }); this.setupInterceptors(); this.setupRateLimiters(); } private setupRateLimiters(): void { this.rateLimiters.set('zillow', new RateLimiter( appConfig.zillowRateLimit, appConfig.zillowRateLimit )); this.rateLimiters.set('rentcast', new RateLimiter( appConfig.rentcastRateLimit, appConfig.rentcastRateLimit )); } private setupInterceptors(): void { // Request interceptor for logging this.axiosInstance.interceptors.request.use( (config) => { if (appConfig.logLevel === 'debug') { console.log(`HTTP Request: ${config.method?.toUpperCase()} ${config.url}`); } return config; }, (error) => { logError(error, 'HTTP Request'); return Promise.reject(error); } ); // Response interceptor for error handling and logging this.axiosInstance.interceptors.response.use( (response: AxiosResponse) => { if (appConfig.logLevel === 'debug') { console.log(`HTTP Response: ${response.status} ${response.config.url}`); } return response; }, (error: AxiosError) => { this.handleHttpError(error); return Promise.reject(error); } ); } private handleHttpError(error: AxiosError): void { const { response, code, config } = error; if (code === 'ECONNABORTED' || code === 'TIMEOUT') { throw new TimeoutError( 'Request timed out', config?.timeout ); } if (response?.status === 429) { const retryAfter = response.headers['retry-after'] ? parseInt(response.headers['retry-after'] as string) : undefined; throw new RateLimitError( 'Rate limit exceeded', retryAfter ); } const statusCode = response?.status; const provider = this.getProviderFromUrl(config?.url); throw new APIError( `HTTP ${statusCode} error from ${provider || 'API'}`, statusCode, provider, error ); } private getProviderFromUrl(url?: string): string | undefined { if (!url) return undefined; if (url.includes('rapidapi.com') || url.includes('zillow')) { return 'Zillow'; } if (url.includes('rentcast.io')) { return 'RentCast'; } return 'Unknown'; } /** * Make HTTP GET request with caching and rate limiting */ async get<T>( url: string, options: { headers?: Record<string, string>; params?: Record<string, any>; provider?: 'zillow' | 'rentcast'; cacheTtl?: number; skipCache?: boolean; } = {} ): Promise<T> { const { provider, cacheTtl, skipCache = false, ...axiosOptions } = options; // Generate cache key const cacheKey = this.generateCacheKey(url, axiosOptions.params); // Try cache first (unless skipped) if (!skipCache) { const cached = this.cache.get<T>(cacheKey); if (cached) { if (appConfig.logLevel === 'debug') { console.log(`Cache hit for ${url}`); } return cached; } } // Apply rate limiting if (provider) { const rateLimiter = this.rateLimiters.get(provider); if (rateLimiter) { await rateLimiter.waitForToken(); } } try { // Make HTTP request with retry logic const response = await this.makeRequestWithRetry<T>(url, axiosOptions); // Cache the response if (!skipCache) { this.cache.set(cacheKey, response.data, cacheTtl || appConfig.cacheTtl); } return response.data; } catch (error) { logError(error as Error, `HTTP GET ${url}`); throw error; } } /** * Make HTTP request with exponential backoff retry */ private async makeRequestWithRetry<T>( url: string, options: any, maxRetries: number = 3, currentRetry: number = 0 ): Promise<AxiosResponse<T>> { try { return await this.axiosInstance.get<T>(url, options); } catch (error) { if (currentRetry >= maxRetries) { throw error; } const isRetryableError = this.isRetryableError(error as AxiosError); if (!isRetryableError) { throw error; } // Exponential backoff: 1s, 2s, 4s const delayMs = Math.pow(2, currentRetry) * 1000; await new Promise(resolve => setTimeout(resolve, delayMs)); return this.makeRequestWithRetry<T>(url, options, maxRetries, currentRetry + 1); } } private isRetryableError(error: AxiosError): boolean { // Retry on network errors or 5xx server errors return !error.response || error.response.status >= 500 || error.code === 'ECONNRESET' || error.code === 'ENOTFOUND'; } private generateCacheKey(url: string, params?: Record<string, any>): string { const paramStr = params ? JSON.stringify(params) : ''; return `${url}:${paramStr}`; } /** * Clear cache for specific pattern or all */ clearCache(pattern?: string): void { if (pattern) { const keys = this.cache.keys(); keys.forEach(key => { if (key.includes(pattern)) { this.cache.del(key); } }); } else { this.cache.flushAll(); } } /** * Get cache stats */ getCacheStats(): { hits: number; misses: number; keys: number } { const stats = this.cache.getStats(); return { hits: stats.hits, misses: stats.misses, keys: stats.keys }; } } // Singleton instance export const httpClient = new HttpClientService();

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/klappe-pm/Real-Estate-MCP'

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