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
JavaScript
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