Skip to main content
Glama
global-error-handler.ts6.09 kB
/** * Global error handler for the MCP server * Provides centralized error handling, logging, and response formatting */ import { AxiosError } from 'axios'; import { TeamCityAPIError } from '@/teamcity/errors'; import { ErrorContext, errorLogger } from '@/utils/error-logger'; import { ErrorResponse, MCPRateLimitError, MCPTeamCityError, MCPTimeoutError, MCPToolError, formatError, } from './error'; export interface GlobalErrorHandlerOptions { includeStackTrace?: boolean; sanitizeErrors?: boolean; logErrors?: boolean; defaultErrorMessage?: string; } /** * Global error handler class */ export class GlobalErrorHandler { private static instance: GlobalErrorHandler; constructor(private readonly options: GlobalErrorHandlerOptions = {}) { this.options = { includeStackTrace: process.env['NODE_ENV'] !== 'production', sanitizeErrors: process.env['NODE_ENV'] === 'production', logErrors: true, defaultErrorMessage: 'An unexpected error occurred', ...options, }; } public static getInstance(options?: GlobalErrorHandlerOptions): GlobalErrorHandler { GlobalErrorHandler.instance ??= new GlobalErrorHandler(options); return GlobalErrorHandler.instance; } /** * Handle error from MCP tool execution */ handleToolError(error: unknown, toolName: string, context?: ErrorContext): ErrorResponse { const enhancedContext: ErrorContext = { ...context, operation: toolName, component: 'MCP_TOOL', }; // Transform specific error types const transformedError = this.transformError(error, enhancedContext); // Log the error if enabled if (this.options.logErrors) { errorLogger.logError( `Error in tool '${toolName}'`, transformedError instanceof Error ? transformedError : new Error(String(transformedError)), enhancedContext ); } // Format error response return formatError(transformedError, enhancedContext); } /** * Handle async operation errors */ handleAsyncError(error: unknown, operationName: string, context?: ErrorContext): never { const enhancedContext: ErrorContext = { ...context, operation: operationName, component: 'ASYNC_HANDLER', }; const transformedError = this.transformError(error, enhancedContext); if (this.options.logErrors) { errorLogger.logError( `Async error in '${operationName}'`, transformedError instanceof Error ? transformedError : new Error(String(transformedError)), enhancedContext ); } throw transformedError; } /** * Transform raw errors into structured MCP errors */ private transformError(error: unknown, context: ErrorContext): Error { // Errors already normalized by our TeamCity client if (error instanceof TeamCityAPIError) { return new MCPTeamCityError( this.sanitizeErrorMessage(error.message), error.statusCode ?? 500, error.code, context.requestId ); } // Already an MCP error if (error instanceof MCPToolError) { // Still sanitize the message if needed if (this.options.sanitizeErrors) { const sanitizedMessage = this.sanitizeErrorMessage(error.message); return new MCPToolError(sanitizedMessage, error.code, error.statusCode, error.data); } return error; } // Axios/HTTP errors if (error instanceof AxiosError) { return this.transformAxiosError(error, context); } // Native errors if (error instanceof Error) { const sanitizedMessage = this.sanitizeErrorMessage(error.message); // Check for specific error patterns if (sanitizedMessage.includes('timeout') || error.name === 'TimeoutError') { return new MCPTimeoutError(context.operation ?? 'unknown', 30000); } if (sanitizedMessage.includes('rate limit') || sanitizedMessage.includes('429')) { return new MCPRateLimitError(); } return new Error(sanitizedMessage); } // Unknown error types return new Error(this.sanitizeErrorMessage(String(error))); } /** * Transform Axios errors into MCPTeamCityError */ private transformAxiosError(axiosError: AxiosError, context: ErrorContext): MCPTeamCityError { const status = axiosError.response?.status ?? 500; const data = axiosError.response?.data; let message = axiosError.message; let teamCityCode: string | undefined; // Extract TeamCity-specific error information if (data != null && typeof data === 'object') { if ('message' in data && typeof data.message === 'string') { message = data.message; } if ('errorCode' in data && typeof data.errorCode === 'string') { teamCityCode = data.errorCode; } } return new MCPTeamCityError( this.sanitizeErrorMessage(message), status, teamCityCode, context.requestId ); } /** * Sanitize error messages for production */ private sanitizeErrorMessage(message: string): string { if (!this.options.sanitizeErrors) { return message; } // Remove sensitive information patterns return message .replace(/token[=:]\s*[^\s&]+/gi, 'token=***') .replace(/password[=:]\s*[^\s&]+/gi, 'password=***') .replace(/apikey[=:]\s*[^\s&]+/gi, 'apikey=***') .replace(/authorization:\s*[^\s&]+/gi, 'authorization: ***'); } /** * Check if error should be retried */ isRetryableError(error: unknown): boolean { if (error instanceof MCPTeamCityError) { // Retry on temporary server errors return error.statusCode >= 500 && error.statusCode < 600; } if (error instanceof MCPTimeoutError) { return true; } if (error instanceof AxiosError) { // Network errors or temporary server issues return !error.response || (error.response.status >= 500 && error.response.status < 600); } return false; } } // Export singleton instance export const globalErrorHandler = GlobalErrorHandler.getInstance();

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/Daghis/teamcity-mcp'

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