@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
200 lines (199 loc) • 10.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');
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 (!siteName || !userEmail || !apiToken) {
methodLogger.warn('Missing Atlassian credentials. Please set ATLASSIAN_SITE_NAME, ATLASSIAN_USER_EMAIL, and ATLASSIAN_API_TOKEN environment variables.');
return null;
}
methodLogger.debug('Using Atlassian credentials');
return {
siteName,
userEmail,
apiToken,
};
}
/**
* 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');
const { siteName, userEmail, apiToken } = credentials;
// Ensure path starts with a slash
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
// Construct the full URL
const baseUrl = `https://${siteName}.atlassian.net`;
const url = `${baseUrl}${normalizedPath}`;
// Set up authentication and headers
const headers = {
Authorization: `Basic ${Buffer.from(`${userEmail}:${apiToken}`).toString('base64')}`,
'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,
// Just log a simplified representation of headers
headers: {
contentType: response.headers.get('content-type'),
contentLength: response.headers.get('content-length'),
},
});
if (!response.ok) {
const errorText = await response.text();
methodLogger.error(`API error: ${response.status} ${response.statusText}`, { errorText, url, method: options.method || 'GET' });
// Try to parse the error response - handle Jira-specific error formats
let errorMessage = `${response.status} ${response.statusText}`;
let parsedError = null;
try {
if (errorText &&
(errorText.startsWith('{') || errorText.startsWith('['))) {
parsedError = JSON.parse(errorText);
// Process the parsed error object to build a comprehensive error message
const errorParts = [];
// Jira-specific error format: errorMessages array
if (parsedError.errorMessages &&
Array.isArray(parsedError.errorMessages) &&
parsedError.errorMessages.length > 0) {
// Format: {"errorMessages":["Issue does not exist or you do not have permission to see it."],"errors":{}}
errorParts.push(parsedError.errorMessages.join('; '));
}
// Jira-specific error format: errors object with field-specific errors
if (parsedError.errors &&
typeof parsedError.errors === 'object' &&
Object.keys(parsedError.errors).length > 0) {
// Format: { "errors": { "jql": "The JQL query is invalid." }, "errorMessages": [], "warningMessages": [] }
const fieldErrors = Object.entries(parsedError.errors)
.map(([key, value]) => `${key}: ${value}`)
.join('; ');
errorParts.push(fieldErrors);
}
// Generic Atlassian API error with a message field
if (parsedError.message) {
// Format: {"message":"Some error message"}
errorParts.push(parsedError.message);
}
// Other Atlassian API error formats (generic)
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) {
errorParts.push(atlassianError.title);
}
if (atlassianError.detail) {
errorParts.push(atlassianError.detail);
}
}
// Check for warnings that might give additional context
if (parsedError.warningMessages &&
Array.isArray(parsedError.warningMessages) &&
parsedError.warningMessages.length > 0) {
errorParts.push(`Warnings: ${parsedError.warningMessages.join('; ')}`);
}
// Combine all error parts into a single message
if (errorParts.length > 0) {
errorMessage = errorParts.join(' | ');
}
}
}
catch (parseError) {
methodLogger.debug(`Error parsing error response:`, parseError);
// Fall back to using the raw error text
if (errorText && errorText.trim()) {
errorMessage = errorText;
}
}
// Classify HTTP errors based on status code
if (response.status === 401) {
throw (0, error_util_js_1.createAuthInvalidError)(`Authentication failed. Jira API: ${errorMessage}`, parsedError || errorText);
}
else if (response.status === 403) {
throw (0, error_util_js_1.createAuthInvalidError)(`Insufficient permissions. Jira API: ${errorMessage}`, parsedError || errorText);
}
else if (response.status === 404) {
throw (0, error_util_js_1.createNotFoundError)(`Resource not found. Jira API: ${errorMessage}`, parsedError || errorText);
}
else if (response.status === 429) {
throw (0, error_util_js_1.createApiError)(`Rate limit exceeded. Jira API: ${errorMessage}`, 429, parsedError || errorText);
}
else if (response.status >= 500) {
throw (0, error_util_js_1.createApiError)(`Jira server error. Detail: ${errorMessage}`, response.status, parsedError || errorText);
}
else {
// For other API errors, create detailed error with context
const requestPath = path.split('?')[0]; // Remove query parameters for cleaner logs
let contextualInfo = '';
// Add some contextual handling for common operations
if (requestPath.includes('/search') &&
parsedError?.errors?.jql) {
contextualInfo = ' Check your JQL syntax for errors.';
}
else if (requestPath.includes('/issue/') &&
options.method === 'POST') {
contextualInfo =
' Check issue fields for validation errors.';
}
throw (0, error_util_js_1.createApiError)(`Jira API request failed. Detail: ${errorMessage}${contextualInfo}`, response.status, parsedError || errorText);
}
}
// 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 (error) {
methodLogger.debug(`Non-JSON response received:`, { error });
}
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 or parsing errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw (0, error_util_js_1.createApiError)(`Network error connecting to Jira API: ${error.message}`, 500, error);
}
else if (error instanceof SyntaxError) {
throw (0, error_util_js_1.createApiError)(`Invalid response from Jira API (parsing error): ${error.message}`, 500, error);
}
throw (0, error_util_js_1.createUnexpectedError)(`Unexpected error while calling Jira API: ${error instanceof Error ? error.message : String(error)}`, error);
}
}