@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
374 lines (373 loc) • 17.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatIssuesList = formatIssuesList;
exports.formatIssueDetails = formatIssueDetails;
exports.formatDevelopmentInfo = formatDevelopmentInfo;
const adf_util_js_1 = require("../utils/adf.util.js");
const formatter_util_js_1 = require("../utils/formatter.util.js");
/**
* Format a list of issues for display
* @param issuesData - Raw issues data from the API
* @param _pagination - Pagination information including count and next cursor (handled by CLI layer)
* @returns Formatted string with issues information in markdown format
*/
function formatIssuesList(issuesData, _pagination) {
const { issues } = issuesData;
if (!issues || issues.length === 0) {
return 'No issues found.';
}
const lines = [(0, formatter_util_js_1.formatHeading)('Jira Issues', 1), ''];
const formattedIssues = (0, formatter_util_js_1.formatNumberedList)(issues, (issue) => {
const lines = [];
lines.push((0, formatter_util_js_1.formatHeading)(`${issue.key}: ${issue.fields.summary}`, 2));
const properties = {
Key: issue.key,
Summary: issue.fields.summary,
Type: issue.fields.issuetype?.name,
Status: issue.fields.status?.name,
Priority: issue.fields.priority?.name,
Project: issue.fields.project?.name,
Assignee: issue.fields.assignee?.displayName,
Reporter: issue.fields.reporter?.displayName,
'Created On': issue.fields.created,
'Updated On': issue.fields.updated,
URL: {
url: `${issuesData.baseUrl}/browse/${issue.key}`,
title: issue.key,
},
};
lines.push((0, formatter_util_js_1.formatBulletList)(properties));
return lines.join('\n');
});
lines.push(formattedIssues);
return lines.join('\n');
}
/**
* Format detailed issue information for display
* @param issueData - Raw issue data from the API
* @returns Formatted string with issue details in markdown format
*/
function formatIssueDetails(issueData) {
// Prepare URL
const issueUrl = issueData.self.replace('/rest/api/3/issue/', '/browse/');
const lines = [
(0, formatter_util_js_1.formatHeading)(`Jira Issue: ${issueData.fields.summary}`, 1),
'',
];
// Add a brief summary line
if (issueData.fields.status) {
const summary = `> A ${issueData.fields.status.name.toLowerCase()} issue in the ${issueData.fields.project?.name} project.`;
lines.push(summary);
lines.push('');
}
// Basic Information section
lines.push((0, formatter_util_js_1.formatHeading)('Basic Information', 2));
const basicProperties = {
ID: issueData.id,
Key: issueData.key,
Project: issueData.fields.project
? `${issueData.fields.project.name} (${issueData.fields.project.key})`
: undefined,
Type: issueData.fields.issuetype?.name,
Status: issueData.fields.status?.name,
Priority: issueData.fields.priority?.name,
};
lines.push((0, formatter_util_js_1.formatBulletList)(basicProperties, (key) => key));
// Add issue type description if available
if (issueData.fields.issuetype?.description) {
lines.push(` *${issueData.fields.issuetype.description}*`);
}
// Description
if (issueData.fields.description) {
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Description', 2));
// Handle different description formats
if (typeof issueData.fields.description === 'string') {
lines.push(issueData.fields.description);
}
else if (typeof issueData.fields.description === 'object') {
lines.push((0, adf_util_js_1.adfToMarkdown)(issueData.fields.description));
}
else {
lines.push('*Description format not supported*');
}
}
// People
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('People', 2));
const peopleProperties = {
Assignee: issueData.fields.assignee?.displayName || 'Unassigned',
Reporter: issueData.fields.reporter?.displayName,
};
// Add creator only if different from reporter
if (issueData.fields.creator &&
(!issueData.fields.reporter ||
issueData.fields.creator.displayName !==
issueData.fields.reporter?.displayName)) {
peopleProperties['Creator'] = issueData.fields.creator.displayName;
}
lines.push((0, formatter_util_js_1.formatBulletList)(peopleProperties, (key) => key));
// Additional people details
if (issueData.fields.assignee &&
issueData.fields.assignee.active !== undefined) {
lines.push(` - **Active**: ${issueData.fields.assignee.active ? 'Yes' : 'No'}`);
}
if (issueData.fields.reporter &&
issueData.fields.reporter.active !== undefined) {
lines.push(` - **Active**: ${issueData.fields.reporter.active ? 'Yes' : 'No'}`);
}
if (issueData.fields.creator &&
(!issueData.fields.reporter ||
issueData.fields.creator.displayName !==
issueData.fields.reporter?.displayName) &&
issueData.fields.creator.active !== undefined) {
lines.push(` - **Active**: ${issueData.fields.creator.active ? 'Yes' : 'No'}`);
}
// Dates
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Dates', 2));
const dateProperties = {
Created: issueData.fields.created,
Updated: issueData.fields.updated,
};
lines.push((0, formatter_util_js_1.formatBulletList)(dateProperties, (key) => key));
// Time tracking
if (issueData.fields.timetracking &&
(issueData.fields.timetracking.originalEstimate ||
issueData.fields.timetracking.remainingEstimate ||
issueData.fields.timetracking.timeSpent)) {
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Time Tracking', 2));
const timeTrackingProperties = {
'Original Estimate': issueData.fields.timetracking.originalEstimate,
'Remaining Estimate': issueData.fields.timetracking.remainingEstimate,
'Time Spent': issueData.fields.timetracking.timeSpent,
};
lines.push((0, formatter_util_js_1.formatBulletList)(timeTrackingProperties, (key) => key));
}
// Attachments
if (issueData.fields.attachment && issueData.fields.attachment.length > 0) {
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Attachments', 2));
issueData.fields.attachment.forEach((attachment, index) => {
lines.push((0, formatter_util_js_1.formatHeading)(attachment.filename, 3));
const attachmentProperties = {
'Content Type': attachment.mimeType,
Size: formatFileSize(attachment.size),
'Created At': attachment.created,
Author: attachment.author?.displayName,
};
lines.push((0, formatter_util_js_1.formatBulletList)(attachmentProperties, (key) => key));
if (attachment.content) {
lines.push(`[Download](${attachment.content})`);
}
// Add separator between attachments
if (index < issueData.fields.attachment.length - 1) {
lines.push('');
}
});
}
// Comments
if (issueData.fields.comment) {
const comments = Array.isArray(issueData.fields.comment)
? issueData.fields.comment
: issueData.fields.comment.comments || [];
if (comments.length > 0) {
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Comments', 2));
comments.forEach((comment, index) => {
lines.push((0, formatter_util_js_1.formatHeading)(`${comment.author?.displayName || 'Anonymous'} - ${(0, formatter_util_js_1.formatDate)(comment.created)}`, 3));
// Format comment body
if (typeof comment.body === 'string') {
lines.push(comment.body);
}
else if (typeof comment.body === 'object') {
lines.push((0, adf_util_js_1.adfToMarkdown)(comment.body));
}
// Add separator between comments
if (index < comments.length - 1) {
lines.push('');
lines.push((0, formatter_util_js_1.formatSeparator)());
lines.push('');
}
});
}
}
// Issue Links
if (issueData.fields.issuelinks && issueData.fields.issuelinks.length > 0) {
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Issue Links', 2));
const issueLinks = issueData.fields.issuelinks;
issueLinks.forEach((link) => {
const relatedIssue = link.outwardIssue || link.inwardIssue;
if (relatedIssue) {
const linkType = link.outwardIssue
? link.type.outward
: link.type.inward;
const relatedIssueUrl = relatedIssue.self.replace('/rest/api/3/issue/', '/browse/');
lines.push(`- ${linkType} ${(0, formatter_util_js_1.formatUrl)(relatedIssueUrl, relatedIssue.key)}: ${relatedIssue.fields?.summary || 'No summary'}`);
}
});
}
// Links section
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Links', 2));
lines.push(`- ${(0, formatter_util_js_1.formatUrl)(issueUrl, 'Open in Jira')}`);
// Footer
lines.push('');
lines.push((0, formatter_util_js_1.formatSeparator)());
lines.push(`*Issue information retrieved at ${(0, formatter_util_js_1.formatDate)(new Date())}*`);
lines.push(`*To view this issue in Jira, visit: ${issueUrl}*`);
return lines.join('\n');
}
/**
* Format file size in human-readable format
* @param bytes - File size in bytes
* @returns Formatted file size string
*/
function formatFileSize(bytes) {
if (bytes === 0)
return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Format development information for display
* @param devInfoSummary - Development information summary
* @param devInfoCommits - Development information commits
* @param devInfoBranches - Development information branches
* @param devInfoPullRequests - Development information pull requests
* @returns Formatted string with development information in markdown format
*/
function formatDevelopmentInfo(devInfoSummary, devInfoCommits, devInfoBranches, devInfoPullRequests) {
const lines = [];
// Check if there's any development info available
if (!devInfoSummary ||
!devInfoSummary.summary ||
(!devInfoSummary.summary.repository?.overall?.count &&
!devInfoSummary.summary.branch?.overall?.count &&
!devInfoSummary.summary.pullrequest?.overall?.count)) {
return ''; // No development info, return empty string
}
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Development Information', 2));
// Development Summary
if (devInfoSummary.summary) {
const summary = devInfoSummary.summary;
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Development Summary', 3));
const summaryProps = {};
if (summary.repository?.overall?.count) {
const lastUpdated = summary.repository.overall.lastUpdated || 'Unknown';
summaryProps['Repositories'] =
`${summary.repository.overall.count} (Last updated: ${lastUpdated !== 'Unknown' ? (0, formatter_util_js_1.formatDate)(lastUpdated) : 'Unknown'})`;
}
if (summary.branch?.overall?.count) {
const lastUpdated = summary.branch.overall.lastUpdated || 'Unknown';
summaryProps['Branches'] =
`${summary.branch.overall.count} (Last updated: ${lastUpdated !== 'Unknown' ? (0, formatter_util_js_1.formatDate)(lastUpdated) : 'Unknown'})`;
}
if (summary.pullrequest?.overall?.count) {
const lastUpdated = summary.pullrequest.overall.lastUpdated || 'Unknown';
summaryProps['Pull Requests'] =
`${summary.pullrequest.overall.count} (Last updated: ${lastUpdated !== 'Unknown' ? (0, formatter_util_js_1.formatDate)(lastUpdated) : 'Unknown'}, Status: ${summary.pullrequest.overall.state || 'Unknown'})`;
}
lines.push((0, formatter_util_js_1.formatBulletList)(summaryProps));
}
// Commits
if (devInfoCommits?.detail && devInfoCommits.detail.length > 0) {
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Commits', 3));
devInfoCommits.detail.forEach((detail) => {
if (detail.repositories && detail.repositories.length > 0) {
detail.repositories.forEach((repo) => {
lines.push(`**Repository**: ${repo.name}`);
if (repo.commits && repo.commits.length > 0) {
lines.push('');
repo.commits.forEach((commit, index) => {
lines.push(`${index + 1}. **${commit.displayId}** - ${commit.message.split('\n')[0]}`);
lines.push(` Author: ${commit.author?.name || 'Unknown'}, Date: ${(0, formatter_util_js_1.formatDate)(commit.authorTimestamp)}`);
if (commit.url) {
lines.push(` ${(0, formatter_util_js_1.formatUrl)('View Commit', commit.url)}`);
}
if (index < repo.commits.length - 1) {
lines.push('');
}
});
}
else {
lines.push(' No commits found');
}
});
}
});
}
// Branches
if (devInfoBranches?.detail && devInfoBranches.detail.length > 0) {
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Branches', 3));
devInfoBranches.detail.forEach((detail) => {
if (detail.branches && detail.branches.length > 0) {
detail.branches.forEach((branch) => {
lines.push(`**Branch**: ${branch.name}`);
lines.push(`**Repository**: ${branch.repository?.name || 'Unknown'}`);
if (branch.lastCommit) {
lines.push(`**Last Commit**: ${branch.lastCommit.displayId} - ${branch.lastCommit.message.split('\n')[0]}`);
lines.push(`**Author**: ${branch.lastCommit.author?.name || 'Unknown'}, **Date**: ${(0, formatter_util_js_1.formatDate)(branch.lastCommit.authorTimestamp)}`);
}
if (branch.url) {
lines.push(`${(0, formatter_util_js_1.formatUrl)('View Branch', branch.url)}`);
}
lines.push('');
});
}
else {
lines.push('No branches found');
}
});
}
// Pull Requests
if (devInfoPullRequests?.detail && devInfoPullRequests.detail.length > 0) {
lines.push('');
lines.push((0, formatter_util_js_1.formatHeading)('Pull Requests', 3));
devInfoPullRequests.detail.forEach((detail) => {
if (detail.pullRequests && detail.pullRequests.length > 0) {
detail.pullRequests.forEach((pr) => {
lines.push(`**${pr.name}** (${pr.status})`);
lines.push(`**Repository**: ${pr.repositoryName}`);
lines.push(`**Author**: ${pr.author?.name || 'Unknown'}`);
if (pr.source?.branch && pr.destination?.branch) {
lines.push(`**Source**: ${pr.source.branch} → **Destination**: ${pr.destination.branch}`);
}
if (pr.reviewers && pr.reviewers.length > 0) {
const approved = pr.reviewers
.filter((r) => r.approved)
.map((r) => r.name)
.join(', ');
const notApproved = pr.reviewers
.filter((r) => !r.approved)
.map((r) => r.name)
.join(', ');
if (approved) {
lines.push(`**Approved by**: ${approved}`);
}
if (notApproved) {
lines.push(`**Awaiting approval from**: ${notApproved}`);
}
}
lines.push(`**Last Updated**: ${(0, formatter_util_js_1.formatDate)(pr.lastUpdate)}`);
if (pr.url) {
lines.push(`${(0, formatter_util_js_1.formatUrl)('View Pull Request', pr.url)}`);
}
lines.push('');
});
}
else {
lines.push('No pull requests found');
}
});
}
return lines.join('\n');
}