Files
Momento/mcp-server/errors.js
Antigravity 0784c94242
Some checks failed
CI / Lint, Test & Build (push) Failing after 57s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): vues structurées tableau/kanban, flashcards et MCP robuste
Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note)
avec activation guidée, tableau éditable, kanban et suppression de colonnes.
Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN.
Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la
robustesse du serveur MCP (config, validation, rate-limit, métriques).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 23:03:16 +00:00

326 lines
8.2 KiB
JavaScript

/**
* 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 || {}),
});
}