Skip to main content
Glama

MCP Atlassian Server

by phuc-nt
issues.ts13.5 kB
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { Logger } from '../../utils/logger.js'; import { getIssue as getIssueApi, searchIssues as searchIssuesApi } from '../../utils/jira-resource-api.js'; import { issueSchema, issuesListSchema, transitionsListSchema, commentsListSchema } from '../../schemas/jira.js'; import { Config, Resources } from '../../utils/mcp-helpers.js'; const logger = Logger.getLogger('JiraResource:Issues'); /** * Helper function to get issue details from Jira */ async function getIssue(config: any, issueKey: string): Promise<any> { return await getIssueApi(config, issueKey); } /** * Helper function to get a list of issues from Jira (supports pagination) */ async function getIssues(config: any, startAt = 0, maxResults = 20, jql = ''): Promise<any> { const jqlQuery = jql && jql.trim() ? jql.trim() : ''; return await searchIssuesApi(config, jqlQuery, maxResults); } /** * Helper function to search issues by JQL from Jira (supports pagination) */ async function searchIssuesByJql(config: any, jql: string, startAt = 0, maxResults = 20): Promise<any> { return await searchIssuesApi(config, jql, maxResults); } /** * Helper function to get a list of transitions for an issue from Jira */ async function getIssueTransitions(config: any, issueKey: string): Promise<any> { try { const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64'); const headers = { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'MCP-Atlassian-Server/1.0.0' }; let baseUrl = config.baseUrl; if (!baseUrl.startsWith('https://')) { baseUrl = `https://${baseUrl}`; } const url = `${baseUrl}/rest/api/3/issue/${issueKey}/transitions`; logger.debug(`Getting Jira issue transitions: ${url}`); const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' }); if (!response.ok) { const statusCode = response.status; const responseText = await response.text(); logger.error(`Jira API error (${statusCode}):`, responseText); throw new Error(`Jira API error: ${responseText}`); } const data = await response.json(); return data.transitions || []; } catch (error) { logger.error(`Error getting Jira issue transitions:`, error); throw error; } } /** * Helper function to get a list of comments for an issue from Jira */ async function getIssueComments(config: any, issueKey: string, startAt = 0, maxResults = 20): Promise<any> { try { const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64'); const headers = { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'MCP-Atlassian-Server/1.0.0' }; let baseUrl = config.baseUrl; if (!baseUrl.startsWith('https://')) { baseUrl = `https://${baseUrl}`; } const url = `${baseUrl}/rest/api/3/issue/${issueKey}/comment?startAt=${startAt}&maxResults=${maxResults}`; logger.debug(`Getting Jira issue comments: ${url}`); const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' }); if (!response.ok) { const statusCode = response.status; const responseText = await response.text(); logger.error(`Jira API error (${statusCode}):`, responseText); throw new Error(`Jira API error: ${responseText}`); } const data = await response.json(); return data; } catch (error) { logger.error(`Error getting Jira issue comments:`, error); throw error; } } /** * Hàm chuyển ADF sang text thuần */ function extractTextFromADF(adf: any): string { if (!adf || typeof adf === 'string') return adf || ''; let text = ''; if (adf.content) { adf.content.forEach((node: any) => { if (node.type === 'paragraph' && node.content) { node.content.forEach((inline: any) => { if (inline.type === 'text') { text += inline.text; } }); text += '\n'; } }); } return text.trim(); } /** * Format Jira issue data to standardized format */ function formatIssueData(issue: any, baseUrl: string): any { return { id: issue.id, key: issue.key, summary: issue.fields?.summary || '', description: extractTextFromADF(issue.fields?.description), rawDescription: issue.fields?.description || null, status: { name: issue.fields?.status?.name || 'Unknown', id: issue.fields?.status?.id || '0' }, assignee: issue.fields?.assignee ? { displayName: issue.fields.assignee.displayName, accountId: issue.fields.assignee.accountId } : null, reporter: issue.fields?.reporter ? { displayName: issue.fields.reporter.displayName, accountId: issue.fields.reporter.accountId } : null, priority: issue.fields?.priority ? { name: issue.fields.priority.name, id: issue.fields.priority.id } : null, labels: issue.fields?.labels || [], created: issue.fields?.created || null, updated: issue.fields?.updated || null, issueType: { name: issue.fields?.issuetype?.name || 'Unknown', id: issue.fields?.issuetype?.id || '0' }, projectKey: issue.fields?.project?.key || '', projectName: issue.fields?.project?.name || '', url: `${baseUrl}/browse/${issue.key}` }; } /** * Format Jira comment data to standardized format */ function formatCommentData(comment: any): any { return { id: comment.id, body: extractTextFromADF(comment.body), rawBody: comment.body || '', author: comment.author ? { displayName: comment.author.displayName, accountId: comment.author.accountId } : null, created: comment.created || null, updated: comment.updated || null }; } /** * Register resources related to Jira issues * @param server MCP Server instance */ export function registerIssueResources(server: McpServer) { logger.info('Registering Jira issue resources...'); // Resource: Issues list (with pagination and JQL support) server.resource( 'jira-issues-list', new ResourceTemplate('jira://issues', { list: async (_extra) => { return { resources: [ { uri: 'jira://issues', name: 'Jira Issues', description: 'List and search all Jira issues', mimeType: 'application/json' } ] }; } }), async (uri, params, _extra) => { try { const config = Config.getAtlassianConfigFromEnv(); const { limit, offset } = Resources.extractPagingParams(params); const jql = params.jql ? Array.isArray(params.jql) ? params.jql[0] : params.jql : ''; const project = params.project ? Array.isArray(params.project) ? params.project[0] : params.project : ''; const status = params.status ? Array.isArray(params.status) ? params.status[0] : params.status : ''; // Build JQL query based on parameters let jqlQuery = jql; if (project && !jqlQuery.includes('project=')) { jqlQuery = jqlQuery ? `${jqlQuery} AND project = ${project}` : `project = ${project}`; } if (status && !jqlQuery.includes('status=')) { jqlQuery = jqlQuery ? `${jqlQuery} AND status = "${status}"` : `status = "${status}"`; } logger.info(`Searching Jira issues with JQL: ${jqlQuery || 'All issues'}`); const response = await searchIssuesApi(config, jqlQuery, limit); // Format issues data const formattedIssues = response.issues.map((issue: any) => formatIssueData(issue, config.baseUrl)); const uriString = typeof uri === 'string' ? uri : uri.href; return Resources.createStandardResource( uriString, formattedIssues, 'issues', issuesListSchema, response.total, limit, offset, `${config.baseUrl}/issues/?jql=${encodeURIComponent(jqlQuery)}` ); } catch (error) { logger.error('Error getting Jira issues:', error); throw error; } } ); // Resource: Issue details server.resource( 'jira-issue-details', new ResourceTemplate('jira://issues/{issueKey}', { list: async (_extra) => { return { resources: [ { uri: 'jira://issues/{issueKey}', name: 'Jira Issue Details', description: 'Get details for a specific Jira issue by key. Replace {issueKey} with the issue key.', mimeType: 'application/json' } ] }; } }), async (uri, params, _extra) => { try { const config = Config.getAtlassianConfigFromEnv(); let normalizedIssueKey = Array.isArray(params.issueKey) ? params.issueKey[0] : params.issueKey; if (!normalizedIssueKey) { throw new Error('Missing issueKey in URI'); } logger.info(`Getting details for Jira issue: ${normalizedIssueKey}`); const issue = await getIssue(config, normalizedIssueKey); const formattedIssue = formatIssueData(issue, config.baseUrl); const uriString = typeof uri === 'string' ? uri : uri.href; return Resources.createStandardResource( uriString, [formattedIssue], 'issue', issueSchema, 1, 1, 0, `${config.baseUrl}/browse/${normalizedIssueKey}` ); } catch (error) { logger.error(`Error getting Jira issue details:`, error); throw error; } } ); // Resource: Issue transitions (available actions/status changes) server.resource( 'jira-issue-transitions', new ResourceTemplate('jira://issues/{issueKey}/transitions', { list: async (_extra) => { return { resources: [ { uri: 'jira://issues/{issueKey}/transitions', name: 'Jira Issue Transitions', description: 'List available transitions for a Jira issue. Replace {issueKey} with the issue key.', mimeType: 'application/json' } ] }; } }), async (uri, params, _extra) => { try { const config = Config.getAtlassianConfigFromEnv(); let normalizedIssueKey = Array.isArray(params.issueKey) ? params.issueKey[0] : params.issueKey; if (!normalizedIssueKey) { throw new Error('Missing issueKey in URI'); } logger.info(`Getting transitions for Jira issue: ${normalizedIssueKey}`); const transitions = await getIssueTransitions(config, normalizedIssueKey); // Format transitions data const formattedTransitions = transitions.map((t: any) => ({ id: t.id, name: t.name, to: { id: t.to.id, name: t.to.name } })); const uriString = typeof uri === 'string' ? uri : uri.href; return Resources.createStandardResource( uriString, formattedTransitions, 'transitions', transitionsListSchema, formattedTransitions.length, formattedTransitions.length, 0, `${config.baseUrl}/browse/${normalizedIssueKey}` ); } catch (error) { logger.error(`Error getting Jira issue transitions:`, error); throw error; } } ); // Resource: Issue comments server.resource( 'jira-issue-comments', new ResourceTemplate('jira://issues/{issueKey}/comments', { list: async (_extra) => { return { resources: [ { uri: 'jira://issues/{issueKey}/comments', name: 'Jira Issue Comments', description: 'List comments for a Jira issue. Replace {issueKey} with the issue key.', mimeType: 'application/json' } ] }; } }), async (uri, params, _extra) => { try { const config = Config.getAtlassianConfigFromEnv(); let normalizedIssueKey = Array.isArray(params.issueKey) ? params.issueKey[0] : params.issueKey; if (!normalizedIssueKey) { throw new Error('Missing issueKey in URI'); } const { limit, offset } = Resources.extractPagingParams(params); logger.info(`Getting comments for Jira issue: ${normalizedIssueKey}`); const commentData = await getIssueComments(config, normalizedIssueKey, offset, limit); // Format comments data const formattedComments = (commentData.comments || []).map((c: any) => formatCommentData(c)); const uriString = typeof uri === 'string' ? uri : uri.href; return Resources.createStandardResource( uriString, formattedComments, 'comments', commentsListSchema, commentData.total || formattedComments.length, limit, offset, `${config.baseUrl}/browse/${normalizedIssueKey}` ); } catch (error) { logger.error(`Error getting Jira issue comments:`, error); throw error; } } ); logger.info('Jira issue resources registered successfully'); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/phuc-nt/mcp-atlassian-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server