@orengrinker/jira-mcp-server
Version:
A comprehensive Model Context Protocol server for Jira integration with issue management, board operations, time tracking, and project management capabilities
249 lines (248 loc) • 9.61 kB
JavaScript
import { JiraError } from './types/index.js';
import { Logger } from './utils/logger.js';
import { RateLimiter } from './utils/rateLimiter.js';
export class JiraApiClient {
config;
logger;
rateLimiter;
authHeader;
constructor() {
this.config = this.getJiraConfig();
this.logger = new Logger('JiraApiClient');
this.rateLimiter = new RateLimiter();
this.authHeader = `Basic ${Buffer.from(`${this.config.email}:${this.config.apiToken}`).toString('base64')}`;
}
getJiraConfig() {
const baseUrl = process.env.JIRA_BASE_URL;
const email = process.env.JIRA_EMAIL;
const apiToken = process.env.JIRA_API_TOKEN;
if (!baseUrl || !email || !apiToken) {
throw new Error('Missing Jira configuration. Please set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.');
}
// Ensure baseUrl doesn't end with slash
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
return { baseUrl: cleanBaseUrl, email, apiToken };
}
async testConnection() {
try {
await this.makeRequest('/myself', { useV3Api: true });
this.logger.info('Jira connection test successful');
}
catch (error) {
this.logger.error('Jira connection test failed:', error);
throw new Error('Failed to connect to Jira. Please check your credentials and network connection.');
}
}
async makeRequest(endpoint, options = {}) {
const { method = 'GET', body, useV3Api = false, useAgileApi = false, headers = {}, } = options;
// Apply rate limiting
await this.rateLimiter.waitForSlot();
const apiPath = useAgileApi ? '/rest/agile/1.0' : useV3Api ? '/rest/api/3' : '/rest/api/2';
const url = `${this.config.baseUrl}${apiPath}${endpoint}`;
const requestHeaders = {
'Authorization': this.authHeader,
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'Enhanced-Jira-MCP-Server/2.0.0',
...headers,
};
this.logger.debug(`Making ${method} request to: ${url}`);
try {
const fetchOptions = {
method,
headers: requestHeaders,
};
// Only add body if it exists
if (body !== undefined) {
fetchOptions.body = JSON.stringify(body);
}
const response = await fetch(url, fetchOptions);
if (!response.ok) {
const errorText = await response.text();
let errorMessage;
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.errorMessages?.join(', ') || errorJson.message || errorText;
}
catch {
errorMessage = errorText;
}
throw new JiraError(`Jira API error: ${response.status} ${response.statusText}`, response.status, errorMessage);
}
const responseText = await response.text();
if (!responseText) {
return {};
}
return JSON.parse(responseText);
}
catch (error) {
this.logger.error(`API request failed for ${url}:`, error);
if (error instanceof JiraError) {
throw error;
}
throw new JiraError('Network error occurred while making API request', 0, error instanceof Error ? error.message : 'Unknown error');
}
}
// Board-related methods
async getBoards(params = {}) {
const queryParams = new URLSearchParams();
if (params.type)
queryParams.append('type', params.type);
if (params.projectKeyOrId)
queryParams.append('projectKeyOrId', params.projectKeyOrId);
const endpoint = `/board${queryParams.toString() ? `?${queryParams}` : ''}`;
return this.makeRequest(endpoint, { useAgileApi: true });
}
async getBoard(boardId) {
return this.makeRequest(`/board/${boardId}`, { useAgileApi: true });
}
async getBoardIssues(boardId, params = {}) {
const queryParams = new URLSearchParams();
if (params.jql)
queryParams.append('jql', params.jql);
if (params.maxResults)
queryParams.append('maxResults', params.maxResults.toString());
if (params.startAt)
queryParams.append('startAt', params.startAt.toString());
if (params.fields)
queryParams.append('fields', params.fields.join(','));
const endpoint = `/board/${boardId}/issue${queryParams.toString() ? `?${queryParams}` : ''}`;
return this.makeRequest(endpoint, { useAgileApi: true });
}
// Issue-related methods
async searchIssues(jql, params = {}) {
const queryParams = new URLSearchParams();
queryParams.append('jql', jql);
if (params.maxResults)
queryParams.append('maxResults', params.maxResults.toString());
if (params.startAt)
queryParams.append('startAt', params.startAt.toString());
if (params.fields)
queryParams.append('fields', params.fields.join(','));
if (params.expand)
queryParams.append('expand', params.expand.join(','));
return this.makeRequest(`/search?${queryParams}`, { useV3Api: true });
}
async getIssue(issueIdOrKey, params = {}) {
const queryParams = new URLSearchParams();
if (params.fields)
queryParams.append('fields', params.fields.join(','));
if (params.expand)
queryParams.append('expand', params.expand.join(','));
const endpoint = `/issue/${issueIdOrKey}${queryParams.toString() ? `?${queryParams}` : ''}`;
return this.makeRequest(endpoint, { useV3Api: true });
}
async addComment(issueIdOrKey, comment) {
const adfBody = {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: comment,
},
],
},
],
},
};
return this.makeRequest(`/issue/${issueIdOrKey}/comment`, {
method: 'POST',
body: adfBody,
useV3Api: true,
});
}
async updateIssue(issueIdOrKey, updateData) {
await this.makeRequest(`/issue/${issueIdOrKey}`, {
method: 'PUT',
body: updateData,
useV3Api: true,
});
}
async createIssue(issueData) {
return this.makeRequest('/issue', {
method: 'POST',
body: issueData,
useV3Api: true,
});
}
async transitionIssue(issueIdOrKey, transitionId, comment) {
const body = {
transition: { id: transitionId }
};
if (comment) {
body.update = {
comment: [{
add: {
body: {
type: 'doc',
version: 1,
content: [{
type: 'paragraph',
content: [{ type: 'text', text: comment }]
}]
}
}
}]
};
}
await this.makeRequest(`/issue/${issueIdOrKey}/transitions`, {
method: 'POST',
body,
useV3Api: true,
});
}
async getIssueTransitions(issueIdOrKey) {
return this.makeRequest(`/issue/${issueIdOrKey}/transitions`, { useV3Api: true });
}
// User-related methods
async getCurrentUser() {
return this.makeRequest('/myself', { useV3Api: true });
}
async searchUsers(query) {
return this.makeRequest(`/user/search?query=${encodeURIComponent(query)}`, { useV3Api: true });
}
async getUser(accountId) {
return this.makeRequest(`/user?accountId=${accountId}`, { useV3Api: true });
}
// Project-related methods
async getProjects() {
return this.makeRequest('/project', { useV3Api: true });
}
async getProject(projectIdOrKey) {
return this.makeRequest(`/project/${projectIdOrKey}`, { useV3Api: true });
}
// Server info
async getServerInfo() {
return this.makeRequest('/serverInfo', { useV3Api: true });
}
// Worklog methods
async addWorklog(issueIdOrKey, timeSpent, comment, startedDate) {
const body = {
timeSpent,
started: startedDate || new Date().toISOString(),
};
if (comment) {
body.comment = {
type: 'doc',
version: 1,
content: [{
type: 'paragraph',
content: [{ type: 'text', text: comment }]
}]
};
}
return this.makeRequest(`/issue/${issueIdOrKey}/worklog`, {
method: 'POST',
body,
useV3Api: true,
});
}
async getWorklogs(issueIdOrKey) {
return this.makeRequest(`/issue/${issueIdOrKey}/worklog`, { useV3Api: true });
}
}