Skip to main content
Glama

JIRA MCP Server

by cosmix
index.ts15.5 kB
#!/usr/bin/env bun import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { JiraApiService } from "./services/jira-api.js"; import { JiraServerApiService } from "./services/jira-server-api.js"; declare module "bun" { interface Env { JIRA_API_TOKEN: string; JIRA_BASE_URL: string; JIRA_USER_EMAIL: string; JIRA_TYPE: string; JIRA_AUTH_TYPE: string; } } const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN; const JIRA_BASE_URL = process.env.JIRA_BASE_URL; const JIRA_USER_EMAIL = process.env.JIRA_USER_EMAIL; const JIRA_TYPE = (process.env.JIRA_TYPE === "server" ? "server" : "cloud") as | "cloud" | "server"; const JIRA_AUTH_TYPE = (process.env.JIRA_AUTH_TYPE === "bearer" ? "bearer" : "basic") as | "basic" | "bearer"; if (!JIRA_API_TOKEN || !JIRA_BASE_URL || !JIRA_USER_EMAIL) { throw new Error( "JIRA_API_TOKEN, JIRA_USER_EMAIL and JIRA_BASE_URL environment variables are required", ); } class JiraServer { private server: Server; private jiraApi: JiraApiService; constructor() { this.server = new Server( { name: "jira-mcp", version: "0.2.0", }, { capabilities: { tools: {}, }, }, ); if (JIRA_TYPE === "server") { this.jiraApi = new JiraServerApiService( JIRA_BASE_URL, JIRA_USER_EMAIL, JIRA_API_TOKEN, JIRA_AUTH_TYPE, ); } else { this.jiraApi = new JiraApiService( JIRA_BASE_URL, JIRA_USER_EMAIL, JIRA_API_TOKEN, JIRA_AUTH_TYPE, ); } this.setupToolHandlers(); this.server.onerror = (error) => {}; process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "search_issues", description: "Search JIRA issues using JQL", inputSchema: { type: "object", properties: { searchString: { type: "string", description: "JQL search string", }, }, required: ["searchString"], additionalProperties: false, }, }, { name: "get_epic_children", description: "Get all child issues in an epic including their comments", inputSchema: { type: "object", properties: { epicKey: { type: "string", description: "The key of the epic issue", }, }, required: ["epicKey"], additionalProperties: false, }, }, { name: "get_issue", description: "Get detailed information about a specific JIRA issue including comments", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The ID or key of the JIRA issue", }, }, required: ["issueId"], additionalProperties: false, }, }, { name: "create_issue", description: "Create a new JIRA issue", inputSchema: { type: "object", properties: { projectKey: { type: "string", description: "The project key where the issue will be created", }, issueType: { type: "string", description: 'The type of issue to create (e.g., "Bug", "Story", "Task")', }, summary: { type: "string", description: "The issue summary/title", }, description: { type: "string", description: "The issue description", }, fields: { type: "object", description: "Additional fields to set on the issue", additionalProperties: true, }, }, required: ["projectKey", "issueType", "summary"], additionalProperties: false, }, }, { name: "update_issue", description: "Update an existing JIRA issue", inputSchema: { type: "object", properties: { issueKey: { type: "string", description: "The key of the issue to update", }, fields: { type: "object", description: "Fields to update on the issue", additionalProperties: true, }, }, required: ["issueKey", "fields"], additionalProperties: false, }, }, { name: "get_transitions", description: "Get available status transitions for a JIRA issue", inputSchema: { type: "object", properties: { issueKey: { type: "string", description: "The key of the issue to get transitions for", }, }, required: ["issueKey"], additionalProperties: false, }, }, { name: "transition_issue", description: "Change the status of a JIRA issue by performing a transition", inputSchema: { type: "object", properties: { issueKey: { type: "string", description: "The key of the issue to transition", }, transitionId: { type: "string", description: "The ID of the transition to perform", }, comment: { type: "string", description: "Optional comment to add with the transition", }, }, required: ["issueKey", "transitionId"], additionalProperties: false, }, }, { name: "add_attachment", description: "Add a file attachment to a JIRA issue", inputSchema: { type: "object", properties: { issueKey: { type: "string", description: "The key of the issue to add attachment to", }, fileContent: { type: "string", description: "Base64 encoded content of the file", }, filename: { type: "string", description: "Name of the file to be attached", }, }, required: ["issueKey", "fileContent", "filename"], additionalProperties: false, }, }, { name: "add_comment", description: "Add a comment to a JIRA issue", inputSchema: { type: "object", properties: { issueIdOrKey: { type: "string", description: "The ID or key of the issue to add the comment to", }, body: { type: "string", description: "The content of the comment (plain text)", }, }, required: ["issueIdOrKey", "body"], additionalProperties: false, }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const args = request.params.arguments as Record<string, any>; switch (request.params.name) { case "search_issues": { if (!args.searchString || typeof args.searchString !== "string") { throw new McpError( ErrorCode.InvalidParams, "Search string is required", ); } const response = await this.jiraApi.searchIssues(args.searchString); return { content: [ { type: "text", text: JSON.stringify(response, null, 2) }, ], }; } case "get_epic_children": { if (!args.epicKey || typeof args.epicKey !== "string") { throw new McpError( ErrorCode.InvalidParams, "Epic key is required", ); } const response = await this.jiraApi.getEpicChildren(args.epicKey); return { content: [ { type: "text", text: JSON.stringify(response, null, 2) }, ], }; } case "get_issue": { if (!args.issueId || typeof args.issueId !== "string") { throw new McpError( ErrorCode.InvalidParams, "Issue ID is required", ); } const response = await this.jiraApi.getIssueWithComments( args.issueId, ); return { content: [ { type: "text", text: JSON.stringify(response, null, 2) }, ], }; } case "create_issue": { // Basic validation if ( !args.projectKey || typeof args.projectKey !== "string" || !args.issueType || typeof args.issueType !== "string" || !args.summary || typeof args.summary !== "string" ) { throw new McpError( ErrorCode.InvalidParams, "projectKey, issueType, and summary are required", ); } const response = await this.jiraApi.createIssue( args.projectKey, args.issueType, args.summary, args.description as string | undefined, args.fields as Record<string, any> | undefined, ); return { content: [ { type: "text", text: JSON.stringify(response, null, 2) }, ], }; } case "update_issue": { if ( !args.issueKey || typeof args.issueKey !== "string" || !args.fields || typeof args.fields !== "object" ) { throw new McpError( ErrorCode.InvalidParams, "issueKey and fields object are required", ); } await this.jiraApi.updateIssue(args.issueKey, args.fields); return { content: [ { type: "text", text: JSON.stringify( { message: `Issue ${args.issueKey} updated successfully` }, null, 2, ), }, ], }; } case "get_transitions": { if (!args.issueKey || typeof args.issueKey !== "string") { throw new McpError( ErrorCode.InvalidParams, "Issue key is required", ); } const response = await this.jiraApi.getTransitions(args.issueKey); return { content: [ { type: "text", text: JSON.stringify(response, null, 2) }, ], }; } case "transition_issue": { if ( !args.issueKey || typeof args.issueKey !== "string" || !args.transitionId || typeof args.transitionId !== "string" ) { throw new McpError( ErrorCode.InvalidParams, "issueKey and transitionId are required", ); } await this.jiraApi.transitionIssue( args.issueKey, args.transitionId, args.comment as string | undefined, ); return { content: [ { type: "text", text: JSON.stringify( { message: `Issue ${args.issueKey} transitioned successfully${args.comment ? " with comment" : ""}`, }, null, 2, ), }, ], }; } case "add_attachment": { if ( !args.issueKey || typeof args.issueKey !== "string" || !args.fileContent || typeof args.fileContent !== "string" || !args.filename || typeof args.filename !== "string" ) { throw new McpError( ErrorCode.InvalidParams, "issueKey, fileContent, and filename are required", ); } const fileBuffer = Buffer.from(args.fileContent, "base64"); const result = await this.jiraApi.addAttachment( args.issueKey, fileBuffer, args.filename, ); return { content: [ { type: "text", text: JSON.stringify( { message: `File ${args.filename} attached successfully to issue ${args.issueKey}`, attachmentId: result.id, filename: result.filename, }, null, 2, ), }, ], }; } case "add_comment": { if ( !args.issueIdOrKey || typeof args.issueIdOrKey !== "string" || !args.body || typeof args.body !== "string" ) { throw new McpError( ErrorCode.InvalidParams, "issueIdOrKey and body are required", ); } const response = await this.jiraApi.addCommentToIssue( args.issueIdOrKey, args.body, ); return { content: [ { type: "text", text: JSON.stringify(response, null, 2) }, ], }; } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`, ); } } catch (error) { // Keep generic error handling if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, error instanceof Error ? error.message : "Unknown error occurred", ); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); // JIRA MCP server running on stdio } } const server = new JiraServer(); server.run().catch(() => {});

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

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