UNPKG

survey-mcp-server

Version:

Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management

318 lines 11.2 kB
import { logger } from '../utils/logger.js'; export var ErrorCode; (function (ErrorCode) { ErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR"; ErrorCode["SANITIZATION_ERROR"] = "SANITIZATION_ERROR"; ErrorCode["DATABASE_ERROR"] = "DATABASE_ERROR"; ErrorCode["SEARCH_ERROR"] = "SEARCH_ERROR"; ErrorCode["EXTERNAL_API_ERROR"] = "EXTERNAL_API_ERROR"; ErrorCode["AUTHENTICATION_ERROR"] = "AUTHENTICATION_ERROR"; ErrorCode["AUTHORIZATION_ERROR"] = "AUTHORIZATION_ERROR"; ErrorCode["NOT_FOUND"] = "NOT_FOUND"; ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR"; ErrorCode["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR"; ErrorCode["TIMEOUT_ERROR"] = "TIMEOUT_ERROR"; ErrorCode["CONFIGURATION_ERROR"] = "CONFIGURATION_ERROR"; })(ErrorCode || (ErrorCode = {})); export class BaseError extends Error { constructor(errorDetails, statusCode = 500, isOperational = true) { super(errorDetails.message); this.name = this.constructor.name; this.code = errorDetails.code; this.statusCode = statusCode; this.isOperational = isOperational; this.field = errorDetails.field; this.details = errorDetails.details; this.correlationId = errorDetails.correlationId || this.generateCorrelationId(); this.timestamp = errorDetails.timestamp || new Date(); this.retryable = errorDetails.retryable || false; Error.captureStackTrace(this, this.constructor); } generateCorrelationId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } toJSON() { return { name: this.name, message: this.message, code: this.code, statusCode: this.statusCode, field: this.field, details: this.details, correlationId: this.correlationId, timestamp: this.timestamp.toISOString(), retryable: this.retryable }; } } export class ValidationError extends BaseError { constructor(message, field, details) { super({ code: ErrorCode.VALIDATION_ERROR, message, field, details }, 400); } } export class SanitizationError extends BaseError { constructor(message, details) { super({ code: ErrorCode.SANITIZATION_ERROR, message, details }, 400); } } export class DatabaseError extends BaseError { constructor(message, details, retryable = false) { super({ code: ErrorCode.DATABASE_ERROR, message, details, retryable }, 500); } } export class SearchError extends BaseError { constructor(message, details, retryable = true) { super({ code: ErrorCode.SEARCH_ERROR, message, details, retryable }, 503); } } export class ExternalApiError extends BaseError { constructor(message, details, retryable = true) { super({ code: ErrorCode.EXTERNAL_API_ERROR, message, details, retryable }, 503); } } export class NotFoundError extends BaseError { constructor(message, details) { super({ code: ErrorCode.NOT_FOUND, message, details }, 404); } } export class ConfigurationError extends BaseError { constructor(message, details) { super({ code: ErrorCode.CONFIGURATION_ERROR, message, details }, 500); } } export class TimeoutError extends BaseError { constructor(message, details) { super({ code: ErrorCode.TIMEOUT_ERROR, message, details, retryable: true }, 408); } } export class ErrorHandlingMiddleware { constructor() { } static getInstance() { if (!ErrorHandlingMiddleware.instance) { ErrorHandlingMiddleware.instance = new ErrorHandlingMiddleware(); } return ErrorHandlingMiddleware.instance; } handleError(error, toolName) { const correlationId = error instanceof BaseError ? error.correlationId : this.generateCorrelationId(); // Log the error this.logError(error, toolName, correlationId); // Convert to structured error response const errorResponse = this.createErrorResponse(error, correlationId); return errorResponse; } wrapAsyncOperation(operation, errorContext) { return new Promise(async (resolve, reject) => { let timeoutId; if (errorContext?.timeout) { timeoutId = setTimeout(() => { reject(new TimeoutError(`Operation timed out after ${errorContext.timeout}ms`, { toolName: errorContext?.toolName, operationType: errorContext?.operationType })); }, errorContext.timeout); } try { const result = await operation(); if (timeoutId) clearTimeout(timeoutId); resolve(result); } catch (error) { if (timeoutId) clearTimeout(timeoutId); // Convert known errors to structured errors const structuredError = this.normalizeError(error, errorContext); reject(structuredError); } }); } logError(error, toolName, correlationId) { const logContext = { correlationId, toolName, errorType: error.constructor.name, stack: error.stack }; if (error instanceof BaseError) { logContext.errorCode = error.code; logContext.statusCode = error.statusCode; logContext.field = error.field; logContext.details = error.details; logContext.retryable = error.retryable; // Log at different levels based on severity if (error.statusCode >= 500) { logger.error(`Error in ${toolName || 'unknown tool'}: ${error.message}`, logContext); } else if (error.statusCode >= 400) { logger.warn(`Client error in ${toolName || 'unknown tool'}: ${error.message}`, logContext); } else { logger.info(`Handled error in ${toolName || 'unknown tool'}: ${error.message}`, logContext); } } else { // Unstructured error - always log as error logger.error(`Unhandled error in ${toolName || 'unknown tool'}: ${error.message}`, logContext); } } createErrorResponse(error, correlationId) { if (error instanceof BaseError) { return [ { type: "text", text: JSON.stringify({ error: true, code: error.code, message: this.sanitizeErrorMessage(error.message), correlationId: error.correlationId, timestamp: error.timestamp.toISOString(), retryable: error.retryable, ...(error.field && { field: error.field }) }, null, 2) } ]; } else { // Unknown error - provide generic response return [ { type: "text", text: JSON.stringify({ error: true, code: ErrorCode.INTERNAL_ERROR, message: "An internal error occurred. Please try again later.", correlationId, timestamp: new Date().toISOString(), retryable: true }, null, 2) } ]; } } normalizeError(error, context) { if (error instanceof BaseError) { return error; } // MongoDB errors if (error.name === 'MongoError' || error.name === 'MongoServerError') { return new DatabaseError('Database operation failed', { originalError: error.message, ...context }, this.isRetryableMongoError(error)); } // Timeout errors if (error.code === 'ETIMEDOUT' || error.message?.includes('timeout')) { return new TimeoutError('Operation timed out', { originalError: error.message, ...context }); } // Network errors if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { return new ExternalApiError('External service unavailable', { originalError: error.message, ...context }, true); } // Generic error return new BaseError({ code: ErrorCode.INTERNAL_ERROR, message: 'An unexpected error occurred', details: { originalError: error.message, ...context }, retryable: false }); } isRetryableMongoError(error) { // MongoDB error codes that are typically retryable const retryableErrorCodes = [ 11000, // Duplicate key (might be retryable in some cases) 16500, // Replica set not found 91, // Shutdown in progress 189, // Primary stepped down 11602, // Interrupted 11601, // Interrupted at shutdown 10107, // Not master 13435, // Not master or secondary 13436, // Not master, no secondary 10054, // Socket error 6 // Host unreachable ]; return retryableErrorCodes.includes(error.code) || error.message?.includes('connection') || error.message?.includes('timeout'); } sanitizeErrorMessage(message) { // Remove sensitive information from error messages const sensitivePatterns = [ /password[=:]\s*[^\s]+/gi, /token[=:]\s*[^\s]+/gi, /key[=:]\s*[^\s]+/gi, /secret[=:]\s*[^\s]+/gi, /mongodb:\/\/[^@]*@[^\/]+/gi, /postgres:\/\/[^@]*@[^\/]+/gi ]; let sanitized = message; for (const pattern of sensitivePatterns) { sanitized = sanitized.replace(pattern, '[REDACTED]'); } return sanitized; } generateCorrelationId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } isOperationalError(error) { if (error instanceof BaseError) { return error.isOperational; } return false; } shouldRetry(error) { if (error instanceof BaseError) { return error.retryable; } return false; } } export const errorHandlingMiddleware = ErrorHandlingMiddleware.getInstance(); //# sourceMappingURL=error-handling.js.map