@pavi_thran_7/jira-mcp-server
Version:
MCP server for Jira integration with ticket creation and time logging
531 lines (481 loc) âĸ 17.1 kB
JavaScript
/**
* @fileoverview Main entry point for Jira MCP server
*/
console.error('đ Loading Jira MCP Server modules...');
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import dotenv from 'dotenv';
console.error('â
Core MCP modules loaded successfully');
// Import tool modules
console.error('đĻ Loading tool modules...');
import { configureJira } from './tools/configure.js';
import { createTicket, getTicket } from './tools/tickets.js';
import { logWork } from './tools/worklog.js';
import { listProjects, listBoards } from './tools/discovery.js';
import { listBillingAccounts } from './tools/billing.js';
console.error('â
All tool modules loaded successfully');
// Load environment variables
console.error('đ§ Loading environment variables...');
dotenv.config();
console.error('â
Environment variables loaded');
/**
* Global Jira client instance
*/
let jiraClient = null;
/**
* Auto-configure Jira client from environment variables
*/
async function autoConfigureJira() {
const baseUrl = process.env.JIRA_BASE_URL;
const email = process.env.JIRA_EMAIL;
const apiToken = process.env.JIRA_API_TOKEN;
if (baseUrl && email && apiToken) {
console.error('đ Found Jira configuration in environment variables');
console.error('đ§ Auto-configuring Jira client...');
try {
// Import JiraClient here to avoid circular dependency
const { JiraClient } = await import('./jira-client.js');
// Create Jira client
const client = new JiraClient({
baseUrl: baseUrl.replace(/\/$/, ''), // Remove trailing slash
email: email,
apiToken: apiToken,
});
// Test authentication
console.error('đ Testing Jira authentication...');
const authResult = await client.testAuth();
if (!authResult.success) {
console.error('â Auto-configuration failed:', authResult.error);
console.error('â ī¸ You can still configure manually using the configure_jira tool');
return false;
}
// Set global client instance
jiraClient = client;
const userInfo = authResult.data;
console.error('â
Jira client auto-configured successfully!');
console.error(`đ¤ Connected as: ${userInfo.displayName} (${userInfo.emailAddress})`);
console.error(`đĸ Jira instance: ${baseUrl}`);
console.error('đ¯ All Jira tools are now ready to use');
return true;
} catch (error) {
console.error('â Error during auto-configuration:', error.message);
console.error('â ī¸ You can still configure manually using the configure_jira tool');
return false;
}
} else {
console.error('âšī¸ No Jira environment variables found');
console.error('đĄ Set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN for auto-configuration');
console.error('đ§ Or use the configure_jira tool to set up connection manually');
return false;
}
}
/**
* Get or create Jira client instance
* @returns {import('./jira-client.js').JiraClient|null} Jira client instance
*/
export function getJiraClient() {
return jiraClient;
}
/**
* Set Jira client instance
* @param {import('./jira-client.js').JiraClient} client - Jira client instance
*/
export function setJiraClient(client) {
jiraClient = client;
}
/**
* Create and configure the MCP server
*/
async function createServer() {
try {
console.error('đ§ Creating MCP server with configuration...');
const server = new Server(
{
name: 'jira-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
console.error('â
MCP server created with capabilities');
/**
* Tool definitions
*/
const tools = [
{
name: 'configure_jira',
description: 'Configure Jira connection with base URL, email, and API token',
inputSchema: {
type: 'object',
properties: {
baseUrl: {
type: 'string',
description: 'Jira instance URL (e.g., https://company.atlassian.net)',
},
email: {
type: 'string',
description: 'User\'s Jira account email',
},
apiToken: {
type: 'string',
description: 'Jira API token for authentication',
},
},
required: ['baseUrl', 'email', 'apiToken'],
},
},
{
name: 'create_ticket',
description: 'Create a new Jira ticket with project and board assignment',
inputSchema: {
type: 'object',
properties: {
projectKey: {
type: 'string',
description: 'Project identifier (e.g., "PROJ", "DEV")',
},
summary: {
type: 'string',
description: 'Ticket title/summary',
},
description: {
type: 'string',
description: 'Detailed ticket description',
},
issueType: {
type: 'string',
description: 'Type of issue (Task, Bug, Story, Epic)',
},
priority: {
type: 'string',
description: 'Priority level (Highest, High, Medium, Low, Lowest)',
},
assignee: {
type: 'string',
description: 'User account ID or email to assign ticket',
},
labels: {
type: 'array',
items: { type: 'string' },
description: 'Array of label strings',
},
boardId: {
type: 'string',
description: 'Specific board ID to add ticket to',
},
},
required: ['projectKey', 'summary', 'issueType'],
},
},
{
name: 'get_ticket',
description: 'Retrieve ticket details and current status',
inputSchema: {
type: 'object',
properties: {
issueKey: {
type: 'string',
description: 'Ticket identifier (e.g., "PROJ-123")',
},
},
required: ['issueKey'],
},
},
{
name: 'log_work',
description: 'Log work hours against existing tickets',
inputSchema: {
type: 'object',
properties: {
issueKey: {
type: 'string',
description: 'Ticket identifier (e.g., "PROJ-123")',
},
timeSpent: {
type: 'string',
description: 'Time duration (e.g., "2h 30m", "1d", "45m")',
},
comment: {
type: 'string',
description: 'Description of work performed',
},
started: {
type: 'string',
description: 'ISO date when work started (defaults to now)',
},
billingAccountId: {
type: 'string',
description: 'Billing account ID for invoicing/cost tracking',
},
},
required: ['issueKey', 'timeSpent'],
},
},
{
name: 'list_projects',
description: 'Get available projects user has access to',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'list_boards',
description: 'Get available boards for a project',
inputSchema: {
type: 'object',
properties: {
projectKey: {
type: 'string',
description: 'Filter boards by project key',
},
},
},
},
{
name: 'list_billing_accounts',
description: 'Get available billing accounts for time logging',
inputSchema: {
type: 'object',
properties: {
projectKey: {
type: 'string',
description: 'Filter billing accounts by project',
},
},
},
},
];
/**
* Handle list tools request
*/
console.error('đ§ Setting up list tools request handler...');
server.setRequestHandler(ListToolsRequestSchema, async () => {
console.error('đ Received list tools request');
return { tools };
});
console.error('â
List tools request handler registered');
/**
* Handle tool calls
*/
console.error('đ§ Setting up tool call request handler...');
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
console.error(`đ ī¸ Received tool call: ${name} with args:`, JSON.stringify(args, null, 2));
try {
switch (name) {
case 'configure_jira':
// Check if already configured
if (jiraClient) {
console.error('âšī¸ Jira client already configured, reconfiguring...');
}
const result = await configureJira(args);
// If configuration was successful, update the global client
if (!result.isError) {
// The configureJira function will set the global client via setJiraClient
console.error('â
Manual configuration completed successfully');
}
return result;
case 'create_ticket':
if (!jiraClient) {
return {
content: [
{
type: 'text',
text: 'Error: Jira not configured. Please run configure_jira first or set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.',
},
],
isError: true,
};
}
console.error(`đĢ Creating ticket in project: ${args.projectKey}`);
return await createTicket(jiraClient, args);
case 'get_ticket':
if (!jiraClient) {
return {
content: [
{
type: 'text',
text: 'Error: Jira not configured. Please run configure_jira first or set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.',
},
],
isError: true,
};
}
console.error(`đ Getting ticket details for: ${args.issueKey}`);
return await getTicket(jiraClient, args);
case 'log_work':
if (!jiraClient) {
return {
content: [
{
type: 'text',
text: 'Error: Jira not configured. Please run configure_jira first or set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.',
},
],
isError: true,
};
}
console.error(`â° Logging work for ticket: ${args.issueKey} (${args.timeSpent})`);
return await logWork(jiraClient, args);
case 'list_projects':
if (!jiraClient) {
return {
content: [
{
type: 'text',
text: 'Error: Jira not configured. Please run configure_jira first or set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.',
},
],
isError: true,
};
}
console.error('đ Listing available projects...');
return await listProjects(jiraClient);
case 'list_boards':
if (!jiraClient) {
return {
content: [
{
type: 'text',
text: 'Error: Jira not configured. Please run configure_jira first or set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.',
},
],
isError: true,
};
}
console.error(`đ Listing boards${args.projectKey ? ` for project: ${args.projectKey}` : ''}...`);
return await listBoards(jiraClient, args);
case 'list_billing_accounts':
if (!jiraClient) {
return {
content: [
{
type: 'text',
text: 'Error: Jira not configured. Please run configure_jira first or set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.',
},
],
isError: true,
};
}
console.error(`đ° Listing billing accounts${args.projectKey ? ` for project: ${args.projectKey}` : ''}...`);
return await listBillingAccounts(jiraClient, args);
default:
return {
content: [
{
type: 'text',
text: `Error: Unknown tool ${name}`,
},
],
isError: true,
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
console.error('â
Tool call request handler registered');
console.error('â
MCP server configuration complete');
return server;
} catch (error) {
console.error('đĨ Error creating MCP server:', error);
console.error('Stack trace:', error.stack);
throw error;
}
}
/**
* Main function to start the server
*/
async function main() {
try {
console.error('đ Initializing Jira MCP Server...');
// Create server instance
console.error('đĄ Creating MCP server instance...');
const server = await createServer();
console.error('â
MCP server instance created successfully');
// Create transport
console.error('đ Creating stdio transport...');
const transport = new StdioServerTransport();
console.error('â
Stdio transport created successfully');
// Connect server to transport
console.error('đ Connecting server to transport...');
await server.connect(transport);
console.error('â
Server connected to transport successfully');
// Auto-configure Jira client from environment variables
console.error('đ Attempting auto-configuration from environment variables...');
await autoConfigureJira();
console.error('đ Jira MCP server running on stdio');
console.error('đ Available tools: configure_jira, create_ticket, get_ticket, log_work, list_projects, list_boards, list_billing_accounts');
console.error('âŗ Server is ready and waiting for requests...');
// Keep the process alive
const keepAlive = () => {
console.error(`đ Server heartbeat - ${new Date().toISOString()}`);
setTimeout(keepAlive, 60000); // Log every minute
};
// Start heartbeat after 1 minute
setTimeout(keepAlive, 60000);
// Handle transport errors
transport.onclose = () => {
console.error('đ Transport connection closed');
process.exit(0);
};
transport.onerror = (error) => {
console.error('â Transport error:', error);
process.exit(1);
};
} catch (error) {
console.error('đĨ Fatal error during server startup:', error);
console.error('Stack trace:', error.stack);
process.exit(1);
}
}
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.error('đ Received SIGINT - shutting down Jira MCP Server...');
process.exit(0);
});
process.on('SIGTERM', async () => {
console.error('đ Received SIGTERM - shutting down Jira MCP Server...');
process.exit(0);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('đĨ Uncaught exception:', error);
console.error('Stack trace:', error.stack);
process.exit(1);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('đĨ Unhandled promise rejection at:', promise, 'reason:', reason);
process.exit(1);
});
// Start the server
// More reliable way to check if script is run directly
const isMainModule = import.meta.url === `file://${process.argv[1]}` ||
import.meta.url.endsWith(process.argv[1]) ||
process.argv[1].includes('index.js');
if (isMainModule) {
console.error('đ Starting Jira MCP Server process...');
main().catch((error) => {
console.error('đĨ Fatal error in main function:', error);
console.error('Stack trace:', error.stack);
process.exit(1);
});
}