Commit 32dbf8a9 authored by MD. SHAHIDUL ISLAM's avatar MD. SHAHIDUL ISLAM

feat: introduce Jira MCP server with tools for fetching, viewing, creating,…

feat: introduce Jira MCP server with tools for fetching, viewing, creating, assigning, and transitioning Jira issues, along with a setup guide and tests.
parents
venv
__pycache__
\ No newline at end of file
# ==============================================================================
# _____ _ _
# | __ \ | | | |
# | | | | ___ | |__ __ _| |_ ___ ___
# | | | |/ _ \| '_ \ / _` | __/ _ \/ __|
# | |__| | (_) | | | | (_| | || __/ (__
# |_____/ \___/|_| |_|\__,_|\__\___|\___|
# ^
# /
#
# Dohatec Jira MCP Server
#
# Description:
# A Model Context Protocol (MCP) server that connects AI assistants to a
# self-hosted Dohatec Jira instance. It provides seamless integration to
# fetch assigned open tasks, retrieve issue details, create new issues,
# assign tasks to the current user, and transition issue statuses directly
# from the AI interface.
#
# Author: Md Shahidul Islam
# ==============================================================================
import os
from mcp.server.fastmcp import FastMCP
from jira import JIRA
# Initialize FastMCP Server
mcp = FastMCP("Dohatec Jira Mcp")
# Initialize Jira Client
JIRA_SERVER = os.environ.get("JIRA_SERVER")
JIRA_USERNAME = os.environ.get("JIRA_USERNAME")
JIRA_PASSWORD = os.environ.get("JIRA_PASSWORD")
# We use Basic Auth explicitly for this local instance.
try:
jira_client = JIRA(
server=JIRA_SERVER,
basic_auth=(JIRA_USERNAME, JIRA_PASSWORD)
)
except Exception as e:
# If initialization fails, FastMCP will still start but tools might fail.
# We log initialization error to stdout (which stderr usually prints to in MCP).
import sys
print(f"Failed to connect to Jira at {JIRA_SERVER}: {e}", file=sys.stderr)
jira_client = None
def _get_jira():
if not jira_client:
raise RuntimeError("Jira client failed to initialize.")
return jira_client
@mcp.tool()
def get_my_assigned_issues() -> str:
"""Fetches all open Jira issues assigned to the current user."""
jira = _get_jira()
# JQL to find user's unresolved issues
jql = 'assignee = currentUser() AND resolution = Unresolved ORDER BY priority DESC, created DESC'
try:
issues = jira.search_issues(jql, maxResults=50)
if not issues:
return "No open issues assigned to you."
result = ["Your Open Assigned Issues:"]
for issue in issues:
result.append(f"- [{issue.key}] {issue.fields.summary} (Status: {issue.fields.status.name}, Priority: {issue.fields.priority.name if issue.fields.priority else 'None'})")
return "\n".join(result)
except Exception as e:
return f"Error fetching issues: {str(e)}"
@mcp.tool()
def get_issue_details(issue_key: str) -> str:
"""Retrieves full details for a specific issue ID (e.g. PCM-131)."""
jira = _get_jira()
try:
issue = jira.issue(issue_key)
assignee = issue.fields.assignee.displayName if issue.fields.assignee else "Unassigned"
description = issue.fields.description or "No description provided."
details = [
f"Issue: {issue.key}",
f"Summary: {issue.fields.summary}",
f"Type: {issue.fields.issuetype.name}",
f"Status: {issue.fields.status.name}",
f"Priority: {issue.fields.priority.name if issue.fields.priority else 'None'}",
f"Assignee: {assignee}",
f"Reporter: {issue.fields.reporter.displayName if issue.fields.reporter else 'Unknown'}",
f"Created: {issue.fields.created}",
f"Updated: {issue.fields.updated}",
f"\nDescription:\n{description}"
]
return "\n".join(details)
except Exception as e:
return f"Error fetching issue {issue_key}: {str(e)}"
@mcp.tool()
def create_issue(project_key: str, summary: str, description: str, issue_type: str = "Task") -> str:
"""Creates a new Jira issue in the specified project."""
jira = _get_jira()
try:
issue_dict = {
'project': {'key': project_key},
'summary': summary,
'description': description,
'issuetype': {'name': issue_type},
}
new_issue = jira.create_issue(fields=issue_dict)
return f"Successfully created issue {new_issue.key}. URL: {JIRA_SERVER}/browse/{new_issue.key}"
except Exception as e:
return f"Error creating issue: {str(e)}"
@mcp.tool()
def assign_issue_to_me(issue_key: str) -> str:
"""Assigns the specified issue to the authenticated user."""
jira = _get_jira()
try:
# Provide the username we authenticated with
jira.assign_issue(issue_key, JIRA_USERNAME)
return f"Successfully assigned issue {issue_key} to {JIRA_USERNAME}."
except Exception as e:
return f"Error assigning issue {issue_key}: {str(e)}"
@mcp.tool()
def transition_issue(issue_key: str, transition_name: str) -> str:
"""Transitions an issue to a new status by name (e.g., 'Ready for QA')."""
jira = _get_jira()
try:
# First, find available transitions
transitions = jira.transitions(issue_key)
transition_id = None
available_names = []
for t in transitions:
available_names.append(t['name'])
# Case-insensitive match
if t['name'].lower() == transition_name.lower():
transition_id = t['id']
break
if not transition_id:
available_str = ", ".join(f"'{name}'" for name in available_names)
return f"Transition '{transition_name}' not found for issue {issue_key}. Available transitions are: {available_str}"
# Execute the transition
jira.transition_issue(issue_key, transition_id)
return f"Successfully transitioned issue {issue_key} to '{transition_name}'."
except Exception as e:
return f"Error transitioning issue {issue_key}: {str(e)}"
if __name__ == "__main__":
# Start the FastMCP server
mcp.run()
import sys
from jira_mcp_server import (
mcp,
jira_client,
get_my_assigned_issues,
get_issue_details,
create_issue,
assign_issue_to_me,
transition_issue
)
print("=== Starting Jira MCP Server API Tests ===\n")
if not jira_client:
print("Failed: Jira client is not initialized.")
sys.exit(1)
# Test 1: Fetch My Issues
print("\n--- Test 1: Get My Assigned Issues ---")
my_issues_result = get_my_assigned_issues()
print(my_issues_result)
# Try fetching details of the first issue if any exist
lines = my_issues_result.split('\n')
test_issue_key = None
if len(lines) > 1 and lines[1].startswith('- ['):
# Extract the issue key like "PCM-131" from "- [PCM-131] Summary..."
test_issue_key = lines[1].split(']')[0].split('[')[1]
if test_issue_key:
# Test 2: Get Issue Details
print(f"\n--- Test 2: Get Issue Details for {test_issue_key} ---")
details_result = get_issue_details(test_issue_key)
print(details_result)
else:
print("\n--- Skipping Test 2: No open assigned issues found to detail. ---")
# Let's verify we can find the available transitions for the test issue
if test_issue_key:
print(f"\n--- Test 3: Checking Transitions for {test_issue_key} ---")
try:
transitions = jira_client.transitions(test_issue_key)
available = [t['name'] for t in transitions]
print(f"Available transitions: {', '.join(available)}")
except Exception as e:
print(f"Failed to fetch transitions: {e}")
print("\n=== Tests Complete ===")
import sys
import time
from jira_mcp_server import (
mcp,
jira_client,
get_my_assigned_issues,
get_issue_details,
create_issue,
assign_issue_to_me,
transition_issue
)
print("=== Starting Jira MCP Server API Tests ===\n")
if not jira_client:
print("Failed: Jira client is not initialized.")
sys.exit(1)
# Test 4: Create a new Issue
print("\n--- Test 4: Create a new Issue ---")
try:
# We use "PCM" project as seen from the user's issues list (e.g. PCM-228)
create_result = create_issue("PCM", "MCP Automated Test Issue", "This issue was created by the FastMCP testing script.")
print(create_result)
# Extract the new issue key from the result string. e.g "Successfully created issue PCM-229. URL:..."
new_test_key = create_result.split(' ')[3].replace('.', '')
# Test 5: Assign Issue to me
if new_test_key.startswith('PCM'):
print(f"\n--- Test 5: Assign Issue {new_test_key} to me ---")
assign_result = assign_issue_to_me(new_test_key)
print(assign_result)
# Give Jira a second to index the transition
time.sleep(2)
print(f"\n--- Test 6: Verify {new_test_key} Details ---")
details = get_issue_details(new_test_key)
print(details)
except Exception as e:
print(f"Tests failed: {e}")
print("\n=== Tests Complete ===")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment