We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/garimiddisuman/jira-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
// src/clients/JiraClient.ts
import fetch from "node-fetch";
import { JiraStory, JiraComment } from "../types/JiraTypes";
import { AppConfig } from "../config/AppConfig";
import { JiraConstants } from "../constants/JiraConstants";
import { Messages } from "../constants/Messages";
import { Logger } from "../utils/logger";
/**
* Jira Client - handles communication with Jira API
*/
export class JiraClient {
private readonly config: AppConfig;
private readonly baseUrl: string;
private readonly authHeader: { Authorization: string };
constructor(config: AppConfig) {
this.config = config;
this.baseUrl = config.getJiraBaseUrl();
this.authHeader = this.createAuthHeader();
}
private createAuthHeader(): { Authorization: string } {
const email = this.config.getJiraEmail();
const token = this.config.getJiraApiToken();
const credentials = Buffer.from(`${email}:${token}`).toString("base64");
return { Authorization: `Basic ${credentials}` };
}
private buildIssueUrl(key: string): string {
const fields = JiraConstants.ISSUE_FIELDS.join(",");
return `${this.baseUrl}${JiraConstants.ISSUE_ENDPOINT}/${key}?fields=${fields}`;
}
public async fetchAllStories(): Promise<Map<string, JiraStory>> {
const storiesMap = new Map<string, JiraStory>();
try {
const boardId = this.config.getJiraBoardId();
if (!boardId) {
Logger.error("JIRA_BOARD_ID is required. Please set it in .env file");
return storiesMap;
}
// Use Jira Agile/Board API to get issues from a specific board
const url = `${this.baseUrl}/rest/agile/1.0/board/${boardId}/issue?maxResults=${JiraConstants.MAX_RESULTS}`;
Logger.info(Messages.FETCHING_STORIES, url);
const response = await fetch(url, {
method: "GET",
headers: this.authHeader,
});
if (!response.ok) {
await this.handleFetchError(response);
return storiesMap;
}
const data: any = await response.json();
if (!data.issues || data.issues.length === 0) {
this.logNoStories();
return storiesMap;
}
// Filter only Story type issues
const stories = data.issues.filter(
(issue: any) =>
issue.fields.issuetype &&
issue.fields.issuetype.name === JiraConstants.ISSUE_TYPE_STORY
);
this.logStoriesFound(stories.length);
this.processIssues(stories, storiesMap);
return storiesMap;
} catch (error) {
this.logFetchError(error);
return storiesMap;
}
}
public async fetchStoryByKey(key: string): Promise<JiraStory | null> {
try {
const url = this.buildIssueUrl(key);
const response = await fetch(url, { headers: this.authHeader });
if (!response.ok) {
Logger.error(
`${Messages.FETCH_ERROR} ${key}: ${response.status} ${response.statusText}`
);
return null;
}
const issue: any = await response.json();
return this.convertIssueToStory(issue);
} catch (error) {
Logger.error(`${Messages.FETCH_ERROR} ${key}:`, error);
return null;
}
}
private async handleFetchError(response: any): Promise<void> {
const errorText = await response.text();
Logger.error(
`${Messages.FETCH_ERROR} ${response.status} ${response.statusText}`,
errorText
);
}
private logNoStories(): void {
const projectKey = this.config.getJiraProjectKey();
Logger.warn(`${Messages.NO_STORIES_FOUND} ${projectKey}`);
}
private logStoriesFound(count: number): void {
const projectKey = this.config.getJiraProjectKey();
Logger.info(
`${Messages.STORIES_LOADED} ${count} stories in project ${projectKey}`
);
}
private logFetchError(error: unknown): void {
Logger.error(`${Messages.FETCH_ERROR}`, error);
}
private processIssues(
issues: any[],
storiesMap: Map<string, JiraStory>
): void {
for (const issue of issues) {
const story = this.convertIssueToStory(issue);
storiesMap.set(issue.key, story);
}
}
private convertIssueToStory(issue: any): JiraStory {
const fields = issue.fields;
return {
key: issue.key,
title: this.extractTitle(fields),
description: this.extractDescription(fields.description),
status: this.extractStatus(fields),
assignee: this.extractAssignee(fields),
reporter: this.extractReporter(fields),
created: fields.created,
updated: fields.updated,
priority: this.extractPriority(fields),
type: this.extractType(fields),
labels: this.extractLabels(fields),
comments: this.extractComments(fields),
};
}
private extractTitle(fields: any): string {
return fields.summary || "";
}
private extractStatus(fields: any): string {
return fields.status?.name || JiraConstants.UNKNOWN_LABEL;
}
private extractAssignee(fields: any): string | undefined {
return fields.assignee?.displayName || fields.assignee?.emailAddress;
}
private extractReporter(fields: any): string | undefined {
return fields.reporter?.displayName || fields.reporter?.emailAddress;
}
private extractPriority(fields: any): string | undefined {
return fields.priority?.name;
}
private extractType(fields: any): string | undefined {
return fields.issuetype?.name;
}
private extractLabels(fields: any): string[] {
return fields.labels || [];
}
private extractComments(fields: any): JiraComment[] {
if (!fields.comment || !fields.comment.comments) {
return [];
}
return fields.comment.comments.map((comment: any) => ({
id: comment.id,
author: this.extractCommentAuthor(comment),
body: this.extractCommentBody(comment),
created: comment.created,
}));
}
private extractCommentAuthor(comment: any): string {
return (
comment.author?.displayName ||
comment.author?.emailAddress ||
JiraConstants.UNKNOWN_LABEL
);
}
private extractCommentBody(comment: any): string {
if (comment.body?.content?.[0]?.content?.[0]?.text) {
return comment.body.content[0].content[0].text;
}
return comment.body || "";
}
private extractDescription(description: any): string {
if (!description) {
return "";
}
if (this.isAtlassianDocumentFormat(description)) {
return this.extractTextFromAdf(description);
}
if (typeof description === "string") {
return description;
}
return "";
}
private isAtlassianDocumentFormat(description: any): boolean {
return description.content && Array.isArray(description.content);
}
private extractTextFromAdf(description: any): string {
let text = "";
for (const node of description.content) {
if (node.content && Array.isArray(node.content)) {
for (const child of node.content) {
if (child.text) {
text += child.text + " ";
}
}
}
}
return text.trim();
}
}