/**
* User-friendly error message system with localization support
*/
import { BaseError, ERROR_CODES, ErrorCode } from '../errors/index.js';
import { isZodError, createUserFriendlyMessage } from './zod-error-parser.js';
/**
* Severity levels for error messages
*/
export type MessageSeverity = 'info' | 'warning' | 'error' | 'critical';
/**
* Error message template with variables
*/
export interface ErrorMessageTemplate {
code: ErrorCode;
severity: MessageSeverity;
title: string;
message: string;
suggestion: string;
technicalDetails?: string;
helpUrl?: string;
variables?: string[];
}
/**
* Localized error message
*/
export interface LocalizedErrorMessage {
title: string;
message: string;
suggestion: string;
technicalDetails?: string;
helpUrl?: string;
severity: MessageSeverity;
timestamp: string;
correlationId?: string;
}
/**
* Error message templates for different error codes
*/
const ERROR_MESSAGE_TEMPLATES: Record<ErrorCode, ErrorMessageTemplate> = {
// API Errors
[ERROR_CODES.API_TIMEOUT]: {
code: ERROR_CODES.API_TIMEOUT,
severity: 'error',
title: 'Service Timeout',
message: 'The OpenRouter API took too long to respond.',
suggestion:
'Please try again in a few moments. If the problem persists, check the OpenRouter service status.',
technicalDetails: 'Request timeout exceeded {timeout}ms',
helpUrl: 'https://openrouter.ai/status',
variables: ['timeout'],
},
[ERROR_CODES.API_RATE_LIMIT]: {
code: ERROR_CODES.API_RATE_LIMIT,
severity: 'warning',
title: 'Rate Limit Exceeded',
message: 'You have exceeded the API rate limit.',
suggestion:
'Please wait a moment before making another request. Consider upgrading your plan for higher limits.',
technicalDetails: 'Rate limit: {limit} requests per {window}',
helpUrl: 'https://openrouter.ai/docs/rate-limits',
variables: ['limit', 'window'],
},
[ERROR_CODES.API_AUTHENTICATION]: {
code: ERROR_CODES.API_AUTHENTICATION,
severity: 'error',
title: 'Authentication Failed',
message: 'Your API key is invalid or has expired.',
suggestion:
'Please check your API key in the configuration settings. Generate a new key if needed.',
technicalDetails: 'Authentication failed with status code {statusCode}',
helpUrl: 'https://openrouter.ai/keys',
variables: ['statusCode'],
},
[ERROR_CODES.API_SERVER_ERROR]: {
code: ERROR_CODES.API_SERVER_ERROR,
severity: 'error',
title: 'Server Error',
message: 'The OpenRouter service is currently experiencing issues.',
suggestion:
'This is a temporary issue. Please try again later or check the service status.',
technicalDetails: 'Server returned status code {statusCode}',
helpUrl: 'https://openrouter.ai/status',
variables: ['statusCode'],
},
// Configuration Errors
[ERROR_CODES.CONFIG_MISSING]: {
code: ERROR_CODES.CONFIG_MISSING,
severity: 'error',
title: 'Missing Configuration',
message: 'Required configuration setting is missing.',
suggestion:
'Please check your configuration file and ensure all required settings are provided.',
technicalDetails: 'Missing configuration key: {key}',
variables: ['key'],
},
[ERROR_CODES.CONFIG_INVALID]: {
code: ERROR_CODES.CONFIG_INVALID,
severity: 'error',
title: 'Invalid Configuration',
message: 'Configuration setting has an invalid value.',
suggestion:
'Please check the configuration documentation and correct the invalid value.',
technicalDetails: 'Invalid value for {key}: {value}',
variables: ['key', 'value'],
},
[ERROR_CODES.CONFIG_TYPE_MISMATCH]: {
code: ERROR_CODES.CONFIG_TYPE_MISMATCH,
severity: 'error',
title: 'Configuration Type Error',
message: 'Configuration setting has the wrong data type.',
suggestion:
'Please check the expected data type and correct the configuration.',
technicalDetails: 'Expected {expectedType} for {key}, got {actualType}',
variables: ['expectedType', 'key', 'actualType'],
},
// Validation Errors
[ERROR_CODES.VALIDATION_REQUIRED]: {
code: ERROR_CODES.VALIDATION_REQUIRED,
severity: 'error',
title: 'Required Field Missing',
message: 'A required field is missing from your request.',
suggestion: 'Please provide all required fields and try again.',
technicalDetails: 'Missing required field: {field}',
variables: ['field'],
},
[ERROR_CODES.VALIDATION_FORMAT]: {
code: ERROR_CODES.VALIDATION_FORMAT,
severity: 'error',
title: 'Invalid Format',
message: 'The provided value does not match the expected format.',
suggestion: 'Please check the format requirements and correct the value.',
technicalDetails: 'Invalid format for {field}: {value}',
variables: ['field', 'value'],
},
[ERROR_CODES.VALIDATION_RANGE]: {
code: ERROR_CODES.VALIDATION_RANGE,
severity: 'error',
title: 'Value Out of Range',
message: 'The provided value is outside the acceptable range.',
suggestion: 'Please provide a value within the acceptable range.',
technicalDetails:
'Value {value} for {field} must be between {min} and {max}',
variables: ['value', 'field', 'min', 'max'],
},
// Network Errors
[ERROR_CODES.NETWORK_TIMEOUT]: {
code: ERROR_CODES.NETWORK_TIMEOUT,
severity: 'error',
title: 'Network Timeout',
message: 'The network request timed out.',
suggestion: 'Please check your internet connection and try again.',
technicalDetails: 'Network timeout after {timeout}ms',
variables: ['timeout'],
},
[ERROR_CODES.NETWORK_CONNECTION]: {
code: ERROR_CODES.NETWORK_CONNECTION,
severity: 'error',
title: 'Connection Failed',
message: 'Unable to connect to the service.',
suggestion: 'Please check your internet connection and firewall settings.',
technicalDetails: 'Connection failed to {url}',
variables: ['url'],
},
[ERROR_CODES.NETWORK_DNS]: {
code: ERROR_CODES.NETWORK_DNS,
severity: 'error',
title: 'DNS Resolution Failed',
message: 'Unable to resolve the service hostname.',
suggestion: 'Please check your DNS settings and try again.',
technicalDetails: 'DNS resolution failed for {hostname}',
variables: ['hostname'],
},
// MCP Protocol Errors
[ERROR_CODES.MCP_INVALID_REQUEST]: {
code: ERROR_CODES.MCP_INVALID_REQUEST,
severity: 'error',
title: 'Invalid MCP Request',
message: 'The request format is not valid for the MCP protocol.',
suggestion:
'Please check the MCP protocol documentation and correct the request format.',
technicalDetails: 'Invalid request format for method {method}',
variables: ['method'],
},
[ERROR_CODES.MCP_METHOD_NOT_FOUND]: {
code: ERROR_CODES.MCP_METHOD_NOT_FOUND,
severity: 'error',
title: 'Method Not Found',
message: 'The requested MCP method is not supported.',
suggestion: 'Please check the available methods and use a supported one.',
technicalDetails: 'Method {method} not found',
variables: ['method'],
},
[ERROR_CODES.MCP_PROTOCOL_VERSION]: {
code: ERROR_CODES.MCP_PROTOCOL_VERSION,
severity: 'error',
title: 'Protocol Version Mismatch',
message: 'The MCP protocol version is not compatible.',
suggestion: 'Please update your MCP client to a compatible version.',
technicalDetails: 'Protocol version {version} not supported',
variables: ['version'],
},
};
/**
* Fallback error message for unknown errors
*/
const FALLBACK_ERROR_MESSAGE: ErrorMessageTemplate = {
code: 'UNKNOWN_ERROR' as ErrorCode,
severity: 'error',
title: 'Unexpected Error',
message: 'An unexpected error occurred.',
suggestion:
'Please try again later. If the problem persists, contact support.',
technicalDetails: 'Error: {error}',
};
/**
* Replace variables in a string template
*/
function replaceVariables(
template: string,
variables: Record<string, string | number>
): string {
return template.replace(/\{(\w+)\}/g, (match, key) => {
const value = variables[key];
return value !== undefined ? String(value) : match;
});
}
/**
* Create a localized error message from an error
*/
export function createErrorMessage(
error: unknown,
variables: Record<string, string | number> = {},
correlationId?: string
): LocalizedErrorMessage {
let template: ErrorMessageTemplate;
// Handle Zod validation errors with enhanced parsing
if (isZodError(error)) {
const parsedError = createUserFriendlyMessage(error);
return {
title: 'Validation Error',
message: parsedError.message,
suggestion:
parsedError.details?.[0]?.suggestion ||
'Please check your input parameters and try again.',
technicalDetails: parsedError.details
? JSON.stringify(parsedError.details, null, 2)
: undefined,
severity: 'error',
timestamp: new Date().toISOString(),
correlationId,
};
}
if (error instanceof BaseError) {
template =
ERROR_MESSAGE_TEMPLATES[error.code as ErrorCode] ||
FALLBACK_ERROR_MESSAGE;
// Add error-specific variables
if (error.context) {
Object.assign(variables, error.context);
}
} else if (error instanceof Error) {
template = FALLBACK_ERROR_MESSAGE;
variables.error = error.message;
} else {
template = FALLBACK_ERROR_MESSAGE;
variables.error = String(error);
}
return {
title: replaceVariables(template.title, variables),
message: replaceVariables(template.message, variables),
suggestion: replaceVariables(template.suggestion, variables),
technicalDetails: template.technicalDetails
? replaceVariables(template.technicalDetails, variables)
: undefined,
helpUrl: template.helpUrl,
severity: template.severity,
timestamp: new Date().toISOString(),
correlationId,
};
}
/**
* Create a user-friendly error response for MCP protocol
*/
export function createMCPErrorResponse(
error: unknown,
variables: Record<string, string | number> = {},
correlationId?: string
): {
code: number;
message: string;
data?: {
title: string;
suggestion: string;
helpUrl?: string;
correlationId?: string;
technicalDetails?: string;
};
} {
const errorMessage = createErrorMessage(error, variables, correlationId);
// Map severity to MCP error codes
const errorCodeMap: Record<MessageSeverity, number> = {
info: -32000,
warning: -32001,
error: -32002,
critical: -32003,
};
return {
code: errorCodeMap[errorMessage.severity],
message: errorMessage.message,
data: {
title: errorMessage.title,
suggestion: errorMessage.suggestion,
helpUrl: errorMessage.helpUrl,
correlationId: errorMessage.correlationId,
technicalDetails: errorMessage.technicalDetails,
},
};
}
/**
* Format error for display in different contexts
*/
export function formatErrorForDisplay(
error: unknown,
format: 'plain' | 'markdown' | 'html' = 'plain',
variables: Record<string, string | number> = {},
correlationId?: string
): string {
const errorMessage = createErrorMessage(error, variables, correlationId);
switch (format) {
case 'markdown':
return formatMarkdownError(errorMessage);
case 'html':
return formatHtmlError(errorMessage);
default:
return formatPlainError(errorMessage);
}
}
/**
* Format error as plain text
*/
function formatPlainError(errorMessage: LocalizedErrorMessage): string {
let output = `${errorMessage.title}: ${errorMessage.message}\n`;
output += `Suggestion: ${errorMessage.suggestion}\n`;
if (errorMessage.helpUrl) {
output += `Help: ${errorMessage.helpUrl}\n`;
}
if (errorMessage.correlationId) {
output += `Reference ID: ${errorMessage.correlationId}\n`;
}
if (errorMessage.technicalDetails) {
output += `Technical Details: ${errorMessage.technicalDetails}\n`;
}
return output.trim();
}
/**
* Format error as markdown
*/
function formatMarkdownError(errorMessage: LocalizedErrorMessage): string {
let output = `## ${errorMessage.title}\n\n`;
output += `${errorMessage.message}\n\n`;
output += `**Suggestion:** ${errorMessage.suggestion}\n\n`;
if (errorMessage.helpUrl) {
output += `**Help:** [View Documentation](${errorMessage.helpUrl})\n\n`;
}
if (errorMessage.correlationId) {
output += `**Reference ID:** \`${errorMessage.correlationId}\`\n\n`;
}
if (errorMessage.technicalDetails) {
output += `**Technical Details:** \`${errorMessage.technicalDetails}\`\n\n`;
}
return output.trim();
}
/**
* Format error as HTML
*/
function formatHtmlError(errorMessage: LocalizedErrorMessage): string {
let output = `<div class="error-message error-${errorMessage.severity}">`;
output += `<h3>${escapeHtml(errorMessage.title)}</h3>`;
output += `<p>${escapeHtml(errorMessage.message)}</p>`;
output += `<p><strong>Suggestion:</strong> ${escapeHtml(errorMessage.suggestion)}</p>`;
if (errorMessage.helpUrl) {
output += `<p><strong>Help:</strong> <a href="${escapeHtml(errorMessage.helpUrl)}" target="_blank">View Documentation</a></p>`;
}
if (errorMessage.correlationId) {
output += `<p><strong>Reference ID:</strong> <code>${escapeHtml(errorMessage.correlationId)}</code></p>`;
}
if (errorMessage.technicalDetails) {
output += `<details><summary>Technical Details</summary><code>${escapeHtml(errorMessage.technicalDetails)}</code></details>`;
}
output += '</div>';
return output;
}
/**
* Escape HTML characters
*/
function escapeHtml(text: string): string {
// Node.js environment - no DOM
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* Get available error codes for reference
*/
export function getAvailableErrorCodes(): ErrorCode[] {
return Object.values(ERROR_CODES);
}
/**
* Get error message template by code
*/
export function getErrorTemplate(
code: ErrorCode
): ErrorMessageTemplate | undefined {
return ERROR_MESSAGE_TEMPLATES[code];
}
/**
* Validate error message template
*/
export function validateErrorTemplate(template: ErrorMessageTemplate): boolean {
const required = ['code', 'severity', 'title', 'message', 'suggestion'];
return required.every(
field => field in template && template[field as keyof ErrorMessageTemplate]
);
}