/** * Memento MCP Server - Structured Error Handling * * Provides consistent error responses across all MCP tools. * Error codes follow MCP and HTTP standards. */ /** * Standard error codes with MCP and HTTP mappings */ export const McpErrors = { // Standard JSON-RPC errors INVALID_REQUEST: { code: -32600, httpCode: 400, message: 'Invalid Request', description: 'The JSON sent is not a valid Request object', }, NOT_FOUND: { code: -32601, httpCode: 404, message: 'Tool not found', description: 'The requested tool does not exist', }, INVALID_PARAMS: { code: -32602, httpCode: 400, message: 'Invalid params', description: 'Invalid method parameter(s)', }, INTERNAL_ERROR: { code: -32603, httpCode: 500, message: 'Internal error', description: 'Internal JSON-RPC error', }, // Custom application errors PARSE_ERROR: { code: -32700, httpCode: 400, message: 'Parse error', description: 'Invalid JSON was received', }, DATABASE_ERROR: { code: -32000, httpCode: 500, message: 'Database error', description: 'Database operation failed', }, AUTH_FAILED: { code: 401, httpCode: 401, message: 'Authentication failed', description: 'Invalid or missing credentials', }, FORBIDDEN: { code: 403, httpCode: 403, message: 'Forbidden', description: 'Insufficient permissions for this operation', }, RATE_LIMITED: { code: 429, httpCode: 429, message: 'Rate limit exceeded', description: 'Too many requests, please retry later', }, TIMEOUT: { code: 408, httpCode: 408, message: 'Request timeout', description: 'Request processing timeout', }, CONFLICT: { code: 409, httpCode: 409, message: 'Conflict', description: 'Resource state conflict', }, UNPROCESSABLE_ENTITY: { code: 422, httpCode: 422, message: 'Unprocessable entity', description: 'Request format is valid but contains semantic errors', }, SERVICE_UNAVAILABLE: { code: 503, httpCode: 503, message: 'Service unavailable', description: 'Service temporarily unavailable', }, }; /** * Error categories for monitoring */ export const ErrorCategories = { VALIDATION: 'validation', AUTHENTICATION: 'authentication', AUTHORIZATION: 'authorization', DATABASE: 'database', NETWORK: 'network', TIMEOUT: 'timeout', RATE_LIMIT: 'rate_limit', INTERNAL: 'internal', }; /** * Category mapping for error codes */ const ErrorCategoryMap = { [McpErrors.INVALID_PARAMS.code]: ErrorCategories.VALIDATION, [McpErrors.PARSE_ERROR.code]: ErrorCategories.VALIDATION, [McpErrors.AUTH_FAILED.code]: ErrorCategories.AUTHENTICATION, [McpErrors.FORBIDDEN.code]: ErrorCategories.AUTHORIZATION, [McpErrors.DATABASE_ERROR.code]: ErrorCategories.DATABASE, [McpErrors.RATE_LIMITED.code]: ErrorCategories.RATE_LIMIT, [McpErrors.TIMEOUT.code]: ErrorCategories.TIMEOUT, [McpErrors.SERVICE_UNAVAILABLE.code]: ErrorCategories.NETWORK, [McpErrors.INTERNAL_ERROR.code]: ErrorCategories.INTERNAL, }; /** * Get error category from error code */ export function getErrorCategory(code) { return ErrorCategoryMap[code] || ErrorCategories.INTERNAL; } /** * Create a standardized MCP error response * * @param {string|number} code - Error code from McpErrors * @param {object} options - Error options * @param {string} [options.detail] - Detailed error message * @param {string} [options.field] - Field that caused the error (for validation errors) * @param {object} [options.context] - Additional context (e.g., { userId, tool, params }) * @param {Error} [options.cause] - Original error that caused this error * @returns {object} MCP error response object */ export function mcpError(code, options = {}) { const { detail, field, context, cause } = options; const errorDef = Object.values(McpErrors).find((e) => e.code === code) || McpErrors.INTERNAL_ERROR; const errorResponse = { _error: true, code: errorDef.code, httpCode: errorDef.httpCode, message: errorDef.message, description: errorDef.description, detail: detail || undefined, field: field || undefined, category: getErrorCategory(errorDef.code), timestamp: new Date().toISOString(), }; if (context) { errorResponse.context = context; } if (cause) { errorResponse.cause = { message: cause.message, name: cause.name, stack: process.env.MCP_LOG_LEVEL === 'debug' ? cause.stack : undefined, }; } return errorResponse; } /** * Create MCP error response content * Wraps error in the format expected by MCP SDK * * @param {string|number} code - Error code from McpErrors * @param {object} options - Error options * @returns {object} MCP content object with error text */ export function mcpErrorContent(code, options = {}) { const error = mcpError(code, options); return { content: [{ type: 'text', text: JSON.stringify(error, null, 2) }], isError: true, }; } /** * Specific error creators for common scenarios */ export function validationError(field, message, context) { return mcpError(McpErrors.INVALID_PARAMS.code, { detail: message, field, context, }); } export function notFoundError(resource, id, context) { return mcpError(McpErrors.NOT_FOUND.code, { detail: `${resource} not found: ${id}`, context, }); } export function authError(message, context) { return mcpError(McpErrors.AUTH_FAILED.code, { detail: message || 'Authentication required', context, }); } export function forbiddenError(message, context) { return mcpError(McpErrors.FORBIDDEN.code, { detail: message || 'Insufficient permissions', context, }); } export function databaseError(cause, context) { return mcpError(McpErrors.DATABASE_ERROR.code, { detail: 'Database operation failed', cause, context, }); } export function rateLimitError(retryAfter, context) { return mcpError(McpErrors.RATE_LIMITED.code, { detail: `Rate limit exceeded. Retry after ${retryAfter}s`, context: { ...context, retryAfter }, }); } export function timeoutError(operation, context) { return mcpError(McpErrors.TIMEOUT.code, { detail: `Operation timed out: ${operation}`, context, }); } export function conflictError(resource, reason, context) { return mcpError(McpErrors.CONFLICT.code, { detail: `${resource}: ${reason}`, context, }); } /** * Wrap an async function with error handling * Converts database errors and other exceptions into MCP errors * * @param {Function} fn - Async function to wrap * @param {object} context - Context to include in errors * @returns {Function} Wrapped function */ export function withErrorHandling(fn, context = {}) { return async (...args) => { try { return await fn(...args); } catch (error) { // Handle specific error types if (error.code && error._error) { // Already an MCP error, rethrow as-is throw error; } if (error.code === 'P2025') { // Prisma record not found throw mcpError(McpErrors.NOT_FOUND.code, { detail: 'Record not found', cause: error, context, }); } if (error.code?.startsWith('P')) { // Prisma database error throw databaseError(error, context); } // Generic error throw mcpError(McpErrors.INTERNAL_ERROR.code, { detail: error.message || 'An unexpected error occurred', cause: error, context, }); } }; } /** * Check if an object is an MCP error */ export function isMcpError(obj) { return obj && typeof obj === 'object' && obj._error === true; } /** * Extract user-friendly message from error */ export function getErrorMessage(error) { if (isMcpError(error)) { return error.detail || error.description || error.message; } if (error instanceof Error) { return error.message; } return 'An unknown error occurred'; } /** * Log error with context */ export function logError(logger, error, additionalContext = {}) { const category = isMcpError(error) ? error.category : ErrorCategories.INTERNAL; const message = getErrorMessage(error); logger.error(`[${category.toUpperCase()}]`, message, { ...additionalContext, ...(error.context || {}), }); }