@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
218 lines (217 loc) • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAtlassianCredentials = getAtlassianCredentials;
exports.fetchAtlassian = fetchAtlassian;
const logger_util_js_1 = require("./logger.util.js");
const config_util_js_1 = require("./config.util.js");
const error_util_js_1 = require("./error.util.js");
// Create a contextualized logger for this file
const transportLogger = logger_util_js_1.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
*/
function getAtlassianCredentials() {
const methodLogger = logger_util_js_1.Logger.forContext('utils/transport.util.ts', 'getAtlassianCredentials');
// First try standard Atlassian credentials (preferred for consistency)
const siteName = config_util_js_1.config.get('ATLASSIAN_SITE_NAME');
const userEmail = config_util_js_1.config.get('ATLASSIAN_USER_EMAIL');
const apiToken = config_util_js_1.config.get('ATLASSIAN_API_TOKEN');
// If standard credentials are available, use them
if (siteName && 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_util_js_1.config.get('ATLASSIAN_BITBUCKET_USERNAME');
const bitbucketAppPassword = config_util_js_1.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
*/
async function fetchAtlassian(credentials, path, options = {}) {
const methodLogger = logger_util_js_1.Logger.forContext('utils/transport.util.ts', 'fetchAtlassian');
// Set up base URL and auth headers based on credential type
let baseUrl;
let authHeader;
if (credentials.useBitbucketAuth) {
// Bitbucket API uses a different base URL and auth format
baseUrl = 'https://api.bitbucket.org';
if (!credentials.bitbucketUsername ||
!credentials.bitbucketAppPassword) {
throw (0, error_util_js_1.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.siteName ||
!credentials.userEmail ||
!credentials.apiToken) {
throw (0, error_util_js_1.createAuthInvalidError)('Missing Atlassian credentials');
}
baseUrl = `https://${credentials.siteName}.atlassian.net`;
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 = {
method: options.method || 'GET',
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
};
methodLogger.debug(`Calling Atlassian API: ${url}`);
try {
const response = await fetch(url, requestOptions);
// 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()),
});
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 (0, error_util_js_1.createAuthInvalidError)(`Bitbucket API: Authentication failed - ${errorMessage}`, originalErrorForMcp);
}
if (response.status === 403) {
throw (0, error_util_js_1.createApiError)(`Bitbucket API: Permission denied - ${errorMessage}`, 403, originalErrorForMcp);
}
if (response.status === 404) {
throw (0, error_util_js_1.createApiError)(`Bitbucket API: Resource not found - ${errorMessage}`, 404, originalErrorForMcp);
}
if (response.status === 429) {
throw (0, error_util_js_1.createApiError)(`Bitbucket API: Rate limit exceeded - ${errorMessage}`, 429, originalErrorForMcp);
}
if (response.status >= 500) {
throw (0, error_util_js_1.createApiError)(`Bitbucket API: Service error - ${errorMessage}`, response.status, originalErrorForMcp);
}
// For other API errors, preserve the original vendor message
throw (0, error_util_js_1.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;
}
// 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();
}
catch (error) {
methodLogger.error(`Request failed`, error);
// If it's already an McpError, just rethrow it
if (error instanceof error_util_js_1.McpError) {
throw 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 (0, error_util_js_1.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 (0, error_util_js_1.createApiError)(`Invalid response format from Bitbucket API: ${error.message}`, 500, error);
}
// Generic error handler for any other types of errors
throw (0, error_util_js_1.createUnexpectedError)(`Unexpected error while calling Bitbucket API: ${error instanceof Error ? error.message : String(error)}`, error);
}
}