// Error handling utilities for MCP Sigmund
import { ErrorDetails } from './types.js';
import { logError } from './logger.js';
export class MCPError extends Error {
public readonly code: string;
public readonly statusCode: number;
public readonly details?: ErrorDetails;
constructor(
message: string,
code: string,
statusCode: number = 500,
details?: ErrorDetails
) {
super(message);
this.name = 'MCPError';
this.code = code;
this.statusCode = statusCode;
this.details = details;
}
}
export class DatabaseError extends MCPError {
constructor(message: string, details?: ErrorDetails) {
super(message, 'DATABASE_ERROR', 500, details);
this.name = 'DatabaseError';
}
}
export class ValidationError extends MCPError {
constructor(message: string, details?: ErrorDetails) {
super(message, 'VALIDATION_ERROR', 400, details);
this.name = 'ValidationError';
}
}
export class QueryError extends MCPError {
constructor(message: string, details?: ErrorDetails) {
super(message, 'QUERY_ERROR', 400, details);
this.name = 'QueryError';
}
}
// Error handler for MCP tools
export function handleMCPError(
error: unknown,
context: string
): {
success: false;
error: string;
code: string;
context: string;
timestamp: string;
} {
logError(`MCP Error in ${context}`, 'error-handler', undefined, {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
name: error instanceof Error ? error.name : 'UnknownError',
timestamp: new Date().toISOString(),
});
if (error instanceof MCPError) {
return {
success: false,
error: error.message,
code: error.code,
context,
timestamp: new Date().toISOString(),
};
}
if (error instanceof Error) {
return {
success: false,
error: error.message,
code: 'UNKNOWN_ERROR',
context,
timestamp: new Date().toISOString(),
};
}
return {
success: false,
error: String(error),
code: 'UNKNOWN_ERROR',
context,
timestamp: new Date().toISOString(),
};
}
// Validation helpers
export function validateDateRange(dateFrom?: string, dateTo?: string): void {
if (dateFrom && !isValidDate(dateFrom)) {
throw new ValidationError(
`Invalid dateFrom format: ${dateFrom}. Expected YYYY-MM-DD`
);
}
if (dateTo && !isValidDate(dateTo)) {
throw new ValidationError(
`Invalid dateTo format: ${dateTo}. Expected YYYY-MM-DD`
);
}
if (dateFrom && dateTo && new Date(dateFrom) > new Date(dateTo)) {
throw new ValidationError('dateFrom cannot be after dateTo');
}
}
export function validateLimit(limit?: number): void {
if (limit !== undefined && (limit < 1 || limit > 10000)) {
throw new ValidationError('Limit must be between 1 and 10000');
}
}
export function validateMonths(months?: number): void {
if (months !== undefined && (months < 1 || months > 60)) {
throw new ValidationError('Months must be between 1 and 60');
}
}
export function validateProvider(provider?: string): void {
if (provider && typeof provider !== 'string') {
throw new ValidationError('Provider must be a string');
}
}
// Utility functions
function isValidDate(dateString: string): boolean {
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateString)) return false;
const date = new Date(dateString);
return date instanceof Date && !isNaN(date.getTime());
}
// Database error handling
export function handleDatabaseError(error: unknown, operation: string): never {
if (error instanceof Error) {
if (error.message.includes('no such table')) {
throw new DatabaseError(`Table not found for operation: ${operation}`, {
message: error.message,
timestamp: new Date().toISOString(),
originalError: error.message,
});
}
if (error.message.includes('database is locked')) {
throw new DatabaseError(
`Database is locked for operation: ${operation}`,
{
message: error.message,
timestamp: new Date().toISOString(),
originalError: error.message,
}
);
}
if (error.message.includes('disk I/O error')) {
throw new DatabaseError(
`Database I/O error for operation: ${operation}`,
{
message: error.message,
timestamp: new Date().toISOString(),
originalError: error.message,
}
);
}
}
throw new DatabaseError(`Database operation failed: ${operation}`, {
message: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
originalError: error,
});
}
// Query validation
export function validateBankingQuery(query: string): void {
const validQueries = [
'accounts',
'transactions',
'balance',
'overview',
'spending_analysis',
'cashflow_analysis',
'providers',
];
if (!validQueries.includes(query)) {
throw new QueryError(
`Invalid query: ${query}. Valid queries are: ${validQueries.join(', ')}`
);
}
}
export function validateDataOperation(operation: string): void {
const validOperations = [
'get_stats',
'validate_data',
'export_data',
'cleanup_data',
];
if (!validOperations.includes(operation)) {
throw new QueryError(
`Invalid operation: ${operation}. Valid operations are: ${validOperations.join(', ')}`
);
}
}
// Input sanitization functions
export function sanitizeStringInput(
input: string,
maxLength: number = 255
): string {
if (typeof input !== 'string') {
throw new ValidationError('Input must be a string');
}
// Remove potentially dangerous characters
const sanitized = input
.replace(/[<>"'&]/g, '') // Remove HTML/XML characters
.replace(/[;()]/g, '') // Remove SQL injection characters
.trim()
.substring(0, maxLength);
return sanitized;
}
export function sanitizeNumericInput(
input: unknown,
min?: number,
max?: number
): number {
const num = Number(input);
if (isNaN(num)) {
throw new ValidationError('Input must be a valid number');
}
if (min !== undefined && num < min) {
throw new ValidationError(`Number must be at least ${min}`);
}
if (max !== undefined && num > max) {
throw new ValidationError(`Number must be at most ${max}`);
}
return num;
}
export function sanitizeDateInput(input: string): string {
if (typeof input !== 'string') {
throw new ValidationError('Date input must be a string');
}
// Validate date format (YYYY-MM-DD)
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(input)) {
throw new ValidationError('Date must be in YYYY-MM-DD format');
}
const date = new Date(input);
if (isNaN(date.getTime())) {
throw new ValidationError('Invalid date provided');
}
return input;
}
export function sanitizeProviderId(input: string): string {
const sanitized = sanitizeStringInput(input, 100);
// Additional validation for provider IDs
if (!/^[a-zA-Z0-9-_]+$/.test(sanitized)) {
throw new ValidationError(
'Provider ID can only contain letters, numbers, hyphens, and underscores'
);
}
return sanitized;
}
export function sanitizeAccountId(input: string): string {
const sanitized = sanitizeStringInput(input, 100);
// Additional validation for account IDs
if (!/^[a-zA-Z0-9-_]+$/.test(sanitized)) {
throw new ValidationError(
'Account ID can only contain letters, numbers, hyphens, and underscores'
);
}
return sanitized;
}
export function sanitizeUserId(input: string): string {
const sanitized = sanitizeStringInput(input, 100);
// Additional validation for user IDs
if (!/^[a-zA-Z0-9-_]+$/.test(sanitized)) {
throw new ValidationError(
'User ID can only contain letters, numbers, hyphens, and underscores'
);
}
return sanitized;
}
export function sanitizeCategory(input: string): string {
const sanitized = sanitizeStringInput(input, 100);
// Additional validation for categories
if (!/^[a-zA-Z0-9\s&-_]+$/.test(sanitized)) {
throw new ValidationError(
'Category can only contain letters, numbers, spaces, ampersands, hyphens, and underscores'
);
}
return sanitized;
}