Skip to main content
Glama
index.js20.6 kB
#!/usr/bin/env node const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } = require('@modelcontextprotocol/sdk/types.js'); const axios = require('axios'); const path = require('path'); // Load environment variables with fallback const envPath = path.join(__dirname, '..', 'config', '.env'); require('dotenv').config({ path: envPath }); class JiraServer { constructor() { this.server = new Server( { name: 'jira-mcp-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.jiraConfig = { url: process.env.JIRA_URL, username: process.env.JIRA_USERNAME, apiToken: process.env.JIRA_API_TOKEN, defaultProject: process.env.DEFAULT_PROJECT_KEY || 'PROJ' }; this.setupToolHandlers(); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'jira_search', description: 'Search Jira issues using JQL (Jira Query Language)', inputSchema: { type: 'object', properties: { jql: { type: 'string', description: 'JQL query string (e.g., "project = PROJ AND status = Open")' }, maxResults: { type: 'number', description: 'Maximum number of results to return (default: 50)', default: 50 } }, required: ['jql'] } }, { name: 'jira_get_issue', description: 'Get detailed information about a specific Jira issue', inputSchema: { type: 'object', properties: { issueKey: { type: 'string', description: 'Jira issue key (e.g., "PROJ-123")' } }, required: ['issueKey'] } }, { name: 'jira_create_issue', description: 'Create a new Jira issue', inputSchema: { type: 'object', properties: { project: { type: 'string', description: 'Project key (e.g., "PROJ")' }, summary: { type: 'string', description: 'Issue summary/title' }, description: { type: 'string', description: 'Issue description' }, issueType: { type: 'string', description: 'Issue type (e.g., "작업", "에픽", "하위 작업", "정보공유")', default: '작업' }, priority: { type: 'string', description: 'Issue priority (e.g., "High", "Medium", "Low")', default: 'Medium' }, assignee: { type: 'string', description: 'Assignee email or username (optional)' }, labels: { type: 'array', items: { type: 'string' }, description: 'Array of labels to add to the issue' } }, required: ['project', 'summary'] } }, { name: 'jira_update_issue', description: 'Update an existing Jira issue', inputSchema: { type: 'object', properties: { issueKey: { type: 'string', description: 'Jira issue key (e.g., "PROJ-123")' }, summary: { type: 'string', description: 'New summary/title' }, description: { type: 'string', description: 'New description' }, assignee: { type: 'string', description: 'Assignee email or username' }, priority: { type: 'string', description: 'Priority (e.g., "High", "Medium", "Low")' }, labels: { type: 'array', items: { type: 'string' }, description: 'Array of labels to set on the issue' } }, required: ['issueKey'] } }, { name: 'jira_transition_issue', description: 'Transition an issue to a new status', inputSchema: { type: 'object', properties: { issueKey: { type: 'string', description: 'Jira issue key (e.g., "PROJ-123")' }, status: { type: 'string', description: 'New status (e.g., "In Progress", "Done", "To Do")' } }, required: ['issueKey', 'status'] } }, { name: 'jira_add_comment', description: 'Add a comment to a Jira issue', inputSchema: { type: 'object', properties: { issueKey: { type: 'string', description: 'Jira issue key (e.g., "PROJ-123")' }, comment: { type: 'string', description: 'Comment text' } }, required: ['issueKey', 'comment'] } }, { name: 'jira_get_project_info', description: 'Get information about a Jira project', inputSchema: { type: 'object', properties: { projectKey: { type: 'string', description: 'Project key (e.g., "PROJ")' } }, required: ['projectKey'] } }, { name: 'jira_get_project_issues', description: 'Get all issues for a specific project', inputSchema: { type: 'object', properties: { projectKey: { type: 'string', description: 'Project key (e.g., "PROJ")' }, maxResults: { type: 'number', description: 'Maximum number of results to return (default: 50)', default: 50 }, status: { type: 'string', description: 'Filter by status (optional)' } }, required: ['projectKey'] } }, { name: 'jira_get_transitions', description: 'Get available transitions for an issue', inputSchema: { type: 'object', properties: { issueKey: { type: 'string', description: 'Jira issue key (e.g., "PROJ-123")' } }, required: ['issueKey'] } } ] }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'jira_search': return await this.searchIssues(args); case 'jira_get_issue': return await this.getIssue(args); case 'jira_create_issue': return await this.createIssue(args); case 'jira_update_issue': return await this.updateIssue(args); case 'jira_transition_issue': return await this.transitionIssue(args); case 'jira_add_comment': return await this.addComment(args); case 'jira_get_project_info': return await this.getProjectInfo(args); case 'jira_get_project_issues': return await this.getProjectIssues(args); case 'jira_get_transitions': return await this.getTransitions(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { throw new McpError( ErrorCode.InternalError, `Error executing ${name}: ${error.response?.data?.errorMessages || error.message}` ); } }); } getAuthHeaders() { const auth = Buffer.from(`${this.jiraConfig.username}:${this.jiraConfig.apiToken}`).toString('base64'); return { 'Authorization': `Basic ${auth}`, 'Accept': 'application/json', 'Content-Type': 'application/json' }; } async makeJiraRequest(method, endpoint, data = null) { const config = { method, url: `${this.jiraConfig.url}/rest/api/3${endpoint}`, headers: this.getAuthHeaders(), }; if (data) { config.data = data; } try { const response = await axios(config); return response; } catch (error) { throw error; } } async searchIssues(args) { const { jql, maxResults = 50 } = args; const response = await this.makeJiraRequest('GET', `/search?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}&fields=summary,status,assignee,priority,created,updated,description`); const issues = response.data.issues.map(issue => ({ key: issue.key, summary: issue.fields.summary, status: issue.fields.status.name, assignee: issue.fields.assignee?.displayName || 'Unassigned', priority: issue.fields.priority?.name || 'None', created: issue.fields.created, updated: issue.fields.updated, url: `${this.jiraConfig.url}/browse/${issue.key}` })); return { content: [ { type: 'text', text: `Found ${issues.length} issues:\n\n` + issues.map(issue => `**${issue.key}**: ${issue.summary}\n` + `Status: ${issue.status} | Assignee: ${issue.assignee} | Priority: ${issue.priority}\n` + `URL: ${issue.url}\n` ).join('\n') } ] }; } async getIssue(args) { const { issueKey } = args; const response = await this.makeJiraRequest('GET', `/issue/${issueKey}`); const issue = response.data; const description = this.extractDescription(issue.fields.description); const result = { key: issue.key, summary: issue.fields.summary, description: description, status: issue.fields.status.name, assignee: issue.fields.assignee?.displayName || 'Unassigned', priority: issue.fields.priority?.name || 'None', issueType: issue.fields.issuetype.name, created: issue.fields.created, updated: issue.fields.updated, labels: issue.fields.labels || [], url: `${this.jiraConfig.url}/browse/${issue.key}` }; return { content: [ { type: 'text', text: `**${result.key}**: ${result.summary}\n\n` + `**Description**: ${result.description}\n` + `**Status**: ${result.status}\n` + `**Assignee**: ${result.assignee}\n` + `**Priority**: ${result.priority}\n` + `**Type**: ${result.issueType}\n` + `**Labels**: ${result.labels.join(', ') || 'None'}\n` + `**Created**: ${result.created}\n` + `**Updated**: ${result.updated}\n` + `**URL**: ${result.url}` } ] }; } extractDescription(description) { if (!description) return 'No description'; // Handle Atlassian Document Format (ADF) if (description.type === 'doc' && description.content) { let text = ''; for (const content of description.content) { if (content.type === 'paragraph' && content.content) { for (const textContent of content.content) { if (textContent.type === 'text') { text += textContent.text; } } text += '\n'; } } return text.trim() || 'No description'; } // Handle plain text if (typeof description === 'string') { return description; } return 'No description'; } async createIssue(args) { const { project, summary, description = '', issueType = '작업', priority = 'Medium', assignee, labels } = args; // First, get project details to validate try { await this.makeJiraRequest('GET', `/project/${project}`); } catch (error) { throw new Error(`Project ${project} not found or not accessible`); } // Get available issue types for the project const projectMetaResponse = await this.makeJiraRequest('GET', `/issue/createmeta?projectKeys=${project}&expand=projects.issuetypes.fields`); const projectMeta = projectMetaResponse.data.projects[0]; if (!projectMeta) { throw new Error(`Unable to get metadata for project ${project}`); } // Find the issue type const issueTypeObj = projectMeta.issuetypes.find(type => type.name.toLowerCase() === issueType.toLowerCase() ); if (!issueTypeObj) { const availableTypes = projectMeta.issuetypes.map(type => type.name).join(', '); throw new Error(`Issue type "${issueType}" not found. Available types: ${availableTypes}`); } const issueData = { fields: { project: { key: project }, summary, issuetype: { id: issueTypeObj.id } } }; // Add description if provided if (description) { issueData.fields.description = { type: 'doc', version: 1, content: [ { type: 'paragraph', content: [ { type: 'text', text: description } ] } ] }; } // Add priority if the field is available const priorityField = issueTypeObj.fields.priority; if (priorityField && priority) { const allowedPriorities = priorityField.allowedValues || []; const priorityObj = allowedPriorities.find(p => p.name.toLowerCase() === priority.toLowerCase() ); if (priorityObj) { issueData.fields.priority = { id: priorityObj.id }; } } // Add assignee if provided if (assignee) { issueData.fields.assignee = { emailAddress: assignee }; } // Add labels if provided if (labels && Array.isArray(labels)) { issueData.fields.labels = labels; } const response = await this.makeJiraRequest('POST', '/issue', issueData); const issueKey = response.data.key; const issueUrl = `${this.jiraConfig.url}/browse/${issueKey}`; return { content: [ { type: 'text', text: `✅ Successfully created issue: **${issueKey}**\n` + `Summary: ${summary}\n` + `Type: ${issueType}\n` + `Priority: ${priority}\n` + `Project: ${project}\n` + `URL: ${issueUrl}` } ] }; } async updateIssue(args) { const { issueKey, summary, description, assignee, priority, labels } = args; const updateData = { fields: {} }; if (summary) updateData.fields.summary = summary; if (description) { updateData.fields.description = { type: 'doc', version: 1, content: [ { type: 'paragraph', content: [ { type: 'text', text: description } ] } ] }; } if (assignee) { updateData.fields.assignee = { emailAddress: assignee }; } if (priority) { updateData.fields.priority = { name: priority }; } if (labels && Array.isArray(labels)) { updateData.fields.labels = labels; } // Update fields if (Object.keys(updateData.fields).length > 0) { await this.makeJiraRequest('PUT', `/issue/${issueKey}`, updateData); } return { content: [ { type: 'text', text: `✅ Successfully updated issue: **${issueKey}**\n` + `URL: ${this.jiraConfig.url}/browse/${issueKey}` } ] }; } async transitionIssue(args) { const { issueKey, status } = args; // Get available transitions const transitionsResponse = await this.makeJiraRequest('GET', `/issue/${issueKey}/transitions`); const transition = transitionsResponse.data.transitions.find(t => t.to.name.toLowerCase() === status.toLowerCase() ); if (!transition) { const availableStatuses = transitionsResponse.data.transitions.map(t => t.to.name).join(', '); throw new Error(`Status "${status}" not available. Available statuses: ${availableStatuses}`); } await this.makeJiraRequest('POST', `/issue/${issueKey}/transitions`, { transition: { id: transition.id } }); return { content: [ { type: 'text', text: `✅ Successfully transitioned issue **${issueKey}** to **${status}**\n` + `URL: ${this.jiraConfig.url}/browse/${issueKey}` } ] }; } async addComment(args) { const { issueKey, comment } = args; const commentData = { body: { type: 'doc', version: 1, content: [ { type: 'paragraph', content: [ { type: 'text', text: comment } ] } ] } }; await this.makeJiraRequest('POST', `/issue/${issueKey}/comment`, commentData); return { content: [ { type: 'text', text: `✅ Successfully added comment to issue: **${issueKey}**\n` + `Comment: ${comment}\n` + `URL: ${this.jiraConfig.url}/browse/${issueKey}` } ] }; } async getProjectInfo(args) { const { projectKey } = args; const response = await this.makeJiraRequest('GET', `/project/${projectKey}`); const project = response.data; return { content: [ { type: 'text', text: `**Project**: ${project.name} (${project.key})\n` + `**Description**: ${project.description || 'No description'}\n` + `**Lead**: ${project.lead?.displayName || 'No lead assigned'}\n` + `**Project Type**: ${project.projectTypeKey}\n` + `**URL**: ${this.jiraConfig.url}/projects/${project.key}` } ] }; } async getProjectIssues(args) { const { projectKey, maxResults = 50, status } = args; let jql = `project = ${projectKey}`; if (status) { jql += ` AND status = "${status}"`; } jql += ' ORDER BY created DESC'; return await this.searchIssues({ jql, maxResults }); } async getTransitions(args) { const { issueKey } = args; const response = await this.makeJiraRequest('GET', `/issue/${issueKey}/transitions`); const transitions = response.data.transitions.map(transition => ({ id: transition.id, name: transition.name, to: transition.to.name })); return { content: [ { type: 'text', text: `Available transitions for **${issueKey}**:\n\n` + transitions.map(t => `• **${t.name}** → ${t.to}`).join('\n') + `\n\nURL: ${this.jiraConfig.url}/browse/${issueKey}` } ] }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); // Silent mode - no console output to avoid JSON parsing errors } } const server = new JiraServer(); server.run().catch(console.error);

Latest Blog Posts

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/SunWooBang/jira-mcp-server'

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