Skip to main content
Glama

Atlassian Bitbucket MCP Server

by aashari
transport.util.ts11.7 kB
import { Logger } from './logger.util.js'; import { config } from './config.util.js'; import { NETWORK_TIMEOUTS, DATA_LIMITS } from './constants.util.js'; import { createAuthInvalidError, createApiError, createUnexpectedError, McpError, } from './error.util.js'; /** * Interface for Atlassian API credentials */ export interface AtlassianCredentials { // Standard Atlassian credentials siteName?: string; userEmail?: string; apiToken?: string; // Bitbucket-specific credentials (alternative approach) bitbucketUsername?: string; bitbucketAppPassword?: string; // Indicates which auth method to use useBitbucketAuth?: boolean; } /** * Interface for HTTP request options */ export interface RequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; headers?: Record<string, string>; body?: unknown; timeout?: number; } // Create a contextualized logger for this file const transportLogger = Logger.forContext('utils/transport.util.ts'); // Log transport utility initialization transportLogger.debug('Transport utility initialized'); /** * Get Atlassian credentials from environment variables * @returns AtlassianCredentials object or null if credentials are missing */ export function getAtlassianCredentials(): AtlassianCredentials | null { const methodLogger = Logger.forContext( 'utils/transport.util.ts', 'getAtlassianCredentials', ); // First try standard Atlassian credentials (preferred for consistency) const siteName = config.get('ATLASSIAN_SITE_NAME'); const userEmail = config.get('ATLASSIAN_USER_EMAIL'); const apiToken = config.get('ATLASSIAN_API_TOKEN'); // If standard credentials are available, use them if (userEmail && apiToken) { methodLogger.debug('Using standard Atlassian credentials'); return { siteName, userEmail, apiToken, useBitbucketAuth: false, }; } // If standard credentials are not available, try Bitbucket-specific credentials const bitbucketUsername = config.get('ATLASSIAN_BITBUCKET_USERNAME'); const bitbucketAppPassword = config.get('ATLASSIAN_BITBUCKET_APP_PASSWORD'); if (bitbucketUsername && bitbucketAppPassword) { methodLogger.debug('Using Bitbucket-specific credentials'); return { bitbucketUsername, bitbucketAppPassword, useBitbucketAuth: true, }; } // If neither set of credentials is available, return null methodLogger.warn( 'Missing Atlassian credentials. Please set either ATLASSIAN_SITE_NAME, ATLASSIAN_USER_EMAIL, and ATLASSIAN_API_TOKEN environment variables, or ATLASSIAN_BITBUCKET_USERNAME and ATLASSIAN_BITBUCKET_APP_PASSWORD for Bitbucket-specific auth.', ); return null; } /** * Fetch data from Atlassian API * @param credentials Atlassian API credentials * @param path API endpoint path (without base URL) * @param options Request options * @returns Response data */ export async function fetchAtlassian<T>( credentials: AtlassianCredentials, path: string, options: RequestOptions = {}, ): Promise<T> { const methodLogger = Logger.forContext( 'utils/transport.util.ts', 'fetchAtlassian', ); const baseUrl = 'https://api.bitbucket.org'; // Set up auth headers based on credential type let authHeader: string; if (credentials.useBitbucketAuth) { // Bitbucket API uses a different auth format if ( !credentials.bitbucketUsername || !credentials.bitbucketAppPassword ) { throw createAuthInvalidError( 'Missing Bitbucket username or app password', ); } authHeader = `Basic ${Buffer.from( `${credentials.bitbucketUsername}:${credentials.bitbucketAppPassword}`, ).toString('base64')}`; } else { // Standard Atlassian API (Jira, Confluence) if (!credentials.userEmail || !credentials.apiToken) { throw createAuthInvalidError('Missing Atlassian credentials'); } authHeader = `Basic ${Buffer.from( `${credentials.userEmail}:${credentials.apiToken}`, ).toString('base64')}`; } // Ensure path starts with a slash const normalizedPath = path.startsWith('/') ? path : `/${path}`; // Construct the full URL const url = `${baseUrl}${normalizedPath}`; // Set up authentication and headers const headers = { Authorization: authHeader, 'Content-Type': 'application/json', Accept: 'application/json', ...options.headers, }; // Prepare request options const requestOptions: RequestInit = { method: options.method || 'GET', headers, body: options.body ? JSON.stringify(options.body) : undefined, }; methodLogger.debug(`Calling Atlassian API: ${url}`); // Set up timeout handling with configurable values const defaultTimeout = config.getNumber( 'ATLASSIAN_REQUEST_TIMEOUT', NETWORK_TIMEOUTS.DEFAULT_REQUEST_TIMEOUT, ); const timeoutMs = options.timeout ?? defaultTimeout; const controller = new AbortController(); const timeoutId = setTimeout(() => { methodLogger.warn(`Request timeout after ${timeoutMs}ms: ${url}`); controller.abort(); }, timeoutMs); // Add abort signal to request options requestOptions.signal = controller.signal; try { const response = await fetch(url, requestOptions); clearTimeout(timeoutId); // Log the raw response status and headers methodLogger.debug( `Raw response received: ${response.status} ${response.statusText}`, { url, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), }, ); // Validate response size to prevent excessive memory usage (CWE-770) const contentLength = response.headers.get('content-length'); if (contentLength) { const responseSize = parseInt(contentLength, 10); if (responseSize > DATA_LIMITS.MAX_RESPONSE_SIZE) { methodLogger.warn( `Response size ${responseSize} bytes exceeds limit of ${DATA_LIMITS.MAX_RESPONSE_SIZE} bytes`, ); throw createApiError( `Response size (${Math.round(responseSize / (1024 * 1024))}MB) exceeds maximum limit of ${Math.round(DATA_LIMITS.MAX_RESPONSE_SIZE / (1024 * 1024))}MB`, 413, { responseSize, limit: DATA_LIMITS.MAX_RESPONSE_SIZE }, ); } } if (!response.ok) { const errorText = await response.text(); methodLogger.error( `API error: ${response.status} ${response.statusText}`, errorText, ); // Try to parse the error response let errorMessage = `${response.status} ${response.statusText}`; let parsedBitbucketError = null; try { if ( errorText && (errorText.startsWith('{') || errorText.startsWith('[')) ) { const parsedError = JSON.parse(errorText); // Extract specific error details from various Bitbucket API response formats if ( parsedError.type === 'error' && parsedError.error && parsedError.error.message ) { // Format: {"type":"error", "error":{"message":"...", "detail":"..."}} parsedBitbucketError = parsedError.error; errorMessage = parsedBitbucketError.message; if (parsedBitbucketError.detail) { errorMessage += ` Detail: ${parsedBitbucketError.detail}`; } } else if (parsedError.error && parsedError.error.message) { // Alternative error format: {"error": {"message": "..."}} parsedBitbucketError = parsedError.error; errorMessage = parsedBitbucketError.message; } else if ( parsedError.errors && Array.isArray(parsedError.errors) && parsedError.errors.length > 0 ) { // Format: {"errors":[{"status":400,"code":"INVALID_REQUEST_PARAMETER","title":"..."}]} const atlassianError = parsedError.errors[0]; if (atlassianError.title) { errorMessage = atlassianError.title; parsedBitbucketError = atlassianError; } } else if (parsedError.message) { // Format: {"message":"Some error message"} errorMessage = parsedError.message; parsedBitbucketError = parsedError; } } } catch (parseError) { methodLogger.debug(`Error parsing error response:`, parseError); // Fall back to the default error message } // Log the parsed error or raw error text methodLogger.debug( 'Parsed Bitbucket error:', parsedBitbucketError || errorText, ); // Use parsedBitbucketError (or errorText if parsing failed) as originalError const originalErrorForMcp = parsedBitbucketError || errorText; // Handle common Bitbucket API error status codes if (response.status === 401) { throw createAuthInvalidError( `Bitbucket API: Authentication failed - ${errorMessage}`, originalErrorForMcp, ); } if (response.status === 403) { throw createApiError( `Bitbucket API: Permission denied - ${errorMessage}`, 403, originalErrorForMcp, ); } if (response.status === 404) { throw createApiError( `Bitbucket API: Resource not found - ${errorMessage}`, 404, originalErrorForMcp, ); } if (response.status === 429) { throw createApiError( `Bitbucket API: Rate limit exceeded - ${errorMessage}`, 429, originalErrorForMcp, ); } if (response.status >= 500) { throw createApiError( `Bitbucket API: Service error - ${errorMessage}`, response.status, originalErrorForMcp, ); } // For other API errors, preserve the original vendor message throw createApiError( `Bitbucket API Error: ${errorMessage}`, response.status, originalErrorForMcp, ); } // Check if the response is expected to be plain text const contentType = response.headers.get('content-type') || ''; if (contentType.includes('text/plain')) { // If we're expecting text (like a diff), return the raw text const textResponse = await response.text(); methodLogger.debug( `Text response received (truncated)`, textResponse.substring(0, 200) + '...', ); return textResponse as unknown as T; } // For JSON responses, proceed as before // Clone the response to log its content without consuming it const clonedResponse = response.clone(); try { const responseJson = await clonedResponse.json(); methodLogger.debug(`Response body:`, responseJson); } catch { methodLogger.debug( `Could not parse response as JSON, returning raw content`, ); } return response.json() as Promise<T>; } catch (error) { clearTimeout(timeoutId); methodLogger.error(`Request failed`, error); // If it's already an McpError, just rethrow it if (error instanceof McpError) { throw error; } // Handle timeout errors if (error instanceof Error && error.name === 'AbortError') { methodLogger.error( `Request timed out after ${timeoutMs}ms: ${url}`, ); throw createApiError( `Request timeout: Bitbucket API did not respond within ${timeoutMs / 1000} seconds`, 408, error, ); } // Handle network errors more explicitly if (error instanceof TypeError) { // TypeError is typically a network/fetch error in this context const errorMessage = error.message || 'Network error occurred'; methodLogger.debug(`Network error details: ${errorMessage}`); throw createApiError( `Network error while calling Bitbucket API: ${errorMessage}`, 500, // This will be classified as NETWORK_ERROR by detectErrorType error, ); } // Handle JSON parsing errors if (error instanceof SyntaxError) { methodLogger.debug(`JSON parsing error: ${error.message}`); throw createApiError( `Invalid response format from Bitbucket API: ${error.message}`, 500, error, ); } // Generic error handler for any other types of errors throw createUnexpectedError( `Unexpected error while calling Bitbucket API: ${error instanceof Error ? error.message : String(error)}`, error, ); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aashari/mcp-server-atlassian-bitbucket'

If you have feedback or need assistance with the MCP directory API, please join our Discord server