ntfy-mcp-server
Version:
An MCP (Model Context Protocol) server designed to interact with the ntfy push notification service. It enables LLMs and AI agents to send notifications to your devices with extensive customization options.
272 lines (271 loc) • 10.7 kB
JavaScript
import { BaseErrorCode, McpError } from '../types-global/errors.js';
import { logger } from './logger.js';
import { sanitizeInputForLogging } from './security.js';
/**
* Simple mapper that maps error types to error codes
*/
const ERROR_TYPE_MAPPINGS = {
'SyntaxError': BaseErrorCode.VALIDATION_ERROR,
'TypeError': BaseErrorCode.VALIDATION_ERROR,
'ReferenceError': BaseErrorCode.INTERNAL_ERROR,
'RangeError': BaseErrorCode.VALIDATION_ERROR,
'URIError': BaseErrorCode.VALIDATION_ERROR,
'EvalError': BaseErrorCode.INTERNAL_ERROR
};
/**
* Common error patterns for automatic classification
*/
const COMMON_ERROR_PATTERNS = [
// Authentication related errors
{ pattern: /auth|unauthorized|unauthenticated|not.*logged.*in|invalid.*token|expired.*token/i, errorCode: BaseErrorCode.UNAUTHORIZED },
// Permission related errors
{ pattern: /permission|forbidden|access.*denied|not.*allowed/i, errorCode: BaseErrorCode.FORBIDDEN },
// Not found errors
{ pattern: /not.*found|missing|no.*such|doesn't.*exist|couldn't.*find/i, errorCode: BaseErrorCode.NOT_FOUND },
// Validation errors
{ pattern: /invalid|validation|malformed|bad request|wrong format/i, errorCode: BaseErrorCode.VALIDATION_ERROR },
// Conflict errors
{ pattern: /conflict|already.*exists|duplicate|unique.*constraint/i, errorCode: BaseErrorCode.CONFLICT },
// Rate limiting
{ pattern: /rate.*limit|too.*many.*requests|throttled/i, errorCode: BaseErrorCode.RATE_LIMITED },
// Timeout errors
{ pattern: /timeout|timed.*out|deadline.*exceeded/i, errorCode: BaseErrorCode.TIMEOUT },
// External service errors
{ pattern: /service.*unavailable|bad.*gateway|gateway.*timeout/i, errorCode: BaseErrorCode.SERVICE_UNAVAILABLE }
];
/**
* Get a readable name for an error
* @param error Error to get name for
* @returns User-friendly error name
*/
function getErrorName(error) {
if (error instanceof Error) {
return error.name || 'Error';
}
if (error === null) {
return 'NullError';
}
if (error === undefined) {
return 'UndefinedError';
}
return typeof error === 'object'
? 'ObjectError'
: 'UnknownError';
}
/**
* Get a message from an error
* @param error Error to get message from
* @returns Error message
*/
function getErrorMessage(error) {
if (error instanceof Error) {
return error.message;
}
if (error === null) {
return 'Null error occurred';
}
if (error === undefined) {
return 'Undefined error occurred';
}
return typeof error === 'string'
? error
: String(error);
}
/**
* Error handler utility class with various error handling methods
*/
export class ErrorHandler {
/**
* Determine the appropriate error code for an error based on patterns and type
* @param error The error to classify
* @returns The appropriate error code
*/
static determineErrorCode(error) {
// If it's already an McpError, use its code
if (error instanceof McpError) {
return error.code;
}
const errorName = getErrorName(error);
const errorMessage = getErrorMessage(error);
// Check if the error type has a direct mapping
if (errorName in ERROR_TYPE_MAPPINGS) {
return ERROR_TYPE_MAPPINGS[errorName];
}
// Check for common error patterns
for (const pattern of COMMON_ERROR_PATTERNS) {
const regex = pattern.pattern instanceof RegExp
? pattern.pattern
: new RegExp(pattern.pattern, 'i');
if (regex.test(errorMessage) || regex.test(errorName)) {
return pattern.errorCode;
}
}
// Default to internal error if no pattern matches
return BaseErrorCode.INTERNAL_ERROR;
}
/**
* Handle operation errors with consistent logging and transformation
* @param error The error that occurred
* @param options Error handling options
* @returns The transformed error
*/
static handleError(error, options) {
const { context, operation, input, rethrow = false, errorCode: explicitErrorCode, includeStack = true, critical = false } = options;
// If it's already an McpError, use it directly but apply additional context
if (error instanceof McpError) {
// Add any additional context
if (context && Object.keys(context).length > 0) {
error.details = { ...error.details, ...context };
}
// Log the error with sanitized input
logger.error(`Error ${operation}: ${error.message}`, {
errorCode: error.code,
requestId: context?.requestId,
input: input ? sanitizeInputForLogging(input) : undefined,
stack: includeStack ? error.stack : undefined,
critical,
...context
});
if (rethrow) {
throw error;
}
return error;
}
// Sanitize input for logging
const sanitizedInput = input ? sanitizeInputForLogging(input) : undefined;
// Log the error with consistent format
logger.error(`Error ${operation}`, {
error: error instanceof Error ? error.message : String(error),
errorType: getErrorName(error),
input: sanitizedInput,
requestId: context?.requestId,
stack: includeStack && error instanceof Error ? error.stack : undefined,
critical,
...context
});
// Choose the error code (explicit > determined > default)
const errorCode = explicitErrorCode ||
ErrorHandler.determineErrorCode(error) ||
BaseErrorCode.INTERNAL_ERROR;
// Transform to appropriate error type
const transformedError = options.errorMapper
? options.errorMapper(error)
: new McpError(errorCode, `Error ${operation}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
originalError: getErrorName(error),
...context
});
// Rethrow if requested
if (rethrow) {
throw transformedError;
}
return transformedError;
}
/**
* Map an error to a specific error type based on error message patterns
* @param error The error to map
* @param mappings Array of pattern and factory mappings
* @param defaultFactory Default factory function if no pattern matches
* @returns The mapped error
*/
static mapError(error, mappings, defaultFactory) {
// If it's already the target type and we have a default factory to check against, return it
if (defaultFactory && error instanceof Error) {
const defaultInstance = defaultFactory(error);
if (error.constructor === defaultInstance.constructor) {
return error;
}
}
const errorMessage = getErrorMessage(error);
// Check each pattern and return the first match
for (const mapping of mappings) {
const matches = mapping.pattern instanceof RegExp
? mapping.pattern.test(errorMessage)
: errorMessage.includes(mapping.pattern);
if (matches) {
return mapping.factory(error, mapping.additionalContext);
}
}
// Return default or original error
if (defaultFactory) {
return defaultFactory(error);
}
return error instanceof Error
? error
: new Error(String(error));
}
/**
* Create a simplified error mapper based on error patterns and codes
* @param patterns Array of error patterns, codes, and messages
* @param defaultErrorCode Default error code if no pattern matches
* @returns Error mapper function
*/
static createErrorMapper(patterns, defaultErrorCode = BaseErrorCode.INTERNAL_ERROR) {
return (error, context) => {
// Already an McpError
if (error instanceof McpError) {
// Add any additional context
if (context && Object.keys(context).length > 0) {
error.details = { ...error.details, ...context };
}
return error;
}
const errorMessage = getErrorMessage(error);
// Check each pattern for a match
for (const { pattern, errorCode, messageTemplate } of patterns) {
const matches = pattern instanceof RegExp
? pattern.test(errorMessage)
: errorMessage.includes(pattern);
if (matches) {
// Use template if provided, otherwise use original error message
const message = messageTemplate
? messageTemplate.replace('{message}', errorMessage)
: errorMessage;
return new McpError(errorCode, message, { originalError: getErrorName(error), ...context });
}
}
// No matches found, use default
return new McpError(defaultErrorCode, errorMessage, { originalError: getErrorName(error), ...context });
};
}
/**
* Format an error for consistent response structure
* @param error The error to format
* @returns Formatted error object
*/
static formatError(error) {
if (error instanceof McpError) {
return {
code: error.code,
message: error.message,
details: error.details || {}
};
}
if (error instanceof Error) {
return {
code: ErrorHandler.determineErrorCode(error),
message: error.message,
details: { errorType: error.name }
};
}
return {
code: BaseErrorCode.UNKNOWN_ERROR,
message: String(error),
details: { errorType: typeof error }
};
}
/**
* Safely execute a function and handle any errors
* @param fn Function to execute
* @param options Error handling options
* @returns The result of the function or error
*/
static async tryCatch(fn, options) {
try {
return await fn();
}
catch (error) {
throw ErrorHandler.handleError(error, { ...options, rethrow: true });
}
}
}
export default ErrorHandler;