@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
212 lines (211 loc) • 9.71 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const vendor_atlassian_issues_service_js_1 = __importDefault(require("../services/vendor.atlassian.issues.service.js"));
const vendor_atlassian_devinfo_service_js_1 = __importDefault(require("../services/vendor.atlassian.devinfo.service.js"));
const logger_util_js_1 = require("../utils/logger.util.js");
const error_handler_util_js_1 = require("../utils/error-handler.util.js");
const error_util_js_1 = require("../utils/error.util.js");
const pagination_util_js_1 = require("../utils/pagination.util.js");
const transport_util_js_1 = require("../utils/transport.util.js");
const atlassian_issues_formatter_js_1 = require("./atlassian.issues.formatter.js");
const defaults_util_js_1 = require("../utils/defaults.util.js");
/**
* Controller for managing Jira issues.
* Provides functionality for listing issues and retrieving issue details.
*/
// Create a contextualized logger for this file
const controllerLogger = logger_util_js_1.Logger.forContext('controllers/atlassian.issues.controller.ts');
// Log controller initialization
controllerLogger.debug('Jira issues controller initialized');
/**
* List Jira issues with optional filtering
* @param options - Optional filter options for the issues list
* @param options.jql - JQL query to filter issues
* @param options.limit - Maximum number of issues to return
* @param options.cursor - Pagination cursor for retrieving the next set of results
* @returns Promise with formatted issue list content and pagination information
*/
async function list(options = {}) {
const methodLogger = logger_util_js_1.Logger.forContext('controllers/atlassian.issues.controller.ts', 'list');
methodLogger.debug('Listing Jira issues...', options);
try {
const credentials = (0, transport_util_js_1.getAtlassianCredentials)();
if (!credentials) {
throw new Error('Atlassian credentials are required for this operation');
}
// Create a defaults object with proper typing
const defaults = {
limit: defaults_util_js_1.DEFAULT_PAGE_SIZE,
jql: '',
cursor: '',
};
// Apply defaults
const mergedOptions = (0, defaults_util_js_1.applyDefaults)(options, defaults);
// Set default JQL to sort by updated date if not provided
let jql = mergedOptions.jql;
if (!jql) {
jql = 'ORDER BY updated DESC';
}
else if (!jql.toUpperCase().includes('ORDER BY')) {
// Append default sorting to existing JQL if it doesn't include ORDER BY
jql += ' ORDER BY updated DESC';
}
// Set default filters and hardcoded values
const filters = {
// Optional filters with defaults
jql: jql,
// Always include all fields
fields: [
'summary',
'description',
'status',
'issuetype',
'priority',
'project',
'assignee',
'reporter',
'creator',
'created',
'updated',
'timetracking',
'comment',
'attachment',
'worklog',
'issuelinks',
],
// Pagination
maxResults: mergedOptions.limit,
startAt: mergedOptions.cursor
? parseInt(mergedOptions.cursor, 10)
: 0,
};
methodLogger.debug('Using filters:', filters);
const issuesData = await vendor_atlassian_issues_service_js_1.default.search(filters);
// Log only the count of issues returned instead of the entire response
const issuesCount = issuesData.issues?.length || 0;
methodLogger.debug(`Retrieved ${issuesCount} issues`);
// Extract pagination information using the utility
const pagination = (0, pagination_util_js_1.extractPaginationInfo)(issuesData, pagination_util_js_1.PaginationType.OFFSET);
// Ensure pagination count is set to the actual number of issues retrieved
pagination.count = issuesCount;
methodLogger.debug(`Next cursor: ${pagination.nextCursor}`);
// Format the issues data for display using the formatter
const formattedIssues = (0, atlassian_issues_formatter_js_1.formatIssuesList)({
issues: issuesData.issues || [],
baseUrl: `https://${credentials.siteName}.atlassian.net`,
}, pagination);
return {
content: formattedIssues,
pagination,
};
}
catch (error) {
// Use the standardized error handler
return (0, error_handler_util_js_1.handleControllerError)(error, {
entityType: 'Issues',
operation: 'listing',
source: 'controllers/atlassian.issues.controller.ts@list',
additionalInfo: { options, jql: options.jql },
});
}
}
/**
* Get details of a specific Jira issue
* @param identifier - Object containing the ID or key of the issue to retrieve
* @param identifier.issueIdOrKey - The ID or key of the issue (e.g., "PROJ-123" or "10001")
* @param _options - Options for retrieving the issue (currently not used, but maintained for future extensibility)
* @returns Promise with formatted issue details content
* @throws Error if issue retrieval fails
*/
async function get(identifier, _options = {}) {
const { issueIdOrKey } = identifier;
const methodLogger = logger_util_js_1.Logger.forContext('controllers/atlassian.issues.controller.ts', 'get');
methodLogger.debug(`Getting Jira issue with ID/key: ${issueIdOrKey}...`);
// Validate issue ID format
if (!issueIdOrKey || issueIdOrKey === 'invalid') {
throw (0, error_util_js_1.createApiError)('Invalid issue ID', 400);
}
try {
// Always include all fields
const fields = [
'summary',
'description',
'status',
'issuetype',
'priority',
'project',
'assignee',
'reporter',
'creator',
'created',
'updated',
'timetracking',
'comment',
'attachment',
'worklog',
'issuelinks',
];
// Get issue details
const issueData = await vendor_atlassian_issues_service_js_1.default.get(issueIdOrKey, {
fields,
});
methodLogger.debug(`Retrieved issue: ${issueIdOrKey}`);
// Format the issue data for display using the formatter
const formattedIssue = (0, atlassian_issues_formatter_js_1.formatIssueDetails)(issueData);
// Get development information if available
let devInfoSummary = null;
let devInfoCommits = null;
let devInfoBranches = null;
let devInfoPullRequests = null;
try {
// Use the issue ID to get development information
methodLogger.debug(`Getting development information for issue ID: ${issueData.id}...`);
// Get summary first to check if there's any dev info available
devInfoSummary = await vendor_atlassian_devinfo_service_js_1.default.getSummary(issueData.id);
// If there's any development information available, get the details
if (devInfoSummary?.summary?.repository?.overall?.count ||
devInfoSummary?.summary?.branch?.overall?.count ||
devInfoSummary?.summary?.pullrequest?.overall?.count) {
// Fetch detailed development information
methodLogger.debug('Development information available, fetching details...');
// Run these in parallel for better performance
[devInfoCommits, devInfoBranches, devInfoPullRequests] =
await Promise.all([
vendor_atlassian_devinfo_service_js_1.default.getCommits(issueData.id),
vendor_atlassian_devinfo_service_js_1.default.getBranches(issueData.id),
vendor_atlassian_devinfo_service_js_1.default.getPullRequests(issueData.id),
]);
methodLogger.debug('Successfully retrieved development information');
}
else {
methodLogger.debug('No development information available for this issue');
}
}
catch (error) {
// Log the error but don't fail the whole request
methodLogger.warn('Failed to retrieve development information:', error);
}
// Format development information if available
const formattedDevInfo = (0, atlassian_issues_formatter_js_1.formatDevelopmentInfo)(devInfoSummary, devInfoCommits, devInfoBranches, devInfoPullRequests);
// Combine the formatted issue details with the formatted development information
const combinedContent = formattedDevInfo
? `${formattedIssue}\n${formattedDevInfo}`
: formattedIssue;
return {
content: combinedContent,
};
}
catch (error) {
// Use the standardized error handler
return (0, error_handler_util_js_1.handleControllerError)(error, {
entityType: 'Issue',
entityId: identifier,
operation: 'retrieving',
source: 'controllers/atlassian.issues.controller.ts@get',
});
}
}
exports.default = { list, get };