UNPKG

@aashari/mcp-server-atlassian-jira

Version:

Node.js/TypeScript MCP server for Atlassian Jira. Equips AI systems (LLMs) with tools to list/get projects, search/get issues (using JQL/ID), and view dev info (commits, PRs). Connects AI capabilities directly into Jira project management and issue tracki

301 lines (300 loc) 13.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ErrorCode = void 0; exports.buildErrorContext = buildErrorContext; exports.detectErrorType = detectErrorType; exports.createUserFriendlyErrorMessage = createUserFriendlyErrorMessage; exports.handleControllerError = handleControllerError; const error_util_js_1 = require("./error.util.js"); const logger_util_js_1 = require("./logger.util.js"); /** * Standard error codes for consistent handling */ var ErrorCode; (function (ErrorCode) { ErrorCode["NOT_FOUND"] = "NOT_FOUND"; ErrorCode["INVALID_CURSOR"] = "INVALID_CURSOR"; ErrorCode["ACCESS_DENIED"] = "ACCESS_DENIED"; ErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR"; ErrorCode["UNEXPECTED_ERROR"] = "UNEXPECTED_ERROR"; ErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR"; ErrorCode["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR"; ErrorCode["JIRA_JQL_ERROR"] = "JIRA_JQL_ERROR"; ErrorCode["JIRA_FIELD_VALIDATION_ERROR"] = "JIRA_FIELD_VALIDATION_ERROR"; ErrorCode["JIRA_RESOURCE_LOCKED"] = "JIRA_RESOURCE_LOCKED"; })(ErrorCode || (exports.ErrorCode = ErrorCode = {})); /** * Helper function to create a consistent error context object * @param entityType Type of entity being processed * @param operation Operation being performed * @param source Source of the error (typically file path and function) * @param entityId Optional identifier of the entity * @param additionalInfo Optional additional information for debugging * @returns A formatted ErrorContext object */ function buildErrorContext(entityType, operation, source, entityId, additionalInfo) { return { entityType, operation, source, ...(entityId && { entityId }), ...(additionalInfo && { additionalInfo }), }; } /** * Detect specific error types from raw errors * @param error The error to analyze * @param context Context information for better error detection * @returns Object containing the error code and status code */ function detectErrorType(error, context = {}) { const methodLogger = logger_util_js_1.Logger.forContext('utils/error-handler.util.ts', 'detectErrorType'); methodLogger.debug(`Detecting error type`, { error, context }); const errorMessage = error instanceof Error ? error.message : String(error); const statusCode = error instanceof Error && 'statusCode' in error ? error.statusCode : undefined; // Network error detection if (errorMessage.includes('network error') || errorMessage.includes('fetch failed') || errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ENOTFOUND') || errorMessage.includes('Failed to fetch') || errorMessage.includes('Network request failed')) { return { code: ErrorCode.NETWORK_ERROR, statusCode: 500 }; } // Rate limiting detection if (errorMessage.includes('rate limit') || errorMessage.includes('too many requests') || statusCode === 429) { return { code: ErrorCode.RATE_LIMIT_ERROR, statusCode: 429 }; } // Jira-specific JQL error detection if (errorMessage.includes('Error in the JQL Query') || errorMessage.includes('The JQL query is invalid') || (errorMessage.includes('JQL') && (errorMessage.includes('syntax') || errorMessage.includes('parse error') || errorMessage.includes('invalid')))) { return { code: ErrorCode.JIRA_JQL_ERROR, statusCode: 400 }; } // Jira-specific field validation error detection if (errorMessage.includes('Field') && (errorMessage.includes('operation') || errorMessage.includes('not found') || errorMessage.includes('required') || errorMessage.includes('invalid'))) { return { code: ErrorCode.JIRA_FIELD_VALIDATION_ERROR, statusCode: 400 }; } // Jira resource locked if (errorMessage.includes('issue is locked') || errorMessage.includes('currently being edited') || errorMessage.includes('in use by another user')) { return { code: ErrorCode.JIRA_RESOURCE_LOCKED, statusCode: 409 }; } // Check for Jira-specific error objects in originalError if (error instanceof Error && 'originalError' in error && error.originalError) { // Try to parse originalError for Jira-specific patterns try { const originalError = typeof error.originalError === 'string' ? JSON.parse(error.originalError) : error.originalError; if (typeof originalError === 'object') { // Check for Jira errorMessages array if (originalError.errorMessages && Array.isArray(originalError.errorMessages)) { const errorMessages = originalError.errorMessages.join(' '); if (errorMessages.includes('JQL')) { return { code: ErrorCode.JIRA_JQL_ERROR, statusCode: 400, }; } if (errorMessages.includes('Field')) { return { code: ErrorCode.JIRA_FIELD_VALIDATION_ERROR, statusCode: 400, }; } } // Check for Jira errors object with field-specific errors if (originalError.errors && typeof originalError.errors === 'object') { const fieldErrors = Object.keys(originalError.errors); if (fieldErrors.includes('jql')) { return { code: ErrorCode.JIRA_JQL_ERROR, statusCode: 400, }; } // If there are field errors, classify as field validation error if (fieldErrors.length > 0) { return { code: ErrorCode.JIRA_FIELD_VALIDATION_ERROR, statusCode: 400, }; } } } } catch (e) { // If we can't parse the originalError, continue with other checks methodLogger.debug('Failed to parse originalError for Jira-specific patterns', e); } } // Not Found detection if (errorMessage.includes('not found') || errorMessage.includes('does not exist') || statusCode === 404) { return { code: ErrorCode.NOT_FOUND, statusCode: 404 }; } // Access Denied detection if (errorMessage.includes('access') || errorMessage.includes('permission') || errorMessage.includes('authorize') || errorMessage.includes('authentication') || statusCode === 401 || statusCode === 403) { return { code: ErrorCode.ACCESS_DENIED, statusCode: statusCode || 403 }; } // Invalid Cursor detection if ((errorMessage.includes('cursor') || errorMessage.includes('startAt') || errorMessage.includes('page')) && (errorMessage.includes('invalid') || errorMessage.includes('not valid'))) { return { code: ErrorCode.INVALID_CURSOR, statusCode: 400 }; } // Validation Error detection if (errorMessage.includes('validation') || errorMessage.includes('invalid') || errorMessage.includes('required') || statusCode === 400 || statusCode === 422) { return { code: ErrorCode.VALIDATION_ERROR, statusCode: statusCode || 400, }; } // Default to unexpected error return { code: ErrorCode.UNEXPECTED_ERROR, statusCode: statusCode || 500, }; } /** * Create user-friendly error messages based on error type and context * @param code The error code * @param context Context information for better error messages * @param originalMessage The original error message * @returns User-friendly error message */ function createUserFriendlyErrorMessage(code, context = {}, originalMessage) { const methodLogger = logger_util_js_1.Logger.forContext('utils/error-handler.util.ts', 'createUserFriendlyErrorMessage'); const { entityType, entityId, operation } = context; // Format entity ID for display let entityIdStr = ''; if (entityId) { if (typeof entityId === 'string') { entityIdStr = entityId; } else { // Handle object entityId (like ProjectIdentifier) entityIdStr = Object.values(entityId).join('/'); } } // Determine entity display name const entity = entityType ? `${entityType}${entityIdStr ? ` ${entityIdStr}` : ''}` : 'Resource'; let message = ''; switch (code) { case ErrorCode.NOT_FOUND: message = `${entity} not found${entityIdStr ? `: ${entityIdStr}` : ''}. Verify the ID is correct and that you have access to this ${entityType?.toLowerCase() || 'resource'}.`; break; case ErrorCode.ACCESS_DENIED: message = `Access denied for ${entity.toLowerCase()}${entityIdStr ? ` ${entityIdStr}` : ''}. Verify your credentials and permissions.`; break; case ErrorCode.INVALID_CURSOR: message = `Invalid pagination cursor. Use the exact cursor string returned from previous results.`; break; case ErrorCode.VALIDATION_ERROR: message = originalMessage || `Invalid data provided for ${operation || 'operation'} ${entity.toLowerCase()}.`; break; case ErrorCode.NETWORK_ERROR: message = `Network error while ${operation || 'connecting to'} Jira. Please check your internet connection and try again.`; break; case ErrorCode.RATE_LIMIT_ERROR: message = `Rate limit exceeded. Please wait a moment and try again, or reduce the frequency of requests to the Jira API.`; break; case ErrorCode.JIRA_JQL_ERROR: message = `Invalid JQL query${entityIdStr ? ` for ${entityIdStr}` : ''}. Please check your syntax and verify that all field names are valid.`; if (originalMessage) { message += ` Error details: ${originalMessage}`; } message += ' Note: JQL functions relying on user context (like currentUser()) may not work with API token authentication.'; break; case ErrorCode.JIRA_FIELD_VALIDATION_ERROR: message = `Field validation error${entityIdStr ? ` for ${entityIdStr}` : ''}. ${originalMessage || 'One or more fields contain invalid data.'}`; break; case ErrorCode.JIRA_RESOURCE_LOCKED: message = `The ${entityType?.toLowerCase() || 'resource'} is currently locked or being edited by another user. Please try again later.`; break; default: message = `An unexpected error occurred while ${operation || 'processing'} ${entity.toLowerCase()}.`; } // Include original message details if appropriate and not already included if (originalMessage && code !== ErrorCode.NOT_FOUND && code !== ErrorCode.ACCESS_DENIED && code !== ErrorCode.JIRA_JQL_ERROR && // Skip for JQL errors as we handled it specially !message.includes(originalMessage)) { message += ` Error details: ${originalMessage}`; } methodLogger.debug(`Created user-friendly message: ${message}`, { code, context, }); return message; } /** * Handle controller errors consistently * @param error The error to handle * @param context Context information for better error messages * @returns Never returns, always throws an error */ function handleControllerError(error, context = {}) { const methodLogger = logger_util_js_1.Logger.forContext('utils/error-handler.util.ts', 'handleControllerError'); // Extract error details const errorMessage = error instanceof Error ? error.message : String(error); const statusCode = error instanceof Error && 'statusCode' in error ? error.statusCode : undefined; // Detect error type using utility const { code, statusCode: detectedStatus } = detectErrorType(error, context); // Combine detected status with explicit status const finalStatusCode = statusCode || detectedStatus; // Format entity information for logging const { entityType, entityId, operation } = context; const entity = entityType || 'resource'; const entityIdStr = entityId ? typeof entityId === 'string' ? entityId : JSON.stringify(entityId) : ''; const actionStr = operation || 'processing'; // Log detailed error information methodLogger.error(`Error ${actionStr} ${entity}${entityIdStr ? `: ${entityIdStr}` : ''}: ${errorMessage}`, error); // Create user-friendly error message for the response const message = code === ErrorCode.VALIDATION_ERROR || code === ErrorCode.JIRA_JQL_ERROR || code === ErrorCode.JIRA_FIELD_VALIDATION_ERROR ? errorMessage // Use original message for validation errors : createUserFriendlyErrorMessage(code, context, errorMessage); // Throw an appropriate API error with the user-friendly message throw (0, error_util_js_1.createApiError)(message, finalStatusCode, error); }