Skip to main content
Glama

pluggedin-mcp-proxy

error-handler.ts7.03 kB
/** * Centralized error handling for MCP proxy */ import { debugError } from './debug-log.js'; import axios from 'axios'; /** * Error types for categorization */ export enum ErrorType { VALIDATION = 'VALIDATION', NETWORK = 'NETWORK', TIMEOUT = 'TIMEOUT', AUTHENTICATION = 'AUTHENTICATION', AUTHORIZATION = 'AUTHORIZATION', RATE_LIMIT = 'RATE_LIMIT', RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', SERVER_ERROR = 'SERVER_ERROR', CLIENT_ERROR = 'CLIENT_ERROR', UNKNOWN = 'UNKNOWN' } /** * Custom error class with additional context */ export class McpProxyError extends Error { constructor( public type: ErrorType, message: string, public statusCode?: number, public details?: Record<string, unknown>, public originalError?: unknown ) { super(message); this.name = 'McpProxyError'; } } /** * Error context for logging and debugging */ interface ErrorContext { action: string; serverName?: string; serverUuid?: string; toolName?: string; resourceUri?: string; promptName?: string; userId?: string; requestId?: string; } /** * Categorizes errors based on their type */ export function categorizeError(error: unknown): ErrorType { if (axios.isAxiosError(error)) { const status = error.response?.status; if (!status) { return error.code === 'ECONNABORTED' ? ErrorType.TIMEOUT : ErrorType.NETWORK; } switch (status) { case 400: return ErrorType.VALIDATION; case 401: return ErrorType.AUTHENTICATION; case 403: return ErrorType.AUTHORIZATION; case 404: return ErrorType.RESOURCE_NOT_FOUND; case 429: return ErrorType.RATE_LIMIT; case 500: case 502: case 503: case 504: return ErrorType.SERVER_ERROR; default: return status >= 400 && status < 500 ? ErrorType.CLIENT_ERROR : ErrorType.SERVER_ERROR; } } if (error instanceof Error) { if (error.message.includes('validation') || error.message.includes('invalid')) { return ErrorType.VALIDATION; } if (error.message.includes('timeout')) { return ErrorType.TIMEOUT; } if (error.message.includes('auth')) { return ErrorType.AUTHENTICATION; } } return ErrorType.UNKNOWN; } /** * Sanitizes error messages to prevent information disclosure */ export function sanitizeErrorMessage(error: unknown, context?: ErrorContext): string { const errorType = categorizeError(error); switch (errorType) { case ErrorType.VALIDATION: return 'Invalid request parameters'; case ErrorType.AUTHENTICATION: return 'Authentication failed'; case ErrorType.AUTHORIZATION: return 'Access denied'; case ErrorType.RATE_LIMIT: return 'Rate limit exceeded. Please try again later'; case ErrorType.TIMEOUT: return 'Request timed out'; case ErrorType.NETWORK: return 'Network error occurred'; case ErrorType.RESOURCE_NOT_FOUND: return context?.toolName ? `Tool '${context.toolName}' not found` : context?.resourceUri ? `Resource '${context.resourceUri}' not found` : 'Resource not found'; case ErrorType.SERVER_ERROR: return 'Service temporarily unavailable'; default: return 'An unexpected error occurred'; } } /** * Logs error with context and returns sanitized error */ export function handleError( error: unknown, context: ErrorContext, includeDetails: boolean = false ): McpProxyError { const errorType = categorizeError(error); const sanitizedMessage = sanitizeErrorMessage(error, context); // Log detailed error for debugging debugError(`[${context.action}] Error:`, { type: errorType, context, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }); // Prepare details for response (if allowed) let details: Record<string, unknown> | undefined; if (includeDetails) { details = { action: context.action, errorType }; if (axios.isAxiosError(error)) { details.statusCode = error.response?.status; } } return new McpProxyError( errorType, sanitizedMessage, axios.isAxiosError(error) ? error.response?.status : undefined, details, error ); } /** * Wraps async functions with error handling */ export function withErrorHandling<T extends (...args: any[]) => Promise<any>>( fn: T, context: Omit<ErrorContext, 'requestId'> ): T { return (async (...args: Parameters<T>) => { const requestId = Math.random().toString(36).substring(7); try { return await fn(...args); } catch (error) { throw handleError(error, { ...context, requestId }); } }) as T; } /** * Circuit breaker for handling repeated failures */ export class CircuitBreaker { private failures = 0; private lastFailureTime = 0; private state: 'closed' | 'open' | 'half-open' = 'closed'; constructor( private readonly threshold: number = 5, private readonly timeout: number = 60000, // 1 minute private readonly resetTimeout: number = 30000 // 30 seconds ) {} async execute<T>(fn: () => Promise<T>): Promise<T> { if (this.state === 'open') { const now = Date.now(); if (now - this.lastFailureTime > this.timeout) { this.state = 'half-open'; } else { throw new McpProxyError( ErrorType.SERVER_ERROR, 'Circuit breaker is open. Service temporarily unavailable.', 503 ); } } try { const result = await fn(); if (this.state === 'half-open') { this.reset(); } return result; } catch (error) { this.recordFailure(); throw error; } } private recordFailure(): void { this.failures++; this.lastFailureTime = Date.now(); if (this.failures >= this.threshold) { this.state = 'open'; setTimeout(() => { this.state = 'half-open'; }, this.resetTimeout); } } private reset(): void { this.failures = 0; this.lastFailureTime = 0; this.state = 'closed'; } getState(): string { return this.state; } } /** * Retry logic with exponential backoff */ export async function retryWithBackoff<T>( fn: () => Promise<T>, maxRetries: number = 3, initialDelay: number = 1000 ): Promise<T> { let lastError: unknown; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Don't retry on client errors (4xx) if (axios.isAxiosError(error) && error.response?.status && error.response.status >= 400 && error.response.status < 500) { throw error; } // Calculate delay with exponential backoff const delay = initialDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; }

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/VeriTeknik/pluggedin-mcp'

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