Integrating MCP Protocol with JIRA: A Practical Guide
On this page
- Integrating MCP Protocol with JIRA: A Practical Guide
- What is MCP Protocol?
- Why Integrate MCP with JIRA?
- Prerequisites
- Setting Up the Project
- Project File Structure
- Creating the MCP Server
- Environment Configuration
- Type Definitions for JIRA
- Advanced Features: Adding Resource Links
- Running the Server
- Testing the Integration
- Error Handling and Best Practices
- Conclusion
- Further Resources
Integrating MCP Protocol with JIRA: A Practical Guide
In this article, we’ll explore how to integrate the Model Context Protocol (MCP) with JIRA’s REST API using TypeScript. This integration will allow Large Language Models (LLMs) to access and interact with JIRA issues through the standardized MCP interface.
What is MCP Protocol?
The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to LLMs. Think of it as a “USB-C port” for AI applications - a universal interface that allows different systems to connect and share information with LLMs in a consistent way.
MCP helps developers build agents and workflows on top of LLMs by providing:
- Standardized integrations - Connect various data sources and tools
- Flexibility - Work with different LLM providers
- Security best practices - Control what data LLMs can access
Why Integrate MCP with JIRA?
JIRA is one of the most widely used project management tools, containing valuable information about tasks, bugs, and project progress. By integrating JIRA with MCP, we enable LLMs to:
- Search for issues using natural language
- Retrieve issue details and descriptions
- Generate summaries of project status
- Assist with project management tasks
This integration creates a powerful workflow where LLMs can access and reason about your project data, making them more effective assistants for software development teams.
Prerequisites
Before we begin, make sure you have:
- Node.js (version 18 or higher)
- A JIRA account with API access
- A JIRA API token (create one here)
- Basic knowledge of TypeScript
Setting Up the Project
Let’s start by creating a new TypeScript project and installing the necessary dependencies:
# Create a new directory for our projectmkdir mcp-jira-integrationcd mcp-jira-integration
# Initialize a new Node.js projectnpm init -y
# Install dependenciesnpm install @modelcontextprotocol/sdk jira.js zod typescriptnpm install --save-dev ts-node @types/node
# Initialize TypeScript configurationnpx tsc --initCreate a basic TypeScript configuration in tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "outDir": "./dist" }, "include": ["src/**/*"]}Project File Structure
Before we dive into the code, let’s understand the file structure of our project:
mcp-jira-integration/├── .env # Environment variables (JIRA credentials)├── .gitignore # Git ignore file├── package.json # Node.js package configuration├── tsconfig.json # TypeScript configuration└── src/ ├── server.ts # Basic MCP server implementation ├── advanced-server.ts # Advanced MCP server with resource links └── types/ └── jira.ts # Type definitions for JIRA API responsesThis structure keeps our code organized and makes it easy to understand the different components of our integration.
Creating the MCP Server
Now, let’s create our MCP server that will integrate with JIRA. Create a new file src/server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { Version3Client } from "jira.js";import { z } from "zod";import dotenv from "dotenv";
// Load environment variables from .env filedotenv.config();
// Validate required environment variablesconst JIRA_HOST = process.env.JIRA_HOST;const JIRA_EMAIL = process.env.JIRA_EMAIL;const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
if (!JIRA_HOST || !JIRA_EMAIL || !JIRA_API_TOKEN) { console.error("Missing required environment variables. Please check your .env file."); process.exit(1);}
// Initialize JIRA clientconst jiraClient = new Version3Client({ host: JIRA_HOST, authentication: { basic: { email: JIRA_EMAIL, apiToken: JIRA_API_TOKEN, }, },});
// Create MCP serverconst server = new McpServer({ name: "jira-mcp-server", version: "1.0.0",});
// Register a tool to search JIRA issuesserver.registerTool( "search-issues", { title: "Search JIRA Issues", description: "Search for JIRA issues using JQL (JIRA Query Language)", inputSchema: { jql: z.string().describe("JQL query string, e.g., 'project = PROJ AND status = Open'"), maxResults: z.number().optional().default(10).describe("Maximum number of results to return"), fields: z.array(z.string()).optional().default(["summary", "description", "status"]).describe("Fields to include in the response"), }, }, async ({ jql, maxResults, fields }) => { try { // Search for issues using the JIRA API const searchResults = await jiraClient.issueSearch.searchForIssuesUsingJql({ jql, maxResults, fields, });
// Format the results const formattedIssues = searchResults.issues?.map((issue) => ({ key: issue.key, summary: issue.fields.summary, description: issue.fields.description, status: issue.fields.status?.name, url: `${JIRA_HOST}/browse/${issue.key}`, }));
return { content: [ { type: "text", text: `Found ${searchResults.total} issues matching your query. Showing ${formattedIssues?.length || 0} results:`, }, { type: "text", text: JSON.stringify(formattedIssues, null, 2), }, ], }; } catch (error) { console.error("Error searching JIRA issues:", error); return { content: [ { type: "text", text: `Error searching JIRA issues: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } });
// Register a tool to get a specific JIRA issueserver.registerTool( "get-issue", { title: "Get JIRA Issue", description: "Get details of a specific JIRA issue by key", inputSchema: { issueKey: z.string().describe("JIRA issue key, e.g., 'PROJ-123'"), fields: z.array(z.string()).optional().default(["summary", "description", "status", "assignee"]).describe("Fields to include in the response"), }, }, async ({ issueKey, fields }) => { try { // Get issue details using the JIRA API const issue = await jiraClient.issues.getIssue({ issueIdOrKey: issueKey, fields: fields.join(","), });
// Format the issue const formattedIssue = { key: issue.key, summary: issue.fields.summary, description: issue.fields.description, status: issue.fields.status?.name, assignee: issue.fields.assignee?.displayName, url: `${JIRA_HOST}/browse/${issue.key}`, };
return { content: [ { type: "text", text: `Issue details for ${issueKey}:`, }, { type: "text", text: JSON.stringify(formattedIssue, null, 2), }, ], }; } catch (error) { console.error("Error getting JIRA issue:", error); return { content: [ { type: "text", text: `Error getting JIRA issue: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } });
// Start the serverasync function startServer() { try { // Test JIRA connection await jiraClient.myself.getCurrentUser(); console.log("Successfully connected to JIRA");
// Start the MCP server await server.listen(); console.log("MCP server started successfully"); } catch (error) { console.error("Failed to start server:", error); process.exit(1); }}
startServer();Environment Configuration
Create a .env file to store your JIRA credentials:
JIRA_HOST=https://your-domain.atlassian.netJIRA_EMAIL=your-email@example.comJIRA_API_TOKEN=your-api-tokenMake sure to add .env to your .gitignore file to avoid committing sensitive information:
node_modules/dist/.envType Definitions for JIRA
To improve type safety and code readability, let’s create a types file for JIRA API responses. Create a new file src/types/jira.ts:
// Define types for JIRA API responsesexport interface JiraIssue { id: string; key: string; self: string; fields: { summary: string; description?: string; status?: { name: string; statusCategory?: { key: string; name: string; }; }; assignee?: { displayName: string; emailAddress?: string; accountId: string; }; created?: string; updated?: string; priority?: { name: string; }; [key: string]: any; // For other fields we might request };}
export interface JiraSearchResponse { expand?: string; startAt: number; maxResults: number; total: number; issues: JiraIssue[];}
export interface JiraProject { id: string; key: string; name: string; description?: string; lead?: { displayName: string; }; url?: string;}These type definitions will help us work with JIRA API responses in a type-safe way, providing better code completion and error checking.
Advanced Features: Adding Resource Links
Let’s enhance our server by adding a resource that provides links to related issues. Create a new file src/advanced-server.ts:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";import { Version3Client } from "jira.js";import { z } from "zod";import dotenv from "dotenv";
// Load environment variablesdotenv.config();
// Validate required environment variablesconst JIRA_HOST = process.env.JIRA_HOST;const JIRA_EMAIL = process.env.JIRA_EMAIL;const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
if (!JIRA_HOST || !JIRA_EMAIL || !JIRA_API_TOKEN) { console.error("Missing required environment variables. Please check your .env file."); process.exit(1);}
// Initialize JIRA clientconst jiraClient = new Version3Client({ host: JIRA_HOST, authentication: { basic: { email: JIRA_EMAIL, apiToken: JIRA_API_TOKEN, }, },});
// Create MCP serverconst server = new McpServer({ name: "jira-mcp-server-advanced", version: "1.0.0",});
// Register a resource for project informationserver.registerResource( "project-info", new ResourceTemplate("jira://project/{projectKey}", { list: undefined }), { title: "JIRA Project Information", description: "Information about a JIRA project", mimeType: "application/json", }, async (uri, { projectKey }) => { try { const project = await jiraClient.projects.getProject({ projectIdOrKey: projectKey, });
return { contents: [ { uri: uri.href, text: JSON.stringify( { key: project.key, name: project.name, description: project.description, lead: project.lead?.displayName, url: `${JIRA_HOST}/browse/${project.key}`, }, null, 2 ), }, ], }; } catch (error) { console.error(`Error fetching project ${projectKey}:`, error); return { contents: [ { uri: uri.href, text: `Error fetching project information: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } });
// Register a tool to search issues and return resource linksserver.registerTool( "search-issues-with-links", { title: "Search JIRA Issues with Links", description: "Search for JIRA issues and return resource links", inputSchema: { jql: z.string().describe("JQL query string"), maxResults: z.number().optional().default(10).describe("Maximum number of results"), projectKey: z.string().describe("JIRA project key"), }, }, async ({ jql, maxResults, projectKey }) => { try { // Search for issues const searchResults = await jiraClient.issueSearch.searchForIssuesUsingJql({ jql, maxResults, fields: ["summary", "status"], });
// Create resource links for each issue const resourceLinks = searchResults.issues?.map((issue) => ({ type: "resource_link" as const, uri: `jira://issue/${issue.key}`, name: issue.key, description: issue.fields.summary, })) || [];
// Add a resource link to the project const projectLink = { type: "resource_link" as const, uri: `jira://project/${projectKey}`, name: `Project ${projectKey}`, description: "Project information", };
return { content: [ { type: "text", text: `Found ${searchResults.total} issues matching your query. Showing ${resourceLinks.length} results:`, }, projectLink, ...resourceLinks, ], }; } catch (error) { console.error("Error searching JIRA issues:", error); return { content: [ { type: "text", text: `Error searching JIRA issues: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } });
// Register a resource for issue detailsserver.registerResource( "issue-details", new ResourceTemplate("jira://issue/{issueKey}", { list: undefined }), { title: "JIRA Issue Details", description: "Detailed information about a JIRA issue", mimeType: "application/json", }, async (uri, { issueKey }) => { try { const issue = await jiraClient.issues.getIssue({ issueIdOrKey: issueKey, fields: "summary,description,status,assignee,created,updated,priority", });
return { contents: [ { uri: uri.href, text: JSON.stringify( { key: issue.key, summary: issue.fields.summary, description: issue.fields.description, status: issue.fields.status?.name, assignee: issue.fields.assignee?.displayName, created: issue.fields.created, updated: issue.fields.updated, priority: issue.fields.priority?.name, url: `${JIRA_HOST}/browse/${issue.key}`, }, null, 2 ), }, ], }; } catch (error) { console.error(`Error fetching issue ${issueKey}:`, error); return { contents: [ { uri: uri.href, text: `Error fetching issue details: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } });
// Start the serverasync function startServer() { try { // Test JIRA connection await jiraClient.myself.getCurrentUser(); console.log("Successfully connected to JIRA");
// Start the MCP server await server.listen(); console.log("MCP server started successfully"); } catch (error) { console.error("Failed to start server:", error); process.exit(1); }}
startServer();Running the Server
Create a script in package.json to run the server:
{ "scripts": { "start": "ts-node src/server.ts", "start:advanced": "ts-node src/advanced-server.ts" }}To run the server:
# Run the basic servernpm start
# Or run the advanced server with resource linksnpm run start:advancedTesting the Integration
Once your server is running, you can test it using any MCP client. Here are some example queries you might try:
-
Search for open issues in a project:
Tool: search-issuesParameters: {"jql": "project = PROJ AND status = 'Open'","maxResults": 5} -
Get details of a specific issue:
Tool: get-issueParameters: {"issueKey": "PROJ-123"} -
Search for issues and get resource links (advanced server):
Tool: search-issues-with-linksParameters: {"jql": "project = PROJ AND priority = High","maxResults": 3,"projectKey": "PROJ"}
Error Handling and Best Practices
When integrating MCP with JIRA, keep these best practices in mind:
-
Secure credentials: Always store API tokens in environment variables, never hardcode them.
-
Handle rate limits: JIRA API has rate limits, so implement retry logic for failed requests.
-
Validate input: Use Zod schemas to validate input parameters and provide clear error messages.
-
Provide context: Return meaningful error messages that help users understand what went wrong.
-
Use resource links: For large datasets, return resource links instead of embedding all content directly.
Conclusion
In this article, we’ve built an MCP server that integrates with JIRA’s REST API, allowing LLMs to search for issues, retrieve details, and access project information. This integration demonstrates the power of MCP as a standardized protocol for connecting AI systems with enterprise tools.
By following similar patterns, you can extend this integration to support more JIRA features or integrate MCP with other enterprise systems. The possibilities are endless!
Further Resources
Happy coding!