UNPKG

@aashari/mcp-server-atlassian-bitbucket

Version:

Node.js/TypeScript MCP server for Atlassian Bitbucket. Enables AI systems (LLMs) to interact with workspaces, repositories, and pull requests via tools (list, get, comment, search). Connects AI directly to version control workflows through the standard MC

434 lines (433 loc) 19.2 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; exports.handleCliError = handleCliError; const error_util_js_1 = require("./error.util.js"); const logger_util_js_1 = require("./logger.util.js"); const error_util_js_2 = require("./error.util.js"); const error_util_js_3 = require("./error.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["PRIVATE_IP_ERROR"] = "PRIVATE_IP_ERROR"; ErrorCode["RESERVED_RANGE_ERROR"] = "RESERVED_RANGE_ERROR"; })(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; // PR ID validation error detection if (errorMessage.includes('Invalid pull request ID') || errorMessage.includes('Pull request ID must be a positive integer')) { return { code: ErrorCode.VALIDATION_ERROR, statusCode: 400 }; } // 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 }; } // Network error detection in originalError if (error instanceof Error && 'originalError' in error && error.originalError) { // Check for TypeError in originalError (common for network issues) if (error.originalError instanceof TypeError) { return { code: ErrorCode.NETWORK_ERROR, statusCode: 500 }; } // Check for network error messages in originalError if (error.originalError instanceof Error && (error.originalError.message.includes('fetch') || error.originalError.message.includes('network') || error.originalError.message.includes('ECON'))) { 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 }; } // Bitbucket-specific error detection if (error instanceof Error && 'originalError' in error && error.originalError) { const originalError = (0, error_util_js_2.getDeepOriginalError)(error.originalError); if (originalError && typeof originalError === 'object') { const oe = originalError; // Check for Bitbucket API error structure if (oe.error && typeof oe.error === 'object') { const bbError = oe.error; const errorMsg = String(bbError.message || '').toLowerCase(); const errorDetail = bbError.detail ? String(bbError.detail).toLowerCase() : ''; methodLogger.debug('Found Bitbucket error structure', { message: errorMsg, detail: errorDetail, }); // Repository not found / Does not exist errors if (errorMsg.includes('repository not found') || errorMsg.includes('does not exist') || errorMsg.includes('no such resource') || errorMsg.includes('not found')) { return { code: ErrorCode.NOT_FOUND, statusCode: 404 }; } // Access and permission errors if (errorMsg.includes('access') || errorMsg.includes('permission') || errorMsg.includes('credentials') || errorMsg.includes('unauthorized') || errorMsg.includes('forbidden') || errorMsg.includes('authentication')) { return { code: ErrorCode.ACCESS_DENIED, statusCode: 403 }; } // Validation errors if (errorMsg.includes('invalid') || (errorMsg.includes('parameter') && errorMsg.includes('error')) || errorMsg.includes('input') || errorMsg.includes('validation') || errorMsg.includes('required field') || errorMsg.includes('bad request')) { return { code: ErrorCode.VALIDATION_ERROR, statusCode: 400, }; } // Rate limiting errors if (errorMsg.includes('rate limit') || errorMsg.includes('too many requests') || errorMsg.includes('throttled')) { return { code: ErrorCode.RATE_LIMIT_ERROR, statusCode: 429, }; } } // Check for alternate Bitbucket error structure: {"type": "error", ...} if (oe.type === 'error') { methodLogger.debug('Found Bitbucket type:error structure', oe); // Check for status code if available in the error object if (typeof oe.status === 'number') { if (oe.status === 404) { return { code: ErrorCode.NOT_FOUND, statusCode: 404 }; } if (oe.status === 403 || oe.status === 401) { return { code: ErrorCode.ACCESS_DENIED, statusCode: oe.status, }; } if (oe.status === 400) { return { code: ErrorCode.VALIDATION_ERROR, statusCode: 400, }; } if (oe.status === 429) { return { code: ErrorCode.RATE_LIMIT_ERROR, statusCode: 429, }; } } } // Check for Bitbucket error structure: {"errors": [{...}]} if (Array.isArray(oe.errors) && oe.errors.length > 0) { const firstError = oe.errors[0]; methodLogger.debug('Found Bitbucket errors array structure', firstError); if (typeof firstError.status === 'number') { if (firstError.status === 404) { return { code: ErrorCode.NOT_FOUND, statusCode: 404 }; } if (firstError.status === 403 || firstError.status === 401) { return { code: ErrorCode.ACCESS_DENIED, statusCode: firstError.status, }; } if (firstError.status === 400) { return { code: ErrorCode.VALIDATION_ERROR, statusCode: 400, }; } if (firstError.status === 429) { return { code: ErrorCode.RATE_LIMIT_ERROR, statusCode: 429, }; } } // Look for error messages in the title or message fields if (firstError.title || firstError.message) { const errorText = String(firstError.title || firstError.message).toLowerCase(); if (errorText.includes('not found')) { return { code: ErrorCode.NOT_FOUND, statusCode: 404 }; } if (errorText.includes('access') || errorText.includes('permission')) { return { code: ErrorCode.ACCESS_DENIED, statusCode: 403, }; } if (errorText.includes('invalid') || errorText.includes('required')) { return { code: ErrorCode.VALIDATION_ERROR, statusCode: 400, }; } if (errorText.includes('rate limit') || errorText.includes('too many requests')) { return { code: ErrorCode.RATE_LIMIT_ERROR, statusCode: 429, }; } } } } } // 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'}.`; // Bitbucket-specific guidance if (entityType === 'Repository' || entityType === 'PullRequest' || entityType === 'Branch') { message += ` Make sure the workspace and ${entityType.toLowerCase()} names are spelled correctly and that you have permission to access it.`; } break; case ErrorCode.ACCESS_DENIED: message = `Access denied for ${entity.toLowerCase()}${entityIdStr ? ` ${entityIdStr}` : ''}. Verify your credentials and permissions.`; // Bitbucket-specific guidance message += ` Ensure your Bitbucket API token/app password has sufficient privileges and hasn't expired. If using a workspace/repository name, check that it's spelled correctly.`; break; case ErrorCode.INVALID_CURSOR: message = `Invalid pagination cursor. Use the exact cursor string returned from previous results.`; // Bitbucket-specific guidance message += ` Bitbucket pagination typically uses page numbers. Check that the page number is valid and within range.`; break; case ErrorCode.VALIDATION_ERROR: message = originalMessage || `Invalid data provided for ${operation || 'operation'} ${entity.toLowerCase()}.`; // The originalMessage already includes error details for VALIDATION_ERROR break; case ErrorCode.NETWORK_ERROR: message = `Network error while ${operation || 'connecting to'} the Bitbucket API. Please check your internet connection and try again.`; break; case ErrorCode.RATE_LIMIT_ERROR: message = `Bitbucket API rate limit exceeded. Please wait a moment and try again, or reduce the frequency of requests.`; // Bitbucket-specific guidance message += ` Bitbucket's API has rate limits per IP address and additional limits for authenticated users.`; break; default: message = `An unexpected error occurred while ${operation || 'processing'} ${entity.toLowerCase()}.`; } // Include original message details if available and appropriate if (originalMessage && code !== ErrorCode.NOT_FOUND && code !== ErrorCode.ACCESS_DENIED) { 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 ? errorMessage : createUserFriendlyErrorMessage(code, context, errorMessage); // Throw an appropriate API error with the user-friendly message throw (0, error_util_js_1.createApiError)(message, finalStatusCode, error); } /** * Handles errors from CLI commands * Logs the error and exits the process with appropriate exit code * * @param error The error to handle */ function handleCliError(error) { const logger = logger_util_js_1.Logger.forContext('utils/error-handler.util.ts', 'handleCliError'); logger.error('CLI error:', error); // Process different error types if (error instanceof error_util_js_3.McpError) { // Format user-friendly error message for MCP errors console.error(`Error: ${error.message}`); // Use specific exit codes based on error type switch (error.errorType) { case 'AUTHENTICATION_REQUIRED': process.exit(2); break; // Not strictly needed after process.exit but added for clarity case 'NOT_FOUND': process.exit(3); break; case 'VALIDATION_ERROR': process.exit(4); break; case 'RATE_LIMIT_EXCEEDED': process.exit(5); break; case 'API_ERROR': process.exit(6); break; default: process.exit(1); break; } } else if (error instanceof Error) { // Standard Error objects console.error(`Error: ${error.message}`); process.exit(1); } else { // Unknown error types console.error(`Unknown error occurred: ${String(error)}`); process.exit(1); } }