@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
326 lines (325 loc) • 15.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const logger_util_js_1 = require("../utils/logger.util.js");
const transport_util_js_1 = require("../utils/transport.util.js");
const vendor_atlassian_issues_types_js_1 = require("./vendor.atlassian.issues.types.js");
const error_util_js_1 = require("../utils/error.util.js");
const validation_util_js_1 = require("../utils/validation.util.js");
// Create a contextualized logger for this file
const serviceLogger = logger_util_js_1.Logger.forContext('services/vendor.atlassian.issues.service.ts');
// Log service initialization
serviceLogger.debug('Jira issues service initialized');
/**
* Base API path for Jira REST API v3
* @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/
* @constant {string}
*/
const API_PATH = '/rest/api/3';
/**
* @namespace VendorAtlassianIssuesService
* @description Service for interacting with Jira Issues API.
* Provides methods for searching issues and retrieving issue details.
* All methods require valid Atlassian credentials configured in the environment.
*/
/**
* Search for Jira issues using JQL and other criteria
*
* Retrieves a list of issues from Jira based on JQL query and other
* search parameters. Supports pagination, field selection, and expansion.
*
* @async
* @memberof VendorAtlassianIssuesService
* @param {SearchIssuesParams} [params={}] - Optional parameters for customizing the search
* @param {string} [params.jql] - JQL query string for filtering issues
* @param {number} [params.startAt] - Pagination start index
* @param {number} [params.maxResults] - Maximum number of results to return
* @param {string[]} [params.fields] - Issue fields to include in response
* @param {string[]} [params.expand] - Issue data to expand in response
* @param {boolean} [params.validateQuery] - Whether to validate the JQL query
* @param {string[]} [params.properties] - Issue properties to include in response
* @param {boolean} [params.fieldsByKeys] - Whether to use field keys instead of IDs
* @param {string} [params.nextPageToken] - Token for retrieving next page of results
* @param {boolean} [params.reconcileIssues] - Whether to reconcile issue data
* @returns {Promise<IssuesResponse>} Promise containing the issues search response with results and pagination info
* @throws {Error} If Atlassian credentials are missing or API request fails
* @example
* // Search for issues with pagination
* const response = await search({
* jql: "project = ABC AND status = 'In Progress'",
* maxResults: 10
* });
*/
async function search(params = {}) {
const methodLogger = logger_util_js_1.Logger.forContext('services/vendor.atlassian.issues.service.ts', 'search');
methodLogger.debug('Searching Jira issues with params:', params);
const credentials = (0, transport_util_js_1.getAtlassianCredentials)();
if (!credentials) {
throw (0, error_util_js_1.createAuthMissingError)('Atlassian credentials required to search issues');
}
// Build query parameters
const queryParams = new URLSearchParams();
// JQL and validation
if (params.jql) {
queryParams.set('jql', params.jql);
}
if (params.validateQuery !== undefined) {
queryParams.set('validateQuery', params.validateQuery.toString());
}
// Pagination
if (params.startAt !== undefined) {
queryParams.set('startAt', params.startAt.toString());
}
if (params.maxResults !== undefined) {
queryParams.set('maxResults', params.maxResults.toString());
}
if (params.nextPageToken) {
queryParams.set('nextPageToken', params.nextPageToken);
}
// Field selection and expansion
if (params.fields?.length) {
queryParams.set('fields', params.fields.join(','));
}
if (params.expand?.length) {
queryParams.set('expand', params.expand.join(','));
}
if (params.properties?.length) {
queryParams.set('properties', params.properties.join(','));
}
if (params.fieldsByKeys !== undefined) {
queryParams.set('fieldsByKeys', params.fieldsByKeys.toString());
}
if (params.reconcileIssues !== undefined) {
queryParams.set('reconcileIssues', params.reconcileIssues.toString());
}
const queryString = queryParams.toString()
? `?${queryParams.toString()}`
: '';
const path = `${API_PATH}/search${queryString}`;
methodLogger.debug(`Calling Jira API: ${path}`);
try {
const rawData = await (0, transport_util_js_1.fetchAtlassian)(credentials, path);
return (0, validation_util_js_1.validateResponse)(rawData, vendor_atlassian_issues_types_js_1.IssuesResponseSchema, 'issues search');
}
catch (error) {
// McpError is already properly structured from fetchAtlassian or validation
if (error instanceof error_util_js_1.McpError) {
throw error;
}
// Unexpected errors need to be wrapped
methodLogger.error('Unexpected error searching issues:', error);
throw (0, error_util_js_1.createApiError)(`Unexpected error searching Jira issues: ${error instanceof Error ? error.message : String(error)}`, 500, error);
}
}
/**
* Get detailed information about a specific Jira issue
*
* Retrieves comprehensive details about a single issue, including metadata,
* description, comments, and more.
*
* @async
* @memberof VendorAtlassianIssuesService
* @param {string} idOrKey - The ID or key of the issue to retrieve
* @param {GetIssueByIdParams} [params={}] - Optional parameters for customizing the response
* @param {string[]} [params.fields] - Issue fields to include in response
* @param {string[]} [params.expand] - Issue data to expand in response
* @param {string[]} [params.properties] - Issue properties to include in response
* @param {boolean} [params.fieldsByKeys] - Whether to use field keys instead of IDs
* @param {boolean} [params.updateHistory] - Whether to update issue view history
* @returns {Promise<Issue>} Promise containing the detailed issue information
* @throws {Error} If Atlassian credentials are missing or API request fails
* @example
* // Get issue details with expanded changelog
* const issue = await get('ABC-123', {
* expand: ['changelog']
* });
*/
async function get(idOrKey, params = {}) {
const methodLogger = logger_util_js_1.Logger.forContext('services/vendor.atlassian.issues.service.ts', 'get');
methodLogger.debug(`Getting Jira issue with ID/key: ${idOrKey}, params:`, params);
const credentials = (0, transport_util_js_1.getAtlassianCredentials)();
if (!credentials) {
throw (0, error_util_js_1.createAuthMissingError)(`Atlassian credentials required to get issue ${idOrKey}`);
}
// Build query parameters
const queryParams = new URLSearchParams();
// Field selection and expansion
if (params.fields?.length) {
queryParams.set('fields', params.fields.join(','));
}
if (params.expand?.length) {
queryParams.set('expand', params.expand.join(','));
}
if (params.properties?.length) {
queryParams.set('properties', params.properties.join(','));
}
if (params.fieldsByKeys !== undefined) {
queryParams.set('fieldsByKeys', params.fieldsByKeys.toString());
}
if (params.updateHistory !== undefined) {
queryParams.set('updateHistory', params.updateHistory.toString());
}
const queryString = queryParams.toString()
? `?${queryParams.toString()}`
: '';
const path = `${API_PATH}/issue/${idOrKey}${queryString}`;
methodLogger.debug(`Calling Jira API: ${path}`);
try {
const rawData = await (0, transport_util_js_1.fetchAtlassian)(credentials, path);
return (0, validation_util_js_1.validateResponse)(rawData, vendor_atlassian_issues_types_js_1.IssueSchema, `issue detail ${idOrKey}`);
}
catch (error) {
// McpError is already properly structured from fetchAtlassian or validation
if (error instanceof error_util_js_1.McpError) {
throw error;
}
// Unexpected errors need to be wrapped
methodLogger.error(`Unexpected error getting issue ${idOrKey}:`, error);
throw (0, error_util_js_1.createApiError)(`Unexpected error retrieving Jira issue ${idOrKey}: ${error instanceof Error ? error.message : String(error)}`, 500, error);
}
}
/**
* Get comments for a specific Jira issue
*
* Retrieves the list of comments for an issue with pagination support.
* Can be sorted and expanded as needed.
*
* @async
* @memberof VendorAtlassianIssuesService
* @param {string} issueIdOrKey - The ID or key of the issue to get comments for
* @param {ListCommentsParams} [params={}] - Optional parameters for customizing the response
* @param {number} [params.startAt] - Pagination start index
* @param {number} [params.maxResults] - Maximum number of results to return
* @param {string} [params.orderBy] - Field and direction to order results by
* @param {string[]} [params.expand] - Comment data to expand in response (e.g., 'renderedBody')
* @returns {Promise<PageOfComments>} Promise containing the comments with pagination information
* @throws {Error} If Atlassian credentials are missing or API request fails
* @example
* // Get comments for an issue with pagination
* const comments = await getComments('ABC-123', {
* maxResults: 10,
* expand: ['renderedBody']
* });
*/
async function getComments(issueIdOrKey, params = {}) {
const methodLogger = logger_util_js_1.Logger.forContext('services/vendor.atlassian.issues.service.ts', 'getComments');
methodLogger.debug(`Getting comments for issue ${issueIdOrKey} with params:`, params);
const credentials = (0, transport_util_js_1.getAtlassianCredentials)();
if (!credentials) {
throw (0, error_util_js_1.createAuthMissingError)(`Atlassian credentials required to get comments for issue ${issueIdOrKey}`);
}
// Build query parameters
const queryParams = new URLSearchParams();
// Pagination
if (params.startAt !== undefined) {
queryParams.set('startAt', params.startAt.toString());
}
if (params.maxResults !== undefined) {
queryParams.set('maxResults', params.maxResults.toString());
}
// Sorting
if (params.orderBy) {
queryParams.set('orderBy', params.orderBy);
}
// Expansion
if (params.expand?.length) {
queryParams.set('expand', params.expand.join(','));
}
const queryString = queryParams.toString()
? `?${queryParams.toString()}`
: '';
const path = `${API_PATH}/issue/${issueIdOrKey}/comment${queryString}`;
methodLogger.debug(`Calling Jira API: ${path}`);
try {
const rawData = await (0, transport_util_js_1.fetchAtlassian)(credentials, path);
return (0, validation_util_js_1.validateResponse)(rawData, vendor_atlassian_issues_types_js_1.PageOfCommentsSchema, `issue comments ${issueIdOrKey}`);
}
catch (error) {
// McpError is already properly structured from fetchAtlassian or validation
if (error instanceof error_util_js_1.McpError) {
throw error;
}
// Unexpected errors need to be wrapped
methodLogger.error(`Unexpected error getting comments for issue ${issueIdOrKey}:`, error);
throw (0, error_util_js_1.createApiError)(`Unexpected error retrieving comments for Jira issue ${issueIdOrKey}: ${error instanceof Error ? error.message : String(error)}`, 500, error);
}
}
/**
* Add a comment to a specific Jira issue
*
* Creates a new comment on the specified issue with the provided content.
* The comment body must be provided in Atlassian Document Format (ADF).
*
* @async
* @memberof VendorAtlassianIssuesService
* @param {string} issueIdOrKey - The ID or key of the issue to add a comment to
* @param {AddCommentParams} commentData - Parameters for the comment to add
* @returns {Promise<z.infer<typeof IssueCommentSchema>>} Promise containing the created comment information
* @throws {Error} If Atlassian credentials are missing or API request fails
* @example
* // Add a comment with ADF content
* const comment = await addComment('ABC-123', {
* body: {
* version: 1,
* type: "doc",
* content: [
* {
* type: "paragraph",
* content: [
* {
* type: "text",
* text: "This is a test comment"
* }
* ]
* }
* ]
* }
* });
*/
async function addComment(issueIdOrKey, commentData) {
const methodLogger = logger_util_js_1.Logger.forContext('services/vendor.atlassian.issues.service.ts', 'addComment');
try {
methodLogger.debug('Adding comment to issue', issueIdOrKey, {
bodyProvided: !!commentData.body,
});
// Get configured credentials
const credentials = (0, transport_util_js_1.getAtlassianCredentials)();
if (!credentials) {
throw (0, error_util_js_1.createAuthMissingError)(`Atlassian credentials required to add comment to issue ${issueIdOrKey}`);
}
// Build API endpoint
const apiEndpoint = `${API_PATH}/issue/${issueIdOrKey}/comment`;
// Add expand parameter for rendered content
const queryParams = new URLSearchParams();
if (commentData.expand?.length) {
queryParams.append('expand', commentData.expand.join(','));
}
// Prepare request URL and options
const url = `${apiEndpoint}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
// Prepare request body - using the structure that works with Jira API
// The body should be an object containing the ADF document
const requestBody = {
body: commentData.body,
// Optional fields that may be added later
...(commentData.visibility
? { visibility: commentData.visibility }
: {}),
};
methodLogger.debug('Calling Jira API:', url);
// Make the API call - note the correct parameter order
const response = await (0, transport_util_js_1.fetchAtlassian)(credentials, url, {
method: 'POST',
body: requestBody,
});
return (0, validation_util_js_1.validateResponse)(response, vendor_atlassian_issues_types_js_1.IssueCommentSchema, `comment creation on issue ${issueIdOrKey}`);
}
catch (error) {
// McpError is already properly structured from fetchAtlassian or validation
if (error instanceof error_util_js_1.McpError) {
throw error;
}
// Unexpected errors need to be wrapped
methodLogger.error(`Unexpected error adding comment to issue ${issueIdOrKey}:`, error);
throw (0, error_util_js_1.createApiError)(`Unexpected error adding comment to Jira issue ${issueIdOrKey}: ${error instanceof Error ? error.message : String(error)}`, 500, error);
}
}
exports.default = { search, get, getComments, addComment };