UNPKG

@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
#!/usr/bin/env node /** * @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); }); }